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.

471 lines
13 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, 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("/followers", user.ListFollowers)
  227. m.Group("/following", func() {
  228. m.Get("", user.ListFollowing)
  229. m.Get("/:target", user.CheckFollowing)
  230. })
  231. m.Get("/starred", user.GetStarredRepos)
  232. m.Get("/subscriptions", user.GetWatchedRepos)
  233. })
  234. }, reqToken())
  235. m.Group("/user", func() {
  236. m.Get("", user.GetAuthenticatedUser)
  237. m.Combo("/emails").Get(user.ListEmails).
  238. Post(bind(api.CreateEmailOption{}), user.AddEmail).
  239. Delete(bind(api.CreateEmailOption{}), user.DeleteEmail)
  240. m.Get("/followers", user.ListMyFollowers)
  241. m.Group("/following", func() {
  242. m.Get("", user.ListMyFollowing)
  243. m.Combo("/:username").Get(user.CheckMyFollowing).Put(user.Follow).Delete(user.Unfollow)
  244. })
  245. m.Group("/keys", func() {
  246. m.Combo("").Get(user.ListMyPublicKeys).
  247. Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
  248. m.Combo("/:id").Get(user.GetPublicKey).
  249. Delete(user.DeletePublicKey)
  250. })
  251. m.Combo("/repos").Get(user.ListMyRepos).
  252. Post(bind(api.CreateRepoOption{}), repo.Create)
  253. m.Group("/starred", func() {
  254. m.Get("", user.GetMyStarredRepos)
  255. m.Group("/:username/:reponame", func() {
  256. m.Get("", user.IsStarring)
  257. m.Put("", user.Star)
  258. m.Delete("", user.Unstar)
  259. }, repoAssignment())
  260. })
  261. m.Get("/subscriptions", user.GetMyWatchedRepos)
  262. }, reqToken())
  263. // Repositories
  264. m.Post("/org/:org/repos", reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
  265. m.Group("/repos", func() {
  266. m.Get("/search", repo.Search)
  267. })
  268. m.Combo("/repositories/:id", reqToken()).Get(repo.GetByID)
  269. m.Group("/repos", func() {
  270. m.Post("/migrate", bind(auth.MigrateRepoForm{}), repo.Migrate)
  271. m.Group("/:username/:reponame", func() {
  272. m.Combo("").Get(repo.Get).Delete(repo.Delete)
  273. m.Group("/hooks", func() {
  274. m.Combo("").Get(repo.ListHooks).
  275. Post(bind(api.CreateHookOption{}), repo.CreateHook)
  276. m.Combo("/:id").Get(repo.GetHook).
  277. Patch(bind(api.EditHookOption{}), repo.EditHook).
  278. Delete(repo.DeleteHook)
  279. }, reqRepoWriter())
  280. m.Group("/collaborators", func() {
  281. m.Get("", repo.ListCollaborators)
  282. m.Combo("/:collaborator").Get(repo.IsCollaborator).
  283. Put(bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
  284. Delete(repo.DeleteCollaborator)
  285. })
  286. m.Get("/raw/*", context.RepoRef(), repo.GetRawFile)
  287. m.Get("/archive/*", repo.GetArchive)
  288. m.Combo("/forks").Get(repo.ListForks).
  289. Post(bind(api.CreateForkOption{}), repo.CreateFork)
  290. m.Group("/branches", func() {
  291. m.Get("", repo.ListBranches)
  292. m.Get("/:branchname", repo.GetBranch)
  293. })
  294. m.Group("/keys", func() {
  295. m.Combo("").Get(repo.ListDeployKeys).
  296. Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
  297. m.Combo("/:id").Get(repo.GetDeployKey).
  298. Delete(repo.DeleteDeploykey)
  299. })
  300. m.Group("/issues", func() {
  301. m.Combo("").Get(repo.ListIssues).Post(bind(api.CreateIssueOption{}), repo.CreateIssue)
  302. m.Group("/comments", func() {
  303. m.Get("", repo.ListRepoIssueComments)
  304. m.Combo("/:id").Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment)
  305. })
  306. m.Group("/:index", func() {
  307. m.Combo("").Get(repo.GetIssue).Patch(bind(api.EditIssueOption{}), repo.EditIssue)
  308. m.Group("/comments", func() {
  309. m.Combo("").Get(repo.ListIssueComments).Post(bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
  310. m.Combo("/:id").Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
  311. Delete(repo.DeleteIssueComment)
  312. })
  313. m.Group("/labels", func() {
  314. m.Combo("").Get(repo.ListIssueLabels).
  315. Post(bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
  316. Put(bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
  317. Delete(repo.ClearIssueLabels)
  318. m.Delete("/:id", repo.DeleteIssueLabel)
  319. })
  320. })
  321. }, mustEnableIssues)
  322. m.Group("/labels", func() {
  323. m.Combo("").Get(repo.ListLabels).
  324. Post(bind(api.CreateLabelOption{}), repo.CreateLabel)
  325. m.Combo("/:id").Get(repo.GetLabel).Patch(bind(api.EditLabelOption{}), repo.EditLabel).
  326. Delete(repo.DeleteLabel)
  327. })
  328. m.Group("/milestones", func() {
  329. m.Combo("").Get(repo.ListMilestones).
  330. Post(reqRepoWriter(), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
  331. m.Combo("/:id").Get(repo.GetMilestone).
  332. Patch(reqRepoWriter(), bind(api.EditMilestoneOption{}), repo.EditMilestone).
  333. Delete(reqRepoWriter(), repo.DeleteMilestone)
  334. })
  335. m.Get("/stargazers", repo.ListStargazers)
  336. m.Get("/subscribers", repo.ListSubscribers)
  337. m.Group("/subscription", func() {
  338. m.Get("", user.IsWatching)
  339. m.Put("", user.Watch)
  340. m.Delete("", user.Unwatch)
  341. })
  342. m.Group("/releases", func() {
  343. m.Combo("").Get(repo.ListReleases).
  344. Post(bind(api.CreateReleaseOption{}), repo.CreateRelease)
  345. m.Combo("/:id").Get(repo.GetRelease).
  346. Patch(bind(api.EditReleaseOption{}), repo.EditRelease).
  347. Delete(repo.DeleteRelease)
  348. })
  349. m.Get("/editorconfig/:filename", context.RepoRef(), repo.GetEditorconfig)
  350. m.Group("/pulls", func() {
  351. m.Combo("").Get(bind(api.ListPullRequestsOptions{}), repo.ListPullRequests).Post(reqRepoWriter(), bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
  352. m.Group("/:index", func() {
  353. m.Combo("").Get(repo.GetPullRequest).Patch(reqRepoWriter(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
  354. m.Combo("/merge").Get(repo.IsPullRequestMerged).Post(reqRepoWriter(), repo.MergePullRequest)
  355. })
  356. }, mustAllowPulls, context.ReferencesGitRepo())
  357. }, repoAssignment())
  358. }, reqToken())
  359. // Organizations
  360. m.Get("/user/orgs", reqToken(), org.ListMyOrgs)
  361. m.Get("/users/:username/orgs", org.ListUserOrgs)
  362. m.Group("/orgs/:orgname", func() {
  363. m.Combo("").Get(org.Get).
  364. Patch(reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit)
  365. m.Group("/members", func() {
  366. m.Get("", org.ListMembers)
  367. m.Combo("/:username").Get(org.IsMember).
  368. Delete(reqOrgOwnership(), org.DeleteMember)
  369. })
  370. m.Group("/public_members", func() {
  371. m.Get("", org.ListPublicMembers)
  372. m.Combo("/:username").Get(org.IsPublicMember).
  373. Put(reqOrgMembership(), org.PublicizeMember).
  374. Delete(reqOrgMembership(), org.ConcealMember)
  375. })
  376. m.Combo("/teams", reqOrgMembership()).Get(org.ListTeams).
  377. Post(bind(api.CreateTeamOption{}), org.CreateTeam)
  378. m.Group("/hooks", func() {
  379. m.Combo("").Get(org.ListHooks).
  380. Post(bind(api.CreateHookOption{}), org.CreateHook)
  381. m.Combo("/:id").Get(org.GetHook).
  382. Patch(reqOrgOwnership(), bind(api.EditHookOption{}), org.EditHook).
  383. Delete(reqOrgOwnership(), org.DeleteHook)
  384. }, reqOrgMembership())
  385. }, orgAssignment(true))
  386. m.Group("/teams/:teamid", func() {
  387. m.Combo("").Get(org.GetTeam).
  388. Patch(reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
  389. Delete(reqOrgOwnership(), org.DeleteTeam)
  390. m.Group("/members", func() {
  391. m.Get("", org.GetTeamMembers)
  392. m.Combo("/:username").
  393. Put(reqOrgOwnership(), org.AddTeamMember).
  394. Delete(reqOrgOwnership(), org.RemoveTeamMember)
  395. })
  396. m.Group("/repos", func() {
  397. m.Get("", org.GetTeamRepos)
  398. m.Combo(":orgname/:reponame").
  399. Put(org.AddTeamRepository).
  400. Delete(org.RemoveTeamRepository)
  401. })
  402. }, reqOrgMembership(), orgAssignment(false, true))
  403. m.Any("/*", func(ctx *context.Context) {
  404. ctx.Error(404)
  405. })
  406. m.Group("/admin", func() {
  407. m.Group("/users", func() {
  408. m.Post("", bind(api.CreateUserOption{}), admin.CreateUser)
  409. m.Group("/:username", func() {
  410. m.Combo("").Patch(bind(api.EditUserOption{}), admin.EditUser).
  411. Delete(admin.DeleteUser)
  412. m.Post("/keys", bind(api.CreateKeyOption{}), admin.CreatePublicKey)
  413. m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
  414. m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
  415. })
  416. })
  417. }, reqAdmin())
  418. }, context.APIContexter())
  419. }