You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

479 lines
14 KiB

Add basic integration test infrastructure (and new endpoint `/api/v1/version` for testing it) (#741) * Implement '/api/v1/version' * Cleanup and various fixes * Enhance run.sh * Add install_test.go * Add parameter utils.Config for testing handlers * Re-organize TestVersion.go * Rename functions * handling process cleanup properly * Fix missing function renaming * Cleanup the 'retry' logic * Cleanup * Remove unneeded logging code * Logging messages tweaking * Logging message tweaking * Fix logging messages * Use 'const' instead of hardwired numbers * We don't really need retries anymore * Move constant ServerHttpPort to install_test.go * Restore mistakenly removed constant * Add required comments to make the linter happy. * Fix comments and naming to address linter's complaints * Detect Gitea executale version automatically * Remove tests/run.sh, `go test` suffices. * Make `make build` a prerequisite of `make test` * Do not sleep before trying * Speedup the server pinging loop * Use defined const instead of hardwired numbers * Remove redundant error handling * Use a dedicated target for running code.gitea.io/tests * Do not make 'test' depend on 'build' target * Rectify the excluded package list * Remove redundant 'exit 1' * Change the API to allow passing test.T to test handlers * Make testing.T an embedded field * Use assert.Equal to comparing results * Add copyright info * Parametrized logging output * Use tmpdir instead * Eliminate redundant casting * Remove unneeded variable * Fix last commit * Add missing copyright info * Replace fmt.Fprintf with fmt.Fprint * rename the xtest to integration-test * Use Symlink instead of hard-link for cross-device linking * Turn debugging logs on * Follow the existing framework for APIs * Output logs only if test.v is true * Re-order import statements * Enhance the error message * Fix comment which breaks the linter's rule * Rename 'integration-test' to 'e2e-test' for saving keystrokes * Add comment to avoid possible confusion * Rename tests -> integration-tests Also change back the Makefile to use `make integration-test`. * Use tests/integration for now * tests/integration -> integrations Slightly flattened directory hierarchy is better. * Update Makefile accordingly * Fix a missing change in Makefile * govendor update code.gitea.io/sdk/gitea * Fix comment of struct fields * Fix conditional nonsense * Fix missing updates regarding version string changes * Make variable naming more consistent * Check http status code * Rectify error messages
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package v1
  5. import (
  6. "strings"
  7. "github.com/go-macaron/binding"
  8. "gopkg.in/macaron.v1"
  9. api "code.gitea.io/sdk/gitea"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/auth"
  12. "code.gitea.io/gitea/modules/context"
  13. "code.gitea.io/gitea/routers/api/v1/admin"
  14. "code.gitea.io/gitea/routers/api/v1/misc"
  15. "code.gitea.io/gitea/routers/api/v1/org"
  16. "code.gitea.io/gitea/routers/api/v1/repo"
  17. "code.gitea.io/gitea/routers/api/v1/user"
  18. )
  19. func repoAssignment() macaron.Handler {
  20. return func(ctx *context.APIContext) {
  21. userName := ctx.Params(":username")
  22. repoName := ctx.Params(":reponame")
  23. var (
  24. owner *models.User
  25. err error
  26. )
  27. // Check if the user is the same as the repository owner.
  28. if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) {
  29. owner = ctx.User
  30. } else {
  31. owner, err = models.GetUserByName(userName)
  32. if err != nil {
  33. if models.IsErrUserNotExist(err) {
  34. ctx.Status(404)
  35. } else {
  36. ctx.Error(500, "GetUserByName", err)
  37. }
  38. return
  39. }
  40. }
  41. ctx.Repo.Owner = owner
  42. // Get repository.
  43. repo, err := models.GetRepositoryByName(owner.ID, repoName)
  44. if err != nil {
  45. if models.IsErrRepoNotExist(err) {
  46. redirectRepoID, err := models.LookupRepoRedirect(owner.ID, repoName)
  47. if err == nil {
  48. context.RedirectToRepo(ctx.Context, redirectRepoID)
  49. } else if models.IsErrRepoRedirectNotExist(err) {
  50. ctx.Status(404)
  51. } else {
  52. ctx.Error(500, "LookupRepoRedirect", err)
  53. }
  54. } else {
  55. ctx.Error(500, "GetRepositoryByName", err)
  56. }
  57. return
  58. }
  59. repo.Owner = owner
  60. if ctx.IsSigned && ctx.User.IsAdmin {
  61. ctx.Repo.AccessMode = models.AccessModeOwner
  62. } else {
  63. mode, err := models.AccessLevel(ctx.User.ID, repo)
  64. if err != nil {
  65. ctx.Error(500, "AccessLevel", err)
  66. return
  67. }
  68. ctx.Repo.AccessMode = mode
  69. }
  70. if !ctx.Repo.HasAccess() {
  71. ctx.Status(404)
  72. return
  73. }
  74. ctx.Repo.Repository = repo
  75. }
  76. }
  77. // Contexter middleware already checks token for user sign in process.
  78. func reqToken() macaron.Handler {
  79. return func(ctx *context.Context) {
  80. if !ctx.IsSigned {
  81. ctx.Error(401)
  82. return
  83. }
  84. }
  85. }
  86. func reqBasicAuth() macaron.Handler {
  87. return func(ctx *context.Context) {
  88. if !ctx.IsBasicAuth {
  89. ctx.Error(401)
  90. return
  91. }
  92. }
  93. }
  94. func reqAdmin() macaron.Handler {
  95. return func(ctx *context.Context) {
  96. if !ctx.IsSigned || !ctx.User.IsAdmin {
  97. ctx.Error(403)
  98. return
  99. }
  100. }
  101. }
  102. func reqRepoWriter() macaron.Handler {
  103. return func(ctx *context.Context) {
  104. if !ctx.Repo.IsWriter() {
  105. ctx.Error(403)
  106. return
  107. }
  108. }
  109. }
  110. func reqOrgMembership() macaron.Handler {
  111. return func(ctx *context.APIContext) {
  112. var orgID int64
  113. if ctx.Org.Organization != nil {
  114. orgID = ctx.Org.Organization.ID
  115. } else if ctx.Org.Team != nil {
  116. orgID = ctx.Org.Team.OrgID
  117. } else {
  118. ctx.Error(500, "", "reqOrgMembership: unprepared context")
  119. return
  120. }
  121. if !models.IsOrganizationMember(orgID, ctx.User.ID) {
  122. if ctx.Org.Organization != nil {
  123. ctx.Error(403, "", "Must be an organization member")
  124. } else {
  125. ctx.Status(404)
  126. }
  127. return
  128. }
  129. }
  130. }
  131. func reqOrgOwnership() macaron.Handler {
  132. return func(ctx *context.APIContext) {
  133. var orgID int64
  134. if ctx.Org.Organization != nil {
  135. orgID = ctx.Org.Organization.ID
  136. } else if ctx.Org.Team != nil {
  137. orgID = ctx.Org.Team.OrgID
  138. } else {
  139. ctx.Error(500, "", "reqOrgOwnership: unprepared context")
  140. return
  141. }
  142. if !models.IsOrganizationOwner(orgID, ctx.User.ID) {
  143. if ctx.Org.Organization != nil {
  144. ctx.Error(403, "", "Must be an organization owner")
  145. } else {
  146. ctx.Status(404)
  147. }
  148. return
  149. }
  150. }
  151. }
  152. func orgAssignment(args ...bool) macaron.Handler {
  153. var (
  154. assignOrg bool
  155. assignTeam bool
  156. )
  157. if len(args) > 0 {
  158. assignOrg = args[0]
  159. }
  160. if len(args) > 1 {
  161. assignTeam = args[1]
  162. }
  163. return func(ctx *context.APIContext) {
  164. ctx.Org = new(context.APIOrganization)
  165. var err error
  166. if assignOrg {
  167. ctx.Org.Organization, err = models.GetUserByName(ctx.Params(":orgname"))
  168. if err != nil {
  169. if models.IsErrUserNotExist(err) {
  170. ctx.Status(404)
  171. } else {
  172. ctx.Error(500, "GetUserByName", err)
  173. }
  174. return
  175. }
  176. }
  177. if assignTeam {
  178. ctx.Org.Team, err = models.GetTeamByID(ctx.ParamsInt64(":teamid"))
  179. if err != nil {
  180. if models.IsErrUserNotExist(err) {
  181. ctx.Status(404)
  182. } else {
  183. ctx.Error(500, "GetTeamById", err)
  184. }
  185. return
  186. }
  187. }
  188. }
  189. }
  190. func mustEnableIssues(ctx *context.APIContext) {
  191. if !ctx.Repo.Repository.EnableUnit(models.UnitTypeIssues) {
  192. ctx.Status(404)
  193. return
  194. }
  195. }
  196. func mustAllowPulls(ctx *context.Context) {
  197. if !ctx.Repo.Repository.AllowsPulls() {
  198. ctx.Status(404)
  199. return
  200. }
  201. }
  202. // RegisterRoutes registers all v1 APIs routes to web application.
  203. // FIXME: custom form error response
  204. func RegisterRoutes(m *macaron.Macaron) {
  205. bind := binding.Bind
  206. m.Group("/v1", func() {
  207. // Miscellaneous
  208. m.Get("/version", misc.Version)
  209. m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
  210. m.Post("/markdown/raw", misc.MarkdownRaw)
  211. // Users
  212. m.Group("/users", func() {
  213. m.Get("/search", user.Search)
  214. m.Group("/:username", func() {
  215. m.Get("", user.GetInfo)
  216. m.Get("/repos", user.ListUserRepos)
  217. m.Group("/tokens", func() {
  218. m.Combo("").Get(user.ListAccessTokens).
  219. Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
  220. }, reqBasicAuth())
  221. })
  222. })
  223. m.Group("/users", func() {
  224. m.Group("/:username", func() {
  225. m.Get("/keys", user.ListPublicKeys)
  226. m.Get("/gpg_keys", user.ListGPGKeys)
  227. m.Get("/followers", user.ListFollowers)
  228. m.Group("/following", func() {
  229. m.Get("", user.ListFollowing)
  230. m.Get("/:target", user.CheckFollowing)
  231. })
  232. m.Get("/starred", user.GetStarredRepos)
  233. m.Get("/subscriptions", user.GetWatchedRepos)
  234. })
  235. }, reqToken())
  236. m.Group("/user", func() {
  237. m.Get("", user.GetAuthenticatedUser)
  238. m.Combo("/emails").Get(user.ListEmails).
  239. Post(bind(api.CreateEmailOption{}), user.AddEmail).
  240. Delete(bind(api.CreateEmailOption{}), user.DeleteEmail)
  241. m.Get("/followers", user.ListMyFollowers)
  242. m.Group("/following", func() {
  243. m.Get("", user.ListMyFollowing)
  244. m.Combo("/:username").Get(user.CheckMyFollowing).Put(user.Follow).Delete(user.Unfollow)
  245. })
  246. m.Group("/keys", func() {
  247. m.Combo("").Get(user.ListMyPublicKeys).
  248. Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
  249. m.Combo("/:id").Get(user.GetPublicKey).
  250. Delete(user.DeletePublicKey)
  251. })
  252. m.Group("/gpg_keys", func() {
  253. m.Combo("").Get(user.ListMyGPGKeys).
  254. Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
  255. m.Combo("/:id").Get(user.GetGPGKey).
  256. Delete(user.DeleteGPGKey)
  257. })
  258. m.Combo("/repos").Get(user.ListMyRepos).
  259. Post(bind(api.CreateRepoOption{}), repo.Create)
  260. m.Group("/starred", func() {
  261. m.Get("", user.GetMyStarredRepos)
  262. m.Group("/:username/:reponame", func() {
  263. m.Get("", user.IsStarring)
  264. m.Put("", user.Star)
  265. m.Delete("", user.Unstar)
  266. }, repoAssignment())
  267. })
  268. m.Get("/subscriptions", user.GetMyWatchedRepos)
  269. }, reqToken())
  270. // Repositories
  271. m.Post("/org/:org/repos", reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
  272. m.Group("/repos", func() {
  273. m.Get("/search", repo.Search)
  274. })
  275. m.Combo("/repositories/:id", reqToken()).Get(repo.GetByID)
  276. m.Group("/repos", func() {
  277. m.Post("/migrate", bind(auth.MigrateRepoForm{}), repo.Migrate)
  278. m.Group("/:username/:reponame", func() {
  279. m.Combo("").Get(repo.Get).Delete(repo.Delete)
  280. m.Group("/hooks", func() {
  281. m.Combo("").Get(repo.ListHooks).
  282. Post(bind(api.CreateHookOption{}), repo.CreateHook)
  283. m.Combo("/:id").Get(repo.GetHook).
  284. Patch(bind(api.EditHookOption{}), repo.EditHook).
  285. Delete(repo.DeleteHook)
  286. }, reqRepoWriter())
  287. m.Group("/collaborators", func() {
  288. m.Get("", repo.ListCollaborators)
  289. m.Combo("/:collaborator").Get(repo.IsCollaborator).
  290. Put(bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
  291. Delete(repo.DeleteCollaborator)
  292. })
  293. m.Get("/raw/*", context.RepoRef(), repo.GetRawFile)
  294. m.Get("/archive/*", repo.GetArchive)
  295. m.Combo("/forks").Get(repo.ListForks).
  296. Post(bind(api.CreateForkOption{}), repo.CreateFork)
  297. m.Group("/branches", func() {
  298. m.Get("", repo.ListBranches)
  299. m.Get("/:branchname", repo.GetBranch)
  300. })
  301. m.Group("/keys", func() {
  302. m.Combo("").Get(repo.ListDeployKeys).
  303. Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
  304. m.Combo("/:id").Get(repo.GetDeployKey).
  305. Delete(repo.DeleteDeploykey)
  306. })
  307. m.Group("/issues", func() {
  308. m.Combo("").Get(repo.ListIssues).Post(bind(api.CreateIssueOption{}), repo.CreateIssue)
  309. m.Group("/comments", func() {
  310. m.Get("", repo.ListRepoIssueComments)
  311. m.Combo("/:id").Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment)
  312. })
  313. m.Group("/:index", func() {
  314. m.Combo("").Get(repo.GetIssue).Patch(bind(api.EditIssueOption{}), repo.EditIssue)
  315. m.Group("/comments", func() {
  316. m.Combo("").Get(repo.ListIssueComments).Post(bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
  317. m.Combo("/:id").Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
  318. Delete(repo.DeleteIssueComment)
  319. })
  320. m.Group("/labels", func() {
  321. m.Combo("").Get(repo.ListIssueLabels).
  322. Post(bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
  323. Put(bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
  324. Delete(repo.ClearIssueLabels)
  325. m.Delete("/:id", repo.DeleteIssueLabel)
  326. })
  327. })
  328. }, mustEnableIssues)
  329. m.Group("/labels", func() {
  330. m.Combo("").Get(repo.ListLabels).
  331. Post(bind(api.CreateLabelOption{}), repo.CreateLabel)
  332. m.Combo("/:id").Get(repo.GetLabel).Patch(bind(api.EditLabelOption{}), repo.EditLabel).
  333. Delete(repo.DeleteLabel)
  334. })
  335. m.Group("/milestones", func() {
  336. m.Combo("").Get(repo.ListMilestones).
  337. Post(reqRepoWriter(), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
  338. m.Combo("/:id").Get(repo.GetMilestone).
  339. Patch(reqRepoWriter(), bind(api.EditMilestoneOption{}), repo.EditMilestone).
  340. Delete(reqRepoWriter(), repo.DeleteMilestone)
  341. })
  342. m.Get("/stargazers", repo.ListStargazers)
  343. m.Get("/subscribers", repo.ListSubscribers)
  344. m.Group("/subscription", func() {
  345. m.Get("", user.IsWatching)
  346. m.Put("", user.Watch)
  347. m.Delete("", user.Unwatch)
  348. })
  349. m.Group("/releases", func() {
  350. m.Combo("").Get(repo.ListReleases).
  351. Post(bind(api.CreateReleaseOption{}), repo.CreateRelease)
  352. m.Combo("/:id").Get(repo.GetRelease).
  353. Patch(bind(api.EditReleaseOption{}), repo.EditRelease).
  354. Delete(repo.DeleteRelease)
  355. })
  356. m.Get("/editorconfig/:filename", context.RepoRef(), repo.GetEditorconfig)
  357. m.Group("/pulls", func() {
  358. m.Combo("").Get(bind(api.ListPullRequestsOptions{}), repo.ListPullRequests).Post(reqRepoWriter(), bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
  359. m.Group("/:index", func() {
  360. m.Combo("").Get(repo.GetPullRequest).Patch(reqRepoWriter(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
  361. m.Combo("/merge").Get(repo.IsPullRequestMerged).Post(reqRepoWriter(), repo.MergePullRequest)
  362. })
  363. }, mustAllowPulls, context.ReferencesGitRepo())
  364. }, repoAssignment())
  365. }, reqToken())
  366. // Organizations
  367. m.Get("/user/orgs", reqToken(), org.ListMyOrgs)
  368. m.Get("/users/:username/orgs", org.ListUserOrgs)
  369. m.Group("/orgs/:orgname", func() {
  370. m.Combo("").Get(org.Get).
  371. Patch(reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit)
  372. m.Group("/members", func() {
  373. m.Get("", org.ListMembers)
  374. m.Combo("/:username").Get(org.IsMember).
  375. Delete(reqOrgOwnership(), org.DeleteMember)
  376. })
  377. m.Group("/public_members", func() {
  378. m.Get("", org.ListPublicMembers)
  379. m.Combo("/:username").Get(org.IsPublicMember).
  380. Put(reqOrgMembership(), org.PublicizeMember).
  381. Delete(reqOrgMembership(), org.ConcealMember)
  382. })
  383. m.Combo("/teams", reqOrgMembership()).Get(org.ListTeams).
  384. Post(bind(api.CreateTeamOption{}), org.CreateTeam)
  385. m.Group("/hooks", func() {
  386. m.Combo("").Get(org.ListHooks).
  387. Post(bind(api.CreateHookOption{}), org.CreateHook)
  388. m.Combo("/:id").Get(org.GetHook).
  389. Patch(reqOrgOwnership(), bind(api.EditHookOption{}), org.EditHook).
  390. Delete(reqOrgOwnership(), org.DeleteHook)
  391. }, reqOrgMembership())
  392. }, orgAssignment(true))
  393. m.Group("/teams/:teamid", func() {
  394. m.Combo("").Get(org.GetTeam).
  395. Patch(reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
  396. Delete(reqOrgOwnership(), org.DeleteTeam)
  397. m.Group("/members", func() {
  398. m.Get("", org.GetTeamMembers)
  399. m.Combo("/:username").
  400. Put(reqOrgOwnership(), org.AddTeamMember).
  401. Delete(reqOrgOwnership(), org.RemoveTeamMember)
  402. })
  403. m.Group("/repos", func() {
  404. m.Get("", org.GetTeamRepos)
  405. m.Combo(":orgname/:reponame").
  406. Put(org.AddTeamRepository).
  407. Delete(org.RemoveTeamRepository)
  408. })
  409. }, reqOrgMembership(), orgAssignment(false, true))
  410. m.Any("/*", func(ctx *context.Context) {
  411. ctx.Error(404)
  412. })
  413. m.Group("/admin", func() {
  414. m.Group("/users", func() {
  415. m.Post("", bind(api.CreateUserOption{}), admin.CreateUser)
  416. m.Group("/:username", func() {
  417. m.Combo("").Patch(bind(api.EditUserOption{}), admin.EditUser).
  418. Delete(admin.DeleteUser)
  419. m.Post("/keys", bind(api.CreateKeyOption{}), admin.CreatePublicKey)
  420. m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
  421. m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
  422. })
  423. })
  424. }, reqAdmin())
  425. }, context.APIContexter())
  426. }