mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
443 lines
14 KiB
Go
443 lines
14 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/models/auth"
|
|
"github.com/hanzoai/git/models/perm"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/queue"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/services/forms"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
type APITestContext struct {
|
|
Reponame string
|
|
Session *TestSession
|
|
Token string
|
|
Username string
|
|
ExpectedCode int
|
|
}
|
|
|
|
func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext {
|
|
session := loginUser(t, username)
|
|
if len(scope) == 0 {
|
|
// FIXME: legacy logic: no scope means all
|
|
scope = []auth.AccessTokenScope{auth.AccessTokenScopeAll}
|
|
}
|
|
token := getTokenForLoggedInUser(t, session, scope...)
|
|
return APITestContext{
|
|
Session: session,
|
|
Token: token,
|
|
Username: username,
|
|
Reponame: reponame,
|
|
}
|
|
}
|
|
|
|
func (ctx APITestContext) GitPath() string {
|
|
return fmt.Sprintf("%s/%s.git", ctx.Username, ctx.Reponame)
|
|
}
|
|
|
|
func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
createRepoOption := &api.CreateRepoOption{
|
|
AutoInit: !empty,
|
|
Description: "Temporary repo",
|
|
Name: ctx.Reponame,
|
|
Private: true,
|
|
Template: true,
|
|
Gitignores: "",
|
|
License: "WTFPL",
|
|
Readme: "Default",
|
|
}
|
|
req := NewRequestWithJSON(t, "POST", "/v1/user/repos", createRepoOption).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
repository := DecodeJSON(t, resp, &api.Repository{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *repository)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/v1/repos/%s/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), editRepoOption).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
repository := DecodeJSON(t, resp, &api.Repository{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *repository)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIAddCollaborator(ctx APITestContext, username string, mode perm.AccessMode) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
permission := api.RepoWritePermissionRead
|
|
|
|
if mode == perm.AccessModeAdmin {
|
|
permission = api.RepoWritePermissionAdmin
|
|
} else if mode > perm.AccessModeRead {
|
|
permission = api.RepoWritePermissionWrite
|
|
}
|
|
addCollaboratorOption := &api.AddCollaboratorOption{
|
|
Permission: &permission,
|
|
}
|
|
req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/v1/repos/%s/%s/collaborators/%s", ctx.Username, ctx.Reponame, username), addCollaboratorOption).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func doAPIForkRepository(ctx APITestContext, username string, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
createForkOption := &api.CreateForkOption{}
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/v1/repos/%s/%s/forks", username, ctx.Reponame), createForkOption).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusAccepted)
|
|
repository := DecodeJSON(t, resp, &api.Repository{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *repository)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIGetRepository(ctx APITestContext, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/v1/repos/%s/%s", ctx.Username, ctx.Reponame)).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
repository := DecodeJSON(t, resp, &api.Repository{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *repository)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIDeleteRepository(ctx APITestContext) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequest(t, "DELETE", fmt.Sprintf("/v1/repos/%s/%s", ctx.Username, ctx.Reponame)).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func doAPICreateUserKey(ctx APITestContext, keyname, keyFile string, callback ...func(*testing.T, api.PublicKey)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
dataPubKey, err := os.ReadFile(keyFile + ".pub")
|
|
assert.NoError(t, err)
|
|
req := NewRequestWithJSON(t, "POST", "/v1/user/keys", &api.CreateKeyOption{
|
|
Title: keyname,
|
|
Key: string(dataPubKey),
|
|
}).AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
publicKey := DecodeJSON(t, resp, &api.PublicKey{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *publicKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIDeleteUserKey(ctx APITestContext, keyID int64) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequest(t, "DELETE", fmt.Sprintf("/v1/user/keys/%d", keyID)).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly bool) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
dataPubKey, err := os.ReadFile(keyFile + ".pub")
|
|
assert.NoError(t, err)
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/v1/repos/%s/%s/keys", ctx.Username, ctx.Reponame), api.CreateKeyOption{
|
|
Title: keyname,
|
|
Key: string(dataPubKey),
|
|
ReadOnly: readOnly,
|
|
}).AddTokenAuth(ctx.Token)
|
|
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
}
|
|
}
|
|
|
|
func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) {
|
|
return func(t *testing.T) (api.PullRequest, error) {
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/v1/repos/%s/%s/pulls", owner, repo), &api.CreatePullRequestOption{
|
|
Head: headBranch,
|
|
Base: baseBranch,
|
|
Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch),
|
|
}).AddTokenAuth(ctx.Token)
|
|
|
|
expected := http.StatusCreated
|
|
if ctx.ExpectedCode != 0 {
|
|
expected = ctx.ExpectedCode
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, expected)
|
|
|
|
decoder := json.NewDecoder(resp.Body)
|
|
pr := api.PullRequest{}
|
|
err := decoder.Decode(&pr)
|
|
return pr, err
|
|
}
|
|
}
|
|
|
|
func doAPIGetPullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) (api.PullRequest, error) {
|
|
return func(t *testing.T) (api.PullRequest, error) {
|
|
req := NewRequest(t, http.MethodGet, fmt.Sprintf("/v1/repos/%s/%s/pulls/%d", owner, repo, index)).
|
|
AddTokenAuth(ctx.Token)
|
|
|
|
expected := http.StatusOK
|
|
if ctx.ExpectedCode != 0 {
|
|
expected = ctx.ExpectedCode
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, expected)
|
|
|
|
decoder := json.NewDecoder(resp.Body)
|
|
pr := api.PullRequest{}
|
|
err := decoder.Decode(&pr)
|
|
return pr, err
|
|
}
|
|
}
|
|
|
|
func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
urlStr := fmt.Sprintf("/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
|
|
|
var req *RequestWrapper
|
|
var resp *httptest.ResponseRecorder
|
|
|
|
for range 6 {
|
|
req = NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
|
|
MergeMessageField: "doAPIMergePullRequest Merge",
|
|
Do: string(repo_model.MergeStyleMerge),
|
|
}).AddTokenAuth(ctx.Token)
|
|
|
|
resp = ctx.Session.MakeRequest(t, req, NoExpectedStatus)
|
|
|
|
if resp.Code != http.StatusMethodNotAllowed {
|
|
break
|
|
}
|
|
err := DecodeJSON(t, resp, &api.APIError{})
|
|
assert.Equal(t, "Please try again later", err.Message)
|
|
queue.GetManager().FlushAll(t.Context(), 5*time.Second)
|
|
<-time.After(1 * time.Second)
|
|
}
|
|
|
|
expected := ctx.ExpectedCode
|
|
if expected == 0 {
|
|
expected = http.StatusOK
|
|
}
|
|
|
|
if !assert.Equal(t, expected, resp.Code,
|
|
"Request: %s %s", req.Method, req.URL.String()) {
|
|
logUnexpectedResponse(t, resp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
urlStr := fmt.Sprintf("/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
|
req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
|
|
Do: string(repo_model.MergeStyleManuallyMerged),
|
|
MergeCommitID: commitID,
|
|
}).AddTokenAuth(ctx.Token)
|
|
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func doAPIAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
urlStr := fmt.Sprintf("/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
|
req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
|
|
MergeMessageField: "doAPIMergePullRequest Merge",
|
|
Do: string(repo_model.MergeStyleMerge),
|
|
MergeWhenChecksSucceed: true,
|
|
}).AddTokenAuth(ctx.Token)
|
|
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func doAPICancelAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequest(t, http.MethodDelete, fmt.Sprintf("/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequestf(t, "GET", "/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, branch).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
branch := DecodeJSON(t, resp, &api.Branch{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *branch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/v1/repos/%s/%s/contents/%s", ctx.Username, ctx.Reponame, treepath), &options).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
contents := DecodeJSON(t, resp, &api.FileResponse{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequestWithJSON(t, "POST", "/v1/orgs", &options).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
contents := DecodeJSON(t, resp, &api.Organization{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/v1/orgs/%s/repos", orgName), &options).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
contents := DecodeJSON(t, resp, &api.Repository{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/v1/orgs/%s/teams", orgName), &options).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
contents := DecodeJSON(t, resp, &api.Team{})
|
|
if len(callback) > 0 {
|
|
callback[0](t, *contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequest(t, "PUT", fmt.Sprintf("/v1/teams/%d/members/%s", teamID, username)).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
req := NewRequest(t, "PUT", fmt.Sprintf("/v1/teams/%d/repos/%s/%s", teamID, orgName, repoName)).
|
|
AddTokenAuth(ctx.Token)
|
|
if ctx.ExpectedCode != 0 {
|
|
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
return
|
|
}
|
|
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
}
|