| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "os" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | func TestPlan14HistoryReconstructionCompletion(t *testing.T) { |
| 10 | for _, boundary := range []string{"before", "intent", "complete", "unsynced"} { |
| 11 | t.Run(boundary, func(t *testing.T) { |
| 12 | h, s, b, pub, wire, now := reconstructionCrashFixture(t) |
| 13 | var g HistoryReconstruction |
| 14 | if err := VerifyRootEnvelope(pub, wire, &g); err != nil { |
| 15 | t.Fatal(err) |
| 16 | } |
| 17 | raw, _ := json.Marshal(g) |
| 18 | resolve := func(HistoryBinding) (string, error) { return "canonical", nil } |
| 19 | if boundary != "before" { |
| 20 | if boundary == "intent" { |
| 21 | h.Store = &reconstructionCrashStore{healthyRetryStore: s, stage: "intent", fail: true} |
| 22 | } |
| 23 | err := h.Reconstruct(pub, wire, b.Account, b.App, now, resolve) |
| 24 | if (boundary == "intent") != (err != nil) { |
| 25 | t.Fatalf("reconstruct: %v", err) |
| 26 | } |
| 27 | } |
| 28 | h = &RootHistory{Store: s} |
| 29 | if boundary == "unsynced" { |
| 30 | s.fail = 1 |
| 31 | } |
| 32 | view, err := h.MigrationStatus(b, resolve) |
| 33 | if boundary == "unsynced" { |
| 34 | if err == nil { |
| 35 | t.Fatal("unsynced completion published") |
| 36 | } |
| 37 | view, err = h.MigrationStatus(b, resolve) |
| 38 | } |
| 39 | if err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | want := "" |
| 43 | if boundary == "complete" || boundary == "unsynced" { |
| 44 | want = reconstructionDigest(raw) |
| 45 | } |
| 46 | if view.LastReconstructionDigest != want { |
| 47 | t.Fatalf("completion=%q want %q", view.LastReconstructionDigest, want) |
| 48 | } |
| 49 | }) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestPlan14HistoryRetirementCompletion(t *testing.T) { |
| 54 | for _, fault := range []string{"valid", "missing", "unsynced", "changed outcome", "pending original", "changed source"} { |
| 55 | t.Run(fault, func(t *testing.T) { |
| 56 | h, s, old, _, g, pub, key := retirementFixture(t) |
| 57 | if fault != "missing" { |
| 58 | if err := retireForTest(h, g, pub, key); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | } |
| 62 | raw, _ := json.Marshal(g) |
| 63 | if fault == "changed outcome" { |
| 64 | old.Verified = true |
| 65 | if err := h.RetainRecovery(old); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | } |
| 69 | if fault == "unsynced" { |
| 70 | h.Store = &retirementSyncStore{Store: s, fail: true} |
| 71 | } |
| 72 | source := "canonical" |
| 73 | if fault == "changed source" { |
| 74 | source = "changed" |
| 75 | } |
| 76 | sources := HistoryRecoverySources{Known: true} |
| 77 | if fault == "pending original" { |
| 78 | old.Settled = false |
| 79 | sources.Unresolved = []HistoryRecovery{old} |
| 80 | } |
| 81 | view, err := h.MigrationStatusWithSources(old.Binding, func(HistoryBinding) (string, error) { return source, nil }, sources) |
| 82 | if fault == "unsynced" { |
| 83 | if err == nil { |
| 84 | t.Fatal("unsynced retirement published") |
| 85 | } |
| 86 | h.Store = s |
| 87 | view, err = h.MigrationStatusWithSources(old.Binding, func(HistoryBinding) (string, error) { return source, nil }, sources) |
| 88 | } |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | want := "" |
| 93 | if fault == "valid" || fault == "unsynced" { |
| 94 | want = reconstructionDigest(raw) |
| 95 | } |
| 96 | if len(view.Hosts) != 1 || view.Hosts[0].RetirementDigests[old.Attempt.ID] != want { |
| 97 | t.Fatalf("status=%+v want %q", view, want) |
| 98 | } |
| 99 | if fault == "pending original" { |
| 100 | if len(view.RecoverySources.Unresolved) != 1 { |
| 101 | t.Fatal("lost original") |
| 102 | } |
| 103 | } |
| 104 | if _, err := s.Read(historyRecoveryKey(old)); os.IsNotExist(err) { |
| 105 | t.Fatal("deleted retained outcome") |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | } |
| 110 | |