carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package historyreadback
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "os"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14func TestPlan14HistoryOperatorWorkflow(t *testing.T) {
15 for _, kind := range []string{"status", "reconstruct", "retire"} {
16 t.Run(kind, func(t *testing.T) {
17 f := newFixture(t)
18 var g *release.HistoryReconstruction
19 var retire *release.HistoryRecoveryRetirement
20 if kind == "reconstruct" {
21 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}}
22 f.status.Revision = 2
23 f.status.AppComplete, f.status.AppRevision, f.status.BakeComplete, f.status.BakeRevision = true, 1, 1, 1
24 f.status.Hosts[0].Revision, f.status.Hosts[0].RefusalStatus.Complete = 1, true
25 f.status.LastReconstructionDigest = digest(g)
26 }
27 if kind == "retire" {
28 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)}
29 f.status.RecoverySources = &release.HistoryRecoverySources{Known: true, Unresolved: []release.HistoryRecovery{{Binding: f.scope.Binding, Attempt: release.HistoryAttempt{ID: "unrelated"}}}}
30 f.status.Hosts[0].RetiredRecoveries = []string{"old"}
31 f.status.Hosts[0].RetirementDigests = map[string]string{"old": digest(retire)}
32 f.status.Hosts[0].RecoveryDigests = map[string]string{"old": retire.OutcomeDigest, "good": retire.VerifiedDigest}
33 }
34 op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, g, retire, f.now)
35 if err != nil {
36 t.Fatal(err)
37 }
38 if _, _, err := f.directory.Store.Get(t.Context(), RequestKey(f.scope)); !errors.Is(err, os.ErrNotExist) {
39 t.Fatal("prepare published before durable save", err)
40 }
41 raw, _ := json.Marshal(op)
42 var resumed Operation
43 if err := json.Unmarshal(raw, &resumed); err != nil {
44 t.Fatal(err)
45 }
46 for i := 0; i < 2; i++ {
47 if err := resumed.Deliver(t.Context(), f.directory, f.now); err != nil {
48 t.Fatal(err)
49 }
50 }
51 if _, err := resumed.Read(t.Context(), f.directory, f.now); err == nil {
52 t.Fatal("delivery counted as completion")
53 }
54 if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil {
55 t.Fatal(err)
56 }
57 got, err := resumed.Read(t.Context(), f.directory, f.now)
58 if err != nil {
59 t.Fatal(err)
60 }
61 if kind == "retire" && len(got.Status.RecoverySources.Unresolved) != 1 {
62 t.Fatal("lost unrelated original")
63 }
64 after, _ := json.Marshal(resumed)
65 if !bytes.Equal(raw, after) {
66 t.Fatal("resume changed authorization or challenge")
67 }
68 })
69 }
70}
71
72func TestPlan14HistoryOperatorConflicts(t *testing.T) {
73 for _, fault := range []string{"changed grant", "wrong scope", "wrong signer", "changed request", "both", "expired", "delivery conflict", "replacement", "wrong replacement"} {
74 t.Run(fault, func(t *testing.T) {
75 f := newFixture(t)
76 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}}
77 old := g
78 old.Operation = "old"
79 if fault == "replacement" {
80 g.Replaces = digest(old)
81 }
82 if fault == "wrong replacement" {
83 g.Replaces = strings.Repeat("f", 64)
84 }
85 op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, &g, nil, f.now)
86 if err != nil {
87 t.Fatal(err)
88 }
89 switch fault {
90 case "changed grant":
91 op.Reconstruction = append(op.Reconstruction, 'x')
92 case "changed request":
93 op.Request = append(op.Request, 'x')
94 case "wrong scope":
95 g.Account = "xyz"
96 op.Reconstruction, _ = release.SignRootEnvelope(f.signer, g)
97 case "wrong signer":
98 op.Reconstruction, _ = release.SignRootEnvelope(release.RootSigner(f.key), g)
99 case "both":
100 op.Retirement = op.Reconstruction
101 case "expired":
102 f.now = f.now.Add(25 * 60 * 60 * 1e9)
103 case "delivery conflict", "replacement", "wrong replacement":
104 wire, err := release.SignRootEnvelope(f.signer, old)
105 if err != nil {
106 t.Fatal(err)
107 }
108 if _, err := f.directory.Store.Put(t.Context(), release.HistoryReconstructionKey(old.Account, old.App, old.Identity, old.Revision), wire, ""); err != nil {
109 t.Fatal(err)
110 }
111 }
112 err = op.Deliver(t.Context(), f.directory, f.now)
113 if fault == "replacement" {
114 if err != nil {
115 t.Fatal(err)
116 }
117 return
118 }
119 if err == nil {
120 t.Fatal("invalid operation delivered")
121 }
122 if _, _, err := f.directory.Store.Get(t.Context(), RequestKey(f.scope)); !errors.Is(err, os.ErrNotExist) {
123 t.Fatal("refused operation published challenge", err)
124 }
125 })
126 }
127}
128
129func TestPlan14HistoryOperatorExactCompletion(t *testing.T) {
130 for _, fault := range []string{"pending", "other authorization", "unknown category", "pending original"} {
131 t.Run(fault, func(t *testing.T) {
132 f := newFixture(t)
133 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}}
134 var retire *release.HistoryRecoveryRetirement
135 reconstruction := &g
136 if fault == "pending original" {
137 reconstruction = nil
138 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)}
139 f.status.RecoverySources = &release.HistoryRecoverySources{Known: true, Unresolved: []release.HistoryRecovery{{Binding: f.scope.Binding, Attempt: release.HistoryAttempt{ID: "old"}}}}
140 f.status.Hosts[0].RetiredRecoveries = []string{"old"}
141 f.status.Hosts[0].RetirementDigests = map[string]string{"old": digest(retire)}
142 f.status.Hosts[0].RecoveryDigests = map[string]string{"old": retire.OutcomeDigest, "good": retire.VerifiedDigest}
143 }
144 op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, reconstruction, retire, f.now)
145 if err != nil {
146 t.Fatal(err)
147 }
148 if err = op.Deliver(t.Context(), f.directory, f.now); err != nil {
149 t.Fatal(err)
150 }
151 f.status.Revision = 2
152 f.status.AppComplete, f.status.AppRevision, f.status.BakeComplete, f.status.BakeRevision = true, 1, 1, 1
153 f.status.Hosts[0].Revision, f.status.Hosts[0].RefusalStatus.Complete = 1, true
154 f.status.LastReconstructionDigest = digest(g)
155 switch fault {
156 case "pending":
157 f.status.Pending = true
158 case "other authorization":
159 f.status.LastReconstructionDigest = strings.Repeat("f", 64)
160 case "unknown category":
161 f.status.Hosts[0].RefusalStatus.Complete = false
162 }
163 if err = Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil {
164 t.Fatal(err)
165 }
166 if _, err = op.Read(t.Context(), f.directory, f.now); err == nil {
167 t.Fatal("incomplete operation reported success")
168 }
169 })
170 }
171}
172