diff --git a/pkg/gitinterface/blob_test.go b/pkg/gitinterface/blob_test.go index b3b864e5..c4332ac1 100644 --- a/pkg/gitinterface/blob_test.go +++ b/pkg/gitinterface/blob_test.go @@ -32,7 +32,16 @@ func TestRepositoryReadBlob(t *testing.T) { t.Run("read non-existing blob", func(t *testing.T) { _, err := repo.ReadBlob(ZeroHash) - assert.NotNil(t, err) + assert.ErrorContains(t, err, "unable to inspect if object is blob") + }) + + t.Run("read non-blob object", func(t *testing.T) { + treeBuilder := NewTreeBuilder(repo) + treeID, err := treeBuilder.WriteTreeFromEntries(nil) + require.Nil(t, err) + + _, err = repo.ReadBlob(treeID) + assert.ErrorContains(t, err, "is not a blob object") }) } diff --git a/pkg/gitinterface/commit_test.go b/pkg/gitinterface/commit_test.go index 46f0db1e..e9ce6a16 100644 --- a/pkg/gitinterface/commit_test.go +++ b/pkg/gitinterface/commit_test.go @@ -18,7 +18,9 @@ import ( artifacts "github.com/gittuf/gittuf/internal/testartifacts" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/secure-systems-lab/go-securesystemslib/signerverifier" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepositoryCommit(t *testing.T) { @@ -247,6 +249,12 @@ func TestRepositoryVerifyCommit(t *testing.T) { err = repo.verifyCommitSignature(context.Background(), gitsignSignedCommitID, sshKey) assert.ErrorIs(t, err, ErrIncorrectVerificationKey) }) + + t.Run("unknown signing method", func(t *testing.T) { + unknownKey := &signerverifier.SSLibKey{KeyType: "unknown"} + err = repo.verifyCommitSignature(t.Context(), sshSignedCommitID, unknownKey) + assert.ErrorIs(t, err, ErrUnknownSigningMethod) + }) } func TestKnowsCommit(t *testing.T) { @@ -304,6 +312,22 @@ func TestKnowsCommit(t *testing.T) { knows, _ := repo.KnowsCommit(unknownCommitID, firstCommitID) assert.False(t, knows) }) + + t.Run("non-commit object as first arg", func(t *testing.T) { + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + _, err = repo.KnowsCommit(blobID, firstCommitID) + assert.ErrorContains(t, err, "is not a commit object") + }) + + t.Run("non-commit object as second arg", func(t *testing.T) { + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + _, err = repo.KnowsCommit(firstCommitID, blobID) + assert.ErrorContains(t, err, "is not a commit object") + }) } func createTestGPGSignedCommit(t *testing.T, repo *Repository) Hash { @@ -457,6 +481,14 @@ func TestRepositoryGetCommitMessage(t *testing.T) { commitMessage, err := repo.GetCommitMessage(commit) assert.Nil(t, err) assert.Equal(t, message, commitMessage) + + t.Run("non-commit object", func(t *testing.T) { + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + _, err = repo.GetCommitMessage(blobID) + assert.ErrorContains(t, err, "is not a commit object") + }) } func TestGetCommitTreeID(t *testing.T) { @@ -501,6 +533,14 @@ func TestGetCommitTreeID(t *testing.T) { secondCommitTreeID, err := repo.GetCommitTreeID(secondCommitID) assert.Nil(t, err) assert.Equal(t, treeWithContentsID, secondCommitTreeID) + + t.Run("non-commit object", func(t *testing.T) { + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + _, err = repo.GetCommitTreeID(blobID) + assert.ErrorContains(t, err, "is not a commit object") + }) } func TestGetCommitParentIDs(t *testing.T) { @@ -537,6 +577,11 @@ func TestGetCommitParentIDs(t *testing.T) { secondCommitParentIDs, err := repo.GetCommitParentIDs(secondCommitID) assert.Nil(t, err) assert.Equal(t, []Hash{initialCommitID}, secondCommitParentIDs) + + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + _, err = repo.GetCommitParentIDs(blobID) + assert.ErrorContains(t, err, "is not a commit object") } func TestGetCommonAncestor(t *testing.T) { @@ -577,4 +622,50 @@ func TestGetCommonAncestor(t *testing.T) { _, err = repo.GetCommonAncestor(commitDisconnected, commitA) assert.NotNil(t, err) + + t.Run("non-commit object as first arg", func(t *testing.T) { + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + _, err = repo.GetCommonAncestor(blobID, commitA) + assert.ErrorContains(t, err, "is not a commit object") + }) + + t.Run("non-commit object as second arg", func(t *testing.T) { + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + _, err = repo.GetCommonAncestor(commitA, blobID) + assert.ErrorContains(t, err, "is not a commit object") + }) +} + +func TestEnsureIsCommit(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, false) + treeBuilder := NewTreeBuilder(repo) + + emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil) + require.Nil(t, err) + + commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false) + require.Nil(t, err) + + blobID, err := repo.WriteBlob([]byte("test")) + require.Nil(t, err) + + t.Run("valid commit", func(t *testing.T) { + err := repo.ensureIsCommit(commitID) + assert.Nil(t, err) + }) + + t.Run("non-commit object", func(t *testing.T) { + err := repo.ensureIsCommit(blobID) + assert.ErrorContains(t, err, "is not a commit object") + }) + + t.Run("non-existent object", func(t *testing.T) { + err := repo.ensureIsCommit(ZeroHash) + assert.ErrorContains(t, err, "unable to inspect if object is commit") + }) } diff --git a/pkg/gitinterface/config_test.go b/pkg/gitinterface/config_test.go index 2a5a28e8..48e9b028 100644 --- a/pkg/gitinterface/config_test.go +++ b/pkg/gitinterface/config_test.go @@ -71,4 +71,12 @@ func TestSetGitConfig(t *testing.T) { require.NoError(t, err) assert.Equal(t, "", config["gpg.format"]) }) + + t.Run("invalid key", func(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, false) + + err := repo.SetGitConfig("", "value") + assert.ErrorContains(t, err, "unable to set") + }) } diff --git a/pkg/gitinterface/object_test.go b/pkg/gitinterface/object_test.go index 976e5747..ca872d8e 100644 --- a/pkg/gitinterface/object_test.go +++ b/pkg/gitinterface/object_test.go @@ -98,6 +98,9 @@ func TestGetObjectType(t *testing.T) { objType, err = repo.GetObjectType(tagID) assert.Nil(t, err) assert.Equal(t, TagObjectType, objType) + + _, err = repo.GetObjectType(ZeroHash) + assert.ErrorContains(t, err, "unable to inspect object type") } func TestGetObjectSize(t *testing.T) { @@ -110,4 +113,7 @@ func TestGetObjectSize(t *testing.T) { objSize, err := repo.GetObjectSize(blobID) assert.Nil(t, err) assert.Equal(t, uint64(6), objSize) + + _, err = repo.GetObjectSize(ZeroHash) + assert.ErrorContains(t, err, "unable to inspect object size") } diff --git a/pkg/gitinterface/references_test.go b/pkg/gitinterface/references_test.go index 799f1ea0..d25118b4 100644 --- a/pkg/gitinterface/references_test.go +++ b/pkg/gitinterface/references_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + artifacts "github.com/gittuf/gittuf/internal/testartifacts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -389,3 +390,74 @@ func TestRemoteReferenceName(t *testing.T) { assert.Equal(t, test.expected, referenceName, fmt.Sprintf("unexpected remote reference for input %s", name)) } } + +func TestAbsoluteReference(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, false) + treeBuilder := NewTreeBuilder(repo) + + emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil) + require.Nil(t, err) + + commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false) + require.Nil(t, err) + + _, err = repo.TagUsingSpecificKey(commitID, "v1.0", "v1.0\n", artifacts.SSHED25519Private) + require.Nil(t, err) + + err = repo.SetReference("refs/custom/myref", commitID) + require.Nil(t, err) + + err = repo.SetReference("refs/remotes/origin/main", commitID) + require.Nil(t, err) + + err = repo.SetSymbolicReference("refs/remotes/origin/HEAD", "refs/remotes/origin/main") + require.Nil(t, err) + + t.Run("symbolic ref HEAD", func(t *testing.T) { + ref, err := repo.AbsoluteReference("HEAD") + assert.Nil(t, err) + assert.Equal(t, "refs/heads/main", ref) + }) + + t.Run("fully qualified branch", func(t *testing.T) { + ref, err := repo.AbsoluteReference("refs/heads/main") + assert.Nil(t, err) + assert.Equal(t, "refs/heads/main", ref) + }) + + t.Run("short branch name", func(t *testing.T) { + ref, err := repo.AbsoluteReference("main") + assert.Nil(t, err) + assert.Equal(t, "refs/heads/main", ref) + }) + + t.Run("tag name", func(t *testing.T) { + ref, err := repo.AbsoluteReference("v1.0") + assert.Nil(t, err) + assert.Equal(t, "refs/tags/v1.0", ref) + }) + + t.Run("custom ref", func(t *testing.T) { + ref, err := repo.AbsoluteReference("custom/myref") + assert.Nil(t, err) + assert.Equal(t, "refs/custom/myref", ref) + }) + + t.Run("remote tracking ref", func(t *testing.T) { + ref, err := repo.AbsoluteReference("origin/main") + assert.Nil(t, err) + assert.Equal(t, "refs/remotes/origin/main", ref) + }) + + t.Run("remote HEAD", func(t *testing.T) { + ref, err := repo.AbsoluteReference("origin") + assert.Nil(t, err) + assert.Equal(t, "refs/remotes/origin/HEAD", ref) + }) + + t.Run("non-existent ref", func(t *testing.T) { + _, err := repo.AbsoluteReference("nonexistent") + assert.ErrorIs(t, err, ErrReferenceNotFound) + }) +} diff --git a/pkg/gitinterface/status_test.go b/pkg/gitinterface/status_test.go index d5e16a8b..306d2fbd 100644 --- a/pkg/gitinterface/status_test.go +++ b/pkg/gitinterface/status_test.go @@ -139,3 +139,85 @@ func TestStatus(t *testing.T) { assert.Nil(t, err) assert.Empty(t, statuses) } + +func TestStatusError(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, true) // bare repo + + _, err := repo.Status() + assert.ErrorContains(t, err, "unable to check status of repository") +} +func TestNewStatusCodeFromByte(t *testing.T) { + t.Run("invalid status code", func(t *testing.T) { + _, err := NewStatusCodeFromByte('X') + assert.ErrorIs(t, err, ErrInvalidStatusCode) + }) + + t.Run("valid status codes", func(t *testing.T) { + testCases := []struct { + input byte + expected StatusCode + }{ + {' ', StatusCodeUnmodified}, + {'M', StatusCodeModified}, + {'T', StatusCodeTypeChanged}, + {'A', StatusCodeAdded}, + {'D', StatusCodeDeleted}, + {'C', StatusCodeCopied}, + {'U', StatusCodeUpdatedUnmerged}, + {'?', StatusCodeUntracked}, + {'!', StatusCodeIgnored}, + } + + for _, tc := range testCases { + code, err := NewStatusCodeFromByte(tc.input) + assert.Nil(t, err) + assert.Equal(t, tc.expected, code) + } + }) +} + +func TestStatusCodeString(t *testing.T) { + t.Run("invalid status code", func(t *testing.T) { + assert.Equal(t, "invalid-code", StatusCode(999).String()) + }) + + t.Run("valid status codes", func(t *testing.T) { + testCases := []struct { + code StatusCode + expected string + }{ + {StatusCodeUnmodified, " "}, + {StatusCodeModified, "M"}, + {StatusCodeTypeChanged, "T"}, + {StatusCodeAdded, "A"}, + {StatusCodeDeleted, "D"}, + {StatusCodeRenamed, "R"}, + {StatusCodeCopied, "C"}, + {StatusCodeUpdatedUnmerged, "U"}, + {StatusCodeUntracked, "?"}, + {StatusCodeIgnored, "!"}, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expected, tc.code.String()) + } + }) +} + +func TestFileStatusUntracked(t *testing.T) { + t.Run("untracked when X is untracked", func(t *testing.T) { + fs := FileStatus{X: StatusCodeUntracked, Y: StatusCodeUnmodified} + assert.True(t, fs.Untracked()) + }) + + t.Run("untracked when Y is untracked", func(t *testing.T) { + fs := FileStatus{X: StatusCodeUnmodified, Y: StatusCodeUntracked} + assert.True(t, fs.Untracked()) + }) + + t.Run("not untracked when neither is untracked", func(t *testing.T) { + fs := FileStatus{X: StatusCodeModified, Y: StatusCodeUnmodified} + assert.False(t, fs.Untracked()) + }) +} diff --git a/pkg/gitinterface/sync_test.go b/pkg/gitinterface/sync_test.go index 0c696be0..93f6f2c2 100644 --- a/pkg/gitinterface/sync_test.go +++ b/pkg/gitinterface/sync_test.go @@ -153,6 +153,14 @@ func TestPushRefSpecRepository(t *testing.T) { err = localRepo.PushRefSpec(remoteName, []string{refSpecs}) assert.Nil(t, err) }) + + t.Run("push to non-existent remote", func(t *testing.T) { + localTmpDir := t.TempDir() + localRepo := CreateTestGitRepository(t, localTmpDir, false) + + err := localRepo.PushRefSpec("nonexistent", []string{refSpecs}) + assert.ErrorContains(t, err, "unable to push") + }) } func TestPushRepository(t *testing.T) { @@ -294,6 +302,22 @@ func TestPushRepository(t *testing.T) { err = localRepo.Push(remoteName, []string{refName}) assert.Nil(t, err) }) + + t.Run("push to non-existent remote", func(t *testing.T) { + localTmpDir := t.TempDir() + localRepo := CreateTestGitRepository(t, localTmpDir, false) + + err := localRepo.Push("nonexistent", []string{refName}) + assert.ErrorContains(t, err, "unable to push") + }) + + t.Run("push with invalid ref", func(t *testing.T) { + localTmpDir := t.TempDir() + localRepo := CreateTestGitRepository(t, localTmpDir, false) + + err := localRepo.Push("origin", []string{"nonexistent-ref"}) + assert.ErrorIs(t, err, ErrReferenceNotFound) + }) } func TestFetchRefSpecRepository(t *testing.T) { @@ -443,6 +467,14 @@ func TestFetchRefSpecRepository(t *testing.T) { require.Nil(t, err) assert.Equal(t, localRef, newLocalRef) }) + + t.Run("fetch from non-existent remote", func(t *testing.T) { + localTmpDir := t.TempDir() + localRepo := CreateTestGitRepository(t, localTmpDir, true) + + err := localRepo.FetchRefSpec("nonexistent", []string{refSpecs}) + assert.ErrorContains(t, err, "unable to fetch") + }) } func TestFetchRepository(t *testing.T) { @@ -591,6 +623,27 @@ func TestFetchRepository(t *testing.T) { require.Nil(t, err) assert.Equal(t, localRef, newLocalRef) }) + + t.Run("fetch from non-existent remote", func(t *testing.T) { + localTmpDir := t.TempDir() + localRepo := CreateTestGitRepository(t, localTmpDir, true) + + err := localRepo.Fetch("nonexistent", []string{refName}, true) + assert.ErrorContains(t, err, "unable to fetch") + }) + + t.Run("fetch with invalid ref", func(t *testing.T) { + localTmpDir := t.TempDir() + remoteTmpDir := t.TempDir() + localRepo := CreateTestGitRepository(t, localTmpDir, true) + _ = CreateTestGitRepository(t, remoteTmpDir, false) + + err := localRepo.CreateRemote(remoteName, remoteTmpDir) + require.Nil(t, err) + + err = localRepo.Fetch(remoteName, []string{"nonexistent-ref"}, true) + assert.ErrorIs(t, err, ErrReferenceNotFound) + }) } func TestFetchObject(t *testing.T) { @@ -617,6 +670,11 @@ func TestFetchObject(t *testing.T) { has = downstreamRepo.HasObject(commitID) assert.True(t, has) + + t.Run("fetch from non-existent remote", func(t *testing.T) { + err := downstreamRepo.FetchObject("nonexistent", ZeroHash) + assert.ErrorContains(t, err, "unable to fetch object") + }) } func TestCloneAndFetchRepository(t *testing.T) { @@ -913,3 +971,13 @@ func TestCloneAndFetchRepository(t *testing.T) { assert.Equal(t, "FETCH_HEAD", dirEntries[0].Name()) }) } + +func TestCreateRemote(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, false) + err := repo.CreateRemote("origin", tmpDir) + assert.Nil(t, err) + + err = repo.CreateRemote("origin", tmpDir) + assert.ErrorContains(t, err, "unable to add remote") +} diff --git a/pkg/gitinterface/tag_test.go b/pkg/gitinterface/tag_test.go index bf4faa0b..ebcb7b54 100644 --- a/pkg/gitinterface/tag_test.go +++ b/pkg/gitinterface/tag_test.go @@ -12,7 +12,9 @@ import ( "github.com/gittuf/gittuf/internal/signerverifier/gpg" "github.com/gittuf/gittuf/internal/signerverifier/ssh" artifacts "github.com/gittuf/gittuf/internal/testartifacts" + "github.com/secure-systems-lab/go-securesystemslib/signerverifier" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetTagTarget(t *testing.T) { @@ -40,6 +42,11 @@ func TestGetTagTarget(t *testing.T) { targetID, err := repo.GetTagTarget(tagID) assert.Nil(t, err) assert.Equal(t, commitID, targetID) + + t.Run("non-existent tag", func(t *testing.T) { + _, err := repo.GetTagTarget(ZeroHash) + assert.ErrorContains(t, err, "unable to resolve tag's target ID") + }) } func TestRepositoryVerifyTag(t *testing.T) { @@ -92,4 +99,31 @@ func TestRepositoryVerifyTag(t *testing.T) { err = repo.verifyTagSignature(context.Background(), gpgSignedTag, gpgKey) assert.Nil(t, err) }) + + t.Run("unknown signing method", func(t *testing.T) { + unknownKey := &signerverifier.SSLibKey{KeyType: "unknown"} + err = repo.verifyTagSignature(context.Background(), gpgSignedTag, unknownKey) + assert.ErrorIs(t, err, ErrUnknownSigningMethod) + }) +} + +func TestEnsureIsTag(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, false) + treeBuilder := NewTreeBuilder(repo) + + emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil) + require.Nil(t, err) + + commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false) + require.Nil(t, err) + + tagID, err := repo.TagUsingSpecificKey(commitID, "test-tag", "test-tag\n", artifacts.SSHED25519Private) + require.Nil(t, err) + + err = repo.ensureIsTag(tagID) + assert.Nil(t, err) + + err = repo.ensureIsTag(commitID) + assert.ErrorContains(t, err, "is not a tag object") } diff --git a/pkg/gitinterface/utils_test.go b/pkg/gitinterface/utils_test.go new file mode 100644 index 00000000..0e298125 --- /dev/null +++ b/pkg/gitinterface/utils_test.go @@ -0,0 +1,35 @@ +// Copyright The gittuf Authors +// SPDX-License-Identifier: Apache-2.0 +package gitinterface + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResetDueToError(t *testing.T) { + tmpDir := t.TempDir() + repo := CreateTestGitRepository(t, tmpDir, false) + + treeBuilder := NewTreeBuilder(repo) + emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil) + require.Nil(t, err) + + commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false) + require.Nil(t, err) + + t.Run("successful reset", func(t *testing.T) { + cause := assert.AnError + err := repo.ResetDueToError(cause, "refs/heads/main", commitID) + assert.ErrorIs(t, err, cause) + }) + + t.Run("invalid ref name", func(t *testing.T) { + cause := assert.AnError + err := repo.ResetDueToError(cause, "invalid ref with spaces", commitID) + assert.ErrorContains(t, err, "unable to reset") + assert.ErrorIs(t, err, cause) + }) +}