| 1 | package historyreadback |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "os" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | func TestPlan14ReadReconstructionCompletion(t *testing.T) { |
| 14 | 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"} { |
| 15 | t.Run(fault, func(t *testing.T) { |
| 16 | f := newFixture(t) |
| 17 | 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}} |
| 18 | if fault == "replacement" { |
| 19 | g.Replaces = strings.Repeat("d", 64) |
| 20 | } |
| 21 | f.status.Revision = 2 |
| 22 | f.status.AppComplete, f.status.AppRevision, f.status.BakeComplete, f.status.BakeRevision = true, 1, 1, 1 |
| 23 | f.status.Hosts[0].Revision = 1 |
| 24 | f.status.Hosts[0].RefusalStatus.Complete = true |
| 25 | f.status.LastReconstructionDigest = digest(g) |
| 26 | switch fault { |
| 27 | case "invalid serialization": |
| 28 | g.IssuedAt = time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) |
| 29 | f.status.LastReconstructionDigest = "" |
| 30 | f.status.Hosts[0].RetirementDigests = nil |
| 31 | case "different operation": |
| 32 | g.Operation = "other" |
| 33 | case "different replacement": |
| 34 | g.Replaces = strings.Repeat("e", 64) |
| 35 | case "different bounds": |
| 36 | g.Production = true |
| 37 | case "namespace": |
| 38 | f.status.Identity = strings.Repeat("f", 64) |
| 39 | case "revision": |
| 40 | f.status.Revision++ |
| 41 | case "pending": |
| 42 | f.status.Pending = true |
| 43 | case "source": |
| 44 | f.status.Hosts[0].Source = "changed" |
| 45 | case "scope": |
| 46 | g.Account = "xyz" |
| 47 | f.status.LastReconstructionDigest = digest(g) |
| 48 | case "unknown app": |
| 49 | f.status.AppComplete = false |
| 50 | case "unknown host": |
| 51 | f.status.Hosts[0].RefusalStatus.Complete = false |
| 52 | case "unknown bake": |
| 53 | f.status.BakeComplete = 0 |
| 54 | case "missing digest": |
| 55 | f.status.LastReconstructionDigest = "" |
| 56 | } |
| 57 | q, err := RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) |
| 58 | if err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | if fault != "delivery only" { |
| 62 | if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | } |
| 66 | if fault == "signature" { |
| 67 | r := Receipt{Schema: 1, Kind: "carlos-release-history-receipt", RequestDigest: digest(q), ObservedAt: f.now, Status: f.status} |
| 68 | wire, err := release.SignRootEnvelope(f.signer, r) |
| 69 | if err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | if err := publish(t.Context(), f.directory.Store, ResponseKey(q), wire); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | } |
| 76 | if fault == "old challenge" { |
| 77 | q, err = RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) |
| 78 | if err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | } |
| 82 | _, err = ReadReconstruction(t.Context(), f.directory, q, g, f.now) |
| 83 | if fault == "valid" || fault == "replacement" { |
| 84 | if err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | } else if err == nil { |
| 88 | t.Fatal("unproven reconstruction reported complete") |
| 89 | } |
| 90 | }) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestPlan14ReadRetirementCompletion(t *testing.T) { |
| 95 | 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"} { |
| 96 | t.Run(fault, func(t *testing.T) { |
| 97 | f := newFixture(t) |
| 98 | 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)} |
| 99 | f.status.RecoverySources = &release.HistoryRecoverySources{Known: true} |
| 100 | f.status.Hosts[0].RetiredRecoveries = []string{g.AttemptID} |
| 101 | f.status.Hosts[0].RetirementDigests = map[string]string{g.AttemptID: digest(g)} |
| 102 | f.status.Hosts[0].RecoveryDigests = map[string]string{g.AttemptID: g.OutcomeDigest, g.VerifiedID: g.VerifiedDigest} |
| 103 | switch fault { |
| 104 | case "invalid serialization": |
| 105 | g.IssuedAt = time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) |
| 106 | f.status.LastReconstructionDigest = "" |
| 107 | f.status.Hosts[0].RetirementDigests = nil |
| 108 | case "different operation": |
| 109 | g.Operation = "other" |
| 110 | case "different outcome": |
| 111 | g.OutcomeDigest = strings.Repeat("f", 64) |
| 112 | case "different witness": |
| 113 | g.VerifiedDigest = strings.Repeat("f", 64) |
| 114 | case "missing digest": |
| 115 | f.status.Hosts[0].RetirementDigests = nil |
| 116 | case "missing retired": |
| 117 | f.status.Hosts[0].RetiredRecoveries = nil |
| 118 | case "unknown originals": |
| 119 | f.status.RecoverySources.Known = false |
| 120 | case "pending original", "unrelated original": |
| 121 | id := g.AttemptID |
| 122 | if fault == "unrelated original" { |
| 123 | id = "unrelated" |
| 124 | } |
| 125 | f.status.RecoverySources.Unresolved = []release.HistoryRecovery{{Binding: g.Binding, Attempt: release.HistoryAttempt{ID: id}}} |
| 126 | case "pending reconstruction": |
| 127 | f.status.Pending = true |
| 128 | case "source": |
| 129 | f.status.Hosts[0].Source = "changed" |
| 130 | case "namespace": |
| 131 | f.status.Identity = strings.Repeat("f", 64) |
| 132 | case "scope": |
| 133 | g.Binding.Host = "other.example" |
| 134 | f.status.Hosts[0].RetirementDigests[g.AttemptID] = digest(g) |
| 135 | } |
| 136 | q, err := RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) |
| 137 | if err != nil { |
| 138 | t.Fatal(err) |
| 139 | } |
| 140 | if fault != "delivery only" { |
| 141 | if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | } |
| 145 | receipt, err := ReadRetirement(t.Context(), f.directory, q, g, f.now) |
| 146 | if fault == "valid" || fault == "unrelated original" { |
| 147 | if err != nil { |
| 148 | t.Fatal(err) |
| 149 | } |
| 150 | if fault == "unrelated original" && len(receipt.Status.RecoverySources.Unresolved) != 1 { |
| 151 | t.Fatal("lost unrelated obligation") |
| 152 | } |
| 153 | } else if err == nil { |
| 154 | t.Fatal("unproven retirement reported complete") |
| 155 | } |
| 156 | if fault == "delivery only" && !errors.Is(err, os.ErrNotExist) { |
| 157 | t.Fatalf("unexpected missing response: %v", err) |
| 158 | } |
| 159 | }) |
| 160 | } |
| 161 | } |
| 162 | |