**Backport:** https://codeberg.org/forgejo/forgejo/pulls/13271
- separate test cases from fixtures
- testcase.go: A `testCase` describes a test using `testData`
- fixture.go: `fixture*` are function that creates the conditions for this test case to run
- move the testData closer to the beginning
- update the README.md
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13271
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
(cherry picked from commit
|
||
|---|---|---|
| .. | ||
| api_authorization.go | ||
| check_token_public_only.go | ||
| fixture.go | ||
| functions.go | ||
| individual_perms_checker.go | ||
| must_allow_pulls.go | ||
| must_enable_attachments.go | ||
| must_enable_issues.go | ||
| must_enable_issues_or_pulls.go | ||
| must_enable_local_issues_if_is_issue.go | ||
| must_enable_wiki.go | ||
| must_not_be_archived.go | ||
| README.md | ||
| repo_access.go | ||
| req_admin.go | ||
| req_any_repo_reader.go | ||
| req_basic_or_rev_proxy_auth.go | ||
| req_explore_sign_in.go | ||
| req_git_hook.go | ||
| req_org_membership.go | ||
| req_org_ownership.go | ||
| req_owner.go | ||
| req_package_access.go | ||
| req_repo_branch_writer.go | ||
| req_repo_reader.go | ||
| req_repo_writer.go | ||
| req_self_or_admin.go | ||
| req_site_admin.go | ||
| req_team_membership.go | ||
| req_token.go | ||
| req_users_explore_enabled.go | ||
| req_valid_comment_id.go | ||
| req_webhooks_enabled.go | ||
| testcase.go | ||
| token_requires_repo_owner_scope.go | ||
| token_requires_scopes.go | ||
| utils.go | ||
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
ReqGitHookfunction inrouters/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.
type functionTest struct {
testCases []*testCase
fulfillNeeds func(t *testing.T, data *testData)
interpret func(t *testing.T, permissions *apiv1_permissions.Permissions, data *testData)
protect func() func()
call func(t *testing.T, ctx apiv1_permissions.Context, permissions *apiv1_permissions.Permissions, data *testData, signature []any)
sequenceFilter []string
}
- The test registers the struct and it will be used when the
TestAPIv1Permissionstest runs - The
testCasesare described using amap[string]stringusing conventions that are to be interpreted by the fixture helpers. For instance"doer": "regularuser"will be interpreted by thefixtureSetDoerhelper and create the userregularuser. - The
fulfillNeedsfunction 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 isAPIAuthorization,TokenRequiresScopes,ReqOrgOwnership, thefulfillNeedsfunction ofTokenRequiresScopesis expected to set a sensible default for the scope, such as"scope": "read:repository" - After
fulfillNeedsis called and thetestDatais assumed to have all the necessary defaults, it will be acted upon by eachinterpretfunction in the sequence. For instance,APIAuthorizationwill interpret the"doer": "regularuser"data by callingfixtureSetDoerto ensure it is created. - If global variables need protection (for instance when changing a setting), they are to be protected by the
protectfunction - Once the
testDatahas 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 thetestDatais designed for that purpose. - The
callfunction, if it exists, is expected to call the function for which the test is designed. Fos instance thecallforReqValidCommentIDrunsapiv1_permissions.ReqValidCommentID(ctx, comment). The function must not have any side effect. Instead it must use what has been created by the fixtures (using theinterpretfunction). - The
sequenceFilteronly keeps some permissions function in the sequence leading to the function under test. For instance when testingReqOrgOwnershipthe sequenceAPIAuthorization,TokenRequiresScopes,ReqOrgOwnershipwill 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 particulartestCaseis 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 fixture.go file contains all the helpers to create those fixtures.
Debugging
- Running the tests in verbose mode
GOTESTCOMPILEDRUNSUFFIX=-test.v TAGS='sqlite sqlite_unlock_notify' make 'test-sqlite#TestAPIv1Permissions' - Browsing the tests such as
...
=== RUN TestAPIv1Permissions/APIAuthorization,TokenRequiresScopes_Admin_fixture_0
functions.go:95: creating fixture data from doer:doerregular,level:read,scope:read:admin
functions.go:98: created fixture data doer:doerregular,level:read,scope:read:admin
functions.go:105: *auth.AccessToken(ID=10 Token=e26bfc1190efcf8c36ef640659af33e87073032c)
functions.go:105: *user.User(Name=doerregular)
functions.go:105: isSigned(true)
functions.go:105: *tests_test.accessTokenAuthenticationResult(*user.User(Name=doerregular) auth.AccessTokenScope(read:admin) *authz.AllAccessAuthorizationReducer)
fixture.go:637: calling permissions.APIAuthorization(ctx)
functions.go:131: + *authz.AllAccessAuthorizationReducer
token_requires_scopes_test.go:67: calling TokenRequiresScopes(ctx, [1], 1)
functions.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
testCasebeing used for running the test, as found in the test file of the last function in the sequence (TokenRequiresScopesin the example) - The
creating fixtureline shows all thetestDataused in thattestCase - The
created fixtureline shows thetestDataadded after calling thefulfillNeedsfunction for each function in the sequence - The indented lines that follow shows the content of the
routers/api/v1/permissions.Permissionobject before calling a permission function. To reduce the verbosity modifications are shown in a diff style fashion. - The
callingline 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 *testData, signature []any)
It is responsible for:
- Calling
t.Logfto display the call about to be made - Calling the function using
ctxas 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.zipartifact - 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