package cli import ( "crypto/sha256" "encoding/hex" "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestBuildReceiptRoundTripBindsRootAndNestedModuleIdentity(t *testing.T) { for _, test := range []struct { name string moduleDir string modulePath string tag string }{ {name: "root", moduleDir: ".", modulePath: "example.com/receipt", tag: "v1.2.3"}, {name: "nested", moduleDir: "integrations/ckconfig", modulePath: "example.com/ckconfig", tag: "integrations/ckconfig/v1.2.3"}, } { t.Run(test.name, func(t *testing.T) { outputDir := t.TempDir() repositoryDir := t.TempDir() moduleDir := repositoryDir if test.moduleDir != "." { moduleDir = filepath.Join(repositoryDir, filepath.FromSlash(test.moduleDir)) if err := os.MkdirAll(moduleDir, 0o755); err != nil { t.Fatal(err) } } filename := "example-v1.2.3-linux-amd64" artifactPath := filepath.Join(outputDir, filename) contents := []byte("built application") if err := os.WriteFile(artifactPath, contents, 0o755); err != nil { t.Fatal(err) } digest := sha256.Sum256(contents) receipt := BuildReceipt{ SchemaVersion: BuildReceiptSchemaVersion, RepositoryDir: repositoryDir, ModuleDir: moduleDir, ModulePath: test.modulePath, Tag: test.tag, CandidateCommit: strings.Repeat("a", 40), Version: "v1.2.3", MainPackage: "./cmd/example", BinaryName: "example", Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}}, Compiler: "go version go1.25.0 linux/amd64", Artifacts: []ReceiptArtifact{{ Path: artifactPath, Filename: filename, Target: ReceiptTarget{OS: "linux", Arch: "amd64"}, SHA256: hex.EncodeToString(digest[:]), }}, } path, err := WriteBuildReceipt(outputDir, receipt) if err != nil { t.Fatal(err) } loaded, err := LoadBuildReceipt(path) if err != nil { t.Fatal(err) } settings := BuildSettings{MainPackage: "./cmd/example", BinaryName: "example", Targets: []string{"linux/amd64", "linux/amd64"}} if !loaded.MatchesBuild(repositoryDir, strings.Repeat("a", 40), "v1.2.3", settings) { t.Fatalf("receipt does not match normalized build settings: %#v", loaded) } settings.BinaryName = "other" if loaded.MatchesBuild(repositoryDir, strings.Repeat("a", 40), "v1.2.3", settings) { t.Fatal("receipt matched changed build settings") } if err := os.WriteFile(artifactPath, []byte("changed artifact"), 0o755); err != nil { t.Fatal(err) } if _, err := LoadBuildReceipt(path); err != nil { t.Fatalf("receipt loading verified changed artifact bytes: %v", err) } if err := os.Remove(artifactPath); err != nil { t.Fatal(err) } if _, err := LoadBuildReceipt(path); err != nil { t.Fatalf("receipt loading required artifact bytes: %v", err) } }) } } func TestLoadBuildReceiptRejectsUnknownFieldsAndMultipleValues(t *testing.T) { for _, test := range []struct { name string body string want string }{ {name: "unknown field", body: `{"schema_version":1,"unexpected":true}`, want: "unknown field"}, {name: "unversioned receipt", body: `{}`, want: "regenerate it with the updated build command"}, {name: "unknown schema", body: `{"schema_version":2}`, want: "regenerate it with the updated build command"}, {name: "multiple values", body: `{} {}`, want: "multiple JSON values"}, } { t.Run(test.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), BuildReceiptName) if err := os.WriteFile(path, []byte(test.body), 0o600); err != nil { t.Fatal(err) } if _, err := LoadBuildReceipt(path); err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("LoadBuildReceipt() error = %v, want %q", err, test.want) } }) } } func TestWriteBuildReceiptRejectsInconsistentRecordedIdentity(t *testing.T) { repositoryDir := t.TempDir() moduleDir := filepath.Join(repositoryDir, "integrations", "ckconfig") if err := os.MkdirAll(moduleDir, 0o755); err != nil { t.Fatal(err) } outputDir := t.TempDir() receipt := BuildReceipt{ SchemaVersion: BuildReceiptSchemaVersion, RepositoryDir: repositoryDir, ModuleDir: moduleDir, ModulePath: "example.com/ckconfig", Tag: "v1.2.3", CandidateCommit: strings.Repeat("a", 40), Version: "v1.2.3", MainPackage: ".", BinaryName: "helper", OutputDir: outputDir, Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}}, Compiler: "go version fixture", Artifacts: []ReceiptArtifact{{ Path: filepath.Join(outputDir, "helper-v1.2.3-linux-amd64"), Filename: "helper-v1.2.3-linux-amd64", Target: ReceiptTarget{OS: "linux", Arch: "amd64"}, SHA256: strings.Repeat("a", 64), }}, } if _, err := WriteBuildReceipt(outputDir, receipt); err == nil || !strings.Contains(err.Error(), "full tag") { t.Fatalf("WriteBuildReceipt() error = %v, want inconsistent full tag", err) } } func TestLoadBuildReceiptRejectsMismatchedModuleIdentity(t *testing.T) { repositoryDir := t.TempDir() outputDir := t.TempDir() artifactPath := filepath.Join(outputDir, "helper-v1.2.3-linux-amd64") contents := []byte("fixture") if err := os.WriteFile(artifactPath, contents, 0o600); err != nil { t.Fatal(err) } digest := sha256.Sum256(contents) receipt := BuildReceipt{ SchemaVersion: BuildReceiptSchemaVersion, RepositoryDir: repositoryDir, ModuleDir: repositoryDir, ModulePath: "example.com/receipt", Tag: "v1.2.3", CandidateCommit: strings.Repeat("a", 40), Version: "v1.2.3", MainPackage: ".", BinaryName: "helper", OutputDir: outputDir, Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}}, Compiler: "go version fixture", Artifacts: []ReceiptArtifact{{ Path: artifactPath, Filename: filepath.Base(artifactPath), Target: ReceiptTarget{OS: "linux", Arch: "amd64"}, SHA256: hex.EncodeToString(digest[:]), }}, } contents, err := json.Marshal(receipt) if err != nil { t.Fatal(err) } var fields map[string]any if err := json.Unmarshal(contents, &fields); err != nil { t.Fatal(err) } fields["module_dir"] = filepath.Join(repositoryDir, "integrations", "ckconfig") contents, err = json.Marshal(fields) if err != nil { t.Fatal(err) } path := filepath.Join(outputDir, BuildReceiptName) if err := os.WriteFile(path, contents, 0o600); err != nil { t.Fatal(err) } if _, err := LoadBuildReceipt(path); err == nil || !strings.Contains(err.Error(), "inconsistent module identity") { t.Fatalf("LoadBuildReceipt() error = %v, want inconsistent module identity", err) } }