1
0

route_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. package lfs
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. "gopkg.in/macaron.v1"
  11. "gogs.io/gogs/internal/auth"
  12. "gogs.io/gogs/internal/database"
  13. "gogs.io/gogs/internal/lfsutil"
  14. )
  15. func TestAuthenticate(t *testing.T) {
  16. tests := []struct {
  17. name string
  18. header http.Header
  19. mockStore func() *MockStore
  20. expStatusCode int
  21. expHeader http.Header
  22. expBody string
  23. }{
  24. {
  25. name: "no authorization",
  26. expStatusCode: http.StatusUnauthorized,
  27. expHeader: http.Header{
  28. "Lfs-Authenticate": []string{`Basic realm="Git LFS"`},
  29. "Content-Type": []string{"application/vnd.git-lfs+json"},
  30. },
  31. expBody: `{"message":"Credentials needed"}` + "\n",
  32. },
  33. {
  34. name: "user has 2FA enabled",
  35. header: http.Header{
  36. "Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
  37. },
  38. mockStore: func() *MockStore {
  39. mockStore := NewMockStore()
  40. mockStore.IsTwoFactorEnabledFunc.SetDefaultReturn(true)
  41. mockStore.AuthenticateUserFunc.SetDefaultReturn(&database.User{}, nil)
  42. return mockStore
  43. },
  44. expStatusCode: http.StatusBadRequest,
  45. expHeader: http.Header{},
  46. expBody: "Users with 2FA enabled are not allowed to authenticate via username and password.",
  47. },
  48. {
  49. name: "both user and access token do not exist",
  50. header: http.Header{
  51. "Authorization": []string{"Basic dXNlcm5hbWU="},
  52. },
  53. mockStore: func() *MockStore {
  54. mockStore := NewMockStore()
  55. mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{})
  56. mockStore.AuthenticateUserFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
  57. return mockStore
  58. },
  59. expStatusCode: http.StatusUnauthorized,
  60. expHeader: http.Header{
  61. "Lfs-Authenticate": []string{`Basic realm="Git LFS"`},
  62. "Content-Type": []string{"application/vnd.git-lfs+json"},
  63. },
  64. expBody: `{"message":"Credentials needed"}` + "\n",
  65. },
  66. {
  67. name: "authenticated by username and password",
  68. header: http.Header{
  69. "Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
  70. },
  71. mockStore: func() *MockStore {
  72. mockStore := NewMockStore()
  73. mockStore.IsTwoFactorEnabledFunc.SetDefaultReturn(false)
  74. mockStore.AuthenticateUserFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
  75. return mockStore
  76. },
  77. expStatusCode: http.StatusOK,
  78. expHeader: http.Header{},
  79. expBody: "ID: 1, Name: unknwon",
  80. },
  81. {
  82. name: "authenticate by access token via username",
  83. header: http.Header{
  84. "Authorization": []string{"Basic dXNlcm5hbWU="},
  85. },
  86. mockStore: func() *MockStore {
  87. mockStore := NewMockStore()
  88. mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil)
  89. mockStore.AuthenticateUserFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
  90. mockStore.GetUserByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
  91. return mockStore
  92. },
  93. expStatusCode: http.StatusOK,
  94. expHeader: http.Header{},
  95. expBody: "ID: 1, Name: unknwon",
  96. },
  97. {
  98. name: "authenticate by access token via password",
  99. header: http.Header{
  100. "Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
  101. },
  102. mockStore: func() *MockStore {
  103. mockStore := NewMockStore()
  104. mockStore.GetAccessTokenBySHA1Func.SetDefaultHook(func(_ context.Context, sha1 string) (*database.AccessToken, error) {
  105. if sha1 == "password" {
  106. return &database.AccessToken{}, nil
  107. }
  108. return nil, database.ErrAccessTokenNotExist{}
  109. })
  110. mockStore.AuthenticateUserFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
  111. mockStore.GetUserByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
  112. return mockStore
  113. },
  114. expStatusCode: http.StatusOK,
  115. expHeader: http.Header{},
  116. expBody: "ID: 1, Name: unknwon",
  117. },
  118. }
  119. for _, test := range tests {
  120. t.Run(test.name, func(t *testing.T) {
  121. if test.mockStore == nil {
  122. test.mockStore = NewMockStore
  123. }
  124. m := macaron.New()
  125. m.Use(macaron.Renderer())
  126. m.Get("/", authenticate(test.mockStore()), func(w http.ResponseWriter, user *database.User) {
  127. _, _ = fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
  128. })
  129. r, err := http.NewRequest("GET", "/", nil)
  130. if err != nil {
  131. t.Fatal(err)
  132. }
  133. r.Header = test.header
  134. rr := httptest.NewRecorder()
  135. m.ServeHTTP(rr, r)
  136. resp := rr.Result()
  137. assert.Equal(t, test.expStatusCode, resp.StatusCode)
  138. assert.Equal(t, test.expHeader, resp.Header)
  139. body, err := io.ReadAll(resp.Body)
  140. if err != nil {
  141. t.Fatal(err)
  142. }
  143. assert.Equal(t, test.expBody, string(body))
  144. })
  145. }
  146. }
  147. func TestAuthorize(t *testing.T) {
  148. tests := []struct {
  149. name string
  150. accessMode database.AccessMode
  151. mockStore func() *MockStore
  152. expStatusCode int
  153. expBody string
  154. }{
  155. {
  156. name: "user does not exist",
  157. accessMode: database.AccessModeNone,
  158. mockStore: func() *MockStore {
  159. mockStore := NewMockStore()
  160. mockStore.GetUserByUsernameFunc.SetDefaultReturn(nil, database.ErrUserNotExist{})
  161. return mockStore
  162. },
  163. expStatusCode: http.StatusNotFound,
  164. },
  165. {
  166. name: "repository does not exist",
  167. accessMode: database.AccessModeNone,
  168. mockStore: func() *MockStore {
  169. mockStore := NewMockStore()
  170. mockStore.GetRepositoryByNameFunc.SetDefaultReturn(nil, database.ErrRepoNotExist{})
  171. mockStore.GetUserByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
  172. return &database.User{Name: username}, nil
  173. })
  174. return mockStore
  175. },
  176. expStatusCode: http.StatusNotFound,
  177. },
  178. {
  179. name: "actor is not authorized",
  180. accessMode: database.AccessModeWrite,
  181. mockStore: func() *MockStore {
  182. mockStore := NewMockStore()
  183. mockStore.AuthorizeRepositoryAccessFunc.SetDefaultHook(func(_ context.Context, _ int64, _ int64, desired database.AccessMode, _ database.AccessModeOptions) bool {
  184. return desired <= database.AccessModeRead
  185. })
  186. mockStore.GetRepositoryByNameFunc.SetDefaultHook(func(ctx context.Context, ownerID int64, name string) (*database.Repository, error) {
  187. return &database.Repository{Name: name}, nil
  188. })
  189. mockStore.GetUserByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
  190. return &database.User{Name: username}, nil
  191. })
  192. return mockStore
  193. },
  194. expStatusCode: http.StatusNotFound,
  195. },
  196. {
  197. name: "actor is authorized",
  198. accessMode: database.AccessModeRead,
  199. mockStore: func() *MockStore {
  200. mockStore := NewMockStore()
  201. mockStore.AuthorizeRepositoryAccessFunc.SetDefaultHook(func(_ context.Context, _ int64, _ int64, desired database.AccessMode, _ database.AccessModeOptions) bool {
  202. return desired <= database.AccessModeRead
  203. })
  204. mockStore.GetRepositoryByNameFunc.SetDefaultHook(func(ctx context.Context, ownerID int64, name string) (*database.Repository, error) {
  205. return &database.Repository{Name: name}, nil
  206. })
  207. mockStore.GetUserByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
  208. return &database.User{Name: username}, nil
  209. })
  210. return mockStore
  211. },
  212. expStatusCode: http.StatusOK,
  213. expBody: "owner.Name: owner, repo.Name: repo",
  214. },
  215. }
  216. for _, test := range tests {
  217. t.Run(test.name, func(t *testing.T) {
  218. mockStore := NewMockStore()
  219. if test.mockStore != nil {
  220. mockStore = test.mockStore()
  221. }
  222. m := macaron.New()
  223. m.Use(macaron.Renderer())
  224. m.Use(func(c *macaron.Context) {
  225. c.Map(&database.User{})
  226. })
  227. m.Get(
  228. "/:username/:reponame",
  229. authorize(mockStore, test.accessMode),
  230. func(w http.ResponseWriter, owner *database.User, repo *database.Repository) {
  231. _, _ = fmt.Fprintf(w, "owner.Name: %s, repo.Name: %s", owner.Name, repo.Name)
  232. },
  233. )
  234. r, err := http.NewRequest("GET", "/owner/repo", nil)
  235. if err != nil {
  236. t.Fatal(err)
  237. }
  238. rr := httptest.NewRecorder()
  239. m.ServeHTTP(rr, r)
  240. resp := rr.Result()
  241. assert.Equal(t, test.expStatusCode, resp.StatusCode)
  242. body, err := io.ReadAll(resp.Body)
  243. if err != nil {
  244. t.Fatal(err)
  245. }
  246. assert.Equal(t, test.expBody, string(body))
  247. })
  248. }
  249. }
  250. func Test_verifyHeader(t *testing.T) {
  251. tests := []struct {
  252. name string
  253. verifyHeader macaron.Handler
  254. header http.Header
  255. expStatusCode int
  256. }{
  257. {
  258. name: "header not found",
  259. verifyHeader: verifyHeader("Accept", contentType, http.StatusNotAcceptable),
  260. expStatusCode: http.StatusNotAcceptable,
  261. },
  262. {
  263. name: "header found",
  264. verifyHeader: verifyHeader("Accept", "application/vnd.git-lfs+json", http.StatusNotAcceptable),
  265. header: http.Header{
  266. "Accept": []string{"application/vnd.git-lfs+json; charset=utf-8"},
  267. },
  268. expStatusCode: http.StatusOK,
  269. },
  270. }
  271. for _, test := range tests {
  272. t.Run(test.name, func(t *testing.T) {
  273. m := macaron.New()
  274. m.Use(macaron.Renderer())
  275. m.Get("/", test.verifyHeader)
  276. r, err := http.NewRequest("GET", "/", nil)
  277. if err != nil {
  278. t.Fatal(err)
  279. }
  280. r.Header = test.header
  281. rr := httptest.NewRecorder()
  282. m.ServeHTTP(rr, r)
  283. resp := rr.Result()
  284. assert.Equal(t, test.expStatusCode, resp.StatusCode)
  285. })
  286. }
  287. }
  288. func Test_verifyOID(t *testing.T) {
  289. m := macaron.New()
  290. m.Get("/:oid", verifyOID(), func(w http.ResponseWriter, oid lfsutil.OID) {
  291. fmt.Fprintf(w, "oid: %s", oid)
  292. })
  293. tests := []struct {
  294. name string
  295. url string
  296. expStatusCode int
  297. expBody string
  298. }{
  299. {
  300. name: "bad oid",
  301. url: "/bad_oid",
  302. expStatusCode: http.StatusBadRequest,
  303. expBody: `{"message":"Invalid oid"}` + "\n",
  304. },
  305. {
  306. name: "good oid",
  307. url: "/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
  308. expStatusCode: http.StatusOK,
  309. expBody: "oid: ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
  310. },
  311. }
  312. for _, test := range tests {
  313. t.Run(test.name, func(t *testing.T) {
  314. r, err := http.NewRequest("GET", test.url, nil)
  315. if err != nil {
  316. t.Fatal(err)
  317. }
  318. rr := httptest.NewRecorder()
  319. m.ServeHTTP(rr, r)
  320. resp := rr.Result()
  321. assert.Equal(t, test.expStatusCode, resp.StatusCode)
  322. body, err := io.ReadAll(resp.Body)
  323. if err != nil {
  324. t.Fatal(err)
  325. }
  326. assert.Equal(t, test.expBody, string(body))
  327. })
  328. }
  329. }
  330. func Test_internalServerError(t *testing.T) {
  331. rr := httptest.NewRecorder()
  332. internalServerError(rr)
  333. resp := rr.Result()
  334. assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
  335. body, err := io.ReadAll(resp.Body)
  336. if err != nil {
  337. t.Fatal(err)
  338. }
  339. assert.Equal(t, `{"message":"Internal server error"}`+"\n", string(body))
  340. }