carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/maintenanceguard"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16func testProjection(t *testing.T, generation int64) MaintenanceProjection {
17 t.Helper()
18 b, e := maintenanceguard.EncodeProjection(maintenanceguard.Projection{Version: 1, Generation: generation, GuardDigest: strings.Repeat("a", 64)})
19 if e != nil {
20 t.Fatal(e)
21 }
22 return b
23}
24func TestMaintenanceProjectionParsing(t *testing.T) {
25 valid := string(testProjection(t, 1))
26 for _, value := range []string{"null", "{}", "[]", `"private"`, strings.Replace(valid, `"version":1`, `"version":2`, 1), strings.Replace(valid, `"generation":1`, `"generation":1.0`, 1), strings.Replace(valid, `"generation":1`, `"generation":0`, 1), strings.Replace(valid, `"version":1`, `"version":1,"version":1`, 1), strings.Replace(valid, `"version":1`, `"Version":1`, 1), strings.Replace(valid, `"version":1`, `"version":1,"extra":true`, 1)} {
27 t.Run(value, func(t *testing.T) {
28 raw := []byte(`{"maintenance":` + value + `}`)
29 r, e := DecodeRecord(raw)
30 if e != ErrMaintenanceProjection {
31 t.Fatal("accepted invalid")
32 }
33 if _, e = json.Marshal(r); e == nil {
34 t.Fatal("invalid disappeared")
35 }
36 var direct Record
37 if json.Unmarshal(raw, &direct) == nil {
38 t.Fatal("field accepted invalid")
39 }
40 if _, e = json.Marshal(direct); e == nil {
41 t.Fatal("ignored field error disappeared")
42 }
43 })
44 }
45 for _, raw := range []string{`{"maintenance":` + valid + `,"maintenance":` + valid + `}`, `{"Maintenance":` + valid + `}`, `{"maintenance":` + valid + `,"MAINTENANCE":null}`, `null`, `{} {}`} {
46 if _, e := DecodeRecord([]byte(raw)); e == nil {
47 t.Fatal("accepted outer ambiguity")
48 }
49 }
50 r, e := DecodeRecord([]byte(`{"maintenance":` + valid + `,"future_record_field":true}`))
51 if e != nil {
52 t.Fatal(e)
53 }
54 if _, e = json.Marshal(r); e != nil {
55 t.Fatal(e)
56 }
57 legacy := Record{Host: "legacy"}
58 b, e := json.Marshal(legacy)
59 if e != nil || bytes.Contains(b, []byte("maintenance")) {
60 t.Fatal("legacy changed", e)
61 }
62}
63func TestMaintenanceDoesNotPromoteJSONMethods(t *testing.T) {
64 type view struct {
65 Record
66 HiddenGate string `json:"gate,omitempty"`
67 Extra string `json:"extra"`
68 }
69 in := view{Record: Record{Gate: "private hash", Maintenance: testProjection(t, 1)}, Extra: "joined"}
70 b, e := json.Marshal(in)
71 if e != nil || bytes.Contains(b, []byte("private hash")) {
72 t.Fatal("gate leaked", e)
73 }
74 var out view
75 if json.Unmarshal(b, &out) != nil || out.Extra != "joined" || len(out.Maintenance) == 0 {
76 t.Fatal("embedded fields swallowed")
77 }
78}
79func TestOrdinaryWritersPreserveProjection(t *testing.T) {
80 s, sqid := setup(t)
81 rec := acmeRecord(sqid)
82 rec.Maintenance = testProjection(t, 1)
83 if CreateRecord(ctx, s, rec) != ErrMaintenanceProjection {
84 t.Fatal("ordinary create installed projection")
85 }
86 key := RecordKey(rec.Account, rec.App, rec.Host)
87 raw, e := json.Marshal(rec)
88 if e != nil {
89 t.Fatal(e)
90 }
91 if _, e = s.Put(ctx, key, raw, ""); e != nil {
92 t.Fatal(e)
93 }
94 for _, mut := range []func(*Record){func(r *Record) { r.Maintenance = nil }, func(r *Record) { r.Maintenance = testProjection(t, 2) }, func(r *Record) { r.CreatedAt = r.CreatedAt.Add(time.Second) }, func(r *Record) { r.Account = "different" }, func(r *Record) { r.App = "different" }, func(r *Record) { r.Host = "different.example" }} {
95 before, etag, e := GetRecordETag(ctx, s, rec.Account, rec.App, rec.Host)
96 if e != nil {
97 t.Fatal(e)
98 }
99 mut(before)
100 if UpdateRecord(ctx, s, *before, etag) == nil {
101 t.Fatal("whole writer changed protected fields")
102 }
103 if _, e = SetRecordMove(ctx, s, rec.Account, rec.App, rec.Host, func(r *Record) error { mut(r); return nil }); e == nil {
104 t.Fatal("callback changed protected fields")
105 }
106 }
107 current, etag, e := GetRecordETag(ctx, s, rec.Account, rec.App, rec.Host)
108 if e != nil {
109 t.Fatal(e)
110 }
111 current.Maintenance = MaintenanceProjection(strings.Replace(string(current.Maintenance), ":", ": ", -1))
112 current.CreatedAt = current.CreatedAt.In(time.FixedZone("same instant", 3600))
113 current.Channel = "new"
114 if e = UpdateRecord(ctx, s, *current, etag); e != nil {
115 t.Fatal("semantic equality refused", e)
116 }
117 if e = SetRecordRegion(ctx, s, rec.Account, rec.App, rec.Host, "uk"); e != nil {
118 t.Fatal(e)
119 }
120 got, e := GetRecord(ctx, s, rec.Account, rec.App, rec.Host)
121 if e != nil || !protectedFieldsEqual(rec, *got) || got.Region != "uk" {
122 t.Fatal("projection lost", e)
123 }
124 absent := acmeRecord(sqid)
125 absent.Host = "other.go.tito.io"
126 if e = CreateRecord(ctx, s, absent); e != nil {
127 t.Fatal(e)
128 }
129 if _, e = SetRecordMove(ctx, s, absent.Account, absent.App, absent.Host, func(r *Record) error { r.Maintenance = testProjection(t, 1); return nil }); e != ErrMaintenanceProjection {
130 t.Fatal("installed into absent record", e)
131 }
132}
133
134type projectionRaceStore struct {
135 release.Store
136 beforePut func()
137}
138
139func (s *projectionRaceStore) Put(c context.Context, k string, b []byte, tag string) (string, error) {
140 if s.beforePut != nil {
141 f := s.beforePut
142 s.beforePut = nil
143 f()
144 }
145 return s.Store.Put(c, k, b, tag)
146}
147func TestProjectionCASRaces(t *testing.T) {
148 for _, retry := range []bool{false, true} {
149 t.Run(map[bool]string{false: "whole", true: "retry"}[retry], func(t *testing.T) {
150 base, sqid := setup(t)
151 rec := acmeRecord(sqid)
152 if e := CreateRecord(ctx, base, rec); e != nil {
153 t.Fatal(e)
154 }
155 old, tag, e := GetRecordETag(ctx, base, rec.Account, rec.App, rec.Host)
156 if e != nil {
157 t.Fatal(e)
158 }
159 wrapped := &projectionRaceStore{Store: base, beforePut: func() {
160 rec.Maintenance = testProjection(t, 1)
161 b, e := json.Marshal(rec)
162 if e != nil {
163 t.Fatal(e)
164 }
165 putReplace(t, base, RecordKey(rec.Account, rec.App, rec.Host), b)
166 }}
167 if retry {
168 e = SetRecordRegion(ctx, wrapped, rec.Account, rec.App, rec.Host, "uk")
169 if e != nil {
170 t.Fatal(e)
171 }
172 } else {
173 old.Channel = "new"
174 e = UpdateRecord(ctx, wrapped, *old, tag)
175 if !errors.Is(e, release.ErrCASConflict) {
176 t.Fatal("stale write won", e)
177 }
178 }
179 got, e := GetRecord(ctx, base, rec.Account, rec.App, rec.Host)
180 if e != nil || len(got.Maintenance) == 0 {
181 t.Fatal("concurrent projection stripped", e)
182 }
183 if !retry && got.Channel != rec.Channel {
184 t.Fatal("failed write applied")
185 }
186 })
187 }
188}
189func TestMalformedProjectionReconcileAndLists(t *testing.T) {
190 s, sqid := setup(t)
191 rec := acmeRecord(sqid)
192 if e := CreateRecord(ctx, s, rec); e != nil {
193 t.Fatal(e)
194 }
195 reg := regWith(t)
196 cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
197 if e := Reconcile(ctx, s, reg, cfg); e != nil {
198 t.Fatal(e)
199 }
200 for _, host := range []string{rec.Host, "new.go.tito.io"} {
201 rec.Host = host
202 if host == "new.go.tito.io" {
203 if e := CreateRecord(ctx, s, rec); e != nil {
204 t.Fatal(e)
205 }
206 }
207 b, e := json.Marshal(rec)
208 if e != nil {
209 t.Fatal(e)
210 }
211 b = append(b[:len(b)-1], []byte(`,"maintenance":null}`)...)
212 putReplace(t, s, RecordKey(sqid, "tito", host), b)
213 if _, _, e = GetRecordETag(ctx, s, sqid, "tito", host); e == nil {
214 t.Fatal("get accepted invalid")
215 }
216 if e = SetRecordRegion(ctx, s, sqid, "tito", host, "uk"); e == nil {
217 t.Fatal("writer accepted invalid")
218 }
219 }
220 if e := Reconcile(ctx, s, reg, cfg); e != nil {
221 t.Fatal(e)
222 }
223 mustGet(t, reg, "acme.go.tito.io")
224 if r, e := reg.Get("new.go.tito.io"); e != nil || r != nil {
225 t.Fatal("malformed created route")
226 }
227 if records, _, e := ListRecords(ctx, s, sqid, "tito", 10); e != nil || len(records) != 0 {
228 t.Fatal("list semantics changed")
229 }
230 if records, e := ListAllRecords(ctx, s); e != nil || len(records) != 0 {
231 t.Fatal("all-list semantics changed")
232 }
233}
234