From b9326183b2b771d5e48fe4b7d858d5d75abcee50 Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Tue, 3 Mar 2026 15:37:32 -0500 Subject: [PATCH] test: satisfy core coverage gates for bundle and errors --- core/bundle/bundle_test.go | 202 +++++++++++++++++++++++++++++++++++++ core/errors/errors_test.go | 54 ++++++++++ 2 files changed, 256 insertions(+) create mode 100644 core/errors/errors_test.go diff --git a/core/bundle/bundle_test.go b/core/bundle/bundle_test.go index 6168e6d..a586cdb 100644 --- a/core/bundle/bundle_test.go +++ b/core/bundle/bundle_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "runtime" "testing" "github.com/Clyra-AI/proof/core/signing" @@ -73,3 +74,204 @@ func TestSignManifestCosignRequiresKeyPath(t *testing.T) { _, err := SignManifestCosign(Manifest{Files: []ManifestEntry{}}, "") require.ErrorContains(t, err, "cosign key path is required") } + +func TestReadManifestBranches(t *testing.T) { + _, err := ReadManifest(t.TempDir()) + require.Error(t, err) + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte("{"), 0o644)) + _, err = ReadManifest(dir) + require.Error(t, err) +} + +func TestWriteManifestError(t *testing.T) { + err := WriteManifest(filepath.Join(t.TempDir(), "missing"), Manifest{}) + require.Error(t, err) +} + +func TestVerifyHashMismatchAndSignatureBranches(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "records.jsonl"), []byte("{}\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"files":[{"path":"records.jsonl","sha256":"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}]}`), 0o644)) + _, err := Verify(dir, VerifyOpts{}) + require.ErrorContains(t, err, "bundle hash mismatch") + + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"files":[{"path":"records.jsonl","sha256":"sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"}]}`), 0o644)) + _, err = Verify(dir, VerifyOpts{VerifySignatures: true}) + require.ErrorContains(t, err, "has no signatures") + + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{ + "files":[{"path":"records.jsonl","sha256":"sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"}], + "signatures":[{"alg":"ed25519","key_id":"k","sig":"x","signed_digest":"d"}] + }`), 0o644)) + _, err = Verify(dir, VerifyOpts{VerifySignatures: true}) + require.ErrorContains(t, err, "public key is required") + + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{ + "files":[{"path":"records.jsonl","sha256":"sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"}], + "signatures":[{"alg":"rsa","key_id":"k","sig":"x","signed_digest":"d"}] + }`), 0o644)) + _, err = Verify(dir, VerifyOpts{VerifySignatures: true}) + require.ErrorContains(t, err, "unsupported bundle signature algorithm") +} + +func TestVerifyAdditionalBranches(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"files":[{"path":"missing.jsonl","sha256":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}`), 0o644)) + _, err := Verify(dir, VerifyOpts{}) + require.Error(t, err) + + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"files":[{"path":"missing.jsonl"}]}`), 0o644)) + _, err = Verify(dir, VerifyOpts{}) + require.Error(t, err) + + recordsDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(recordsDir, "records.jsonl"), []byte("{}\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(recordsDir, "manifest.json"), []byte(`{"files":[{"path":"records.jsonl","sha256":"sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"}]}`), 0o644)) + key1, err := signing.GenerateKey() + require.NoError(t, err) + _, err = SignFile(recordsDir, key1) + require.NoError(t, err) + key2, err := signing.GenerateKey() + require.NoError(t, err) + _, err = Verify(recordsDir, VerifyOpts{ + VerifySignatures: true, + PublicKey: signing.PublicKey{Public: key2.Public}, + }) + require.Error(t, err) +} + +func TestSignManifestAndSignFileErrorBranches(t *testing.T) { + _, err := SignManifest(Manifest{Files: []ManifestEntry{}}, signing.SigningKey{}) + require.ErrorContains(t, err, "private key is required") + + _, err = SignFile(t.TempDir(), signing.SigningKey{}) + require.Error(t, err) + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"files":[]}`), 0o644)) + _, err = SignFileCosign(dir, "") + require.ErrorContains(t, err, "cosign key path is required") + + if runtime.GOOS != "windows" { + key, genErr := signing.GenerateKey() + require.NoError(t, genErr) + + readonly := t.TempDir() + manifestPath := filepath.Join(readonly, "manifest.json") + require.NoError(t, os.WriteFile(manifestPath, []byte(`{"files":[]}`), 0o644)) + require.NoError(t, os.Chmod(manifestPath, 0o444)) + defer func() { _ = os.Chmod(manifestPath, 0o644) }() + + _, err = SignFile(readonly, key) + require.Error(t, err) + } +} + +func TestVerifyCosignSignatureMissingMaterial(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "records.jsonl"), []byte("{}\n"), 0o644)) + manifest := Manifest{ + Files: []ManifestEntry{ + { + Path: "records.jsonl", + SHA256: "sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356", + }, + }, + } + digest, err := ManifestDigest(manifest) + require.NoError(t, err) + manifest.Signatures = []signing.Signature{ + { + Alg: "cosign", + KeyID: "cosign:test", + Sig: "ZmFrZS1zaWduYXR1cmU=", + SignedDigest: digest, + }, + } + raw, err := json.Marshal(manifest) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), raw, 0o644)) + + _, err = Verify(dir, VerifyOpts{VerifySignatures: true}) + require.ErrorContains(t, err, "requires --cosign-key or --cosign-cert") +} + +func TestSignManifestCosignAndSignFileCosignSuccess(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake cosign shell helper is unix-only") + } + fakeBinDir := writeFakeCosign(t) + t.Setenv("PATH", fakeBinDir+":"+os.Getenv("PATH")) + + signed, err := SignManifestCosign(Manifest{Files: []ManifestEntry{}}, "fake.key") + require.NoError(t, err) + require.Len(t, signed.Signatures, 1) + require.Equal(t, "cosign", signed.Signatures[0].Alg) + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"files":[]}`), 0o644)) + out, err := SignFileCosign(dir, "fake.key") + require.NoError(t, err) + require.Len(t, out.Signatures, 1) + + persisted, err := ReadManifest(dir) + require.NoError(t, err) + require.Len(t, persisted.Signatures, 1) +} + +func TestVerifyCosignSignaturePath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake cosign shell helper is unix-only") + } + fakeBinDir := writeFakeCosign(t) + t.Setenv("PATH", fakeBinDir+":"+os.Getenv("PATH")) + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "records.jsonl"), []byte("{}\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{ + "files":[{"path":"records.jsonl","sha256":"sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"}] + }`), 0o644)) + signed, err := SignFileCosign(dir, "fake.key") + require.NoError(t, err) + require.Len(t, signed.Signatures, 1) + + _, err = Verify(dir, VerifyOpts{ + VerifySignatures: true, + Cosign: signing.CosignVerifyOpts{ + KeyPath: "fake.key", + }, + }) + require.NoError(t, err) +} + +func writeFakeCosign(t *testing.T) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "cosign") + script := `#!/usr/bin/env sh +set -eu +mode="$1" +shift +if [ "$mode" = "sign-blob" ]; then + out="" + while [ "$#" -gt 0 ]; do + if [ "$1" = "--output-signature" ]; then + out="$2" + shift 2 + continue + fi + shift + done + printf "ZmFrZS1zaWduYXR1cmU=\n" > "$out" + exit 0 +fi +if [ "$mode" = "verify-blob" ]; then + exit 0 +fi +exit 0 +` + require.NoError(t, os.WriteFile(path, []byte(script), 0o755)) + return dir +} diff --git a/core/errors/errors_test.go b/core/errors/errors_test.go new file mode 100644 index 0000000..a3de78a --- /dev/null +++ b/core/errors/errors_test.go @@ -0,0 +1,54 @@ +package errors + +import ( + stdliberrors "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewAndOptions(t *testing.T) { + err := New( + KindValidation, + "schema.invalid", + "schema validation failed", + WithField("event"), + WithPath("v1/types/decision.schema.json"), + ) + require.Error(t, err) + + typed, ok := As(err) + require.True(t, ok) + require.Equal(t, KindValidation, typed.Kind) + require.Equal(t, "schema.invalid", typed.Code) + require.Equal(t, "schema validation failed", typed.Message) + require.Equal(t, "event", typed.Field) + require.Equal(t, "v1/types/decision.schema.json", typed.Path) + require.Equal(t, "schema validation failed", typed.Error()) +} + +func TestWrapAndUnwrap(t *testing.T) { + root := stdliberrors.New("low-level failure") + err := Wrap(KindInternal, "record.marshal", "marshal payload", root) + require.Error(t, err) + require.Equal(t, "marshal payload: low-level failure", err.Error()) + require.True(t, stdliberrors.Is(err, root)) +} + +func TestAsNotTyped(t *testing.T) { + typed, ok := As(stdliberrors.New("plain")) + require.False(t, ok) + require.Nil(t, typed) +} + +func TestNilErrorBehaviors(t *testing.T) { + var typed *Error + require.Equal(t, "", typed.Error()) + require.Nil(t, typed.Unwrap()) +} + +func TestErrorFallsBackToCauseMessage(t *testing.T) { + root := stdliberrors.New("fallback") + err := New(KindInternal, "x", "", WithCause(root)) + require.EqualError(t, err, "fallback") +}