gitforge/tests/integration/quota_use_test.go
Mathieu Fenniak de5f38c4ea feat: enable auth to raw resources, release downloads, & attachments via authorized integrations (#12776)
A handful of routes, described in this PR as "mixed routes", are currently accessible by both web-based sessions and authenticated API users.  The goal of this PR is to allow access to these routes for Authorized Integrations as well, bringing them to full API compatibility (to my knowledge) with other authentication methods.  These routes are impacted:
- `/{username}/{repo}/raw/*`
- `/{username}/{repo}/archive/*`
- `/{username}/{repo}/releases/download/{vTag}/{fileName}`
- `/{username}/{repo}/attachments/{uuid}`
- `/attachments/{uuid}`

The major work in this PR was to refactoring the existing authentication methods so that "path based matching" that they were currently doing was no longer required, as I didn't want to introduce that into Authorized Integrations.  All the path based matching is removed in this PR, and authentication methods are enabled entirely by the middleware applied to their endpoints.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12776
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2026-05-29 02:11:43 +02:00

1223 lines
33 KiB
Go

// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"bytes"
"fmt"
"io"
"maps"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"forgejo.org/models/db"
git_model "forgejo.org/models/git"
org_model "forgejo.org/models/organization"
quota_model "forgejo.org/models/quota"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/lfs"
"forgejo.org/modules/setting"
api "forgejo.org/modules/structs"
"forgejo.org/modules/test"
"forgejo.org/routers"
app_context "forgejo.org/services/context"
repo_service "forgejo.org/services/repository"
"forgejo.org/tests"
"forgejo.org/tests/forgery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWebQuotaEnforcementRepoMigrate(t *testing.T) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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) {
onApplicationRun(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 TestQuotaGitEnforcement(t *testing.T) {
onApplicationRun(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 TestQuotaGitLfsEnforcement(t *testing.T) {
defer test.MockVariableValue(&setting.LFS.StartServer, true)()
onApplicationRun(t, func(t *testing.T, u *url.URL) {
env := createQuotaWebEnv(t)
defer env.Cleanup()
t.Run("UploadHandler", func(t *testing.T) {
// Uploading to our repo => 413
env.As(t, env.Users.Limited).
With(Context{Repo: env.Users.Limited.Repo}).
AuthBasic().
PushLFSObject().
ExpectStatus(http.StatusRequestEntityTooLarge)
// Uploading to the limited org repo => 413
env.As(t, env.Users.Limited).
With(Context{Repo: env.Orgs.Limited.Repo}).
AuthBasic().
PushLFSObject().
ExpectStatus(http.StatusRequestEntityTooLarge)
// Uploading to the unlimited org repo => 200
env.As(t, env.Users.Limited).
With(Context{Repo: env.Orgs.Unlimited.Repo}).
AuthBasic().
PushLFSObject().
ExpectStatus(http.StatusOK)
})
t.Run("BatchHandler", func(t *testing.T) {
// Uploading to our repo => 413
env.As(t, env.Users.Limited).
With(Context{Repo: env.Users.Limited.Repo}).
AuthBasic().
BatchPushLFSObject().
ExpectStatus(http.StatusRequestEntityTooLarge)
// Uploading to the limited org repo => 413
env.As(t, env.Users.Limited).
With(Context{Repo: env.Orgs.Limited.Repo}).
AuthBasic().
BatchPushLFSObject().
ExpectStatus(http.StatusRequestEntityTooLarge)
// Uploading to the unlimited org repo => 200
env.As(t, env.Users.Limited).
With(Context{Repo: env.Orgs.Unlimited.Repo}).
AuthBasic().
BatchPushLFSObject().
ExpectStatus(http.StatusOK)
})
})
}
func TestQuotaConfigDefault(t *testing.T) {
onApplicationRun(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
gitPath string
request *RequestWrapper
response *httptest.ResponseRecorder
authBasic bool
}
type Context struct {
Repo *repo_model.Repository
Payload *Payload
}
func (ctx *quotaWebEnvAsContext) With(opts Context) *quotaWebEnvAsContext {
if opts.Repo != nil {
ctx.Repo = opts.Repo
}
if opts.Payload != nil {
maps.Copy(ctx.Payload, *opts.Payload)
}
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) AuthBasic() *quotaWebEnvAsContext {
ctx.t.Helper()
ctx.authBasic = true
return ctx
}
func (ctx *quotaWebEnvAsContext) ExpectStatus(status int) *quotaWebEnvAsContext {
ctx.t.Helper()
if ctx.authBasic {
ctx.response = MakeRequest(ctx.t, ctx.request.AddBasicAuth(ctx.Doer.User.Name), status)
} else {
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.Equal(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(app_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
ctx.request = NewRequestWithValues(ctx.t, "POST", page, payload)
return ctx
}
func (ctx *quotaWebEnvAsContext) PostToRepoPage(page string) *quotaWebEnvAsContext {
ctx.t.Helper()
return ctx.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)
ctx.request = NewRequestWithBody(ctx.t, "POST", fmt.Sprintf("%s/%s/attachments", ctx.Repo.Link(), attachmentType), body)
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) PushLFSObject() *quotaWebEnvAsContext {
ctx.t.Helper()
p := lfs.Pointer{Oid: "6ccce4863b70f258d691f59609d31b4502e1ba5199942d3bc5d35d17a4ce771d", Size: 5}
ctx.request = NewRequestWithBody(ctx.t, "PUT",
fmt.Sprintf("%s.git/info/lfs/objects/%s/%d",
ctx.Repo.Link(), p.Oid, p.Size), strings.NewReader("gitea"))
ctx.t.Cleanup(func() {
git_model.RemoveLFSMetaObjectByOid(db.DefaultContext, ctx.Repo.ID, p.Oid)
})
return ctx
}
func (ctx *quotaWebEnvAsContext) BatchPushLFSObject() *quotaWebEnvAsContext {
ctx.t.Helper()
batch := &lfs.BatchRequest{
Operation: "upload",
Objects: []lfs.Pointer{
{Oid: "d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0153", Size: 1},
},
}
ctx.request = NewRequestWithJSON(ctx.t, "POST",
fmt.Sprintf("%s.git/info/lfs/objects/batch", ctx.Repo.Link()), batch).
SetHeader("Accept", lfs.AcceptHeader).
SetHeader("Content-Type", lfs.MediaType)
ctx.t.Cleanup(func() {
git_model.RemoveLFSMetaObjectByOid(db.DefaultContext, ctx.Repo.ID, batch.Objects[0].Oid)
})
return ctx
}
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.
env.As(t, env.Users.Limited).
With(Context{
Payload: payload,
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.
env.As(t, env.Users.Limited).
With(Context{
Payload: payload,
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.
env.As(t, env.Users.Limited).
With(Context{
Payload: payload,
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
user.User = forgery.CreateUser(t, nil)
user.Session = loginUser(t, user.User.Name)
// Create a repository for the user
repo := forgery.CreateRepository(t, user.User, &forgery.CreateRepositoryOptions{
Files: forgery.FilesInit{},
})
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
org.Org = forgery.CreateOrganisation(t, owner)
// Create a repository for the org
repo := forgery.CreateRepository(t, org.Org.AsUser(), &forgery.CreateRepositoryOptions{
Files: forgery.FilesInit{},
})
org.Repo = repo
// Create a quota group for them
group, err := quota_model.CreateGroup(db.DefaultContext, org.Org.Name)
require.NoError(t, err)
org.QuotaGroup = group
// Create a rule
rule, err := quota_model.CreateRule(db.DefaultContext, org.Org.Name, 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
}