chore: tests for REST API permission check

This commit is contained in:
limiting-factor 2026-06-14 12:41:43 +02:00
commit 97595369c7
No known key found for this signature in database
GPG key ID: FBFC3FECD17D904F
40 changed files with 3129 additions and 0 deletions

View file

@ -0,0 +1,106 @@
Tests for `routers/api/v1/permissions`
Each permission function implemented in the `routers/api/v1/permissions` has a matching test in this package. For instance:
- the `ReqGitHook` function in `routers/api/v1/permissions/req_git_hook.go`
- is tested in `routers/api/v1/permissions/tests/req_git_hook_test.go`
To keep the tests maintainable despite the large number of fixtures and permission sequences, the tests for a function are described in a structure instead of being implemented by a `Test...` function.
```go
type functionTest struct {
fixtures []*fixtureType
fulfillNeeds func(t *testing.T, data *fixtureData)
interpret func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData)
protect func() func()
call func(t *testing.T, ctx apiv1_permissions.Context, permissions *apiv1_permissions.Permissions, data *fixtureData, signature []any)
sequenceFilter []string
}
```
- The test registers the struct and it will be used when the `TestAPIv1Permissions` test runs
- The `fixtures` are described using a `map[string]string` using conventions that are to be interpreted by the fixture helpers. For instance `"doer": "regularuser"` will be interpreted by the `fixtureSetDoer` helper and create the user `regularuser`.
- The `fulfillNeeds` function will be called for each function that comes earlier in the sequence, to ensure it gets sensible defaults allowing it to run successfully. For instance if the sequence is `APIAuthorization,TokenRequiresScopes,ReqOrgOwnership`, the `fulfillNeeds` function of `TokenRequiresScopes` is expected to set a sensible default for the scope, such as `"scope": "read:repository"`
- After `fulfillNeeds` is called and the fixture data is assumed to have all the necessary defaults, it will be acted upon by each `interpret` function in the sequence. For instance, `APIAuthorization` will interpret the `"doer": "regularuser"` data by calling `fixtureSetDoer` to ensure it is created.
- If global variables need protection (for instance when changing a setting), they are to be protected by the `protect` function
- Once the fixture has been interpreted, each permission function in the sequence is called in order. They are all expected to complete successfully. Except for the last one, which is the function under test, that may error out if the fixture is designed for that purpose.
- The `call` function, if it exists, is expected to call the function for which the test is designed. Fos instance the `call` for `ReqValidCommentID` runs `apiv1_permissions.ReqValidCommentID(ctx, comment)`. The function must not have any side effect. Instead it must use whatever data has been created by the fixture (using the `interpret` function).
- The `sequenceFilter` only keeps some permissions function in the sequence leading to the function under test. For instance when testing `ReqOrgOwnership` the sequence `APIAuthorization,TokenRequiresScopes,ReqOrgOwnership` will be used. In some cases it is useful to simplify the tests in case the shortest sequence leading to a function contains functions that will interfere if a particular fixture is set.
## Permission function signatures
The signature of every permission function has at least one argument which is a `routers/api/v1/permissions.Context` interface. It may also have additional arguments provided when building the routes. For instance `TokenRequiresScopes` may be given a list of scope categories. Such arguments do not vary depending on the context because they are preset when the route is built. In addition the function may have arguments that are extracted from the environment. For instance `ReqValidCommentID` may be given the content of the `id` field from the body of a JSON payload.
The string representation of the signature is:
- The function name if there are no arguments provided when building the routes. For instance `APIAuthorization`
- The function name followed by a whitespace list of arguments provided when building the routes. For instance `TokenRequiresScopes Repository User`
## Fixtures helpers
All fixtures are dynamically created (they are not using the global fixtures found in `models/fixtures`). The `fixtures_test.go` file contains all the helpers to create those fixtures.
## Debugging
- Running the tests in verbose mode `make GOTESTFLAGS=-v GO_TEST_PACKAGES=forgejo.org/routers/api/v1/permissions/tests/... 'test#Test' `
- Browsing the tests such as
```
...
=== RUN TestAPIv1Permissions/APIAuthorization,TokenRequiresScopes_Admin_fixture_0
functions_test.go:95: creating fixture data from doer:doerregular,level:read,scope:read:admin
functions_test.go:98: created fixture data doer:doerregular,level:read,scope:read:admin
functions_test.go:105: *auth.AccessToken(ID=10 Token=e26bfc1190efcf8c36ef640659af33e87073032c)
functions_test.go:105: *user.User(Name=doerregular)
functions_test.go:105: isSigned(true)
functions_test.go:105: *tests_test.accessTokenAuthenticationResult(*user.User(Name=doerregular) auth.AccessTokenScope(read:admin) *authz.AllAccessAuthorizationReducer)
fixture_test.go:637: calling permissions.APIAuthorization(ctx)
functions_test.go:131: + *authz.AllAccessAuthorizationReducer
token_requires_scopes_test.go:67: calling TokenRequiresScopes(ctx, [1], 1)
functions_test.go:131: + []auth.AccessTokenScopeCategory([1])
...
```
- The name of the test is the sequence of middleware under test (`APIAuthorization`, `TokenRequiresScopes`)
- It is followed by the index of the fixture being used for running the test, as found in the test file of the last function in the sequence (`TokenRequiresScopes` in the example)
- The `creating fixture` line shows all the data contained in that fixture
- The `created fixture` line shows the data added after calling the `fulfillNeeds` function for each function in the sequence
- The indented lines that follow shows the content of the `routers/api/v1/permissions.Permission` object before calling a permission function. To reduce the verbosity modifications are shown in a diff style fashion.
- The `calling` line shows the function and its arguments before it is called
- Running a single test (note the `/` is replaced with a `.` in the test name to comply with the Makefile rule) `make RACE_ENABLED=true GOTESTFLAGS=-v GO_TEST_PACKAGES=forgejo.org/routers/api/v1/permissions/tests/... 'test#TestAPIv1Permissions.APIAuthorization,TokenRequiresScopes_Repository,RepoAccess,CheckTokenPublicOnly,ReqToken,ReqRepoReader_TypeCode,CheckForkDestination'`
## The call function
`func(t *testing.T, ctx apiv1_permissions.Context, permissions *apiv1_permissions.Permissions, data *fixtureData, signature []any)`
It is responsible for:
- Calling `t.Logf` to display the call about to be made
- Calling the function using `ctx` as a first argument
The `permissions` and `data` arguments are provided, as computed by the `interpret` function.
The `signature[0]` is the function itself and could be called with `signature[0].Call`.
The `signature[1:]` list are the mandatory arguments to the function call.
## Test coverage
### `routers/api/v1/permissions`
- At the root of the source tree
- `COVERAGE_TEST_PACKAGES="forgejo.org/routers/api/v1/permissions forgejo.org/routers/api/v1/permissions/tests" make coverage-run coverage-show-percentage | grep v1/permissions | grep -v v1/permissions/permissions.go | grep -v v1/permissions/tests | sed -e 's/\t\t*/ /g' -e 's|forgejo.org/routers/||'`
- `uncover coverage/textfmt.out ReqOrgOwnership`
### Forgejo development branch
- Run https://codeberg.org/forgejo/forgejo/actions?workflow=coverage.yml
- Download the `coverage.zip` artifact
- Extract it in `/tmp/coverage/merged`
- At the root of the source tree checked out at the same SHA that coverage used
- Convert with `go tool covdata textfmt -i=/tmp/coverage/merged -o=/tmp/coverage/textfmt.out`
- Show percentages per function `go tool cover -func=/tmp/coverage/textfmt.out`
- Show line covered and missed for a function `uncover /tmp/coverage/textfmt.out repoAssignment`
## References
Design discussion https://codeberg.org/forgejo/design/issues/63

View file

@ -0,0 +1,41 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.APIAuthorization, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "doerregular")
if data.Get("doer") == user_model.ActionsUserName {
data.SetDefault("repository", "userowner/repositorypublic")
}
data.SetDefault("scope", "read:repository")
data.SetDefault("level", "read")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
if data.Has("repository") && data.Get("doer") == user_model.ActionsUserName {
fixtureSetRepository(t, permissions, data)
}
fixtureSetDoer(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
}),
},
},
})

View file

@ -0,0 +1,213 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"fmt"
"strings"
"testing"
auth_model "forgejo.org/models/auth"
org_model "forgejo.org/models/organization"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
const (
categoryActivityPub = "activitypub"
categoryAdmin = "admin"
categoryNotification = "notification"
categoryOrganization = "organization"
categoryPackage = "package"
categoryIssue = "issue"
categoryRepository = "repository"
categoryUser = "user"
)
var categoryStringToCategory = map[string]auth_model.AccessTokenScopeCategory{
categoryActivityPub: auth_model.AccessTokenScopeCategoryActivityPub,
categoryAdmin: auth_model.AccessTokenScopeCategoryAdmin,
categoryNotification: auth_model.AccessTokenScopeCategoryNotification,
categoryOrganization: auth_model.AccessTokenScopeCategoryOrganization,
categoryPackage: auth_model.AccessTokenScopeCategoryPackage,
categoryIssue: auth_model.AccessTokenScopeCategoryIssue,
categoryRepository: auth_model.AccessTokenScopeCategoryRepository,
categoryUser: auth_model.AccessTokenScopeCategoryUser,
}
var _ = registerFunctionTestWithCall(apiv1_permissions.CheckTokenPublicOnly, functionTest{
sequenceFilter: []string{
"APIAuthorization",
"CheckTokenPublicOnly",
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
data.SetDefault("doer", "regularuser")
data.SetDefault("repository", "regularuser/repositorypublic")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
if data.Has("user") {
fixtureCreateUser(t, &user_model.User{Name: data.Get("user")})
}
if data.Has("org") {
fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: data.Get("doer")})
}
if data.Has("packageOwner") {
fixtureCreateUser(t, &user_model.User{Name: data.Get("packageOwner")})
}
if data.Has("requiredScopeCategories") {
var categories []auth_model.AccessTokenScopeCategory
for categoryString := range strings.SplitSeq(data.Get("requiredScopeCategories"), ",") {
categories = append(categories, categoryStringToCategory[categoryString])
}
permissions.SetRequiredScopeCategories(categories)
}
fixtureSetRepository(t, permissions, data)
},
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
t.Helper()
var user *user_model.User
if data.Has("user") {
user = fixtureGetUser(t, data.Get("user"))
}
var org *org_model.Organization
if data.Has("org") {
if data.Has("orgAsUser") {
user = fixtureGetUser(t, data.Get("org"))
} else {
org = fixtureGetOrg(t, data.Get("org"))
}
}
var packageOwner *user_model.User
if data.Has("packageOwner") {
packageOwner = fixtureGetUser(t, data.Get("packageOwner"))
}
t.Logf("calling CheckTokenPublicOnly(ctx, %+v, %+v, %+v)", user, org, packageOwner)
apiv1_permissions.CheckTokenPublicOnly(ctx, user, org.AsUser(), packageOwner)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryRepository,
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositoryprivate",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryRepository,
}),
error: "token scope is limited to public repos",
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryIssue,
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositoryprivate",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryIssue,
}),
error: "token scope is limited to public issues",
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryNotification,
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositoryprivate",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryNotification,
}),
error: "token scope is limited to public notifications",
},
{
data: newFixtureData(map[string]string{
"user": "regularuser",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryUser,
}),
},
{
data: newFixtureData(map[string]string{
"user": "privateuser",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryUser,
}),
error: "token scope is limited to public users",
},
{
data: newFixtureData(map[string]string{
"user": "regularuser",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryActivityPub,
}),
},
{
data: newFixtureData(map[string]string{
"user": "privateuser",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryActivityPub,
}),
error: "token scope is limited to public activitypub",
},
{
data: newFixtureData(map[string]string{
"org": "regularorg",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryOrganization,
}),
},
{
data: newFixtureData(map[string]string{
"org": "privateorg",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryOrganization,
}),
error: "token scope is limited to public orgs",
},
{
data: newFixtureData(map[string]string{
"org": "privateorg",
"orgAsUser": "true",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryOrganization,
}),
error: "token scope is limited to public orgs",
},
{
data: newFixtureData(map[string]string{
"packageOwner": "regularuser",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryPackage,
}),
},
{
data: newFixtureData(map[string]string{
"packageOwner": "privateuser",
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
"requiredScopeCategories": categoryPackage,
}),
error: "token scope is limited to public packages",
},
},
})

View file

@ -0,0 +1,622 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
// See README.md for a documentation of the test logic
import (
"fmt"
"maps"
"slices"
"strings"
"testing"
actions_model "forgejo.org/models/actions"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
issues_model "forgejo.org/models/issues"
org_model "forgejo.org/models/organization"
"forgejo.org/models/perm"
access_model "forgejo.org/models/perm/access"
repo_model "forgejo.org/models/repo"
unit_model "forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/optional"
"forgejo.org/modules/structs"
"forgejo.org/modules/util"
"forgejo.org/modules/web/routing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
apiv1_permissions_tests "forgejo.org/routers/api/v1/permissions/tests"
"forgejo.org/services/auth"
"forgejo.org/services/authz"
issue_service "forgejo.org/services/issue"
packages_service "forgejo.org/services/packages"
pull_service "forgejo.org/services/pull"
"forgejo.org/tests/forgery"
"github.com/stretchr/testify/require"
)
func fixtureCreateToken(t *testing.T, user *user_model.User, scope auth_model.AccessTokenScope, repoIDs ...int64) (*auth_model.AccessToken, error) {
t.Helper()
scope, err := scope.Normalize()
require.NoError(t, err)
resourceAllRepos := len(repoIDs) == 0
accessToken := &auth_model.AccessToken{
UID: user.ID,
Name: util.CryptoRandomString(10),
Scope: scope,
ResourceAllRepos: resourceAllRepos,
}
require.NoError(t, auth_model.NewAccessToken(t.Context(), accessToken))
if len(repoIDs) > 0 {
var resourceRepos []*auth_model.AccessTokenResourceRepo
for _, repoID := range repoIDs {
resourceRepos = append(resourceRepos, &auth_model.AccessTokenResourceRepo{
TokenID: accessToken.ID,
RepoID: repoID,
})
}
require.NoError(t, auth_model.InsertAccessTokenResourceRepos(t.Context(), accessToken.ID, resourceRepos))
}
return accessToken, nil
}
func fixtureCreateIssue(t *testing.T, user *user_model.User, repo *repo_model.Repository, title, content string) *issues_model.Issue {
t.Helper()
issue := &issues_model.Issue{
RepoID: repo.ID,
Title: title,
Content: content,
PosterID: user.ID,
Poster: user,
}
err := issue_service.NewIssue(t.Context(), repo, issue, nil, nil, nil)
require.NoError(t, err)
return issue
}
func fixtureGetUser(t *testing.T, name string) *user_model.User {
t.Helper()
existingUser, err := user_model.GetUserByName(t.Context(), name)
if err == nil {
return existingUser
} else if !user_model.IsErrUserNotExist(err) {
require.NoError(t, err)
}
return nil
}
func fixtureGetOrg(t *testing.T, name string) *org_model.Organization {
t.Helper()
return (*org_model.Organization)(fixtureGetUser(t, name))
}
func fixtureCreateUser(t *testing.T, user *user_model.User) *user_model.User {
t.Helper()
if existingUser := fixtureGetUser(t, user.Name); existingUser != nil {
return existingUser
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{}
user.Email = user.Name + "@test.forgejo.org"
user.Passwd = "password"
if strings.Contains(user.Name, "private") {
visibility := structs.VisibleTypePrivate
overwriteDefault.Visibility = &visibility
} else if strings.Contains(user.Name, "limited") {
visibility := structs.VisibleTypeLimited
overwriteDefault.Visibility = &visibility
}
require.NoError(t, user_model.CreateUser(t.Context(), user, overwriteDefault))
return user
}
func fixtureCreateOrg(t *testing.T, org *org_model.Organization, owner *user_model.User) *org_model.Organization {
t.Helper()
if existing := fixtureGetOrg(t, org.Name); existing != nil {
return existing
}
owner = fixtureCreateUser(t, owner)
if strings.Contains(org.Name, "private") {
org.Visibility = structs.VisibleTypePrivate
}
require.NoError(t, org_model.CreateOrganization(t.Context(), org, owner))
return org
}
func fixtureCreateTeams(t *testing.T, org *org_model.Organization, teams string) {
t.Helper()
for team := range strings.SplitSeq(teams, ",") {
teamName, memberName, found := strings.Cut(team, ":")
require.True(t, found)
fixtureCreateTeam(t, org, memberName, &forgery.CreateTeamOptions{
Name: teamName,
Mode: perm.AccessModeWrite,
})
}
}
func fixtureCreateTeam(t *testing.T, org *org_model.Organization, memberName string, opts *forgery.CreateTeamOptions) *org_model.Team {
t.Helper()
member := fixtureCreateUser(t, &user_model.User{Name: memberName})
opts.Members = []*user_model.User{member}
team := forgery.CreateTeam(t, org, opts)
require.NotNil(t, team)
return team
}
func fixtureSetPackageOwner(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if !fixtureData.Has("packageOwner") {
return
}
owner := fixtureCreateUser(t, &user_model.User{Name: fixtureData.Get("packageOwner")})
permissions.SetPackageOwner(owner)
mode, err := packages_service.DeterminePackageAccessMode(permissions.GetContext(), permissions.GetPackageOwner(), permissions.GetDoer())
require.NoError(t, err)
permissions.SetPackageAccessMode(mode)
}
func fixtureSetDoer(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if !fixtureData.Has("doer") {
return
}
if doer := permissions.GetDoer(); doer != nil {
if doer.Name != fixtureData.Get("doer") {
panic(fmt.Sprintf("attempting to override already doer %s with %s", doer.Name, fixtureData.Get("doer")))
}
return
}
name := fixtureData.Get("doer")
if name == user_model.ActionsUserName {
fixtureSetDoerActionsUser(t, permissions, fixtureData)
} else {
fixtureSetDoerRegularUser(t, permissions, fixtureData)
}
}
var _ auth.AuthenticationResult = &actionsTaskTokenAuthenticationResult{}
type actionsTaskTokenAuthenticationResult struct {
*auth.BaseAuthenticationResult
user *user_model.User
taskID int64
}
func (r *actionsTaskTokenAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] {
return optional.None[auth_model.AccessTokenScope]()
}
func (r *actionsTaskTokenAuthenticationResult) User() *user_model.User {
return r.user
}
func (r *actionsTaskTokenAuthenticationResult) ActionsTaskID() optional.Option[int64] {
return optional.Some(r.taskID)
}
func fixtureSetDoerActionsUser(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
permissions.SetDoer(user_model.NewActionsUser())
repository := permissions.GetRepository()
require.NotNil(t, repository)
repositoryID := repository.ID
if fixtureData.Get("task.RepoID") == "unrelated" {
repositoryID = 13245
}
task := &actions_model.ActionTask{
RepoID: repositoryID,
}
if fixtureData.Get("task.IsForkPullRequest") == "true" {
task.IsForkPullRequest = true
}
task.GenerateToken()
{
_, err := db.GetEngine(t.Context()).Insert(task)
require.NoError(t, err)
require.NotZero(t, task.ID)
}
permissions.SetAuthentication(&actionsTaskTokenAuthenticationResult{user: permissions.GetDoer(), taskID: task.ID})
permissions.SetReducer(&authz.AllAccessAuthorizationReducer{})
permission, err := access_model.GetUserRepoPermissionWithReducer(permissions.GetContext(), permissions.GetRepository(), permissions.GetDoer(), permissions.GetReducer())
require.NoError(t, err)
permissions.SetPermission(&permission)
}
var _ auth.AuthenticationResult = &basicPasswordAuthenticationResult{}
type basicPasswordAuthenticationResult struct {
*auth.BaseAuthenticationResult
user *user_model.User
}
func (*basicPasswordAuthenticationResult) IsPasswordAuthentication() bool {
return true
}
func (r *basicPasswordAuthenticationResult) User() *user_model.User {
return r.user
}
var _ auth.AuthenticationResult = &accessTokenAuthenticationResult{}
type accessTokenAuthenticationResult struct {
*auth.BaseAuthenticationResult
user *user_model.User
scope auth_model.AccessTokenScope
reducer authz.AuthorizationReducer
}
func (r *accessTokenAuthenticationResult) User() *user_model.User {
return r.user
}
func (r *accessTokenAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] {
return optional.Some(r.scope)
}
func (r *accessTokenAuthenticationResult) Reducer() authz.AuthorizationReducer {
return r.reducer
}
var _ auth.AuthenticationResult = &reverseProxyAuthenticationResult{}
type reverseProxyAuthenticationResult struct {
*auth.BaseAuthenticationResult
user *user_model.User
}
func (r *reverseProxyAuthenticationResult) User() *user_model.User {
return r.user
}
func (*reverseProxyAuthenticationResult) IsReverseProxyAuthentication() bool {
return true
}
func fixtureSetDoerRegularUser(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
var scope auth_model.AccessTokenScope
if fixtureData.Has("scope") {
scope = auth_model.AccessTokenScope(fixtureData.Get("scope"))
} else {
scope = auth_model.AccessTokenScopeAll
}
if fixtureData.Has("doer") {
doer := fixtureData.Get("doer")
if doer != "anonymous" {
isAdmin := strings.Contains(doer, "admin")
user := &user_model.User{
Name: doer,
IsAdmin: isAdmin,
}
fixtureCreateUser(t, user)
permissions.SetDoer(user)
}
} else {
panic(fmt.Errorf("attempting to set doer with no name"))
}
if permissions.GetDoer() == nil {
permissions.SetAuthentication(&auth.UnauthenticatedResult{})
} else {
token, err := fixtureCreateToken(t, permissions.GetDoer(), scope)
require.NoError(t, err)
tokenReducer, err := authz.GetAuthorizationReducerForAccessToken(t.Context(), token)
require.NoError(t, err)
permissions.SetIsSigned(true)
switch fixtureData.Get("authentication") {
case "basic":
permissions.SetAuthentication(&basicPasswordAuthenticationResult{user: permissions.GetDoer()})
case "proxy":
permissions.SetAuthentication(&reverseProxyAuthenticationResult{user: permissions.GetDoer()})
default:
permissions.SetToken(token)
permissions.SetAuthentication(&accessTokenAuthenticationResult{user: permissions.GetDoer(), scope: token.Scope, reducer: tokenReducer})
}
}
}
func fixtureCreateBranch(t *testing.T, permissions *apiv1_permissions.Permissions, branch string) {
t.Helper()
repository := permissions.GetRepository()
require.NotNil(t, repository)
gitRepo, err := git.OpenRepository(t.Context(), repository.RepoPath())
require.NoError(t, err)
defaultBranch, err := git.GetDefaultBranch(t.Context(), repository.RepoPath())
require.NoError(t, err)
require.NoError(t, gitRepo.CreateBranch(branch, defaultBranch))
}
func fixtureCreatePullRequest(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if !fixtureData.Has("pullRequest") {
return
}
repository := permissions.GetRepository()
require.NotNil(t, repository)
poster := fixtureGetUser(t, fixtureData.Get("pullRequestAuthor"))
require.NotNil(t, poster)
ctx, committer, err := db.TxContext(t.Context())
require.NoError(t, err)
defer committer.Close()
idx, err := db.GetNextResourceIndex(ctx, "issue_index", repository.ID)
if err != nil {
panic(fmt.Errorf("generate issue index failed: %w", err))
}
issue := &issues_model.Issue{
Index: idx,
RepoID: repository.ID,
IsPull: true,
Title: fixtureData.Get("pullRequest"),
PosterID: poster.ID,
Poster: poster,
}
sess := db.GetEngine(ctx)
if _, err = sess.NoAutoTime().Insert(issue); err != nil {
panic(err)
}
issue.PullRequest = &issues_model.PullRequest{}
pr := issue.PullRequest
pr.Index = issue.Index
pr.IssueID = issue.ID
pr.HeadRepoID = repository.ID
pr.BaseRepoID = repository.ID
pr.HeadBranch = fixtureData.Get("pullRequestBranch")
_, err = sess.NoAutoTime().Insert(pr)
require.NoError(t, err)
require.NoError(t, committer.Commit())
require.NoError(t, pr.LoadBaseRepo(ctx))
require.NoError(t, pr.LoadHeadRepo(ctx))
require.NoError(t, pull_service.PushToBaseRepo(ctx, pr))
}
func fixtureSetRepository(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if !fixtureData.Has("repository") {
return
}
if repository := permissions.GetRepository(); repository != nil {
if repository.FullName() != fixtureData.Get("repository") {
panic(fmt.Sprintf("attempting to override already repository %s with %s", repository.FullName(), fixtureData.Get("repository")))
}
return
}
ownerName, repoName, found := strings.Cut(fixtureData.Get("repository"), "/")
require.True(t, found)
owner := fixtureCreateUser(t, &user_model.User{Name: ownerName})
opts := &forgery.CreateRepositoryOptions{
Name: repoName,
IsPrivate: strings.Contains(repoName, "private"),
}
if fixtureData.Get("repository-init") == "true" {
opts.Files = forgery.FilesInit{}
}
repository := forgery.CreateRepository(t, owner, opts)
// some of it is redundant with the config but that makes
// the tests immune to changes in the defaults
for _, unitType := range unit_model.DefaultRepoUnits {
forgery.EnableRepoUnit(t, repository, unitType, nil)
}
if strings.Contains(repoName, "archived") {
require.NoError(t, repo_model.SetArchiveRepoState(t.Context(), repository, true))
}
permissions.SetRepository(repository)
}
func dataToString(t *testing.T, fixtureData *fixtureData, key string) string {
t.Helper()
require.True(t, fixtureData.Has(key))
return fixtureData.Get(key)
}
func fixtureGetIssue(t *testing.T, fixtureData *fixtureData) *issues_model.Issue {
t.Helper()
var issue issues_model.Issue
found, err := db.GetEngine(t.Context()).Where("name = ?", dataToString(t, fixtureData, "issue")).Get(&issue)
require.NoError(t, err)
if !found {
return nil
}
return &issue
}
func fixtureSetIssue(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if fixtureGetIssue(t, fixtureData) == nil {
authorName := fixtureData.Get("issueAuthor")
author := fixtureCreateUser(t, &user_model.User{Name: authorName})
_ = fixtureCreateIssue(t, author, permissions.GetRepository(), dataToString(t, fixtureData, "issue"), "issue description")
}
}
func fixtureGetComment(t *testing.T, fixtureData *fixtureData) *issues_model.Comment {
var comment issues_model.Comment
found, err := db.GetEngine(t.Context()).Where("content = ?", dataToString(t, fixtureData, "comment")).Get(&comment)
require.NoError(t, err)
if !found {
return nil
}
comment.LoadIssue(t.Context())
return &comment
}
func fixtureCreateComment(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if fixtureGetComment(t, fixtureData) == nil {
authorName := fixtureData.Get("issueAuthor")
author := fixtureCreateUser(t, &user_model.User{Name: authorName})
_, err := issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeComment,
Doer: author,
Issue: fixtureGetIssue(t, fixtureData),
Repo: permissions.GetRepository(),
Content: dataToString(t, fixtureData, "comment"),
})
require.NoError(t, err)
}
}
func fixtureDisableRepoUnit(t *testing.T, permissions *apiv1_permissions.Permissions, unitType unit_model.Type) {
t.Helper()
repo := permissions.GetRepository()
require.NotNil(t, repo)
forgery.DisableRepoUnits(t, repo, unitType)
}
func fixtureDisableUnits(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
t.Helper()
if !fixtureData.Has("disable-units") {
return
}
for unit := range strings.SplitSeq(fixtureData.Get("disable-units"), ",") {
unitType := unit_model.TypeFromKey(unit)
if unitType == unit_model.TypeInvalid {
panic(fmt.Errorf("unable to find a unit matching '%s'", unit))
}
fixtureDisableRepoUnit(t, permissions, unitType)
}
}
type fixtureData struct {
entries map[string]string
}
func (o *fixtureData) Set(key, value string) {
o.entries[key] = value
}
func (o *fixtureData) SetDefault(key, value string) {
if !o.Has(key) {
o.Set(key, value)
}
}
func (o *fixtureData) Get(key string) string {
return o.entries[key]
}
func (o *fixtureData) Has(key string) bool {
_, has := o.entries[key]
return has
}
func (o *fixtureData) String() string {
var s []string
for k, e := range o.entries {
s = append(s, fmt.Sprintf("%s:%s", k, e))
}
slices.Sort(s)
return strings.Join(s, ",")
}
func newFixtureData(data map[string]string) *fixtureData {
fixtureData := &fixtureData{
entries: make(map[string]string, 10),
}
for key, value := range data {
fixtureData.Set(key, value)
}
return fixtureData
}
func (o *fixtureData) Clone() *fixtureData {
return &fixtureData{entries: maps.Clone(o.entries)}
}
type fixtureType struct {
data *fixtureData
error string
used bool
}
func (o *fixtureType) Clone() *fixtureType {
f := *o
f.data = o.data.Clone()
return &f
}
// See README.md for a documentation of the test logic that uses
// this test description.
type functionTest struct {
// The fixture will be constructed, when this function is the last
// one of the chain. It will go through the fulfillNeeds and
// interpret of the previous functions in the chain, as well as its
// own interpret.
fixtures []*fixtureType
// List the settings which might be updated while interpreting the fixtureData
// so that they are restored upon test completion.
protectSettingsBool []*bool
// number of static arguments to pass to call's last argument
staticArgs int
// call the middleware (set automatically by [registerFunctionTest])
call func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, staticArgs []any)
sequenceFilter []string
fulfillNeeds func(t *testing.T, data *fixtureData)
interpret func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData)
}
func buildSignatureStringToFunctionTest(t *testing.T) {
for signatureString, signature := range apiv1_permissions_tests.GetSignatureStringToSignature() {
for prefix, builder := range prefixToFunctionTestBuilder {
if strings.HasPrefix(signatureString, prefix) {
builder(t, signatureString, signature)
}
}
}
}
func registerFunctionTest(fun func(apiv1_permissions.Context), test functionTest) bool {
shortName := routing.GetFuncShortName(fun)
test.call = func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, _ []any) {
t.Logf("calling %s(ctx)", shortName)
fun(ctx)
}
return registerFunctionTestWithCall(fun, test)
}
func registerFunctionTestWithCall(fun any, test functionTest) bool {
signatureString := apiv1_permissions_tests.SignatureToString([]any{fun})
if _, has := signatureStringToFunctionTest[signatureString]; has {
panic(fmt.Errorf("attempt to register %s twice", signatureString))
}
if test.call == nil {
panic("'call' field is required")
}
signatureStringToFunctionTest[signatureString] = test
return true
}
var signatureStringToFunctionTest = map[string]functionTest{}
type functionTestBuilder func(t *testing.T, signatureString string, signature []any)
func registerFunctionTestBuilder(prefixes []string, builder functionTestBuilder) bool {
for _, prefix := range prefixes {
if _, has := prefixToFunctionTestBuilder[prefix]; has {
panic(fmt.Errorf("attempt to register %s twice", prefix))
}
prefixToFunctionTestBuilder[prefix] = builder
}
return true
}
var prefixToFunctionTestBuilder = map[string]functionTestBuilder{}

View file

@ -0,0 +1,267 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
// See README.md for a documentation of the test logic
import (
"fmt"
"os"
"slices"
"strings"
"testing"
"forgejo.org/models/unittest"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/web/routing"
apiv1 "forgejo.org/routers/api/v1"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
apiv1_permissions_tests "forgejo.org/routers/api/v1/permissions/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createFixture(t *testing.T, signatures [][]any, permissions *apiv1_permissions.Permissions, data *fixtureData) {
for _, signature := range signatures[:len(signatures)-1] {
signatureString := apiv1_permissions_tests.SignatureToString(signature)
functionTest, has := signatureStringToFunctionTest[signatureString]
require.True(t, has)
if functionTest.fulfillNeeds != nil {
functionTest.fulfillNeeds(t, data)
}
}
for _, signature := range signatures {
signatureString := apiv1_permissions_tests.SignatureToString(signature)
functionTest, has := signatureStringToFunctionTest[signatureString]
require.True(t, has)
if functionTest.interpret != nil {
functionTest.interpret(t, permissions, data)
}
}
return
}
func getFunctionTest(t *testing.T, signatures [][]any) functionTest {
lastSignature := signatures[len(signatures)-1]
lastSignatureString := apiv1_permissions_tests.SignatureToString(lastSignature)
test, has := signatureStringToFunctionTest[lastSignatureString]
require.True(t, has, lastSignatureString)
return test
}
func getFixtures(t *testing.T, signatures [][]any) []*fixtureType {
return getFunctionTest(t, signatures).fixtures
}
func protectVariables(t *testing.T, signatures [][]any) {
for _, signature := range signatures {
signatureString := apiv1_permissions_tests.SignatureToString(signature)
functionTest, has := signatureStringToFunctionTest[signatureString]
require.True(t, has)
for _, b := range functionTest.protectSettingsBool {
t.Cleanup(test.MockProtect(b))
}
}
}
func testSequence(t *testing.T, signatures [][]any, onlyForSuccess bool) int {
t.Helper()
signaturesString := apiv1_permissions_tests.SignaturesToString(signatures)
var fixtures []*fixtureType
if onlyForSuccess {
for _, fixture := range getFixtures(t, signatures) {
if fixture.error == "" {
fixtures = []*fixtureType{fixture}
break
}
}
require.NotEmpty(t, fixtures, "%s must have at least one fixture with no error", signaturesString)
} else {
fixtures = getFixtures(t, signatures)
}
for i, fixture := range fixtures {
runName := signaturesString
if len(fixtures) > 1 {
runName = fmt.Sprintf("%s fixture %d", signaturesString, i)
}
t.Run(runName, func(t *testing.T) {
protectVariables(t, signatures)
unittest.LoadFixtures() // reset the database to clear any side effect of running the test
permissions := &apiv1_permissions.Permissions{}
permissions.SetContext(t.Context())
t.Logf("creating fixture data from %v", fixture.data)
modifiedFixture := fixture.Clone()
createFixture(t, signatures, permissions, modifiedFixture.data)
t.Logf("created fixture data %v", modifiedFixture.data)
var previousPerms []string
showPermissionsDiff := func() {
newPerms := permissions.Strings()
if previousPerms == nil {
for _, s := range newPerms {
t.Logf("\t%s", s)
}
} else {
// easier to compute the additions first (since we can destroy previousPerms)
// nicer to show the additions after the deletion
var additions []string
for _, s := range newPerms {
if i := slices.Index(previousPerms, s); i >= 0 {
if i == 0 {
// most frequent case
previousPerms = previousPerms[1:]
continue
}
last := len(previousPerms) - 1
if i != last {
previousPerms[i] = previousPerms[last]
}
previousPerms = previousPerms[:last]
continue
}
additions = append(additions, s)
}
for _, s := range previousPerms {
t.Logf("\t- %s", s)
}
for _, s := range additions {
t.Logf("\t+ %s", s)
}
}
previousPerms = newPerms
}
showPermissionsDiff()
var permissionsContext apiv1_permissions.Context = permissions
for i, signature := range signatures {
signatureString := apiv1_permissions_tests.SignatureToString(signature)
functionTest, has := signatureStringToFunctionTest[signatureString]
require.True(t, has)
args := signature[1:]
if len(args) != functionTest.staticArgs {
t.Fatalf("%s expects %d static arguments, got %d: %#v", routing.GetFuncShortName(signature[0]), functionTest.staticArgs, len(args), args)
}
functionTest.call(t, permissionsContext, modifiedFixture.data, args)
showPermissionsDiff()
if i == len(signatures)-1 {
fixture.used = true
if fixture.error != "" {
assert.NotZero(t, permissions.GetStatus())
assert.Contains(t, permissions.GetMessage(), fixture.error)
} else {
assert.Zero(t, permissions.GetStatus(), permissions.GetMessage())
}
} else {
assert.Zero(t, permissions.GetStatus(), permissions.GetMessage())
}
}
})
}
return len(fixtures)
}
func getPermissionSequenceForFunction(t *testing.T, sequence [][]any) [][]any {
sequenceString := apiv1_permissions_tests.SignaturesToString(sequence)
sequenceFilter := getFunctionTest(t, sequence).sequenceFilter
if sequenceFilter == nil {
return sequence
}
var filteredSequence [][]any
if len(sequenceFilter) > len(sequence) {
panic(fmt.Errorf("%s is longer than %v", sequenceString, sequenceFilter))
}
for _, signature := range sequence {
if len(sequenceFilter) == 0 {
break
}
signatureString := apiv1_permissions_tests.SignatureToString(signature)
if signatureString == sequenceFilter[0] || strings.HasPrefix(signatureString, sequenceFilter[0]+" ") {
filteredSequence = append(filteredSequence, signature)
sequenceFilter = sequenceFilter[1:]
}
}
if len(sequenceFilter) > 0 {
panic(fmt.Errorf("%s filtered by %v does not consume all filters and %v is left", sequenceString, getFunctionTest(t, sequence).sequenceFilter, sequenceFilter))
}
if len(filteredSequence) == 0 {
panic(fmt.Errorf("%s filtered by %v is an empty sequence", sequenceString, getFunctionTest(t, sequence).sequenceFilter))
}
getPrefix := func(signature []any) string {
signatureString := apiv1_permissions_tests.SignatureToString(signature)
prefix, _, _ := strings.Cut(signatureString, " ")
return prefix
}
lastFilteredFunctionString := getPrefix(filteredSequence[len(filteredSequence)-1])
lastFunctionString := getPrefix(sequence[len(sequence)-1])
if lastFilteredFunctionString != lastFunctionString {
panic(fmt.Errorf("%s filtered by %v ends with the function %s instead of %s", sequenceString, getFunctionTest(t, sequence).sequenceFilter, lastFilteredFunctionString, lastFunctionString))
}
return filteredSequence
}
func getPermissionSequencesForFunctions(t *testing.T) [][][]any {
var sequences [][][]any
for _, sequence := range apiv1_permissions_tests.GetShortestPermissionSequenceForEachSignature() {
sequences = append(sequences, getPermissionSequenceForFunction(t, sequence))
}
return sequences
}
func TestAPIv1Permissions(t *testing.T) {
defer test.MockVariableValue(&setting.Service.DefaultAllowCreateOrganization, true)()
defer test.MockVariableValue(&setting.IsInTesting, true)()
defer test.MockVariableValue(&setting.DisableGitHooks, false)()
unittest.PrepareTestEnv(t)
// because setting.IsInTesting == true, it will record the
// middleware sequence of each route it builds
apiv1.Routes()
buildSignatureStringToFunctionTest(t)
runs := 0
t.Logf("running all fixtures for each permission function")
for _, sequence := range getPermissionSequencesForFunctions(t) {
runs += testSequence(t, sequence, false)
}
t.Logf("verify all unique permission sequences can run successfully")
uniqueSequences := apiv1_permissions_tests.GetUniquePermissionsSequences()
for _, sequence := range uniqueSequences {
runs += testSequence(t, sequence, true)
}
hasRunArg := func() bool {
for _, arg := range os.Args {
if arg != "-test.run=Test" && strings.HasPrefix(arg, "-test.run") {
return true
}
if arg != "-run=Test" && strings.HasPrefix(arg, "-run") {
return true
}
}
return false
}
// this sanity check will fail if only a selection of tests is run
// with -run=TestMyTest
if !hasRunArg() {
unusedFixtures := false
for signatureString, test := range signatureStringToFunctionTest {
for _, fixture := range test.fixtures {
if !fixture.used {
t.Logf("%s fixture %v not used", signatureString, fixture.data)
unusedFixtures = true
}
}
}
assert.False(t, unusedFixtures)
}
t.Logf("%d unique sequence of permission functions", len(uniqueSequences))
t.Logf("%d permissions functions", len(signatureStringToFunctionTest))
t.Logf("used a total of %d fixtures", runs)
}

View file

@ -0,0 +1,45 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.IndividualPermsChecker, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("user", data.Get("doer"))
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
if data.Has("user") && data.Get("user") != "anonymous" {
name := data.Get("user")
fixtureCreateUser(t, &user_model.User{Name: name})
permissions.SetUser(fixtureGetUser(t, name))
}
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"user": "IndividualPermsChecker",
}),
},
{
data: newFixtureData(map[string]string{
"user": "IndividualPermsCheckerprivate",
}),
error: "Visit Project",
},
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
"user": "IndividualPermsCheckerlimited",
}),
error: "Visit Project",
},
},
})

View file

@ -0,0 +1,16 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package tests_test
import (
"testing"
"forgejo.org/models/unittest"
_ "forgejo.org/modules/testimport"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}

View file

@ -0,0 +1,38 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.MustAllowPulls, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.Set("repository-init", "true")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"repository-init": "true",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"repository-init": "true",
"disable-units": "repo.pulls",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,31 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/modules/setting"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.MustEnableAttachments, functionTest{
protectSettingsBool: []*bool{
&setting.Attachment.Enabled,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
setting.Attachment.Enabled = data.Get("Attachment.Enabled") != "false"
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{}),
},
{
data: newFixtureData(map[string]string{
"Attachment.Enabled": "false",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,37 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.MustEnableIssuesOrPulls, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.Set("repository-init", "true")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"repository-init": "true",
"disable-units": "repo.pulls,repo.issues",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,32 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.MustEnableIssues, functionTest{
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"disable-units": "repo.issues",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,73 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTestWithCall(apiv1_permissions.MustEnableLocalIssuesIfIsIssue, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("issue", "issueOne")
data.SetDefault("issueAuthor", "issueAuthor")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
if data.Has("pullRequest") {
require.True(t, data.Has("pullRequestBranch"))
fixtureCreateBranch(t, permissions, data.Get("pullRequestBranch"))
require.True(t, data.Has("pullRequestAuthor"))
require.True(t, data.Has("pullRequest"))
fixtureCreatePullRequest(t, permissions, data)
require.Equal(t, data.Get("issue"), data.Get("pullRequest"))
} else {
fixtureCreateUser(t, &user_model.User{Name: data.Get("issueAuthor")})
fixtureSetIssue(t, permissions, data)
}
},
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
t.Helper()
index := fixtureGetIssue(t, data).Index
t.Logf("calling MustEnableLocalIssuesIfIsIssue(ctx, %d)", index)
apiv1_permissions.MustEnableLocalIssuesIfIsIssue(ctx, index)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"issue": "issue5000",
"issueAuthor": "issueAuthor",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"issue": "issue5000",
"issueAuthor": "issueAuthor",
"disable-units": "repo.issues",
}),
error: "Not Found",
},
{ // does not fail because it is an issue instead of a pull request
data: newFixtureData(map[string]string{
"doer": "userowner",
"repository": "userowner/repositorypublic",
"repository-init": "true",
"pullRequestAuthor": "userowner",
"pullRequestBranch": "MustEnableLocalIssuesIfIsIssue",
"pullRequest": "MustEnableLocalIssuesIfIsIssue",
"issue": "MustEnableLocalIssuesIfIsIssue",
"disable-units": "repo.issues",
}),
},
},
})

View file

@ -0,0 +1,32 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.MustEnableWiki, functionTest{
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"disable-units": "repo.wiki",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,24 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.MustNotBeArchived, functionTest{
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositoryarchived",
}),
error: "is archived",
},
},
})

View file

@ -0,0 +1,76 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.RepoAccess, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("repository", "userowner/repositorypublic")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureSetRepository(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
"repository": "userowner/repositoryprivate",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositoryprivate",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
"repository": "userowner/repositoryprivate",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"doer": user_model.ActionsUserName,
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": user_model.ActionsUserName,
"repository": "userowner/repositorypublic",
"task.RepoID": "unrelated",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"doer": user_model.ActionsUserName,
"repository": "userowner/repositorypublic",
"task.IsForkPullRequest": "true",
}),
},
},
})

View file

@ -0,0 +1,69 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
unit_model "forgejo.org/models/unit"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTestBuilder([]string{"ReqAdmin ", "ReqAdmin"}, func(t *testing.T, signatureString string, signature []any) {
t.Helper()
unitTypes := signature[1].([]unit_model.Type)
fixtures := []*fixtureType{
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"doer": "doeradmin",
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"doer": "userowner",
}),
},
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"doer": "regularuser",
}),
error: "user should be an owner or a collaborator with admin write of a repository",
},
}
for _, unitType := range unitTypes {
unit := unitsTypeToString(unitType)
fixtures = append(fixtures, &fixtureType{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"doer": "doeradmin",
"disable-units": unit,
}),
error: "Not Found",
})
}
signatureStringToFunctionTest[signatureString] = functionTest{
sequenceFilter: []string{
"APIAuthorization",
"RepoAccess",
signatureString,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.Set("doer", "doeradmin")
},
fixtures: fixtures,
staticArgs: 1,
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
unitTypes := args[0].([]unit_model.Type)
t.Logf("calling ReqAdmin(ctx, %+v)", unitTypes)
apiv1_permissions.ReqAdmin(ctx, unitTypes)
},
}
})

View file

@ -0,0 +1,40 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqAnyRepoReader, functionTest{
sequenceFilter: []string{
"APIAuthorization",
"RepoAccess",
"ReqAnyRepoReader",
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
"repository": "userowner/repositoryprivate",
}),
},
// This fixture is unreachable because this permissions function is always used after
// a RepoAccess that enforces the same restriction for non admin users
// {
// data: newFixtureData(map[string]string{
// "doer": "doerregular",
// "repository": "userowner/repositoryprivate",
// }),
// error: "Denied",
// },
},
},
)

View file

@ -0,0 +1,56 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/modules/setting"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqBasicOrRevProxyAuth, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "regularuser")
data.SetDefault("Service.EnableReverseProxyAuthAPI", "true")
data.SetDefault("authentication", "proxy")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureSetDoer(t, permissions, data)
setting.Service.EnableReverseProxyAuthAPI = data.Get("Service.EnableReverseProxyAuthAPI") == "true"
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"Service.EnableReverseProxyAuthAPI": "true",
"authentication": "proxy",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"Service.EnableReverseProxyAuthAPI": "false",
"authentication": "basic",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"Service.EnableReverseProxyAuthAPI": "true",
"authentication": "token",
}),
error: "auth method not allowed",
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"Service.EnableReverseProxyAuthAPI": "false",
"authentication": "token",
}),
error: "auth method not allowed",
},
},
})

View file

@ -0,0 +1,48 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/modules/setting"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqExploreSignIn, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "regularuser")
},
protectSettingsBool: []*bool{
&setting.Service.RequireSignInView,
&setting.Service.Explore.RequireSigninView,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureSetDoer(t, permissions, data)
setting.Service.RequireSignInView = data.Get("Service.RequireSignInView") == "true"
setting.Service.Explore.RequireSigninView = data.Get("Service.Explore.RequireSigninView") == "true"
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
"Service.RequireSignInView": "true",
}),
error: "you must be signed in",
},
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
"Service.Explore.RequireSigninView": "true",
}),
error: "you must be signed in",
},
},
})

View file

@ -0,0 +1,38 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/modules/setting"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqGitHook, functionTest{
protectSettingsBool: []*bool{
&setting.DisableGitHooks,
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "doeradmin")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
setting.DisableGitHooks = data.Get("DisableGitHooks") == "true"
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
"DisableGitHooks": "true",
}),
error: "must be allowed to edit Git hooks",
},
},
})

View file

@ -0,0 +1,100 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
org_model "forgejo.org/models/organization"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTest(apiv1_permissions.ReqOrgMembership, functionTest{
sequenceFilter: []string{
"APIAuthorization",
"TokenRequiresScopes",
"ReqOrgMembership",
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("org", "ReqOrgMembership")
data.SetDefault("setOrg", "true")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
orgOwner := data.Get("doer")
if data.Has("orgOwner") {
orgOwner = data.Get("orgOwner")
}
var org *org_model.Organization
if data.Has("org") {
fixtureCreateUser(t, &user_model.User{Name: orgOwner})
org = fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: orgOwner})
}
if data.Get("setOrg") == "true" {
permissions.SetOrg(org)
}
if data.Get("setTeam") == "true" {
team, err := org_model.GetTeam(t.Context(), org.ID, org_model.OwnerTeamName)
require.NoError(t, err)
permissions.SetTeam(team)
}
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"org": "ReqOrgMembershipOrg",
"setOrg": "true",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
"setOrg": "true",
}),
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgMembershipOrg",
"doer": "regularuser",
"setOrg": "true",
}),
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgMembershipOrg",
"orgOwner": "ReqOrgMembershipOrgOwner",
"doer": "regularuser",
"setOrg": "true",
}),
error: "Must be an organization member",
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgMembershipOrg",
"doer": "regularuser",
"setTeam": "true",
}),
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgMembershipOrg",
"orgOwner": "ReqOrgMembershipOrgOwner",
"doer": "regularuser",
"setTeam": "true",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"setOrg": "true",
}),
error: "unprepared context",
},
},
})

View file

@ -0,0 +1,100 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
org_model "forgejo.org/models/organization"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTest(apiv1_permissions.ReqOrgOwnership, functionTest{
sequenceFilter: []string{
"APIAuthorization",
"TokenRequiresScopes",
"ReqOrgOwnership",
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("org", "ReqOrgOwnershipOrg")
data.SetDefault("setOrg", "true")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
orgOwner := data.Get("doer")
if data.Has("orgOwner") {
orgOwner = data.Get("orgOwner")
}
var org *org_model.Organization
if data.Has("org") {
fixtureCreateUser(t, &user_model.User{Name: orgOwner})
org = fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: orgOwner})
}
if data.Get("setOrg") == "true" {
permissions.SetOrg(org)
}
if data.Get("setTeam") == "true" {
team, err := org_model.GetTeam(t.Context(), org.ID, org_model.OwnerTeamName)
require.NoError(t, err)
permissions.SetTeam(team)
}
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"org": "ReqOrgOwnershipOrg",
"setOrg": "true",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
"setOrg": "true",
}),
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgOwnershipOrg",
"doer": "regularuser",
"setOrg": "true",
}),
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgOwnershipOrg",
"orgOwner": "ReqOrgOwnershipOrgOwner",
"doer": "regularuser",
"setOrg": "true",
}),
error: "Must be an organization owner",
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgOwnershipOrg",
"doer": "regularuser",
"setTeam": "true",
}),
},
{
data: newFixtureData(map[string]string{
"org": "ReqOrgOwnershipOrg",
"orgOwner": "ReqOrgOwnershipOrgOwner",
"doer": "regularuser",
"setTeam": "true",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"setOrg": "true",
}),
error: "reqOrgOwnership: unprepared context",
},
},
})

View file

@ -0,0 +1,63 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
unit_model "forgejo.org/models/unit"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTestBuilder([]string{"ReqOwner ", "ReqOwner"}, func(t *testing.T, signatureString string, signature []any) {
t.Helper()
unitTypes := signature[1].([]unit_model.Type)
fixtures := []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "userowner",
"repository": "userowner/repositorypublic",
"scope": "read:user,write:repository",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regular",
"repository": "userowner/repositorypublic",
"scope": "read:user,write:repository",
}),
error: "user should be the owner of the repo",
},
}
for _, unitType := range unitTypes {
unit := unitsTypeToString(unitType)
fixtures = append(fixtures, &fixtureType{
data: newFixtureData(map[string]string{
"disable-units": unit,
}),
error: "Not Found",
})
}
signatureStringToFunctionTest[signatureString] = functionTest{
sequenceFilter: []string{
"APIAuthorization",
"RepoAccess",
"ReqOwner",
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.Set("doer", "doeradmin")
},
fixtures: fixtures,
staticArgs: 1,
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
unitTypes := args[0].([]unit_model.Type)
t.Logf("calling ReqOwner(ctx, %+v)", unitTypes)
apiv1_permissions.ReqOwner(ctx, unitTypes)
},
}
})

View file

@ -0,0 +1,52 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/models/perm"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTestBuilder([]string{"ReqPackageAccess "}, func(_ *testing.T, signatureString string, signature []any) {
signatureStringToFunctionTest[signatureString] = functionTest{
sequenceFilter: []string{
"APIAuthorization",
signatureString,
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "doerregular")
if data.Get("packageOwner") == "doer" {
data.Set("packageOwner", data.Get("doer"))
}
data.SetDefault("packageOwner", data.Get("doer"))
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureSetPackageOwner(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"packageOwner": "doer",
"doer": "doeradmin",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "userregular",
"packageOwner": "userprivate",
}),
error: "user should have specific permission or be a site admin",
},
},
staticArgs: 1,
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
mode := args[0].(perm.AccessMode)
t.Logf("calling ReqPackageAccess(ctx, %s)", mode)
apiv1_permissions.ReqPackageAccess(ctx, mode)
},
}
})

View file

@ -0,0 +1,61 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"strings"
"testing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTestWithCall(apiv1_permissions.ReqRepoBranchWriter, functionTest{
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
require.True(t, data.Has("pullRequestBranch"))
fixtureCreateBranch(t, permissions, data.Get("pullRequestBranch"))
require.True(t, data.Has("pullRequestAuthor"))
require.True(t, data.Has("pullRequest"))
fixtureCreatePullRequest(t, permissions, data)
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
owner, _, found := strings.Cut(data.Get("repository"), "/")
require.True(t, found)
data.Set("doer", owner)
data.SetDefault("repository-init", "true")
data.SetDefault("pullRequestAuthor", owner)
data.SetDefault("pullRequestBranch", "ReqRepoBranchWriter")
data.SetDefault("pullRequest", "ReqRepoBranchWriter")
},
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
branch := data.Get("pullRequestBranch")
t.Logf("calling ReqRepoBranchWriter(ctx, %s)", branch)
apiv1_permissions.ReqRepoBranchWriter(ctx, branch)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "userowner",
"repository": "userowner/repositorypublic",
"repository-init": "true",
"pullRequestAuthor": "userowner",
"pullRequestBranch": "ReqRepoBranchWriter",
"pullRequest": "ReqRepoBranchWriter",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"repository": "userowner/repositorypublic",
"repository-init": "true",
"pullRequestAuthor": "userowner",
"pullRequestBranch": "ReqRepoBranchWriter",
"pullRequest": "ReqRepoBranchWriter",
}),
error: "user should have a permission to write to this branch",
},
},
})

View file

@ -0,0 +1,53 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
unit_model "forgejo.org/models/unit"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTestBuilder([]string{"ReqRepoReader "}, func(t *testing.T, signatureString string, signature []any) {
t.Helper()
unitType := signature[1].(unit_model.Type)
unit := unitsTypeToString(unitType)
signatureStringToFunctionTest[signatureString] = functionTest{
sequenceFilter: []string{
"APIAuthorization",
"RepoAccess",
signatureString,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{}),
},
{
data: newFixtureData(map[string]string{
"disable-units": unit,
}),
error: "Not Found",
},
// This fixture is unreachable because this permissions function is always used after
// a RepoAccess that enforces the same restriction for non admin users
// {
// data: newFixtureData(map[string]string{
// "doer": "regularuser",
// "repository": "userowner/repositoryprivate",
// }),
// error: "user should have specific read permission or be a repo admin or a site admin",
// },
},
staticArgs: 1,
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
unitType := args[0].(unit_model.Type)
t.Logf("calling ReqRepoReader(ctx, %s)", unitType)
apiv1_permissions.ReqRepoReader(ctx, unitType)
},
}
})

View file

@ -0,0 +1,72 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"strings"
"testing"
unit_model "forgejo.org/models/unit"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTestBuilder([]string{"ReqRepoWriter "}, func(t *testing.T, signatureString string, signature []any) {
t.Helper()
unitTypes := signature[1].([]unit_model.Type)
units := unitsTypeToString(unitTypes...)
scopes := unitsToScopes(unitTypes, "write")
signatureStringToFunctionTest[signatureString] = functionTest{
sequenceFilter: []string{
"APIAuthorization",
"RepoAccess",
signatureString,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureDisableUnits(t, permissions, data)
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
if data.Has("repository") {
owner, _, found := strings.Cut(data.Get("repository"), "/")
require.True(t, found)
data.Set("doer", owner)
} else {
data.SetDefault("repository", "userowner/repositorypublic")
data.SetDefault("doer", "userowner")
}
data.SetDefault("level", "write")
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"repository": "userowner/repositorypublic",
"doer": "userowner",
"scope": scopes,
}),
},
{
data: newFixtureData(map[string]string{
"disable-units": units,
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"repository": "userowner/repositorypublic",
"scope": "write:issue",
}),
error: "user should have a permission to write to a repo",
},
},
staticArgs: 1,
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
unitType := args[0].([]unit_model.Type)
t.Logf("calling ReqRepoWriter(ctx, %s)", unitType)
apiv1_permissions.ReqRepoWriter(ctx, unitType)
},
}
})

View file

@ -0,0 +1,48 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqSelfOrAdmin, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "doeradmin")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
if data.Has("user") && data.Get("user") != "anonymous" {
name := data.Get("user")
user := permissions.GetUser()
if user == nil {
fixtureCreateUser(t, &user_model.User{Name: name})
permissions.SetUser(fixtureGetUser(t, name))
}
}
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"user": "regularuser",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"user": "otheruser",
}),
error: "doer should be the site admin or be same as the contextUser",
},
},
})

View file

@ -0,0 +1,30 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqSiteAdmin, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "doeradmin")
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
}),
error: "user should be the site admin",
},
},
})

View file

@ -0,0 +1,98 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
org_model "forgejo.org/models/organization"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTest(apiv1_permissions.ReqTeamMembership, functionTest{
sequenceFilter: []string{
"APIAuthorization",
"TokenRequiresScopes",
"ReqTeamMembership",
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("org", "ReqTeamMembership")
data.SetDefault("team", org_model.OwnerTeamName)
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
orgOwner := data.Get("doer")
if data.Has("orgOwner") {
orgOwner = data.Get("orgOwner")
}
var org *org_model.Organization
if data.Has("org") {
fixtureCreateUser(t, &user_model.User{Name: orgOwner})
org = fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: orgOwner})
}
if data.Has("teams") {
fixtureCreateTeams(t, org, data.Get("teams"))
}
if data.Has("team") {
team, err := org_model.GetTeam(t.Context(), org.ID, data.Get("team"))
require.NoError(t, err)
permissions.SetTeam(team)
}
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"org": "ReqTeamMembership",
"team": org_model.OwnerTeamName,
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doeradmin",
"org": "ReqTeamMembership",
"team": org_model.OwnerTeamName,
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"orgOwner": "orgOwner",
"org": "ReqTeamMembership",
"teams": "team1:regularuser",
"team": "team1",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"orgOwner": "orgOwner",
"org": "ReqTeamMembership",
"teams": "team1:regularuser,team2:otheruser",
"team": "team2",
}),
error: "Must be a team member",
},
{
data: newFixtureData(map[string]string{
"doer": "regularuser",
"orgOwner": "orgOwner",
"org": "ReqTeamMembership",
"teams": "team2:otheruser",
"team": "team2",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"org": "ReqTeamMembership",
}),
error: "reqTeamMembership: unprepared context",
},
},
})

View file

@ -0,0 +1,36 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqToken, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("doer", "doerregular")
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
}),
},
{
data: newFixtureData(map[string]string{
"doer": user_model.ActionsUserName,
}),
},
{
data: newFixtureData(map[string]string{
"doer": "anonymous",
}),
error: "token is required",
},
},
})

View file

@ -0,0 +1,31 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/modules/setting"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqUsersExploreEnabled, functionTest{
protectSettingsBool: []*bool{
&setting.Service.Explore.DisableUsersPage,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
setting.Service.Explore.DisableUsersPage = data.Get("Service.Explore.DisableUsersPage") == "true"
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{}),
},
{
data: newFixtureData(map[string]string{
"Service.Explore.DisableUsersPage": "true",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,89 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTestWithCall(apiv1_permissions.ReqValidCommentID, functionTest{
sequenceFilter: []string{
"APIAuthorization",
"RepoAccess",
"ReqValidCommentID",
},
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("issue", "issueOne")
data.SetDefault("issueAuthor", "issueAuthor")
data.SetDefault("comment", "comment for ReqValidCommentID")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureCreateUser(t, &user_model.User{Name: data.Get("issueAuthor")})
fixtureSetIssue(t, permissions, data)
fixtureCreateComment(t, permissions, data)
},
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
t.Helper()
comment := fixtureGetComment(t, data)
if data.Has("NilIssue") {
comment.Issue = nil
}
if data.Has("InconsistentID") {
comment.Issue.RepoID = 123456
}
t.Logf("calling ReqValidCommentID(ctx, %+v)", comment)
apiv1_permissions.ReqValidCommentID(ctx, comment)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"issue": "issueOne",
"issueAuthor": "issueAuthor",
"comment": "comment for ReqValidCommentID",
}),
},
// This fixture is unreachable because this permissions function is always used after
// a RepoAccess that enforces the same restriction for non admin users
// {
// data: newFixtureData(map[string]string{
// "doer": "doerregular",
// "repository": "userowner/repositoryprivate",
// "issue": "issueOne",
// "issueAuthor": "issueAuthor",
// "comment": "comment for ReqValidCommentID",
// }),
// error: "Not Found",
// },
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"issue": "issueOne",
"issueAuthor": "issueAuthor",
"comment": "comment for ReqValidCommentID",
"NilIssue": "true",
}),
error: "Not Found",
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"repository": "userowner/repositorypublic",
"issue": "issueOne",
"issueAuthor": "issueAuthor",
"comment": "comment for ReqValidCommentID",
"InconsistentID": "true",
}),
error: "Not Found",
},
},
})

View file

@ -0,0 +1,31 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"testing"
"forgejo.org/modules/setting"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTest(apiv1_permissions.ReqWebhooksEnabled, functionTest{
protectSettingsBool: []*bool{
&setting.DisableWebhooks,
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
setting.DisableWebhooks = data.Get("DisableWebhooks") == "true"
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{}),
},
{
data: newFixtureData(map[string]string{
"DisableWebhooks": "true",
}),
error: "webhooks disabled by administrator",
},
},
})

View file

@ -3,6 +3,8 @@
package tests
// See README.md for a documentation of the test logic
import (
"cmp"
"fmt"

View file

@ -0,0 +1,79 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"strings"
"testing"
org_model "forgejo.org/models/organization"
user_model "forgejo.org/models/user"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
"github.com/stretchr/testify/require"
)
var _ = registerFunctionTestWithCall(apiv1_permissions.TokenRequiresRepoOwnerScope, functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
if !data.Has("owner") {
if data.Has("repository") {
owner, _, found := strings.Cut(data.Get("repository"), "/")
require.True(t, found)
data.Set("owner", owner)
} else {
data.Set("owner", "doerregular")
}
}
data.SetDefault("level", "read")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
ownerName := data.Get("owner")
if strings.Contains(ownerName, "org") {
fixtureCreateOrg(t, &org_model.Organization{Name: ownerName}, &user_model.User{Name: "orgOwner" + ownerName})
require.NotNil(t, fixtureGetUser(t, ownerName))
} else {
fixtureCreateUser(t, &user_model.User{Name: ownerName})
}
},
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
t.Helper()
owner := fixtureGetUser(t, data.Get("owner"))
level := levelStringToLevel(data.Get("level"))
t.Logf("calling TokenRequiresRepoOwnerScope(ctx, %+v, %v)", owner, level)
apiv1_permissions.TokenRequiresRepoOwnerScope(ctx, owner, level)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"owner": "doerregular",
"scope": "read:user",
"level": "read",
}),
},
{
data: newFixtureData(map[string]string{
"owner": "doerregular",
"scope": "read:user",
"level": "write",
}),
error: "token does not have at least one of required scope(s): [write:user]",
},
{
data: newFixtureData(map[string]string{
"owner": "regularorg",
"scope": "read:organization",
"level": "read",
}),
},
{
data: newFixtureData(map[string]string{
"owner": "regularorg",
"scope": "read:organization",
"level": "write",
}),
error: "token does not have at least one of required scope(s): [write:organization]",
},
},
})

View file

@ -0,0 +1,98 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"fmt"
"strings"
"testing"
auth_model "forgejo.org/models/auth"
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
)
var _ = registerFunctionTestBuilder([]string{"TokenRequiresScopes "}, func(t *testing.T, signatureString string, signature []any) {
t.Helper()
categories := signature[1].([]auth_model.AccessTokenScopeCategory)
var scopes []string
for _, category := range categories {
var scope auth_model.AccessTokenScope
switch category {
case auth_model.AccessTokenScopeCategoryActivityPub:
scope = auth_model.AccessTokenScopeReadActivityPub
case auth_model.AccessTokenScopeCategoryAdmin:
scope = auth_model.AccessTokenScopeReadAdmin
case auth_model.AccessTokenScopeCategoryNotification:
scope = auth_model.AccessTokenScopeReadNotification
case auth_model.AccessTokenScopeCategoryOrganization:
scope = auth_model.AccessTokenScopeReadOrganization
case auth_model.AccessTokenScopeCategoryPackage:
scope = auth_model.AccessTokenScopeReadPackage
case auth_model.AccessTokenScopeCategoryIssue:
scope = auth_model.AccessTokenScopeReadIssue
case auth_model.AccessTokenScopeCategoryRepository:
scope = auth_model.AccessTokenScopeReadRepository
case auth_model.AccessTokenScopeCategoryUser:
scope = auth_model.AccessTokenScopeReadUser
default:
panic(fmt.Errorf("unexpected category %v", category))
}
scopes = append(scopes, string(scope))
}
readscope := strings.Join(scopes, ",")
t.Logf("%s scopes %s", signatureString, readscope)
signatureStringToFunctionTest[signatureString] = functionTest{
fulfillNeeds: func(t *testing.T, data *fixtureData) {
t.Helper()
data.SetDefault("repository", "userowner/repositorypublic")
data.SetDefault("doer", "doerregular")
if data.Has("scope") {
scope := data.Get("scope")
if !strings.Contains(scope, readscope) {
writescope := strings.ReplaceAll(readscope, "read", "write")
data.Set("scope", strings.Join([]string{scope, writescope}, ","))
}
} else {
data.Set("scope", readscope)
}
data.SetDefault("level", "read")
},
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
fixtureSetRepository(t, permissions, data)
},
staticArgs: 1,
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, args []any) {
level := levelStringToLevel(data.Get("level"))
categories := args[0].([]auth_model.AccessTokenScopeCategory)
t.Logf("calling TokenRequiresScopes(ctx, %v, %v)", categories, level)
apiv1_permissions.TokenRequiresScopes(ctx, categories, level)
},
fixtures: []*fixtureType{
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"scope": readscope,
"level": "read",
}),
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"scope": readscope,
"level": "write",
}),
error: "token does not have at least one of required scope(s)",
},
{
data: newFixtureData(map[string]string{
"doer": "doerregular",
"scope": "read:misc",
"level": "read",
}),
error: "token does not have at least one of required scope(s)",
},
},
}
})

View file

@ -0,0 +1,42 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package tests
import (
"fmt"
"slices"
"strings"
auth_model "forgejo.org/models/auth"
)
func requiredScopesToString(scopeCategories ...auth_model.AccessTokenScopeCategory) string {
var categories []string
for _, category := range scopeCategories {
switch category {
case auth_model.AccessTokenScopeCategoryActivityPub:
categories = append(categories, "ActivityPub")
case auth_model.AccessTokenScopeCategoryAdmin:
categories = append(categories, "Admin")
case auth_model.AccessTokenScopeCategoryMisc:
categories = append(categories, "Misc")
case auth_model.AccessTokenScopeCategoryNotification:
categories = append(categories, "Notification")
case auth_model.AccessTokenScopeCategoryOrganization:
categories = append(categories, "Organization")
case auth_model.AccessTokenScopeCategoryPackage:
categories = append(categories, "Package")
case auth_model.AccessTokenScopeCategoryIssue:
categories = append(categories, "Issue")
case auth_model.AccessTokenScopeCategoryRepository:
categories = append(categories, "Repository")
case auth_model.AccessTokenScopeCategoryUser:
categories = append(categories, "User")
default:
panic(fmt.Errorf("unkwnon scope category %v", category))
}
}
slices.Sort(categories)
return strings.Join(categories, "")
}

View file

@ -0,0 +1,67 @@
// Copyright 2026 The Forgejo Authors.
// SPDX-License-Identifier: GPLv3-or-later
package tests_test
import (
"fmt"
"strings"
auth_model "forgejo.org/models/auth"
unit_model "forgejo.org/models/unit"
)
func levelStringToLevel(levelString string) auth_model.AccessTokenScopeLevel {
level := auth_model.Read
if levelString != "" {
switch levelString {
case "read":
level = auth_model.Read
case "write":
level = auth_model.Write
case "noaccess":
level = auth_model.NoAccess
default:
panic(fmt.Sprintf("unexpected level '%s'", levelString))
}
}
return level
}
func unitsTypeToString(unitTypes ...unit_model.Type) string {
var unitStrings []string
for _, unitType := range unitTypes {
var unit *unit_model.Unit
for _, u := range unit_model.Units {
if u.Type == unitType {
unit = &u
break
}
}
if unit == nil {
panic(fmt.Errorf("unable to find a unit with type %v", unitType))
}
unitStrings = append(unitStrings, unit.NameKey)
}
return strings.Join(unitStrings, ",")
}
func unitsToScopes(unitTypes []unit_model.Type, levelString string) string {
var scopeStrings []string
for _, unitType := range unitTypes {
unit := strings.TrimPrefix(unitsTypeToString(unitType), "repo.")
var scope string
switch unit {
case "issues":
scope = "issue"
case "code", "pulls", "wiki", "project", "actions", "releases":
scope = "repository"
case "packages":
scope = "package"
default:
panic(fmt.Errorf("unexpected unit type %v", unitType))
}
scopeStrings = append(scopeStrings, fmt.Sprintf("%s:%s", levelString, scope))
}
return strings.Join(scopeStrings, ",")
}

73
tests/forgery/team.go Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPLv3-or-later
package forgery
import (
"testing"
"forgejo.org/models"
"forgejo.org/models/db"
org_model "forgejo.org/models/organization"
"forgejo.org/models/perm"
unit_model "forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"github.com/stretchr/testify/require"
)
type CreateTeamOptions struct {
Name string
CanCreateOrgRepo bool
Mode perm.AccessMode
Members []*user_model.User
}
func CreateTeam(t *testing.T, org *org_model.Organization, opts *CreateTeamOptions) *org_model.Team {
t.Helper()
if opts == nil {
opts = &CreateTeamOptions{
Mode: perm.AccessModeRead,
}
}
if opts.Name == "" {
opts.Name = "team-" + uniqueSafeName(t.Name())
}
team := &org_model.Team{
OrgID: org.ID,
Name: opts.Name,
LowerName: opts.Name,
IncludesAllRepositories: true,
AccessMode: opts.Mode,
CanCreateOrgRepo: opts.CanCreateOrgRepo,
}
require.NoError(t, db.Insert(t.Context(), team))
units := make([]org_model.TeamUnit, 0, len(unit_model.AllRepoUnitTypes))
for _, tp := range unit_model.AllRepoUnitTypes {
up := opts.Mode
if tp == unit_model.TypeExternalTracker || tp == unit_model.TypeExternalWiki {
up = perm.AccessModeRead
}
units = append(units, org_model.TeamUnit{
OrgID: org.ID,
TeamID: team.ID,
Type: tp,
AccessMode: up,
})
}
require.NoError(t, db.Insert(t.Context(), &units))
for _, user := range opts.Members {
_, err := models.InsertTeamMember(t.Context(), team, user.ID)
require.NoError(t, err)
}
return team
}