package historyreadback import ( "bytes" "encoding/json" "errors" "os" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestPlan14HistoryOperatorWorkflow(t *testing.T) { for _, kind := range []string{"status", "reconstruct", "retire"} { t.Run(kind, func(t *testing.T) { f := newFixture(t) var g *release.HistoryReconstruction var retire *release.HistoryRecoveryRetirement if kind == "reconstruct" { g = &release.HistoryReconstruction{Schema: 1, Kind: "carlos-release-history", Identity: f.status.Identity, Revision: 1, Operation: "operator", IssuedAt: f.now, Account: "abc", App: "web", Hosts: []release.HistoryReconstructionHost{f.status.Hosts[0].HistoryReconstructionHost}} 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, f.status.Hosts[0].RefusalStatus.Complete = 1, true f.status.LastReconstructionDigest = digest(g) } if kind == "retire" { retire = &release.HistoryRecoveryRetirement{Schema: 1, Kind: "carlos-release-recovery-retirement", Identity: f.status.Identity, Operation: "operator", IssuedAt: f.now, Binding: f.scope.Binding, Source: "canonical", AttemptID: "old", OutcomeDigest: strings.Repeat("d", 64), VerifiedID: "good", VerifiedDigest: strings.Repeat("e", 64)} f.status.RecoverySources = &release.HistoryRecoverySources{Known: true, Unresolved: []release.HistoryRecovery{{Binding: f.scope.Binding, Attempt: release.HistoryAttempt{ID: "unrelated"}}}} f.status.Hosts[0].RetiredRecoveries = []string{"old"} f.status.Hosts[0].RetirementDigests = map[string]string{"old": digest(retire)} f.status.Hosts[0].RecoveryDigests = map[string]string{"old": retire.OutcomeDigest, "good": retire.VerifiedDigest} } op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, g, retire, f.now) if err != nil { t.Fatal(err) } if _, _, err := f.directory.Store.Get(t.Context(), RequestKey(f.scope)); !errors.Is(err, os.ErrNotExist) { t.Fatal("prepare published before durable save", err) } raw, _ := json.Marshal(op) var resumed Operation if err := json.Unmarshal(raw, &resumed); err != nil { t.Fatal(err) } for i := 0; i < 2; i++ { if err := resumed.Deliver(t.Context(), f.directory, f.now); err != nil { t.Fatal(err) } } if _, err := resumed.Read(t.Context(), f.directory, f.now); err == nil { t.Fatal("delivery counted as completion") } if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { t.Fatal(err) } got, err := resumed.Read(t.Context(), f.directory, f.now) if err != nil { t.Fatal(err) } if kind == "retire" && len(got.Status.RecoverySources.Unresolved) != 1 { t.Fatal("lost unrelated original") } after, _ := json.Marshal(resumed) if !bytes.Equal(raw, after) { t.Fatal("resume changed authorization or challenge") } }) } } func TestPlan14HistoryOperatorConflicts(t *testing.T) { for _, fault := range []string{"changed grant", "wrong scope", "wrong signer", "changed request", "both", "expired", "delivery conflict", "replacement", "wrong replacement"} { 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: "operator", IssuedAt: f.now, Account: "abc", App: "web", Hosts: []release.HistoryReconstructionHost{f.status.Hosts[0].HistoryReconstructionHost}} old := g old.Operation = "old" if fault == "replacement" { g.Replaces = digest(old) } if fault == "wrong replacement" { g.Replaces = strings.Repeat("f", 64) } op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, &g, nil, f.now) if err != nil { t.Fatal(err) } switch fault { case "changed grant": op.Reconstruction = append(op.Reconstruction, 'x') case "changed request": op.Request = append(op.Request, 'x') case "wrong scope": g.Account = "xyz" op.Reconstruction, _ = release.SignRootEnvelope(f.signer, g) case "wrong signer": op.Reconstruction, _ = release.SignRootEnvelope(release.RootSigner(f.key), g) case "both": op.Retirement = op.Reconstruction case "expired": f.now = f.now.Add(25 * 60 * 60 * 1e9) case "delivery conflict", "replacement", "wrong replacement": wire, err := release.SignRootEnvelope(f.signer, old) if err != nil { t.Fatal(err) } if _, err := f.directory.Store.Put(t.Context(), release.HistoryReconstructionKey(old.Account, old.App, old.Identity, old.Revision), wire, ""); err != nil { t.Fatal(err) } } err = op.Deliver(t.Context(), f.directory, f.now) if fault == "replacement" { if err != nil { t.Fatal(err) } return } if err == nil { t.Fatal("invalid operation delivered") } if _, _, err := f.directory.Store.Get(t.Context(), RequestKey(f.scope)); !errors.Is(err, os.ErrNotExist) { t.Fatal("refused operation published challenge", err) } }) } } func TestPlan14HistoryOperatorExactCompletion(t *testing.T) { for _, fault := range []string{"pending", "other authorization", "unknown category", "pending original"} { 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: "operator", IssuedAt: f.now, Account: "abc", App: "web", Hosts: []release.HistoryReconstructionHost{f.status.Hosts[0].HistoryReconstructionHost}} var retire *release.HistoryRecoveryRetirement reconstruction := &g if fault == "pending original" { reconstruction = nil retire = &release.HistoryRecoveryRetirement{Schema: 1, Kind: "carlos-release-recovery-retirement", Identity: f.status.Identity, Operation: "operator", IssuedAt: f.now, Binding: f.scope.Binding, Source: "canonical", AttemptID: "old", OutcomeDigest: strings.Repeat("d", 64), VerifiedID: "good", VerifiedDigest: strings.Repeat("e", 64)} f.status.RecoverySources = &release.HistoryRecoverySources{Known: true, Unresolved: []release.HistoryRecovery{{Binding: f.scope.Binding, Attempt: release.HistoryAttempt{ID: "old"}}}} f.status.Hosts[0].RetiredRecoveries = []string{"old"} f.status.Hosts[0].RetirementDigests = map[string]string{"old": digest(retire)} f.status.Hosts[0].RecoveryDigests = map[string]string{"old": retire.OutcomeDigest, "good": retire.VerifiedDigest} } op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, reconstruction, retire, f.now) if err != nil { t.Fatal(err) } if err = op.Deliver(t.Context(), f.directory, f.now); err != nil { t.Fatal(err) } 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, f.status.Hosts[0].RefusalStatus.Complete = 1, true f.status.LastReconstructionDigest = digest(g) switch fault { case "pending": f.status.Pending = true case "other authorization": f.status.LastReconstructionDigest = strings.Repeat("f", 64) case "unknown category": f.status.Hosts[0].RefusalStatus.Complete = false } if err = Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { t.Fatal(err) } if _, err = op.Read(t.Context(), f.directory, f.now); err == nil { t.Fatal("incomplete operation reported success") } }) } }