summaryrefslogtreecommitdiffstats
path: root/tests/integration/quota_use_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration/quota_use_test.go')
-rw-r--r--tests/integration/quota_use_test.go1147
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
+}