332 lines
14 KiB
Go
332 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
releasekit "go.maximumdirect.net/releasekit"
|
|
"go.maximumdirect.net/releasekit/internal/publish"
|
|
)
|
|
|
|
func TestRecoveryDisplaysRedactCredentialPathsAndMetadata(t *testing.T) {
|
|
secret := "audit-secret"
|
|
raw := "https://audit-user:" + secret + "@example.invalid/attempt.json"
|
|
for _, jsonOutput := range []bool{false, true} {
|
|
args := []string{"reconcile", "--record", raw}
|
|
if jsonOutput {
|
|
args = append(args, "--json")
|
|
}
|
|
var out, diagnostics bytes.Buffer
|
|
if status := run(t.Context(), args, &out, &diagnostics); status != 2 {
|
|
t.Fatalf("load status=%d", status)
|
|
}
|
|
if strings.Contains(out.String()+diagnostics.String(), secret) {
|
|
t.Fatalf("credential leaked: %s%s", &out, &diagnostics)
|
|
}
|
|
}
|
|
repo := newCLIRepository(t)
|
|
store := t.TempDir()
|
|
evidence := publish.Evidence{RepositoryDir: raw, ModuleDir: raw, ModulePath: raw, Version: raw, Tag: raw, CandidateCommit: raw, TagObject: raw, Remote: raw, RecordedAt: raw}
|
|
path, err := publish.SaveEvidence(t.Context(), repo, store, evidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, command := range [][]string{{"reconcile", "--record", path}, {"publication-attempts", "--record-dir", store}} {
|
|
for _, jsonOutput := range []bool{false, true} {
|
|
args := append([]string{}, command...)
|
|
if jsonOutput {
|
|
args = append(args, "--json")
|
|
}
|
|
var out, diagnostics bytes.Buffer
|
|
run(t.Context(), args, &out, &diagnostics)
|
|
if strings.Contains(out.String()+diagnostics.String(), secret) {
|
|
t.Fatalf("metadata leaked: %s%s", &out, &diagnostics)
|
|
}
|
|
}
|
|
}
|
|
loaded, err := publish.LoadEvidence(path)
|
|
if err != nil || loaded != evidence {
|
|
t.Fatalf("private metadata changed: %#v, %v", loaded, err)
|
|
}
|
|
var out bytes.Buffer
|
|
reportPublicationRecord(releasekit.PublicationResult{RecordPath: raw, Phase: releasekit.PublicationPush}, fmt.Errorf("push failed"), &out)
|
|
if strings.Contains(out.String(), secret) {
|
|
t.Fatalf("handoff leaked credentials: %s", &out)
|
|
}
|
|
}
|
|
|
|
func TestReconcileCommandClassifiesRecordsFromAnotherWorkingDirectory(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
remote := filepath.Join(t.TempDir(), "bare remote.git")
|
|
cliGitRun(t, repository, "init", "--bare", "--initial-branch=main", remote)
|
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
|
cliGitRun(t, repository, "tag", "-a", "v1.2.3", "-m", "first tag object")
|
|
cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3")
|
|
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
tagObject := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
|
|
|
|
store := filepath.Join(t.TempDir(), "records with ' punctuation")
|
|
evidence := publish.Evidence{
|
|
RepositoryDir: repository,
|
|
ModuleDir: repository,
|
|
ModulePath: "example.com/cli-candidate",
|
|
Version: "v1.2.3",
|
|
Tag: "v1.2.3",
|
|
CandidateCommit: commit,
|
|
TagObject: tagObject,
|
|
Endpoint: remote,
|
|
Remote: "origin",
|
|
RecordedAt: "2026-11-02T00:00:00Z",
|
|
}
|
|
record, err := publish.SaveEvidence(t.Context(), repository, store, evidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cliGitRun(t, repository, "switch", "-c", "changed-head")
|
|
cliGitRun(t, repository, "commit", "--allow-empty", "-m", "change current HEAD")
|
|
cliGitRun(t, repository, "tag", "-d", "v1.2.3")
|
|
if err := os.WriteFile(filepath.Join(repository, "uncommitted recovery check.txt"), []byte("dirty"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
otherDirectory := t.TempDir()
|
|
relativeRecord, err := filepath.Rel(otherDirectory, record)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
previousDirectory, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(otherDirectory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := os.Chdir(previousDirectory); err != nil {
|
|
t.Errorf("restore working directory: %v", err)
|
|
}
|
|
})
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"reconcile", "--record", relativeRecord, "--json"}, &stdout, &stderr)
|
|
if status != 0 || stderr.Len() != 0 {
|
|
t.Fatalf("published status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
|
|
}
|
|
var published map[string]any
|
|
if err := json.Unmarshal(stdout.Bytes(), &published); err != nil {
|
|
t.Fatalf("decode published JSON: %v; output = %q", err, stdout.String())
|
|
}
|
|
if published["state"] != string("published") || published["record_path"] != relativeRecord {
|
|
t.Fatalf("published JSON = %#v", published)
|
|
}
|
|
if _, ok := published["diagnostic"]; ok {
|
|
t.Fatalf("successful reconciliation includes a diagnostic: %#v", published)
|
|
}
|
|
evidenceJSON, ok := published["evidence"].(map[string]any)
|
|
if !ok || evidenceJSON["candidate_commit"] != commit || evidenceJSON["tag_object"] != tagObject {
|
|
t.Fatalf("safe evidence = %#v", published["evidence"])
|
|
}
|
|
if evidenceJSON["module_path"] != evidence.ModulePath || evidenceJSON["candidateCommit"] != nil {
|
|
t.Fatalf("evidence does not use the versioned snake-case shape: %#v", evidenceJSON)
|
|
}
|
|
|
|
cliGitRun(t, repository, "push", "origin", ":refs/tags/v1.2.3")
|
|
cliGitRun(t, repository, "tag", "-f", "-a", "v1.2.3", "-m", "replacement tag object")
|
|
cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3")
|
|
var conflictOut, conflictErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", record, "--json"}, &conflictOut, &conflictErr)
|
|
if status != 1 || conflictErr.Len() != 0 {
|
|
t.Fatalf("conflict status = %d, stdout = %q, stderr = %q", status, conflictOut.String(), conflictErr.String())
|
|
}
|
|
var conflict map[string]any
|
|
if err := json.Unmarshal(conflictOut.Bytes(), &conflict); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if conflict["state"] != string("conflict") || conflict["observed_tag_object"] == tagObject {
|
|
t.Fatalf("conflict JSON = %#v", conflict)
|
|
}
|
|
|
|
var humanOut, humanErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", record}, &humanOut, &humanErr)
|
|
if status != 1 || !strings.Contains(humanOut.String(), "expected tag-object="+tagObject) || !strings.Contains(humanOut.String(), "observed tag-object=") || !strings.Contains(humanOut.String(), "Do not replace a conflicting remote tag") {
|
|
t.Fatalf("human conflict status = %d, stdout = %q, stderr = %q", status, humanOut.String(), humanErr.String())
|
|
}
|
|
|
|
partialEvidence := evidence
|
|
partialEvidence.TagObject = cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
|
|
partialEvidence.CandidateCommit = ""
|
|
partialRecord, err := publish.SaveEvidence(t.Context(), repository, store, partialEvidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var unknownOut, unknownErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", partialRecord, "--json"}, &unknownOut, &unknownErr)
|
|
if status != 3 || unknownErr.Len() != 0 {
|
|
t.Fatalf("unknown status = %d, stdout = %q, stderr = %q", status, unknownOut.String(), unknownErr.String())
|
|
}
|
|
var unknown map[string]any
|
|
if err := json.Unmarshal(unknownOut.Bytes(), &unknown); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unknownDiagnostic, ok := unknown["diagnostic"].(map[string]any)
|
|
if unknown["state"] != "unknown" || !ok || unknownDiagnostic["kind"] != "missing_evidence" {
|
|
t.Fatalf("unknown JSON = %#v", unknown)
|
|
}
|
|
var unknownHumanOut, unknownHumanErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", partialRecord}, &unknownHumanOut, &unknownHumanErr)
|
|
if status != 3 || !strings.Contains(unknownHumanOut.String(), "state=unknown") || !strings.Contains(unknownHumanErr.String(), "candidate_commit") {
|
|
t.Fatalf("human unknown status = %d, stdout = %q, stderr = %q", status, unknownHumanOut.String(), unknownHumanErr.String())
|
|
}
|
|
|
|
cliGitRun(t, repository, "push", "origin", ":refs/tags/v1.2.3")
|
|
var absentOut, absentErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", record, "--json"}, &absentOut, &absentErr)
|
|
if status != 1 || absentErr.Len() != 0 {
|
|
t.Fatalf("absent status = %d, stdout = %q, stderr = %q", status, absentOut.String(), absentErr.String())
|
|
}
|
|
var absent map[string]any
|
|
if err := json.Unmarshal(absentOut.Bytes(), &absent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if absent["state"] != "not_published" {
|
|
t.Fatalf("absence JSON = %#v", absent)
|
|
}
|
|
if _, ok := absent["diagnostic"]; ok {
|
|
t.Fatalf("known absence includes an unresolved diagnostic: %#v", absent)
|
|
}
|
|
var absentHumanOut, absentHumanErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", record}, &absentHumanOut, &absentHumanErr)
|
|
if status != 1 || !strings.Contains(absentHumanOut.String(), "state=not_published") || !strings.Contains(absentHumanOut.String(), "current absence only") {
|
|
t.Fatalf("human absence status = %d, stdout = %q, stderr = %q", status, absentHumanOut.String(), absentHumanErr.String())
|
|
}
|
|
|
|
brokenRecord := filepath.Join(store, "broken record's.json")
|
|
if err := os.WriteFile(brokenRecord, []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var brokenOut, brokenErr bytes.Buffer
|
|
status = run(context.Background(), []string{"reconcile", "--record", brokenRecord, "--json"}, &brokenOut, &brokenErr)
|
|
if status != 2 || brokenErr.Len() != 0 {
|
|
t.Fatalf("record-format status = %d, stdout = %q, stderr = %q", status, brokenOut.String(), brokenErr.String())
|
|
}
|
|
var broken map[string]any
|
|
if err := json.Unmarshal(brokenOut.Bytes(), &broken); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
brokenDiagnostic, ok := broken["diagnostic"].(map[string]any)
|
|
if !ok || brokenDiagnostic["kind"] != "record_format" || broken["record_path"] != brokenRecord {
|
|
t.Fatalf("record-format JSON = %#v", broken)
|
|
}
|
|
for _, field := range []string{"evidence", "state", "observed_tag_object", "observed_commit"} {
|
|
if _, ok := broken[field]; ok {
|
|
t.Errorf("record-format JSON includes %q: %#v", field, broken)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPublicationAttemptsCommandRedactsCredentialsAndReportsPartialDiscovery(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
store := filepath.Join(t.TempDir(), "external records")
|
|
evidence := publish.Evidence{
|
|
RepositoryDir: repository,
|
|
ModulePath: "example.com/cli-candidate",
|
|
Version: "v1.2.3",
|
|
Tag: "v1.2.3",
|
|
Endpoint: "https://cli-user:cli-secret@example.test/repository.git",
|
|
Remote: "origin",
|
|
RecordedAt: "2026-11-02T00:00:00Z",
|
|
}
|
|
if _, err := publish.SaveEvidence(t.Context(), repository, store, evidence); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
brokenPath := filepath.Join(store, "damaged.json")
|
|
if err := os.WriteFile(brokenPath, []byte(`{"format_version":99,"evidence":{}}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"publication-attempts", "--record-dir", store, "--json"}, &stdout, &stderr)
|
|
if status != 1 || stderr.Len() != 0 {
|
|
t.Fatalf("partial discovery status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
|
|
}
|
|
output := stdout.String()
|
|
if strings.Contains(output, "cli-user") || strings.Contains(output, "cli-secret") || !strings.Contains(output, "https://redacted@example.test/repository.git") {
|
|
t.Fatalf("structured discovery exposed or failed to redact endpoint credentials: %q", output)
|
|
}
|
|
var listed map[string]any
|
|
if err := json.Unmarshal(stdout.Bytes(), &listed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
attempts, ok := listed["attempts"].([]any)
|
|
if !ok || len(attempts) != 1 {
|
|
t.Fatalf("attempts = %#v", listed["attempts"])
|
|
}
|
|
diagnostics, ok := listed["diagnostics"].([]any)
|
|
if !ok || len(diagnostics) != 1 {
|
|
t.Fatalf("diagnostics = %#v", listed["diagnostics"])
|
|
}
|
|
if listed["schema_version"] != float64(1) || listed["command"] != "publication-attempts" || listed["diagnostic"] != nil {
|
|
t.Fatalf("listing document = %#v", listed)
|
|
}
|
|
entry := attempts[0].(map[string]any)
|
|
if entry["record_path"] == "" {
|
|
t.Fatalf("attempt is missing record_path: %#v", entry)
|
|
}
|
|
entryEvidence := entry["evidence"].(map[string]any)
|
|
if entryEvidence["module_path"] != evidence.ModulePath || entryEvidence["recorded_at"] != evidence.RecordedAt {
|
|
t.Fatalf("attempt evidence = %#v", entryEvidence)
|
|
}
|
|
diagnostic := diagnostics[0].(map[string]any)
|
|
if diagnostic["kind"] != "record_format" || diagnostic["record_path"] != brokenPath {
|
|
t.Fatalf("record diagnostic = %#v", diagnostic)
|
|
}
|
|
|
|
emptyStore := filepath.Join(t.TempDir(), "empty store")
|
|
if err := os.Mkdir(emptyStore, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var emptyOut, emptyErr bytes.Buffer
|
|
status = run(context.Background(), []string{"publication-attempts", "--record-dir", emptyStore, "--json"}, &emptyOut, &emptyErr)
|
|
if status != 0 || emptyErr.Len() != 0 {
|
|
t.Fatalf("empty discovery status = %d, stdout = %q, stderr = %q", status, emptyOut.String(), emptyErr.String())
|
|
}
|
|
var empty map[string]any
|
|
if err := json.Unmarshal(emptyOut.Bytes(), &empty); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(empty["attempts"].([]any)) != 0 || len(empty["diagnostics"].([]any)) != 0 || empty["diagnostic"] != nil {
|
|
t.Fatalf("empty listing document = %#v", empty)
|
|
}
|
|
|
|
var failedOut, failedErr bytes.Buffer
|
|
status = run(context.Background(), []string{"publication-attempts", "--record-dir", filepath.Join(emptyStore, "missing"), "--json"}, &failedOut, &failedErr)
|
|
if status != 1 || failedErr.Len() != 0 {
|
|
t.Fatalf("enumeration failure status = %d, stdout = %q, stderr = %q", status, failedOut.String(), failedErr.String())
|
|
}
|
|
var failed map[string]any
|
|
if err := json.Unmarshal(failedOut.Bytes(), &failed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failedDiagnostic, ok := failed["diagnostic"].(map[string]any)
|
|
if !ok || failedDiagnostic["kind"] != "storage_enumeration" {
|
|
t.Fatalf("enumeration failure JSON = %#v", failed)
|
|
}
|
|
}
|
|
|
|
func TestRecoveryArgumentErrorsUseExistingUsagePath(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"reconcile", "--json"}, &stdout, &stderr)
|
|
if status != 2 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "reconcile requires --record FILE") {
|
|
t.Fatalf("argument failure status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
|
|
}
|
|
if strings.Contains(stdout.String(), "schema_version") || strings.Contains(stderr.String(), fmt.Sprint("\"command\"")) {
|
|
t.Fatalf("argument failure unexpectedly used structured output: stdout=%q stderr=%q", stdout.String(), stderr.String())
|
|
}
|
|
}
|