274 lines
9.6 KiB
Go
274 lines
9.6 KiB
Go
package publish
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
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":2,"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 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 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 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)
|
|
}
|
|
}
|