mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-27 12:53:57 +01:00
3b70949651
This adds a new configuration setting: `[quota.default].TOTAL`, which will be used if no groups are configured for a particular user. The new option makes it possible to entirely skip configuring quotas via the API if all that one wants is a total size. Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
1147 lines
32 KiB
Go
1147 lines
32 KiB
Go
// 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
|
|
}
|