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]>
352 lines
16 KiB
Go
352 lines
16 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
auth_model "github.com/hanzoai/git/models/auth"
|
|
"github.com/hanzoai/git/models/unittest"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/process"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/test"
|
|
"github.com/hanzoai/git/tests"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestGPGGit(t *testing.T) {
|
|
tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring
|
|
err := os.Chmod(tmpDir, 0o700)
|
|
assert.NoError(t, err)
|
|
|
|
t.Setenv("GNUPGHOME", tmpDir)
|
|
|
|
// Need to create a root key
|
|
rootKeyPair, err := importTestingKey()
|
|
require.NoError(t, err, "importTestingKey")
|
|
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "forge")()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "[email protected]")()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})()
|
|
|
|
testGitSigning(t)
|
|
}
|
|
|
|
func TestSSHGit(t *testing.T) {
|
|
tmpDir := t.TempDir() // use a temp dir to store the SSH keys
|
|
err := os.Chmod(tmpDir, 0o700)
|
|
assert.NoError(t, err)
|
|
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
require.NoError(t, err, "ed25519.GenerateKey")
|
|
sshPubKey, err := ssh.NewPublicKey(pub)
|
|
require.NoError(t, err, "ssh.NewPublicKey")
|
|
|
|
err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600)
|
|
require.NoError(t, err, "os.WriteFile id_ed25519.pub")
|
|
block, err := ssh.MarshalPrivateKey(priv, "")
|
|
require.NoError(t, err, "ssh.MarshalPrivateKey")
|
|
err = os.WriteFile(tmpDir+"/id_ed25519", pem.EncodeToMemory(block), 0o600)
|
|
require.NoError(t, err, "os.WriteFile id_ed25519")
|
|
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, tmpDir+"/id_ed25519.pub")()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "forge")()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "[email protected]")()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningFormat, "ssh")()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})()
|
|
defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})()
|
|
|
|
testGitSigning(t)
|
|
}
|
|
|
|
func testGitSigning(t *testing.T) {
|
|
username := "user2"
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
|
baseAPITestContext := NewAPITestContext(t, username, "repo1")
|
|
|
|
onGitRun(t, func(t *testing.T, u *url.URL) {
|
|
u.Path = baseAPITestContext.GitPath()
|
|
|
|
t.Run("Unsigned-Initial", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
|
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
assert.NotNil(t, branch.Commit)
|
|
assert.NotNil(t, branch.Commit.Verification)
|
|
assert.False(t, branch.Commit.Verification.Verified)
|
|
assert.Empty(t, branch.Commit.Verification.Signature)
|
|
}))
|
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
|
|
assert.False(t, response.Verification.Verified)
|
|
}))
|
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
|
|
assert.False(t, response.Verification.Verified)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
|
t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
|
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
|
|
assert.False(t, response.Verification.Verified)
|
|
}))
|
|
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
|
t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
|
|
assert.False(t, response.Verification.Verified)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"never"}
|
|
t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
|
|
assert.False(t, response.Verification.Verified)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"always"}
|
|
t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
|
|
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
|
|
require.NotNil(t, response.Verification, "no verification provided with response! %v", response)
|
|
require.True(t, response.Verification.Verified)
|
|
assert.Equal(t, "[email protected]", response.Verification.Signer.Email)
|
|
}))
|
|
t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile(
|
|
t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
|
|
require.NotNil(t, response.Verification, "no verification provided with response! %v", response)
|
|
require.True(t, response.Verification.Verified)
|
|
assert.Equal(t, "[email protected]", response.Verification.Signer.Email)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
|
t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
|
|
t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
|
|
require.NotNil(t, response.Verification, "no verification provided with response! %v", response)
|
|
require.True(t, response.Verification.Verified)
|
|
assert.Equal(t, "[email protected]", response.Verification.Signer.Email)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.InitialCommit = []string{"always"}
|
|
t.Run("AlwaysSign-Initial", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
|
|
t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch)
|
|
require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit)
|
|
require.True(t, branch.Commit.Verification.Verified)
|
|
assert.Equal(t, "[email protected]", branch.Commit.Verification.Signer.Email)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"never"}
|
|
t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
|
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
|
|
assert.False(t, response.Verification.Verified)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
|
t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
|
|
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
|
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
|
|
require.True(t, response.Verification.Verified)
|
|
assert.Equal(t, "[email protected]", response.Verification.Signer.Email)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.CRUDActions = []string{"always"}
|
|
t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
|
|
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
|
|
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
|
|
require.True(t, response.Verification.Verified)
|
|
assert.Equal(t, "[email protected]", response.Verification.Signer.Email)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
|
t.Run("UnsignedMerging", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
|
|
assert.NoError(t, err)
|
|
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
|
})
|
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
assert.NotNil(t, branch.Commit)
|
|
assert.NotNil(t, branch.Commit.Verification)
|
|
assert.False(t, branch.Commit.Verification.Verified)
|
|
assert.Empty(t, branch.Commit.Verification.Signature)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.Merges = []string{"basesigned"}
|
|
t.Run("BaseSignedMerging", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
|
|
assert.NoError(t, err)
|
|
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
|
})
|
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
assert.NotNil(t, branch.Commit)
|
|
assert.NotNil(t, branch.Commit.Verification)
|
|
assert.False(t, branch.Commit.Verification.Verified)
|
|
assert.Empty(t, branch.Commit.Verification.Signature)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
|
t.Run("CommitsSignedMerging", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
|
|
assert.NoError(t, err)
|
|
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
|
})
|
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
assert.NotNil(t, branch.Commit)
|
|
assert.NotNil(t, branch.Commit.Verification)
|
|
assert.True(t, branch.Commit.Verification.Verified)
|
|
}))
|
|
})
|
|
|
|
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
|
setting.Repository.Signing.CRUDActions = []string{"always"}
|
|
t.Run("UpdateRebaseSigned", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
testCtx := NewAPITestContext(t, username, "update-rebase-signed", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
|
|
|
|
var repoID int64
|
|
t.Run("GetRepository", doAPIGetRepository(testCtx, func(t *testing.T, repo api.Repository) {
|
|
repoID = repo.ID
|
|
}))
|
|
enableRepoAllowUpdateWithRebase(t, repoID, true)
|
|
|
|
t.Run("CreateFeatureCommit", crudActionCreateFile(
|
|
t, testCtx, user, "master", "feature", "signed-feature.txt"))
|
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "feature")(t)
|
|
require.NoError(t, err)
|
|
|
|
content := base64.StdEncoding.EncodeToString([]byte("update base"))
|
|
t.Run("UpdateBase", doAPICreateFile(testCtx, "signed-base.txt", &api.CreateFileOptions{
|
|
FileOptions: api.FileOptions{
|
|
BranchName: "master",
|
|
Message: "update base",
|
|
Author: api.Identity{
|
|
Name: user.FullName,
|
|
Email: user.Email,
|
|
},
|
|
Committer: api.Identity{
|
|
Name: user.FullName,
|
|
Email: user.Email,
|
|
},
|
|
},
|
|
ContentBase64: content,
|
|
}))
|
|
|
|
req := NewRequestf(t, "POST", "/v1/repos/%s/%s/pulls/%d/update?style=rebase", testCtx.Username, testCtx.Reponame, pr.Index).
|
|
AddTokenAuth(testCtx.Token)
|
|
testCtx.Session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
t.Run("CheckFeatureBranchSigned", doAPIGetBranch(testCtx, "feature", func(t *testing.T, branch api.Branch) {
|
|
require.NotNil(t, branch.Commit)
|
|
require.NotNil(t, branch.Commit.Verification)
|
|
assert.True(t, branch.Commit.Verification.Verified)
|
|
}))
|
|
})
|
|
})
|
|
}
|
|
|
|
func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
|
|
return doAPICreateFile(ctx, path, &api.CreateFileOptions{
|
|
FileOptions: api.FileOptions{
|
|
BranchName: from,
|
|
NewBranchName: to,
|
|
Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path),
|
|
Author: api.Identity{
|
|
Name: user.FullName,
|
|
Email: user.Email,
|
|
},
|
|
Committer: api.Identity{
|
|
Name: user.FullName,
|
|
Email: user.Email,
|
|
},
|
|
},
|
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte("This is new text for " + path)),
|
|
}, callback...)
|
|
}
|
|
|
|
func importTestingKey() (*openpgp.Entity, error) {
|
|
keyPath := filepath.Join(setting.GetGitTestSourceRoot(), "tests/integration/private-testing.key")
|
|
if _, _, err := process.GetManager().Exec("gpg --import "+keyPath, "gpg", "--import", keyPath); err != nil {
|
|
return nil, err
|
|
}
|
|
keyringFile, err := os.Open(keyPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer keyringFile.Close()
|
|
|
|
block, err := armor.Decode(keyringFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyring, err := openpgp.ReadKeyRing(block.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Keyring access failed: '%w'", err)
|
|
}
|
|
|
|
// There should only be one entity in this file.
|
|
return keyring[0], nil
|
|
}
|