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]>
319 lines
12 KiB
Go
319 lines
12 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "github.com/hanzoai/git/models/auth"
|
|
issues_model "github.com/hanzoai/git/models/issues"
|
|
"github.com/hanzoai/git/models/perm"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/models/unit"
|
|
"github.com/hanzoai/git/models/unittest"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/gitrepo"
|
|
pull_service "github.com/hanzoai/git/services/pull"
|
|
repo_service "github.com/hanzoai/git/services/repository"
|
|
files_service "github.com/hanzoai/git/services/repository/files"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAPIPullUpdate(t *testing.T) {
|
|
onGitRun(t, func(t *testing.T, gitURL *url.URL) {
|
|
// Create PR to test
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
|
pr := createOutdatedPR(t, user, org26)
|
|
require.NoError(t, pr.LoadBaseRepo(t.Context()))
|
|
require.NoError(t, pr.LoadIssue(t.Context()))
|
|
|
|
// Test GetDiverging
|
|
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, diffCount.Behind)
|
|
assert.Equal(t, 1, diffCount.Ahead)
|
|
assert.Equal(t, diffCount.Behind, pr.CommitsBehind)
|
|
assert.Equal(t, diffCount.Ahead, pr.CommitsAhead)
|
|
|
|
session := loginUser(t, "user2")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
req := NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token)
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// Test GetDiverging after update
|
|
diffCount, err = gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, diffCount.Behind)
|
|
assert.Equal(t, 2, diffCount.Ahead)
|
|
assert.Eventually(t, func() bool {
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
return diffCount.Behind == pr.CommitsBehind && diffCount.Ahead == pr.CommitsAhead
|
|
}, 5*time.Second, 20*time.Millisecond)
|
|
})
|
|
}
|
|
|
|
// TestAPIPullUpdatePublicOnlyToken verifies that a public-only API token cannot
|
|
// update (push into) a PR whose head repo is private, even when the base repo
|
|
// named in the route is public.
|
|
func TestAPIPullUpdatePublicOnlyToken(t *testing.T) {
|
|
onGitRun(t, func(t *testing.T, gitURL *url.URL) {
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
|
pr := createOutdatedPR(t, user, org26)
|
|
require.NoError(t, pr.LoadBaseRepo(t.Context()))
|
|
require.NoError(t, pr.LoadHeadRepo(t.Context()))
|
|
require.NoError(t, pr.LoadIssue(t.Context()))
|
|
|
|
// make the head repo private while the base repo stays public
|
|
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(),
|
|
&repo_model.Repository{ID: pr.HeadRepo.ID, IsPrivate: true}, "is_private"))
|
|
|
|
// a public-only write token must be refused (404), not perform the push
|
|
publicOnlyToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopePublicOnly)
|
|
req := NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(publicOnlyToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// the head branch must still be outdated (no push happened)
|
|
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, diffCount.Behind)
|
|
|
|
// a normal write token still works, proving the guard is scope-specific
|
|
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository)
|
|
req = NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func updateRepoPullRequestConfig(t *testing.T, repoID int64, update func(*repo_model.PullRequestsConfig)) {
|
|
t.Helper()
|
|
|
|
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
|
|
update(repoUnit.PullRequestsConfig())
|
|
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
|
|
}
|
|
|
|
func enableRepoAllowUpdateWithRebase(t *testing.T, repoID int64, allow bool) {
|
|
updateRepoPullRequestConfig(t, repoID, func(c *repo_model.PullRequestsConfig) { c.AllowRebaseUpdate = allow })
|
|
}
|
|
|
|
// setupOutdatedPRWithConfig creates an outdated PR (user2 base, org26 fork), loads its
|
|
// base repo, head repo, and issue, then applies the supplied PullRequestsConfig mutation.
|
|
func setupOutdatedPRWithConfig(t *testing.T, mutate func(*repo_model.PullRequestsConfig)) *issues_model.PullRequest {
|
|
t.Helper()
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
|
pr := createOutdatedPR(t, user, org26)
|
|
require.NoError(t, pr.LoadBaseRepo(t.Context()))
|
|
require.NoError(t, pr.LoadHeadRepo(t.Context()))
|
|
require.NoError(t, pr.LoadIssue(t.Context()))
|
|
if mutate != nil {
|
|
updateRepoPullRequestConfig(t, pr.BaseRepo.ID, mutate)
|
|
}
|
|
return pr
|
|
}
|
|
|
|
func TestAPIPullUpdateByRebase(t *testing.T) {
|
|
onGitRun(t, func(t *testing.T, gitURL *url.URL) {
|
|
// Create PR to test
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
|
pr := createOutdatedPR(t, user, org26)
|
|
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
|
|
|
// Test GetDiverging
|
|
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, diffCount.Behind)
|
|
assert.Equal(t, 1, diffCount.Ahead)
|
|
assert.NoError(t, pr.LoadIssue(t.Context()))
|
|
|
|
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
|
|
|
|
session := loginUser(t, "user2")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
req := NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token)
|
|
session.MakeRequest(t, req, http.StatusForbidden)
|
|
|
|
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
|
|
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
|
|
|
// use a user which have write access to the pr but not write permission to the head repository to do the rebase
|
|
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
|
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite)
|
|
assert.NoError(t, err)
|
|
token40 := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req = NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token40)
|
|
session.MakeRequest(t, req, http.StatusForbidden)
|
|
|
|
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite)
|
|
assert.NoError(t, err)
|
|
|
|
req = NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token40)
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// Test GetDiverging after update
|
|
diffCount, err = gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, diffCount.Behind)
|
|
assert.Equal(t, 1, diffCount.Ahead)
|
|
})
|
|
}
|
|
|
|
// TestAPIPullUpdateStyleSettings first checks that a disabled explicit style is
|
|
// forbidden, then guards back-compat: even when the repo's DefaultUpdateStyle is
|
|
// rebase, an API call with no `style` parameter must still perform a merge
|
|
// update so existing API clients don't silently flip behavior.
|
|
func TestAPIPullUpdateStyleSettings(t *testing.T) {
|
|
onGitRun(t, func(t *testing.T, gitURL *url.URL) {
|
|
pr := setupOutdatedPRWithConfig(t, func(c *repo_model.PullRequestsConfig) {
|
|
c.AllowMergeUpdate = false
|
|
c.AllowRebaseUpdate = true
|
|
c.DefaultUpdateStyle = repo_model.UpdateStyleRebase
|
|
})
|
|
|
|
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
|
require.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite))
|
|
require.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite))
|
|
|
|
session := loginUser(t, "user40")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update?style=merge", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token)
|
|
session.MakeRequest(t, req, http.StatusForbidden)
|
|
|
|
updateRepoPullRequestConfig(t, pr.BaseRepo.ID, func(c *repo_model.PullRequestsConfig) {
|
|
c.AllowMergeUpdate = true
|
|
})
|
|
|
|
req = NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
|
AddTokenAuth(token)
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// merge update produces a merge commit on top of the head commit (Ahead=2),
|
|
// rebase update would replay the head commit alone (Ahead=1).
|
|
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, diffCount.Behind)
|
|
assert.Equal(t, 2, diffCount.Ahead)
|
|
})
|
|
}
|
|
|
|
func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
|
|
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
|
|
Name: "repo-pr-update",
|
|
Description: "repo-tmp-pr-update description",
|
|
AutoInit: true,
|
|
Gitignores: "C,C++",
|
|
License: "MIT",
|
|
Readme: "Default",
|
|
IsPrivate: false,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, baseRepo)
|
|
|
|
headRepo, err := repo_service.ForkRepository(t.Context(), actor, forkOrg, repo_service.ForkRepoOptions{
|
|
BaseRepo: baseRepo,
|
|
Name: "repo-pr-update",
|
|
Description: "desc",
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, headRepo)
|
|
|
|
// create a commit on base Repo
|
|
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, actor, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: "File_A",
|
|
ContentReader: strings.NewReader("File A"),
|
|
},
|
|
},
|
|
Message: "Add File A",
|
|
OldBranch: "master",
|
|
NewBranch: "master",
|
|
Author: &files_service.IdentityOptions{
|
|
GitUserName: actor.Name,
|
|
GitUserEmail: actor.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
GitUserName: actor.Name,
|
|
GitUserEmail: actor.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// create a commit on head Repo
|
|
_, err = files_service.ChangeRepoFiles(t.Context(), headRepo, actor, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: "File_B",
|
|
ContentReader: strings.NewReader("File B"),
|
|
},
|
|
},
|
|
Message: "Add File on PR branch",
|
|
OldBranch: "master",
|
|
NewBranch: "newBranch",
|
|
Author: &files_service.IdentityOptions{
|
|
GitUserName: actor.Name,
|
|
GitUserEmail: actor.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
GitUserName: actor.Name,
|
|
GitUserEmail: actor.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// create Pull
|
|
pullIssue := &issues_model.Issue{
|
|
RepoID: baseRepo.ID,
|
|
Title: "Test Pull -to-update-",
|
|
PosterID: actor.ID,
|
|
Poster: actor,
|
|
IsPull: true,
|
|
}
|
|
pullRequest := &issues_model.PullRequest{
|
|
HeadRepoID: headRepo.ID,
|
|
BaseRepoID: baseRepo.ID,
|
|
HeadBranch: "newBranch",
|
|
BaseBranch: "master",
|
|
HeadRepo: headRepo,
|
|
BaseRepo: baseRepo,
|
|
Type: issues_model.PullRequestNative,
|
|
}
|
|
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
|
err = pull_service.NewPullRequest(t.Context(), prOpts)
|
|
assert.NoError(t, err)
|
|
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
|
|
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
|
|
|
return issue.PullRequest
|
|
}
|