mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-16 07:46:35 +00:00
test: scaffold forgery package
This commit is contained in:
parent
0e577ed6c9
commit
94e003abb5
3 changed files with 316 additions and 0 deletions
112
tests/forgery/fs.go
Normal file
112
tests/forgery/fs.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
files_service "forgejo.org/services/repository/files"
|
||||
)
|
||||
|
||||
type MapFS = fstest.MapFS
|
||||
|
||||
// when a file has one of this mode, it needs specific git handling
|
||||
// other non-zero modes are rejected
|
||||
const (
|
||||
modeSubmodule = fs.ModeNamedPipe // hacky, but
|
||||
modeSymlink = fs.ModeSymlink
|
||||
)
|
||||
|
||||
func MapFile(data string) *fstest.MapFile {
|
||||
return &fstest.MapFile{
|
||||
Data: []byte(data),
|
||||
}
|
||||
}
|
||||
|
||||
func MapSymlink(target string) *fstest.MapFile {
|
||||
return &fstest.MapFile{
|
||||
Data: []byte(target),
|
||||
Mode: modeSymlink,
|
||||
}
|
||||
}
|
||||
|
||||
func MapSubmodule(sha string) *fstest.MapFile {
|
||||
return &fstest.MapFile{
|
||||
Data: []byte(sha),
|
||||
Mode: modeSubmodule,
|
||||
}
|
||||
}
|
||||
|
||||
func initRepo(doer *user_model.User, repo *repo_model.Repository, format git.ObjectFormat, fsys fs.FS, commitMessage string) (string, error) {
|
||||
t, err := files_service.NewTemporaryUploadRepository(git.DefaultContext, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer t.Close()
|
||||
if err := t.Init(format.Name()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
|
||||
var content io.Reader
|
||||
mode := git.EntryModeBlob
|
||||
switch d.Type() {
|
||||
case modeSymlink:
|
||||
target, err := fs.ReadLink(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content = strings.NewReader(target)
|
||||
mode = git.EntryModeSymlink
|
||||
case modeSubmodule:
|
||||
mode = git.EntryModeCommit
|
||||
fallthrough
|
||||
case 0:
|
||||
f, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content = f
|
||||
default:
|
||||
return fmt.Errorf("unexpected file type in forgery.CreateRepository %s: %s", path, d.Type())
|
||||
}
|
||||
|
||||
// add object to the database
|
||||
objectHash, err := t.HashObject(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add the object to the index
|
||||
return t.AddObjectToIndex(mode.String(), objectHash, path)
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
treeHash, err := t.WriteTree()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
commitHash, err := t.CommitTreeWithDate("", doer, doer, treeHash, commitMessage, false, now, now)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return commitHash, t.Push(doer, commitHash, repo.DefaultBranch)
|
||||
}
|
||||
138
tests/forgery/repo.go
Normal file
138
tests/forgery/repo.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// 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)
|
||||
}
|
||||
66
tests/forgery/user.go
Normal file
66
tests/forgery/user.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgery
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
org_model "forgejo.org/models/organization"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var nameCleaner = regexp.MustCompile(`[^a-zA-Z0-9-]+`) // exclude "_", to prevent multiple consecutive dashes
|
||||
|
||||
// uniqueSafeName replaces specials chars with _ and appends a random hex suffix
|
||||
func uniqueSafeName(testName string) string {
|
||||
return nameCleaner.ReplaceAllLiteralString(testName, "_") + "-" + strconv.FormatUint(uint64(rand.Uint32()), 16)
|
||||
}
|
||||
|
||||
type CreateUserOptions struct {
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
const userPassword = "password"
|
||||
|
||||
func CreateUser(t testing.TB, opts *CreateUserOptions) *user_model.User {
|
||||
t.Helper()
|
||||
|
||||
if opts == nil {
|
||||
opts = &CreateUserOptions{}
|
||||
}
|
||||
u := &user_model.User{}
|
||||
|
||||
name := "user-" + uniqueSafeName(t.Name())
|
||||
|
||||
u.Name = name
|
||||
u.Email = name + "@test.forgejo.org"
|
||||
u.Passwd = userPassword
|
||||
u.IsAdmin = opts.IsAdmin
|
||||
|
||||
err := user_model.CreateUser(t.Context(), u)
|
||||
require.NoError(t, err)
|
||||
return u
|
||||
}
|
||||
|
||||
func CreateOrganisation(t testing.TB, owner *user_model.User) *org_model.Organization {
|
||||
t.Helper()
|
||||
|
||||
if owner == nil {
|
||||
owner = CreateUser(t, nil) // if specific options are needed, create the owner manually
|
||||
}
|
||||
o := &org_model.Organization{}
|
||||
|
||||
name := "org-" + uniqueSafeName(t.Name())
|
||||
|
||||
o.Name = name
|
||||
|
||||
err := org_model.CreateOrganization(t.Context(), o, owner)
|
||||
require.NoError(t, err)
|
||||
return o
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue