gittuf/pkg/gitinterface/repository_test.go
Paulo Gomes 9e269f46fb
Introduce gitstore.Storer and make gittuf's core dependency-light
Restructure storage so gittuf verification can run over backends other
than the git binary (e.g. go-git):

- pkg/githash: concrete Git object hash, stdlib-only.
  gitinterface.Hash aliases it.
- pkg/gitstore: the single Storer interface (24 methods) that all
  storage consumers program against, plus the shared
  ErrReferenceNotFound sentinel. *gitinterface.Repository satisfies it
  structurally (compile-time asserted). Also defines ConfigKey, the
  canonical type for the Git config settings gittuf reads.
- pkg/rsl (from internal/rsl): entry model, codec, and readers over
  gitstore.Storer; zero gitinterface/sigstore dependencies. rsl.Hash
  aliases githash.Hash; nil is the unset-Hash sentinel and IsZero
  matches nil and empty as well as both format zeros (no
  object-format-unaware ZeroHash). Entry commits (empty tree on the
  RSL ref) are owned by the package; no storer adapter.
- internal/signerverifier/gitobject: verifies commit/tag signatures
  over (payload, signature) bytes, Rekor URL as an option. The storage
  half is Repository.GetObjectSignature. Removes sigstore, cosign, and
  gitsign from gitinterface's dependency tree.
- internal/propagation: propagation workflow, moved off pkg/rsl's
  public API (its tuf directive types are internal).
- internal/{attestations,cache,policy}: storage via gitstore.Storer;
  tree writing via WriteTree(blobs, subtrees).

Breaking changes to pkg/gitinterface: Repository.VerifySignature and
the verification sentinels are removed (use gitobject.Verify);
ErrReferenceNotFound now aliases gitstore's. Repository.GetGitConfig
(which returned the whole config map) is replaced by
LookupConfig(gitstore.ConfigKey), returning a single setting's value.
Policy resolves the Rekor override from git config once per
verification and extracts signed payloads once per object instead of
per key attempt.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
2026-08-03 21:27:49 +01:00

306 lines
9 KiB
Go

// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0
package gitinterface
import (
"os"
"path/filepath"
"sync"
"testing"
"github.com/gittuf/gittuf/pkg/gitstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var _ gitstore.Storer = (*Repository)(nil)
func TestErrReferenceNotFoundIsGitstoreSentinel(t *testing.T) {
t.Parallel()
assert.ErrorIs(t, ErrReferenceNotFound, gitstore.ErrReferenceNotFound)
}
func TestRepository(t *testing.T) {
t.Run("repository.isBare", func(t *testing.T) {
t.Run("bare=true", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, true)
assert.True(t, repo.IsBare())
})
t.Run("bare=false", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
assert.False(t, repo.IsBare())
})
})
t.Run("with specified path, not bare", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, false)
repo, err := LoadRepository(tmpDir)
assert.Nil(t, err)
expectedPath, err := filepath.EvalSymlinks(filepath.Join(tmpDir, ".git"))
require.Nil(t, err)
actualPath, err := filepath.EvalSymlinks(repo.GetGitDir())
require.Nil(t, err)
assert.Equal(t, expectedPath, actualPath)
})
t.Run("with specified path, is bare", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, true)
repo, err := LoadRepository(tmpDir)
assert.Nil(t, err)
expectedPath, err := filepath.EvalSymlinks(tmpDir)
require.Nil(t, err)
actualPath, err := filepath.EvalSymlinks(repo.GetGitDir())
require.Nil(t, err)
assert.Equal(t, expectedPath, actualPath)
})
t.Run("empty path", func(t *testing.T) {
_, err := LoadRepository("")
assert.ErrorIs(t, err, ErrRepositoryPathNotSpecified)
})
t.Run("invalid path", func(t *testing.T) {
tmpDir := t.TempDir()
if _, has, err := findGitDirPath(tmpDir); err == nil && has {
tmpDir = filepath.Join(tmpDir, "invalid-repository")
require.Nil(t, os.Mkdir(tmpDir, 0o700))
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, ".git"), []byte("not a gitdir file"), 0o600))
}
_, err := LoadRepository(tmpDir)
assert.Error(t, err)
})
}
func TestEnsureNoCompatObjectFormat(t *testing.T) {
t.Run("no compat object format", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
assert.Nil(t, repo.ensureNoCompatObjectFormat())
})
t.Run("compat object format", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
require.Nil(t, repo.SetGitConfig("extensions.compatObjectFormat", "sha1"))
assert.ErrorIs(t, repo.ensureNoCompatObjectFormat(), ErrCompatObjectFormatUnsupported)
_, err := LoadRepository(tmpDir)
assert.ErrorIs(t, err, ErrCompatObjectFormatUnsupported)
})
t.Run("missing config", func(t *testing.T) {
repo := &Repository{gitDirPath: t.TempDir()}
err := repo.ensureNoCompatObjectFormat()
assert.ErrorContains(t, err, "unable to read repository config")
})
t.Run("invalid config", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), []byte("[extensions\n"), 0o600))
repo := &Repository{gitDirPath: tmpDir}
err := repo.ensureNoCompatObjectFormat()
assert.ErrorContains(t, err, "unable to parse repository config")
})
}
func TestFindGitDirPath(t *testing.T) {
t.Run("worktree git directory", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, false)
nestedDir := filepath.Join(tmpDir, "nested", "dir")
require.Nil(t, os.MkdirAll(nestedDir, 0o700))
gitDirPath, has, err := findGitDirPath(nestedDir)
require.Nil(t, err)
assert.True(t, has)
assert.Equal(t, filepath.Join(tmpDir, ".git"), gitDirPath)
})
t.Run("bare git directory", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, true)
gitDirPath, has, err := findGitDirPath(tmpDir)
require.Nil(t, err)
assert.True(t, has)
assert.Equal(t, tmpDir, gitDirPath)
})
t.Run("gitdir file", func(t *testing.T) {
tmpDir := t.TempDir()
worktreePath := filepath.Join(tmpDir, "worktree")
require.Nil(t, os.MkdirAll(worktreePath, 0o700))
gitDirPath := filepath.Join(tmpDir, "actual.git")
require.Nil(t, os.WriteFile(filepath.Join(worktreePath, ".git"), []byte("gitdir: ../actual.git\n"), 0o600))
gotGitDirPath, has, err := findGitDirPath(worktreePath)
require.Nil(t, err)
assert.True(t, has)
assert.Equal(t, gitDirPath, gotGitDirPath)
})
t.Run("invalid gitdir file", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, ".git"), []byte("not a gitdir file"), 0o600))
_, has, err := findGitDirPath(tmpDir)
assert.False(t, has)
assert.ErrorContains(t, err, "invalid gitdir file")
})
t.Run("no git directory", func(t *testing.T) {
tmpDir := t.TempDir()
if _, has, err := findGitDirPath(tmpDir); err == nil && has {
tmpDir = "/dev"
if _, has, err := findGitDirPath(tmpDir); err != nil || has {
t.Skip("unable to find a filesystem path outside a Git repository")
}
}
_, has, err := findGitDirPath(tmpDir)
require.Nil(t, err)
assert.False(t, has)
})
}
func TestReadGitDirFile(t *testing.T) {
t.Run("absolute gitdir path", func(t *testing.T) {
tmpDir := t.TempDir()
gitDirPath := filepath.Join(tmpDir, "actual.git")
gitDirFilePath := filepath.Join(tmpDir, ".git")
require.Nil(t, os.WriteFile(gitDirFilePath, []byte("gitdir: "+gitDirPath+"\n"), 0o600))
gotGitDirPath, err := readGitDirFile(gitDirFilePath, tmpDir)
require.Nil(t, err)
assert.Equal(t, gitDirPath, gotGitDirPath)
})
t.Run("missing gitdir file", func(t *testing.T) {
_, err := readGitDirFile(filepath.Join(t.TempDir(), ".git"), t.TempDir())
assert.Error(t, err)
})
}
func TestIsBareGitDir(t *testing.T) {
t.Run("config without head", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), nil, 0o600))
assert.False(t, isBareGitDir(tmpDir))
})
t.Run("head as directory", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), nil, 0o600))
require.Nil(t, os.Mkdir(filepath.Join(tmpDir, "HEAD"), 0o700))
assert.False(t, isBareGitDir(tmpDir))
})
}
func TestRepositoryObjectFormat(t *testing.T) {
t.Run("sha1", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(ObjectFormatSHA1))
assert.Equal(t, ObjectFormatSHA1, repo.GetObjectFormat())
assert.Equal(t, "0000000000000000000000000000000000000000", repo.ZeroHash().String())
loaded, err := LoadRepository(tmpDir)
require.Nil(t, err)
assert.Equal(t, ObjectFormatSHA1, loaded.GetObjectFormat())
})
t.Run("sha256", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
assert.Equal(t, ObjectFormatSHA256, repo.GetObjectFormat())
assert.Equal(t, "0000000000000000000000000000000000000000000000000000000000000000", repo.ZeroHash().String())
loaded, err := LoadRepository(tmpDir)
require.Nil(t, err)
assert.Equal(t, ObjectFormatSHA256, loaded.GetObjectFormat())
})
t.Run("GetGoGitRepository", func(t *testing.T) {
for _, bare := range []bool{false, true} {
name := "worktree"
if bare {
name = "bare"
}
t.Run(name, func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, bare)
ggr, err := repo.GetGoGitRepository()
require.NoError(t, err, "GetGoGitRepository on %s repo", name)
_, err = ggr.Head()
// Empty repo: ErrReferenceNotFound is fine; what matters is the
// repo opened. Anything else (esp. ErrRepositoryNotExists) is a
// failure to open the storage.
if err != nil {
assert.ErrorContains(t, err, "reference not found")
}
})
}
})
t.Run("GetGoGitRepository on .git-suffixed bare repo", func(t *testing.T) {
// Forge-style bare repos are conventionally named <name>.git.
dir := filepath.Join(t.TempDir(), "demo.git")
repo := CreateTestGitRepository(t, dir, true)
_, err := repo.GetGoGitRepository()
require.NoError(t, err)
})
}
func TestLoadRepositoryConcurrent(t *testing.T) {
// LoadRepository must not change the process working directory; concurrent
// callers for different paths must each resolve their own gitDirPath.
dirA := t.TempDir()
dirB := t.TempDir()
_ = CreateTestGitRepository(t, dirA, true)
_ = CreateTestGitRepository(t, dirB, false)
wantA, err := filepath.EvalSymlinks(dirA)
require.NoError(t, err)
wantB, err := filepath.EvalSymlinks(filepath.Join(dirB, ".git"))
require.NoError(t, err)
const iterations = 20
var wg sync.WaitGroup
for i := 0; i < iterations; i++ {
wg.Add(2)
go func() {
defer wg.Done()
r, err := LoadRepository(dirA)
if assert.NoError(t, err) {
assert.Equal(t, wantA, r.GetGitDir())
}
}()
go func() {
defer wg.Done()
r, err := LoadRepository(dirB)
if assert.NoError(t, err) {
assert.Equal(t, wantB, r.GetGitDir())
}
}()
}
wg.Wait()
}