package publish import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" "sort" "strings" "testing" ) func TestLoadEvidenceValidatesKnownByteFieldsAsBase64Strings(t *testing.T) { for _, test := range []struct{ name, value, want, failure string }{ {"valid", `"djEuMi4z"`, "v1.2.3", ""}, {"empty", `""`, "", ""}, {"malformed base64", `"not base64!"`, "", "base64"}, {"numeric array", `[118,49,46,50,46,51]`, "", "type"}, {"object", `{}`, "", "type"}, {"boolean", `true`, "", "type"}, {"number", `123`, "", "type"}, {"null", `null`, "", "schema"}, } { t.Run(test.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), "record.json") contents := `{"format_version":2,"evidence":{},"evidence_bytes":{"tag":` + test.value + `}}` if err := os.WriteFile(path, []byte(contents), 0600); err != nil { t.Fatal(err) } loaded, err := LoadEvidence(path) if test.failure == "" { if err != nil || loaded.Tag != test.want { t.Fatalf("byte string = %#v, %v", loaded, err) } return } var typed *RecoveryError if !errors.As(err, &typed) || typed.Kind != "record_format" || typed.Unwrap() == nil { t.Fatalf("byte format failure = %v", err) } switch test.failure { case "type": var cause *json.UnmarshalTypeError if !errors.As(err, &cause) { t.Fatalf("lost JSON type cause: %v", err) } case "base64": var cause base64.CorruptInputError if !errors.As(err, &cause) { t.Fatalf("lost base64 cause: %v", err) } } }) } } func TestRecordsRoundTripNonUTF8Evidence(t *testing.T) { repo := recordRepo(t) raw := "path-\xff" evidence := Evidence{RepositoryDir: raw, ModuleDir: raw, ModulePath: raw, Version: raw, Tag: raw, CandidateCommit: raw, TagObject: raw, Endpoint: raw, Remote: raw, RecordedAt: raw} path, err := SaveEvidence(t.Context(), repo, t.TempDir(), evidence) if err != nil { t.Fatal(err) } loaded, err := LoadEvidence(path) if err != nil || loaded != evidence { t.Fatalf("lossy evidence: %#v, %v", loaded, err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var envelope recordEnvelope if err := json.Unmarshal(data, &envelope); err != nil || envelope.FormatVersion != recordByteFormatVersion { t.Fatalf("byte record envelope: %#v, %v", envelope, err) } // Unknown fields remain ignorable; known byte fields retain real codec errors. for _, test := range []struct { field string wantError bool }{{"endpoint", true}, {"future_field", false}} { envelope.EvidenceBytes = json.RawMessage(`{"` + test.field + `":"not base64!"}`) data, err := json.Marshal(envelope) if err != nil { t.Fatal(err) } if err := os.WriteFile(path, data, 0600); err != nil { t.Fatal(err) } _, err = LoadEvidence(path) if (err != nil) != test.wantError { t.Fatalf("byte field %s error=%v", test.field, err) } } } func TestRecordLoadedObservationKeepsNonUTF8EndpointIdentity(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("raw pathname bytes are a POSIX behavior") } repo := recordRepo(t) base := t.TempDir() original := filepath.Join(base, "remote-\xff.git") decoy := filepath.Join(base, "remote-\uFFFD.git") for _, remote := range []string{original, decoy} { recordGit(t, repo, "init", "--bare", remote) } recordGit(t, repo, "tag", "-a", "v1.2.3", "-m", "Release") recordGit(t, repo, "push", decoy, "refs/tags/v1.2.3") evidence := Evidence{RepositoryDir: repo, ModuleDir: repo, Endpoint: original, Tag: "v1.2.3", TagObject: strings.TrimSpace(recordGit(t, repo, "rev-parse", "v1.2.3")), CandidateCommit: strings.TrimSpace(recordGit(t, repo, "rev-parse", "HEAD"))} path, err := SaveEvidence(t.Context(), repo, "", evidence) if err != nil { t.Fatal(err) } loaded, err := LoadEvidence(path) if err != nil || loaded.Endpoint != original { t.Fatalf("endpoint changed: %q, %v", loaded.Endpoint, err) } for _, selected := range []Evidence{evidence, loaded} { observed, err := Reconcile(t.Context(), selected) if err != nil || observed.State != NotPublished { t.Fatalf("original endpoint observation = %#v, %v", observed, err) } } loaded.Endpoint = decoy observed, err := Reconcile(t.Context(), loaded) if err != nil || observed.State != Published { t.Fatalf("decoy does not distinguish identity: %#v, %v", observed, err) } } func recordGit(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v: %s", args, err, out) } return string(out) } func recordRepo(t *testing.T) string { t.Helper() dir := t.TempDir() recordGit(t, dir, "init", "-q") recordGit(t, dir, "config", "user.name", "Test") recordGit(t, dir, "config", "user.email", "test@example.invalid") if err := os.WriteFile(filepath.Join(dir, "file"), []byte("x"), 0600); err != nil { t.Fatal(err) } recordGit(t, dir, "add", "file") recordGit(t, dir, "commit", "-qm", "init") return dir } func TestLoadEvidenceAcceptsPartialAndRejectsMalformedRecords(t *testing.T) { dir := t.TempDir() partial := filepath.Join(dir, "partial.json") if err := os.WriteFile(partial, []byte(`{"format_version":1,"evidence":{}}`), 0600); err != nil { t.Fatal(err) } got, err := LoadEvidence(partial) if err != nil || got != (Evidence{}) { t.Fatalf("partial record = %#v, %v", got, err) } for name, data := range map[string]string{"bad-json": "{", "unsupported": `{"format_version":3,"evidence":{}}`, "type": `{"format_version":1,"evidence":{"tag":false}}`, "trailing": `{"format_version":1,"evidence":{}} {}`} { t.Run(name, func(t *testing.T) { path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatal(err) } _, err := LoadEvidence(path) var recovery *RecoveryError if !errors.As(err, &recovery) || recovery.Kind != "record_format" { t.Fatalf("LoadEvidence() error=%v", err) } }) } } func TestLoadEvidenceRequiresOneDocumentAndPreservesSyntaxCause(t *testing.T) { const valid = `{"format_version":1,"evidence":{"tag":"v1.2.3","future":{}},"future":true}` for _, test := range []struct { name, data string valid bool }{ {"single", valid, true}, {"whitespace", valid + "\n \t\r", true}, {"first incomplete", "{", false}, {"first syntax", "x", false}, {"second object", valid + ` {"unused":[1,2,3]}`, false}, {"incomplete suffix", valid + " {", false}, {"invalid suffix", valid + " x", false}, } { t.Run(test.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), "record.json") if err := os.WriteFile(path, []byte(test.data), 0600); err != nil { t.Fatal(err) } loaded, err := LoadEvidence(path) if test.valid { if err != nil || loaded.Tag != "v1.2.3" { t.Fatalf("valid record = %#v, %v", loaded, err) } return } var recovery *RecoveryError var syntax *json.SyntaxError if !errors.As(err, &recovery) || recovery.Kind != "record_format" || !errors.As(err, &syntax) { t.Fatalf("lost format/parser cause: %v", err) } }) } } func TestLoadEvidenceReadFailureHasTypedError(t *testing.T) { _, err := LoadEvidence(filepath.Join(t.TempDir(), "missing.json")) var recovery *RecoveryError if !errors.As(err, &recovery) || recovery.Kind != "record_read" || !errors.Is(err, os.ErrNotExist) { t.Fatalf("LoadEvidence() error = %v", err) } } func TestSaveEvidenceUsesPrivateUniqueRecordsAndLinkedWorktreeMetadata(t *testing.T) { repo := recordRepo(t) worktree := filepath.Join(t.TempDir(), "linked") recordGit(t, repo, "worktree", "add", "--detach", worktree) evidence := Evidence{Tag: "v1.2.3", Endpoint: "https://user:secret@example.invalid/repo.git"} first, err := SaveEvidence(t.Context(), worktree, "", evidence) if err != nil { t.Fatal(err) } second, err := SaveEvidence(t.Context(), worktree, "", evidence) if err != nil { t.Fatal(err) } if first == second { t.Fatalf("record paths collided: %q", first) } common := recordGit(t, repo, "rev-parse", "--path-format=absolute", "--git-common-dir") wantDir := filepath.Join(strings.TrimSpace(common), "releasekit", "publication-attempts") if filepath.Dir(first) != wantDir { t.Fatalf("record dir=%q want %q", filepath.Dir(first), wantDir) } dirInfo, err := os.Stat(wantDir) if err != nil { t.Fatal(err) } if dirInfo.Mode().Perm() != 0700 { t.Fatalf("record directory permissions=%o", dirInfo.Mode().Perm()) } info, err := os.Stat(first) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0600 { t.Fatalf("record permissions=%o", info.Mode().Perm()) } loaded, err := LoadEvidence(first) if err != nil { t.Fatal(err) } if loaded.Endpoint != evidence.Endpoint || loaded.RecordedAt == "" { t.Fatalf("loaded evidence=%#v", loaded) } var envelope map[string]json.RawMessage data, err := os.ReadFile(first) if err != nil { t.Fatal(err) } if err := json.Unmarshal(data, &envelope); err != nil { t.Fatal(err) } if !strings.Contains(string(data), "user:secret@example.invalid") { t.Fatal("private record did not retain the exact endpoint") } } func TestSaveEvidenceResolvesRelativeAndIgnoredOverrides(t *testing.T) { repo := recordRepo(t) if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("saved-records/\n"), 0600); err != nil { t.Fatal(err) } if _, err := SaveEvidence(t.Context(), repo, "saved-records", Evidence{Tag: "v1.2.3"}); err != nil { t.Fatalf("ignored override: %v", err) } if _, err := os.Stat(filepath.Join(repo, "saved-records")); err != nil { t.Fatal(err) } external := filepath.Join(t.TempDir(), "external-store") path, err := SaveEvidence(t.Context(), repo, external, Evidence{Tag: "v2.0.0"}) if err != nil || !strings.HasPrefix(path, external+string(filepath.Separator)) { t.Fatalf("absolute external override path=%q error=%v", path, err) } } func TestSaveEvidenceAllowsExplicitGitMetadataStores(t *testing.T) { for _, layout := range []string{"ordinary", "linked", "separate"} { t.Run(layout, func(t *testing.T) { repo := recordRepo(t) switch layout { case "linked": linked := filepath.Join(t.TempDir(), "linked") recordGit(t, repo, "worktree", "add", "--detach", linked) repo = linked case "separate": recordGit(t, repo, "init", "--separate-git-dir", filepath.Join(t.TempDir(), "metadata")) } defaultPath, err := SaveEvidence(t.Context(), repo, "", Evidence{}) if err != nil { t.Fatal(err) } privateDir := strings.TrimSuffix(recordGit(t, repo, "rev-parse", "--absolute-git-dir"), "\n") for _, directory := range []string{filepath.Dir(defaultPath), filepath.Join(privateDir, "other-private-records")} { path, err := SaveEvidence(t.Context(), repo, directory, Evidence{Tag: "v1.2.3"}) if err != nil || filepath.Dir(path) != directory { t.Fatalf("explicit metadata store %q = %q, %v", directory, path, err) } } if status := recordGit(t, repo, "status", "--porcelain"); status != "" { t.Fatalf("metadata records appeared in candidate: %s", status) } }) } } func TestRecordStoragePreservesWhitespaceInRepositoryAndMetadataPaths(t *testing.T) { for _, separate := range []bool{false, true} { t.Run(fmt.Sprintf("separate=%t", separate), func(t *testing.T) { original := recordRepo(t) repo := original + " " if err := os.Rename(original, repo); err != nil { t.Fatal(err) } // recordRepo's temporary-directory cleanup owns the old spelling. t.Cleanup(func() { _ = os.RemoveAll(repo) }) if separate { recordGit(t, repo, "init", "--separate-git-dir", filepath.Join(t.TempDir(), "metadata ")) } if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("saved/\n"), 0600); err != nil { t.Fatal(err) } recordGit(t, repo, "add", ".gitignore") recordGit(t, repo, "commit", "-qm", "Ignore saved records") for _, override := range []string{"", "saved"} { path, err := SaveEvidence(t.Context(), repo, override, Evidence{RepositoryDir: repo, Tag: "v1.2.3"}) if err != nil { t.Fatal(err) } listed, err := ListAttempts(t.Context(), repo, override) if err != nil || len(listed.Attempts) != 1 || listed.Attempts[0].RecordPath != path || listed.Attempts[0].Evidence.RepositoryDir != repo { t.Fatalf("read/write disagreement for %q: %#v, %v", override, listed, err) } if override != "" && filepath.Dir(path) != filepath.Join(repo, override) { t.Fatalf("changed repository path: %q", path) } } if _, err := SaveEvidence(t.Context(), repo, filepath.Join(repo, "unignored"), Evidence{}); err == nil { t.Fatal("absolute unignored store was misclassified as external") } if status := recordGit(t, repo, "status", "--porcelain"); status != "" { t.Fatalf("recording changed candidate: %s", status) } }) } } func TestSaveEvidenceRejectsUnignoredOverrideInsideWorktree(t *testing.T) { repo := recordRepo(t) _, err := SaveEvidence(t.Context(), repo, "records", Evidence{Tag: "v1.2.3"}) var recovery *RecoveryError if !errors.As(err, &recovery) || recovery.Kind != "record_write" { t.Fatalf("SaveEvidence() error=%v", err) } if _, err := os.Stat(filepath.Join(repo, "records")); err != nil { t.Fatalf("expected parent directory to exist without record: %v", err) } if _, err := os.Stat(filepath.Join(repo, "records", "attempt")); !os.IsNotExist(err) { t.Fatalf("unexpected record: %v", err) } } func TestSaveEvidenceChecksEachActualFilename(t *testing.T) { repo := recordRepo(t) if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("selective/attempt-[0-7]*.json\n"), 0600); err != nil { t.Fatal(err) } recordGit(t, repo, "add", ".gitignore") recordGit(t, repo, "commit", "-qm", "Selective ignore") dir := filepath.Join(repo, "selective") next := func(name string) func(string) (string, error) { return func(directory string) (string, error) { return filepath.Join(directory, name), nil } } path, err := saveEvidenceNamed(t.Context(), repo, repo, dir, dir, Evidence{Tag: "v1.0.0"}, next("attempt-0.json")) if err != nil { t.Fatal(err) } recordGit(t, repo, "check-ignore", "--", path) // A collision must validate the replacement name as well. calls := 0 _, err = saveEvidenceNamed(t.Context(), repo, repo, dir, dir, Evidence{Tag: "v2.0.0"}, func(directory string) (string, error) { calls++ name := "attempt-0.json" if calls > 1 { name = "attempt-c.json" } return filepath.Join(directory, name), nil }) if err == nil || calls != 2 { t.Fatalf("unignored replacement: calls=%d error=%v", calls, err) } if _, err := os.Stat(filepath.Join(dir, "attempt-c.json")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("unignored file was created: %v", err) } if status := recordGit(t, repo, "status", "--porcelain"); status != "" { t.Fatalf("recording dirtied candidate: %s", status) } loaded, err := LoadEvidence(path) if err != nil || loaded.Tag != "v1.0.0" { t.Fatalf("collision changed original record: %#v, %v", loaded, err) } } func TestListAttemptsReturnsReadableRecordsAndDiagnosticsInPathOrder(t *testing.T) { repo := recordRepo(t) directory := filepath.Join(t.TempDir(), "attempts") if err := os.Mkdir(directory, 0700); err != nil { t.Fatal(err) } second, _ := SaveEvidence(t.Context(), repo, directory, Evidence{Tag: "v2.0.0"}) first, _ := SaveEvidence(t.Context(), repo, directory, Evidence{Tag: "v1.0.0"}) if err := os.WriteFile(filepath.Join(directory, "broken.json"), []byte("{"), 0600); err != nil { t.Fatal(err) } listed, err := ListAttempts(t.Context(), repo, directory) if err != nil || len(listed.Attempts) != 2 || len(listed.Diagnostics) != 1 { t.Fatalf("list=%#v error=%v", listed, err) } wantPaths := []string{first, second} sort.Strings(wantPaths) if listed.Attempts[0].RecordPath != wantPaths[0] || listed.Attempts[1].RecordPath != wantPaths[1] { t.Fatalf("attempt order=%v", listed.Attempts) } if listed.Diagnostics[0].Kind != "record_format" || listed.Diagnostics[0].RecordPath == "" { t.Fatalf("diagnostic=%#v", listed.Diagnostics[0]) } } func TestListAttemptsFiltersMixedDirectoryAcrossBatches(t *testing.T) { directory := t.TempDir() for i := range 400 { if err := os.WriteFile(filepath.Join(directory, fmt.Sprintf("unrelated-%04d.txt", i)), []byte("not a record"), 0600); err != nil { t.Fatal(err) } } if err := os.Mkdir(filepath.Join(directory, "directory.json"), 0700); err != nil { t.Fatal(err) } names := []string{"z.json", "b.json", "a.json"} for _, name := range names { if err := os.WriteFile(filepath.Join(directory, name), []byte(`{"format_version":1,"evidence":{"tag":"v1.2.3"}}`), 0600); err != nil { t.Fatal(err) } } listed, err := ListAttempts(t.Context(), "", directory) if err != nil || len(listed.Diagnostics) != 0 || len(listed.Attempts) != len(names) { t.Fatalf("filtered result = %#v, %v", listed, err) } sort.Strings(names) for i, name := range names { if listed.Attempts[i].RecordPath != filepath.Join(directory, name) { t.Fatalf("incorrect path order: %#v", listed.Attempts) } } } func TestListAttemptsRetainsEntriesWithEnumerationFailure(t *testing.T) { directory := t.TempDir() for name, contents := range map[string]string{ "z.json": `{"format_version":1,"evidence":{"tag":"v2.0.0"}}`, "a.json": `{"format_version":1,"evidence":{"tag":"v1.0.0"}}`, "broken.json": "{", "unrelated.txt": "ignored", } { if err := os.WriteFile(filepath.Join(directory, name), []byte(contents), 0600); err != nil { t.Fatal(err) } } entries, err := os.ReadDir(directory) if err != nil { t.Fatal(err) } failure := errors.New("controlled directory read failure") for _, empty := range []bool{false, true} { calls := 0 listed, err := listAttempts(t.Context(), "", directory, func(ctx context.Context, dir string) ([]string, error) { return readRecordPaths(ctx, dir, func(int) ([]os.DirEntry, error) { calls++ if empty { return nil, failure } return entries, failure }) }) var recovery *RecoveryError if !errors.As(err, &recovery) || recovery.Kind != "storage_enumeration" || !errors.Is(err, failure) || calls != 1 { t.Fatalf("enumeration failure lost: %#v, %v; reads=%d", listed, err, calls) } if empty { if len(listed.Attempts) != 0 || len(listed.Diagnostics) != 0 { t.Fatalf("empty read: %#v", listed) } } else if len(listed.Attempts) != 2 || len(listed.Diagnostics) != 1 || filepath.Base(listed.Attempts[0].RecordPath) != "a.json" || filepath.Base(listed.Attempts[1].RecordPath) != "z.json" || listed.Diagnostics[0].Kind != "record_format" { t.Fatalf("partial read dropped/reordered records: %#v", listed) } } } func TestListAttemptsDistinguishesEmptyMissingAndCanceledStorage(t *testing.T) { empty := t.TempDir() listed, err := ListAttempts(t.Context(), "", empty) if err != nil || len(listed.Attempts) != 0 || len(listed.Diagnostics) != 0 { t.Fatalf("empty list=%#v error=%v", listed, err) } _, err = ListAttempts(t.Context(), "", filepath.Join(t.TempDir(), "missing")) var recovery *RecoveryError if !errors.As(err, &recovery) || recovery.Kind != "storage_enumeration" { t.Fatalf("missing storage error=%v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() _, err = ListAttempts(ctx, "", empty) if !errors.As(err, &recovery) || recovery.Kind != "canceled" || !errors.Is(err, context.Canceled) { t.Fatalf("canceled error=%v", err) } }