diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /tests/integration/quota_use_test.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | tests/integration/quota_use_test.go | 1147 |
1 files changed, 1147 insertions, 0 deletions
diff --git a/tests/integration/quota_use_test.go b/tests/integration/quota_use_test.go new file mode 100644 index 0000000..39c5c1a --- /dev/null +++ b/tests/integration/quota_use_test.go @@ -0,0 +1,1147 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + quota_model "code.gitea.io/gitea/models/quota" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/routers" + forgejo_context "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" + "code.gitea.io/gitea/tests" + + gouuid "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWebQuotaEnforcementRepoMigrate(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + env.RunVisitAndPostToPageTests(t, "/repo/migrate", &Payload{ + "repo_name": "migration-test", + "clone_addr": env.Users.Limited.Repo.Link() + ".git", + "service": fmt.Sprintf("%d", api.ForgejoService), + }, http.StatusOK) + }) +} + +func TestWebQuotaEnforcementRepoCreate(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + env.RunVisitAndPostToPageTests(t, "/repo/create", nil, http.StatusOK) + }) +} + +func TestWebQuotaEnforcementRepoFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + page := fmt.Sprintf("%s/fork", env.Users.Limited.Repo.Link()) + env.RunVisitAndPostToPageTests(t, page, &Payload{ + "repo_name": "fork-test", + }, http.StatusSeeOther) + }) +} + +func TestWebQuotaEnforcementIssueAttachment(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + // Uploading to our repo => 413 + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + CreateIssueAttachment("test.txt"). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Uploading to the limited org repo => 413 + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Limited.Repo}). + CreateIssueAttachment("test.txt"). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Uploading to the unlimited org repo => 200 + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Unlimited.Repo}). + CreateIssueAttachment("test.txt"). + ExpectStatus(http.StatusOK) + }) +} + +func TestWebQuotaEnforcementMirrorSync(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + var mirrorRepo *repo_model.Repository + + env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + mirrorRepo = ctx.CreateMirror() + }). + With(Context{ + Repo: mirrorRepo, + Payload: &Payload{"action": "mirror-sync"}, + }). + PostToPage(mirrorRepo.Link() + "/settings"). + ExpectStatus(http.StatusOK). + ExpectFlashMessage("Quota exceeded, not pulling changes.") + }) +} + +func TestWebQuotaEnforcementRepoContentEditing(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + // We're only going to test the GET requests here, because the entire combo + // is covered by a route check. + + // Lets create a helper! + runCheck := func(t *testing.T, path string, successStatus int) { + t.Run("#"+path, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Uploading to a limited user's repo => 413 + env.As(t, env.Users.Limited). + VisitPage(env.Users.Limited.Repo.Link() + path). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Limited org => 413 + env.As(t, env.Users.Limited). + VisitPage(env.Orgs.Limited.Repo.Link() + path). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Unlimited org => 200 + env.As(t, env.Users.Limited). + VisitPage(env.Orgs.Unlimited.Repo.Link() + path). + ExpectStatus(successStatus) + }) + } + + paths := []string{ + "/_new/main", + "/_edit/main/README.md", + "/_delete/main", + "/_upload/main", + "/_diffpatch/main", + } + + for _, path := range paths { + runCheck(t, path, http.StatusOK) + } + + // Run another check for `_cherrypick`. It's cumbersome to dig out a valid + // commit id, so we'll use a fake, and treat 404 as a success: it's not 413, + // and that's all we care about for this test. + runCheck(t, "/_cherrypick/92cfceb39d57d914ed8b14d0e37643de0797ae56/main", http.StatusNotFound) + }) +} + +func TestWebQuotaEnforcementRepoBranches(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + t.Run("create", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + runTest := func(t *testing.T, path string) { + t.Run("#"+path, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + env.As(t, env.Users.Limited). + With(Context{Payload: &Payload{"new_branch_name": "quota"}}). + PostToRepoPage("/branches/_new" + path). + ExpectStatus(http.StatusRequestEntityTooLarge) + + env.As(t, env.Users.Limited). + With(Context{ + Payload: &Payload{"new_branch_name": "quota"}, + Repo: env.Orgs.Limited.Repo, + }). + PostToRepoPage("/branches/_new" + path). + ExpectStatus(http.StatusRequestEntityTooLarge) + + env.As(t, env.Users.Limited). + With(Context{ + Payload: &Payload{"new_branch_name": "quota"}, + Repo: env.Orgs.Unlimited.Repo, + }). + PostToRepoPage("/branches/_new" + path). + ExpectStatus(http.StatusNotFound) + }) + } + + // We're testing the first two against things that don't exist, so that + // all three consistently return 404 if no quota enforcement happens. + runTest(t, "/branch/no-such-branch") + runTest(t, "/tag/no-such-tag") + runTest(t, "/commit/92cfceb39d57d914ed8b14d0e37643de0797ae56") + }) + + t.Run("delete & restore", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + ctx.With(Context{Payload: &Payload{"new_branch_name": "to-delete"}}). + PostToRepoPage("/branches/_new/branch/main"). + ExpectStatus(http.StatusSeeOther) + }) + + env.As(t, env.Users.Limited). + PostToRepoPage("/branches/delete?name=to-delete"). + ExpectStatus(http.StatusOK) + + env.As(t, env.Users.Limited). + PostToRepoPage("/branches/restore?name=to-delete"). + ExpectStatus(http.StatusOK) + }) + }) +} + +func TestWebQuotaEnforcementRepoReleases(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + env.RunVisitAndPostToRepoPageTests(t, "/releases/new", &Payload{ + "tag_name": "quota", + "tag_target": "main", + "title": "test release", + }, http.StatusSeeOther) + + t.Run("attachments", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Uploading to our repo => 413 + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + CreateReleaseAttachment("test.txt"). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Uploading to the limited org repo => 413 + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Limited.Repo}). + CreateReleaseAttachment("test.txt"). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Uploading to the unlimited org repo => 200 + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Unlimited.Repo}). + CreateReleaseAttachment("test.txt"). + ExpectStatus(http.StatusOK) + }) + }) +} + +func TestWebQuotaEnforcementRepoPulls(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + // To create a pull request, we first fork the two limited repos into the + // unlimited org. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + ForkRepoInto(env.Orgs.Unlimited) + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Limited.Repo}). + ForkRepoInto(env.Orgs.Unlimited) + + // Then, create pull requests from the forks, back to the main repos + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + CreatePullFrom(env.Orgs.Unlimited) + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Limited.Repo}). + CreatePullFrom(env.Orgs.Unlimited) + + // Trying to merge the pull request will fail for both, though, due to being + // over quota. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + With(Context{Payload: &Payload{"do": "merge"}}). + PostToRepoPage("/pulls/1/merge"). + ExpectStatus(http.StatusRequestEntityTooLarge) + + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Limited.Repo}). + With(Context{Payload: &Payload{"do": "merge"}}). + PostToRepoPage("/pulls/1/merge"). + ExpectStatus(http.StatusRequestEntityTooLarge) + }) +} + +func TestWebQuotaEnforcementRepoTransfer(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + t.Run("direct transfer", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Trying to transfer the repository to a limited organization fails. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + With(Context{Payload: &Payload{ + "action": "transfer", + "repo_name": env.Users.Limited.Repo.FullName(), + "new_owner_name": env.Orgs.Limited.Org.Name, + }}). + PostToRepoPage("/settings"). + ExpectStatus(http.StatusOK). + ExpectFlashMessageContains("over quota", "The repository has not been transferred") + + // Trying to transfer to a different, also limited user, also fails. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + With(Context{Payload: &Payload{ + "action": "transfer", + "repo_name": env.Users.Limited.Repo.FullName(), + "new_owner_name": env.Users.Contributor.User.Name, + }}). + PostToRepoPage("/settings"). + ExpectStatus(http.StatusOK). + ExpectFlashMessageContains("over quota", "The repository has not been transferred") + }) + + t.Run("accept & reject", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Trying to transfer to a different user, with quota lifted, starts the transfer + env.As(t, env.Users.Contributor). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + env.As(ctx.t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + With(Context{Payload: &Payload{ + "action": "transfer", + "repo_name": env.Users.Limited.Repo.FullName(), + "new_owner_name": env.Users.Contributor.User.Name, + }}). + PostToRepoPage("/settings"). + ExpectStatus(http.StatusSeeOther). + ExpectFlashCookieContains("This repository has been marked for transfer and awaits confirmation") + }) + + // Trying to accept the transfer, with quota in effect, fails + env.As(t, env.Users.Contributor). + With(Context{Repo: env.Users.Limited.Repo}). + PostToRepoPage("/action/accept_transfer"). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Rejecting the transfer, however, succeeds. + env.As(t, env.Users.Contributor). + With(Context{Repo: env.Users.Limited.Repo}). + PostToRepoPage("/action/reject_transfer"). + ExpectStatus(http.StatusSeeOther) + }) + }) +} + +func TestGitQuotaEnforcement(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + // Lets create a little helper that runs a task for three of our repos: the + // user's repo, the limited org repo, and the unlimited org's. + // + // We expect the last one to always work, and the expected status of the + // other two is decided by the caller. + runTestForAllRepos := func(t *testing.T, task func(t *testing.T, repo *repo_model.Repository) error, expectSuccess bool) { + t.Helper() + + err := task(t, env.Users.Limited.Repo) + if expectSuccess { + require.NoError(t, err) + } else { + require.Error(t, err) + } + + err = task(t, env.Orgs.Limited.Repo) + if expectSuccess { + require.NoError(t, err) + } else { + require.Error(t, err) + } + + err = task(t, env.Orgs.Unlimited.Repo) + require.NoError(t, err) + } + + // Run tests with quotas disabled + runTestForAllReposWithQuotaDisabled := func(t *testing.T, task func(t *testing.T, repo *repo_model.Repository) error) { + t.Helper() + + t.Run("with quota disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, false)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + runTestForAllRepos(t, task, true) + }) + } + + t.Run("push branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Pushing a new branch is denied if the user is over quota. + runTestForAllRepos(t, func(t *testing.T, repo *repo_model.Repository) error { + return env.As(t, env.Users.Limited). + With(Context{Repo: repo}). + LocalClone(u). + Push("HEAD:new-branch") + }, false) + + // Pushing a new branch is always allowed if quota is disabled + runTestForAllReposWithQuotaDisabled(t, func(t *testing.T, repo *repo_model.Repository) error { + return env.As(t, env.Users.Limited). + With(Context{Repo: repo}). + LocalClone(u). + Push("HEAD:new-branch-wo-quota") + }) + }) + + t.Run("push tag", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Pushing a tag is denied if the user is over quota. + runTestForAllRepos(t, func(t *testing.T, repo *repo_model.Repository) error { + return env.As(t, env.Users.Limited). + With(Context{Repo: repo}). + LocalClone(u). + Tag("new-tag"). + Push("new-tag") + }, false) + + // ...but succeeds if the quota feature is disabled + runTestForAllReposWithQuotaDisabled(t, func(t *testing.T, repo *repo_model.Repository) error { + return env.As(t, env.Users.Limited). + With(Context{Repo: repo}). + LocalClone(u). + Tag("new-tag-wo-quota"). + Push("new-tag-wo-quota") + }) + }) + + t.Run("Agit PR", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Opening an Agit PR is *always* accepted. At least for now. + runTestForAllRepos(t, func(t *testing.T, repo *repo_model.Repository) error { + return env.As(t, env.Users.Limited). + With(Context{Repo: repo}). + LocalClone(u). + Push("HEAD:refs/for/main/agit-pr-branch") + }, true) + }) + + t.Run("delete branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Deleting a branch is respected, and allowed. + err := env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + err := ctx. + LocalClone(u). + Push("HEAD:branch-to-delete") + require.NoError(ctx.t, err) + }). + Push(":branch-to-delete") + require.NoError(t, err) + }) + + t.Run("delete tag", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Deleting a tag is always allowed. + err := env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + err := ctx. + LocalClone(u). + Tag("tag-to-delete"). + Push("tag-to-delete") + require.NoError(ctx.t, err) + }). + Push(":tag-to-delete") + require.NoError(t, err) + }) + + t.Run("mixed push", func(t *testing.T) { + t.Run("all deletes", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Pushing multiple deletes is allowed. + err := env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + err := ctx. + LocalClone(u). + Tag("mixed-push-tag"). + Push("mixed-push-tag", "HEAD:mixed-push-branch") + require.NoError(ctx.t, err) + }). + Push(":mixed-push-tag", ":mixed-push-branch") + require.NoError(t, err) + }) + + t.Run("new & delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Pushing a mix of deletions & a new branch is rejected together. + err := env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + err := ctx. + LocalClone(u). + Tag("mixed-push-tag"). + Push("mixed-push-tag", "HEAD:mixed-push-branch") + require.NoError(ctx.t, err) + }). + Push(":mixed-push-tag", ":mixed-push-branch", "HEAD:mixed-push-branch-new") + require.Error(t, err) + + // ...unless quota is disabled + t.Run("with quota disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, false)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + err := env.As(t, env.Users.Limited). + WithoutQuota(func(ctx *quotaWebEnvAsContext) { + err := ctx. + LocalClone(u). + Tag("mixed-push-tag-2"). + Push("mixed-push-tag-2", "HEAD:mixed-push-branch-2") + require.NoError(ctx.t, err) + }). + Push(":mixed-push-tag-2", ":mixed-push-branch-2", "HEAD:mixed-push-branch-new-2") + require.NoError(t, err) + }) + }) + }) + }) +} + +func TestQuotaConfigDefault(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + env := createQuotaWebEnv(t) + defer env.Cleanup() + + t.Run("with config-based default", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Quota.Default.Total, 0)() + + env.As(t, env.Users.Ungrouped). + With(Context{ + Payload: &Payload{ + "uid": env.Users.Ungrouped.ID().AsString(), + "repo_name": "quota-config-default", + }, + }). + PostToPage("/repo/create"). + ExpectStatus(http.StatusRequestEntityTooLarge) + }) + + t.Run("without config-based default", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + env.As(t, env.Users.Ungrouped). + With(Context{ + Payload: &Payload{ + "uid": env.Users.Ungrouped.ID().AsString(), + "repo_name": "quota-config-default", + }, + }). + PostToPage("/repo/create"). + ExpectStatus(http.StatusSeeOther) + }) + }) +} + +/********************** + * Here be dragons! * + * * + * . * + * .> )\;`a__ * + * ( _ _)/ /-." ~~ * + * `( )_ )/ * + * <_ <_ sb/dwb * + **********************/ + +type quotaWebEnv struct { + Users quotaWebEnvUsers + Orgs quotaWebEnvOrgs + + cleaners []func() +} + +type quotaWebEnvUsers struct { + Limited quotaWebEnvUser + Contributor quotaWebEnvUser + Ungrouped quotaWebEnvUser +} + +type quotaWebEnvOrgs struct { + Limited quotaWebEnvOrg + Unlimited quotaWebEnvOrg +} + +type quotaWebEnvOrg struct { + Org *org_model.Organization + + Repo *repo_model.Repository + + QuotaGroup *quota_model.Group + QuotaRule *quota_model.Rule +} + +type quotaWebEnvUser struct { + User *user_model.User + Session *TestSession + Repo *repo_model.Repository + + QuotaGroup *quota_model.Group + QuotaRule *quota_model.Rule +} + +type Payload map[string]string + +type quotaWebEnvAsContext struct { + t *testing.T + + Doer *quotaWebEnvUser + Repo *repo_model.Repository + + Payload Payload + + CSRFPath *string + + gitPath string + + request *RequestWrapper + response *httptest.ResponseRecorder +} + +type Context struct { + Repo *repo_model.Repository + Payload *Payload + CSRFPath *string +} + +func (ctx *quotaWebEnvAsContext) With(opts Context) *quotaWebEnvAsContext { + if opts.Repo != nil { + ctx.Repo = opts.Repo + } + if opts.Payload != nil { + for key, value := range *opts.Payload { + ctx.Payload[key] = value + } + } + if opts.CSRFPath != nil { + ctx.CSRFPath = opts.CSRFPath + } + return ctx +} + +func (ctx *quotaWebEnvAsContext) VisitPage(page string) *quotaWebEnvAsContext { + ctx.t.Helper() + + ctx.request = NewRequest(ctx.t, "GET", page) + + return ctx +} + +func (ctx *quotaWebEnvAsContext) VisitRepoPage(page string) *quotaWebEnvAsContext { + ctx.t.Helper() + + return ctx.VisitPage(ctx.Repo.Link() + page) +} + +func (ctx *quotaWebEnvAsContext) ExpectStatus(status int) *quotaWebEnvAsContext { + ctx.t.Helper() + + ctx.response = ctx.Doer.Session.MakeRequest(ctx.t, ctx.request, status) + + return ctx +} + +func (ctx *quotaWebEnvAsContext) ExpectFlashMessage(value string) { + ctx.t.Helper() + + htmlDoc := NewHTMLParser(ctx.t, ctx.response.Body) + flashMessage := strings.TrimSpace(htmlDoc.Find(`.flash-message`).Text()) + + assert.EqualValues(ctx.t, value, flashMessage) +} + +func (ctx *quotaWebEnvAsContext) ExpectFlashMessageContains(parts ...string) { + ctx.t.Helper() + + htmlDoc := NewHTMLParser(ctx.t, ctx.response.Body) + flashMessage := strings.TrimSpace(htmlDoc.Find(`.flash-message`).Text()) + + for _, part := range parts { + assert.Contains(ctx.t, flashMessage, part) + } +} + +func (ctx *quotaWebEnvAsContext) ExpectFlashCookieContains(parts ...string) { + ctx.t.Helper() + + flashCookie := ctx.Doer.Session.GetCookie(forgejo_context.CookieNameFlash) + assert.NotNil(ctx.t, flashCookie) + + // Need to decode the cookie twice + flashValue, err := url.QueryUnescape(flashCookie.Value) + require.NoError(ctx.t, err) + flashValue, err = url.QueryUnescape(flashValue) + require.NoError(ctx.t, err) + + for _, part := range parts { + assert.Contains(ctx.t, flashValue, part) + } +} + +func (ctx *quotaWebEnvAsContext) ForkRepoInto(org quotaWebEnvOrg) { + ctx.t.Helper() + + ctx. + With(Context{Payload: &Payload{ + "uid": org.ID().AsString(), + "repo_name": ctx.Repo.Name + "-fork", + }}). + PostToRepoPage("/fork"). + ExpectStatus(http.StatusSeeOther) +} + +func (ctx *quotaWebEnvAsContext) CreatePullFrom(org quotaWebEnvOrg) { + ctx.t.Helper() + + url := fmt.Sprintf("/compare/main...%s:main", org.Org.Name) + ctx. + With(Context{Payload: &Payload{ + "title": "PR test", + }}). + PostToRepoPage(url). + ExpectStatus(http.StatusOK) +} + +func (ctx *quotaWebEnvAsContext) PostToPage(page string) *quotaWebEnvAsContext { + ctx.t.Helper() + + payload := ctx.Payload + csrfPath := page + if ctx.CSRFPath != nil { + csrfPath = *ctx.CSRFPath + } + + payload["_csrf"] = GetCSRF(ctx.t, ctx.Doer.Session, csrfPath) + + ctx.request = NewRequestWithValues(ctx.t, "POST", page, payload) + + return ctx +} + +func (ctx *quotaWebEnvAsContext) PostToRepoPage(page string) *quotaWebEnvAsContext { + ctx.t.Helper() + + csrfPath := ctx.Repo.Link() + return ctx.With(Context{CSRFPath: &csrfPath}).PostToPage(ctx.Repo.Link() + page) +} + +func (ctx *quotaWebEnvAsContext) CreateAttachment(filename, attachmentType string) *quotaWebEnvAsContext { + ctx.t.Helper() + + body := &bytes.Buffer{} + image := generateImg() + + // Setup multi-part + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", filename) + require.NoError(ctx.t, err) + _, err = io.Copy(part, &image) + require.NoError(ctx.t, err) + err = writer.Close() + require.NoError(ctx.t, err) + + csrf := GetCSRF(ctx.t, ctx.Doer.Session, ctx.Repo.Link()) + + ctx.request = NewRequestWithBody(ctx.t, "POST", fmt.Sprintf("%s/%s/attachments", ctx.Repo.Link(), attachmentType), body) + ctx.request.Header.Add("X-Csrf-Token", csrf) + ctx.request.Header.Add("Content-Type", writer.FormDataContentType()) + + return ctx +} + +func (ctx *quotaWebEnvAsContext) CreateIssueAttachment(filename string) *quotaWebEnvAsContext { + ctx.t.Helper() + + return ctx.CreateAttachment(filename, "issues") +} + +func (ctx *quotaWebEnvAsContext) CreateReleaseAttachment(filename string) *quotaWebEnvAsContext { + ctx.t.Helper() + + return ctx.CreateAttachment(filename, "releases") +} + +func (ctx *quotaWebEnvAsContext) WithoutQuota(task func(ctx *quotaWebEnvAsContext)) *quotaWebEnvAsContext { + ctx.t.Helper() + + defer ctx.Doer.SetQuota(-1)() + task(ctx) + + return ctx +} + +func (ctx *quotaWebEnvAsContext) CreateMirror() *repo_model.Repository { + ctx.t.Helper() + + doer := ctx.Doer.User + + repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, doer, doer, repo_service.CreateRepoOptions{ + Name: "test-mirror", + IsMirror: true, + Status: repo_model.RepositoryBeingMigrated, + }) + require.NoError(ctx.t, err) + + return repo +} + +func (ctx *quotaWebEnvAsContext) LocalClone(u *url.URL) *quotaWebEnvAsContext { + ctx.t.Helper() + + gitPath := ctx.t.TempDir() + + doGitInitTestRepository(gitPath, git.Sha1ObjectFormat)(ctx.t) + + oldPath := u.Path + oldUser := u.User + defer func() { + u.Path = oldPath + u.User = oldUser + }() + u.Path = ctx.Repo.FullName() + ".git" + u.User = url.UserPassword(ctx.Doer.User.LowerName, userPassword) + + doGitAddRemote(gitPath, "origin", u)(ctx.t) + + ctx.gitPath = gitPath + + return ctx +} + +func (ctx *quotaWebEnvAsContext) Push(params ...string) error { + ctx.t.Helper() + + gitRepo, err := git.OpenRepository(git.DefaultContext, ctx.gitPath) + require.NoError(ctx.t, err) + defer gitRepo.Close() + + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin"). + AddArguments(git.ToTrustedCmdArgs(params)...). + RunStdString(&git.RunOpts{Dir: ctx.gitPath}) + + return err +} + +func (ctx *quotaWebEnvAsContext) Tag(tagName string) *quotaWebEnvAsContext { + ctx.t.Helper() + + gitRepo, err := git.OpenRepository(git.DefaultContext, ctx.gitPath) + require.NoError(ctx.t, err) + defer gitRepo.Close() + + _, _, err = git.NewCommand(git.DefaultContext, "tag"). + AddArguments(git.ToTrustedCmdArgs([]string{tagName})...). + RunStdString(&git.RunOpts{Dir: ctx.gitPath}) + require.NoError(ctx.t, err) + + return ctx +} + +func (user *quotaWebEnvUser) SetQuota(limit int64) func() { + previousLimit := user.QuotaRule.Limit + + user.QuotaRule.Limit = limit + user.QuotaRule.Edit(db.DefaultContext, &limit, nil) + + return func() { + user.QuotaRule.Limit = previousLimit + user.QuotaRule.Edit(db.DefaultContext, &previousLimit, nil) + } +} + +func (user *quotaWebEnvUser) ID() convertAs { + return convertAs{ + asString: fmt.Sprintf("%d", user.User.ID), + } +} + +func (org *quotaWebEnvOrg) ID() convertAs { + return convertAs{ + asString: fmt.Sprintf("%d", org.Org.ID), + } +} + +type convertAs struct { + asString string +} + +func (cas convertAs) AsString() string { + return cas.asString +} + +func (env *quotaWebEnv) Cleanup() { + for i := len(env.cleaners) - 1; i >= 0; i-- { + env.cleaners[i]() + } +} + +func (env *quotaWebEnv) As(t *testing.T, user quotaWebEnvUser) *quotaWebEnvAsContext { + t.Helper() + + ctx := quotaWebEnvAsContext{ + t: t, + Doer: &user, + Repo: user.Repo, + + Payload: Payload{}, + } + return &ctx +} + +func (env *quotaWebEnv) RunVisitAndPostToRepoPageTests(t *testing.T, page string, payload *Payload, successStatus int) { + t.Helper() + + // Visiting the user's repo page fails due to being over quota. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Users.Limited.Repo}). + VisitRepoPage(page). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Posting as the limited user, to the limited repo, fails due to being over + // quota. + csrfPath := env.Users.Limited.Repo.Link() + env.As(t, env.Users.Limited). + With(Context{ + Payload: payload, + CSRFPath: &csrfPath, + Repo: env.Users.Limited.Repo, + }). + PostToRepoPage(page). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Visiting the limited org's repo page fails due to being over quota. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Limited.Repo}). + VisitRepoPage(page). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Posting as the limited user, to a limited org's repo, fails for the same + // reason. + csrfPath = env.Orgs.Limited.Repo.Link() + env.As(t, env.Users.Limited). + With(Context{ + Payload: payload, + CSRFPath: &csrfPath, + Repo: env.Orgs.Limited.Repo, + }). + PostToRepoPage(page). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Visiting the repo page for the unlimited org succeeds. + env.As(t, env.Users.Limited). + With(Context{Repo: env.Orgs.Unlimited.Repo}). + VisitRepoPage(page). + ExpectStatus(http.StatusOK) + + // Posting as the limited user, to an unlimited org's repo, succeeds. + csrfPath = env.Orgs.Unlimited.Repo.Link() + env.As(t, env.Users.Limited). + With(Context{ + Payload: payload, + CSRFPath: &csrfPath, + Repo: env.Orgs.Unlimited.Repo, + }). + PostToRepoPage(page). + ExpectStatus(successStatus) +} + +func (env *quotaWebEnv) RunVisitAndPostToPageTests(t *testing.T, page string, payload *Payload, successStatus int) { + t.Helper() + + // Visiting the page is always fine. + env.As(t, env.Users.Limited). + VisitPage(page). + ExpectStatus(http.StatusOK) + + // Posting as the Limited user fails, because it is over quota. + env.As(t, env.Users.Limited). + With(Context{Payload: payload}). + With(Context{ + Payload: &Payload{ + "uid": env.Users.Limited.ID().AsString(), + }, + }). + PostToPage(page). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Posting to a limited org also fails, for the same reason. + env.As(t, env.Users.Limited). + With(Context{Payload: payload}). + With(Context{ + Payload: &Payload{ + "uid": env.Orgs.Limited.ID().AsString(), + }, + }). + PostToPage(page). + ExpectStatus(http.StatusRequestEntityTooLarge) + + // Posting to an unlimited repo works, however. + env.As(t, env.Users.Limited). + With(Context{Payload: payload}). + With(Context{ + Payload: &Payload{ + "uid": env.Orgs.Unlimited.ID().AsString(), + }, + }). + PostToPage(page). + ExpectStatus(successStatus) +} + +func createQuotaWebEnv(t *testing.T) *quotaWebEnv { + t.Helper() + + // *** helpers *** + + makeUngroupedUser := func(t *testing.T) quotaWebEnvUser { + t.Helper() + + user := quotaWebEnvUser{} + + // Create the user + userName := gouuid.NewString() + apiCreateUser(t, userName) + user.User = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: userName}) + user.Session = loginUser(t, userName) + + // Create a repository for the user + repo, _, _ := tests.CreateDeclarativeRepoWithOptions(t, user.User, tests.DeclarativeRepoOptions{}) + user.Repo = repo + + return user + } + + // Create a user, its quota group & rule + makeUser := func(t *testing.T, limit int64) quotaWebEnvUser { + t.Helper() + + user := makeUngroupedUser(t) + userName := user.User.Name + + // Create a quota group for them + group, err := quota_model.CreateGroup(db.DefaultContext, userName) + require.NoError(t, err) + user.QuotaGroup = group + + // Create a rule + rule, err := quota_model.CreateRule(db.DefaultContext, userName, limit, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}) + require.NoError(t, err) + user.QuotaRule = rule + + // Add the rule to the group + err = group.AddRuleByName(db.DefaultContext, rule.Name) + require.NoError(t, err) + + // Add the user to the group + err = group.AddUserByID(db.DefaultContext, user.User.ID) + require.NoError(t, err) + + return user + } + + // Create a user, its quota group & rule + makeOrg := func(t *testing.T, owner *user_model.User, limit int64) quotaWebEnvOrg { + t.Helper() + + org := quotaWebEnvOrg{} + + // Create the org + userName := gouuid.NewString() + org.Org = &org_model.Organization{ + Name: userName, + } + err := org_model.CreateOrganization(db.DefaultContext, org.Org, owner) + require.NoError(t, err) + + // Create a repository for the org + orgUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: org.Org.ID}) + repo, _, _ := tests.CreateDeclarativeRepoWithOptions(t, orgUser, tests.DeclarativeRepoOptions{}) + org.Repo = repo + + // Create a quota group for them + group, err := quota_model.CreateGroup(db.DefaultContext, userName) + require.NoError(t, err) + org.QuotaGroup = group + + // Create a rule + rule, err := quota_model.CreateRule(db.DefaultContext, userName, limit, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}) + require.NoError(t, err) + org.QuotaRule = rule + + // Add the rule to the group + err = group.AddRuleByName(db.DefaultContext, rule.Name) + require.NoError(t, err) + + // Add the org to the group + err = group.AddUserByID(db.DefaultContext, org.Org.ID) + require.NoError(t, err) + + return org + } + + env := quotaWebEnv{} + env.cleaners = []func(){ + test.MockVariableValue(&setting.Quota.Enabled, true), + test.MockVariableValue(&testWebRoutes, routers.NormalRoutes()), + } + + // Create the limited user and the various orgs, and a contributor who's not + // in any of the orgs. + env.Users.Limited = makeUser(t, int64(0)) + env.Users.Contributor = makeUser(t, int64(0)) + env.Orgs.Limited = makeOrg(t, env.Users.Limited.User, int64(0)) + env.Orgs.Unlimited = makeOrg(t, env.Users.Limited.User, int64(-1)) + + env.Users.Ungrouped = makeUngroupedUser(t) + + return &env +} |