package historyreadback import ( "errors" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) func TestPlan14ReadReconstructionCompletion(t *testing.T) { for _, fault := range []string{"valid", "invalid serialization", "replacement", "delivery only", "different operation", "different replacement", "different bounds", "namespace", "revision", "pending", "source", "scope", "unknown app", "unknown host", "unknown bake", "missing digest", "signature", "old challenge"} { t.Run(fault, func(t *testing.T) { f := newFixture(t) g := release.HistoryReconstruction{Schema: 1, Kind: "carlos-release-history", Identity: f.status.Identity, Revision: 1, Operation: "reconstruct", IssuedAt: f.now, Account: f.scope.Binding.Account, App: f.scope.Binding.App, Hosts: []release.HistoryReconstructionHost{f.status.Hosts[0].HistoryReconstructionHost}} if fault == "replacement" { g.Replaces = strings.Repeat("d", 64) } f.status.Revision = 2 f.status.AppComplete, f.status.AppRevision, f.status.BakeComplete, f.status.BakeRevision = true, 1, 1, 1 f.status.Hosts[0].Revision = 1 f.status.Hosts[0].RefusalStatus.Complete = true f.status.LastReconstructionDigest = digest(g) switch fault { case "invalid serialization": g.IssuedAt = time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) f.status.LastReconstructionDigest = "" f.status.Hosts[0].RetirementDigests = nil case "different operation": g.Operation = "other" case "different replacement": g.Replaces = strings.Repeat("e", 64) case "different bounds": g.Production = true case "namespace": f.status.Identity = strings.Repeat("f", 64) case "revision": f.status.Revision++ case "pending": f.status.Pending = true case "source": f.status.Hosts[0].Source = "changed" case "scope": g.Account = "xyz" f.status.LastReconstructionDigest = digest(g) case "unknown app": f.status.AppComplete = false case "unknown host": f.status.Hosts[0].RefusalStatus.Complete = false case "unknown bake": f.status.BakeComplete = 0 case "missing digest": f.status.LastReconstructionDigest = "" } q, err := RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) if err != nil { t.Fatal(err) } if fault != "delivery only" { if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { t.Fatal(err) } } if fault == "signature" { r := Receipt{Schema: 1, Kind: "carlos-release-history-receipt", RequestDigest: digest(q), ObservedAt: f.now, Status: f.status} wire, err := release.SignRootEnvelope(f.signer, r) if err != nil { t.Fatal(err) } if err := publish(t.Context(), f.directory.Store, ResponseKey(q), wire); err != nil { t.Fatal(err) } } if fault == "old challenge" { q, err = RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) if err != nil { t.Fatal(err) } } _, err = ReadReconstruction(t.Context(), f.directory, q, g, f.now) if fault == "valid" || fault == "replacement" { if err != nil { t.Fatal(err) } } else if err == nil { t.Fatal("unproven reconstruction reported complete") } }) } } func TestPlan14ReadRetirementCompletion(t *testing.T) { for _, fault := range []string{"valid", "invalid serialization", "different operation", "different outcome", "different witness", "missing digest", "missing retired", "unknown originals", "pending original", "unrelated original", "pending reconstruction", "source", "namespace", "scope", "delivery only"} { t.Run(fault, func(t *testing.T) { f := newFixture(t) g := release.HistoryRecoveryRetirement{Schema: 1, Kind: "carlos-release-recovery-retirement", Identity: f.status.Identity, Operation: "retire", IssuedAt: f.now, Binding: f.scope.Binding, Source: "canonical", AttemptID: "failed", OutcomeDigest: strings.Repeat("d", 64), VerifiedID: "good", VerifiedDigest: strings.Repeat("e", 64)} f.status.RecoverySources = &release.HistoryRecoverySources{Known: true} f.status.Hosts[0].RetiredRecoveries = []string{g.AttemptID} f.status.Hosts[0].RetirementDigests = map[string]string{g.AttemptID: digest(g)} f.status.Hosts[0].RecoveryDigests = map[string]string{g.AttemptID: g.OutcomeDigest, g.VerifiedID: g.VerifiedDigest} switch fault { case "invalid serialization": g.IssuedAt = time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) f.status.LastReconstructionDigest = "" f.status.Hosts[0].RetirementDigests = nil case "different operation": g.Operation = "other" case "different outcome": g.OutcomeDigest = strings.Repeat("f", 64) case "different witness": g.VerifiedDigest = strings.Repeat("f", 64) case "missing digest": f.status.Hosts[0].RetirementDigests = nil case "missing retired": f.status.Hosts[0].RetiredRecoveries = nil case "unknown originals": f.status.RecoverySources.Known = false case "pending original", "unrelated original": id := g.AttemptID if fault == "unrelated original" { id = "unrelated" } f.status.RecoverySources.Unresolved = []release.HistoryRecovery{{Binding: g.Binding, Attempt: release.HistoryAttempt{ID: id}}} case "pending reconstruction": f.status.Pending = true case "source": f.status.Hosts[0].Source = "changed" case "namespace": f.status.Identity = strings.Repeat("f", 64) case "scope": g.Binding.Host = "other.example" f.status.Hosts[0].RetirementDigests[g.AttemptID] = digest(g) } q, err := RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) if err != nil { t.Fatal(err) } if fault != "delivery only" { if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { t.Fatal(err) } } receipt, err := ReadRetirement(t.Context(), f.directory, q, g, f.now) if fault == "valid" || fault == "unrelated original" { if err != nil { t.Fatal(err) } if fault == "unrelated original" && len(receipt.Status.RecoverySources.Unresolved) != 1 { t.Fatal("lost unrelated obligation") } } else if err == nil { t.Fatal("unproven retirement reported complete") } if fault == "delivery only" && !errors.Is(err, os.ErrNotExist) { t.Fatalf("unexpected missing response: %v", err) } }) } }