gitforge/tests/forgery/repo.go
oliverpool 88ba174119 tests: better factory with forgery package (#11356)
### Context

Following the feedback in forgejo/discussions#170 (and my ambitious attempt in forgejo/forgejo#10985), it appears that having an easy-to-use factory package would greatly help get rid of the global fixtures.

I think that the global fixtures are quite harmful (recent example: https://codeberg.org/forgejo/forgejo/pulls/9906#issuecomment-10826066):
- hard to write (contributor must know where to add them)
- hard to change (may break some unrelated tests)
- hard to review (not located near the test code)
- they require the tests to execute sequentially

### Proposed way forward

The `forgery` package (the name represents faking/crafting and sounds good with Forgejo) is meant to replace global yaml fixtures with local go factories. The forgery can currently:
- create users
- create repos
- create organisations

This allowed me to drop `CreateDeclarativeRepoWithOptions` (and deprecate `CreateDeclarativeRepo`).

I think that further changes should be delayed to other PRs (I have a local branch to create `Project`)

### Documentation

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

### Release notes

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

*The decision if the pull request will be shown in the release notes is up to the mergers / release team.*

The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11356
Reviewed-by: limiting-factor <limiting-factor@noreply.codeberg.org>
2026-05-12 20:57:02 +02:00

138 lines
4 KiB
Go

// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgery
import (
"cmp"
"io/fs"
"testing"
repo_model "forgejo.org/models/repo"
unit_model "forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
repo_service "forgejo.org/services/repository"
wiki_service "forgejo.org/services/wiki"
"github.com/stretchr/testify/require"
"xorm.io/xorm/convert"
)
type CreateRepositoryOptions struct {
Name string // if nil a unique name (derived from the test name) will be generated
// Content of the initial commit, if nil the git repo will be left uninitialized.
// Use [MapFS] or [FilesInit] to setup the initial files.
Files fs.FS
ObjectFormat git.ObjectFormat // If nil, SHA1
IsTemplate bool
IsPrivate bool
LatestSha *string // if not nil, the commit sha after initializing the repo with the Files will be written to this ref
SkipCleanup bool // if true the repo will not be deleted at the end of the test (can be useful to debug locally)
}
// FilesInit specifies the templates to use upon repository initialization.
type FilesInit struct {
Readme string
Gitignores string
License string
}
func (FilesInit) Open(name string) (fs.File, error) {
panic("FilesInit is only a sentinel value")
}
// CreateRepository returns the repo, owner and opts can be nil
func CreateRepository(t testing.TB, owner *user_model.User, opts *CreateRepositoryOptions) *repo_model.Repository {
t.Helper()
if owner == nil {
owner = CreateUser(t, nil) // if specific options are needed, create the owner manually
}
if opts == nil {
opts = &CreateRepositoryOptions{}
}
repoName := opts.Name
if repoName == "" {
repoName = "repo-" + uniqueSafeName(t.Name())
}
gitFormat := cmp.Or(opts.ObjectFormat, git.Sha1ObjectFormat)
// Create the repository
createOptions := repo_service.CreateRepoOptions{
Name: repoName,
Description: "Test Repo",
DefaultBranch: "main",
IsTemplate: opts.IsTemplate,
ObjectFormatName: gitFormat.Name(),
IsPrivate: opts.IsPrivate,
}
if fi, ok := opts.Files.(FilesInit); ok {
createOptions.AutoInit = true
createOptions.Readme = cmp.Or(fi.Readme, "Default")
createOptions.Gitignores = fi.Gitignores
createOptions.License = fi.License
}
repo, err := repo_service.CreateRepositoryDirectly(t.Context(), owner, owner, createOptions)
require.NoError(t, err)
if !opts.SkipCleanup {
t.Cleanup(func() {
_ = repo_service.DeleteRepository(t.Context(), owner, repo, false)
})
}
require.NotEmpty(t, repo)
if !createOptions.AutoInit && opts.Files != nil {
sha, err := initRepo(owner, repo, gitFormat, opts.Files, "init")
require.NoError(t, err)
if opts.LatestSha != nil {
*opts.LatestSha = sha
}
// reload the repo since pushing a commit might update the model via the push_update queue (IsEmpty for instance)
repo, err = repo_model.GetRepositoryByID(t.Context(), repo.ID)
require.NoError(t, err)
}
repo.Owner = owner
return repo
}
func InitWiki(t testing.TB, repo *repo_model.Repository, branch string) {
// Set the wiki branch in the database first
repo.WikiBranch = branch
err := repo_model.UpdateRepositoryCols(t.Context(), repo, "wiki_branch")
require.NoError(t, err)
// Initialize the wiki
err = wiki_service.InitWiki(t.Context(), repo)
require.NoError(t, err)
// Add a new wiki page
err = wiki_service.AddWikiPage(t.Context(), repo.Owner, repo, "Home", "Welcome to the wiki!", "Add a Home page")
require.NoError(t, err)
}
// config may be nil
func EnableRepoUnit(t testing.TB, repo *repo_model.Repository, unit unit_model.Type, config convert.Conversion) {
t.Helper()
err := repo_service.UpdateRepositoryUnits(t.Context(), repo, []repo_model.RepoUnit{{
RepoID: repo.ID,
Type: unit,
Config: config,
}}, nil)
require.NoError(t, err)
}
func DisableRepoUnits(t testing.TB, repo *repo_model.Repository, units ...unit_model.Type) {
t.Helper()
err := repo_service.UpdateRepositoryUnits(t.Context(), repo, nil, units)
require.NoError(t, err)
}