carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "slices"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/certwarm"
10 "github.com/carlosframework/platform/internal/domains"
11 "github.com/carlosframework/platform/internal/status"
12)
13
14func TestDomainStateVocabulary(t *testing.T) {
15 now := time.Unix(1_000_000, 0)
16 future, past := now.Unix()+86400, now.Unix()-86400
17
18 for name, tc := range map[string]struct {
19 reading status.AliasReading
20 seen bool
21 wantState string
22 wantReason string
23 }{
24 "no box has reported": {seen: false, wantState: StateChecking, wantReason: "no-reading"},
25 "our lookup failed": {reading: status.AliasReading{DNSStatus: "unresolved"}, seen: true, wantState: StateUnknown, wantReason: "lookup-failed"},
26 "no record yet": {reading: status.AliasReading{DNSStatus: ""}, seen: true, wantState: StatePendingDNS, wantReason: "no-record"},
27 "points elsewhere": {reading: status.AliasReading{DNSStatus: "elsewhere"}, seen: true, wantState: StateMisdirected, wantReason: "points-elsewhere"},
28 "pointing, no cert": {reading: status.AliasReading{DNSStatus: "pointing"}, seen: true, wantState: StateIssuingCert, wantReason: "awaiting-certificate"},
29 "pointing, dead cert": {reading: status.AliasReading{DNSStatus: "pointing", CertExpiresAt: past}, seen: true, wantState: StateIssuingCert, wantReason: "awaiting-certificate"},
30 "pointing, live cert": {reading: status.AliasReading{DNSStatus: "pointing", CertExpiresAt: future}, seen: true, wantState: StateLive, wantReason: "serving"},
31 "a status we don't know": {reading: status.AliasReading{DNSStatus: "quantum"}, seen: true, wantState: StateUnknown, wantReason: "unrecognised-status"},
32 } {
33 state, reason, detail := domainState(tc.reading, tc.seen, nil, now)
34 if state != tc.wantState || reason != tc.wantReason {
35 t.Errorf("%s: (%q, %q), want (%q, %q)", name, state, reason, tc.wantState, tc.wantReason)
36 }
37 if strings.TrimSpace(detail) == "" {
38 t.Errorf("%s: no human sentence", name)
39 }
40 }
41}
42
43// "we could not check" must never be phrased as "you have not done your
44// part". A resolver failure on our side and a genuinely missing record are
45// the two cases most likely to be collapsed, and collapsing them blames a
46// customer for our outage.
47func TestDomainStateNeverBlamesTheCustomerForOurOutage(t *testing.T) {
48 now := time.Unix(1_000_000, 0)
49 ourFault, _, ourDetail := domainState(status.AliasReading{DNSStatus: "unresolved"}, true, nil, now)
50 theirs, _, _ := domainState(status.AliasReading{DNSStatus: ""}, true, nil, now)
51 if ourFault == theirs {
52 t.Fatalf("a failed lookup and a missing record both read %q", ourFault)
53 }
54 low := strings.ToLower(ourDetail)
55 if !strings.Contains(low, "our side") {
56 t.Errorf("the lookup-failure sentence does not own the problem: %q", ourDetail)
57 }
58 for _, blame := range []string{"create", "you have not", "your record is missing"} {
59 if strings.Contains(low, blame) {
60 t.Errorf("the lookup-failure sentence asks the customer to act (%q): %q", blame, ourDetail)
61 }
62 }
63}
64
65// Every state the capability block advertises must be reachable, and every
66// state the deriver can return must be advertised. A vocabulary that names a
67// state nothing produces is the dishonesty that got "verifying" renamed.
68func TestAdvertisedStatesAreExactlyTheReachableOnes(t *testing.T) {
69 now := time.Unix(1_000_000, 0)
70 produced := map[string]bool{}
71 // The warm inputs are as real as the readings: a fresh record with the
72 // attempt count the threshold names is exactly what the issuer writes,
73 // so "failed" earns its place in the vocabulary the same way every other
74 // state does — by being produced from an input the platform can produce.
75 failing := &certwarm.Record{
76 Attempts: domainFailedAttempts, Class: certwarm.ClassOther,
77 WrittenAt: now.Unix(), NextAttemptAt: now.Unix() + 3600,
78 }
79 for _, tc := range []struct {
80 r status.AliasReading
81 seen bool
82 warm *certwarm.Record
83 }{
84 {seen: false},
85 {r: status.AliasReading{DNSStatus: "unresolved"}, seen: true},
86 {r: status.AliasReading{DNSStatus: ""}, seen: true},
87 {r: status.AliasReading{DNSStatus: "elsewhere"}, seen: true},
88 {r: status.AliasReading{DNSStatus: "pointing"}, seen: true},
89 {r: status.AliasReading{DNSStatus: "pointing", CertExpiresAt: now.Unix() + 86400}, seen: true},
90 {r: status.AliasReading{DNSStatus: "pointing"}, seen: true, warm: failing},
91 } {
92 s, _, _ := domainState(tc.r, tc.seen, tc.warm, now)
93 produced[s] = true
94 }
95 // "unverified" comes from the claim, not the readings (#215): a claim
96 // stored pending is exactly what a member's unproven attach writes.
97 s, _, _ := claimState(domains.Claim{Pending: true}, status.AliasReading{DNSStatus: "pointing"}, true, nil, now)
98 produced[s] = true
99 for _, s := range domainStates {
100 if !produced[s] {
101 t.Errorf("state %q is advertised but nothing produces it", s)
102 }
103 }
104 for s := range produced {
105 if !slices.Contains(domainStates, s) {
106 t.Errorf("state %q is produced but not advertised", s)
107 }
108 }
109}
110
111// "failed" is a claim about HISTORY — that the platform has been trying and
112// failing for hours — so the only thing that may produce it is a record of
113// that history. Three ways of having no such record must all still read as
114// "issuing-cert", and each of them was a live bug shape before §3e: no
115// record at all (the ordinary case), a record from an issuer that has since
116// wedged, and a record that has not yet reached the threshold.
117func TestFailedIsOnlyEverSaidOnFreshEnoughEvidence(t *testing.T) {
118 now := time.Unix(1_000_000, 0)
119 pointing := status.AliasReading{DNSStatus: "pointing"}
120 fresh := func(r certwarm.Record) *certwarm.Record {
121 r.WrittenAt, r.NextAttemptAt = now.Unix(), now.Unix()+3600
122 return &r
123 }
124 for _, tc := range []struct {
125 name string
126 warm *certwarm.Record
127 want string
128 }{
129 {"no record at all", nil, StateIssuingCert},
130 {"below the threshold", fresh(certwarm.Record{Attempts: domainFailedAttempts - 1}), StateIssuingCert},
131 {"at the threshold", fresh(certwarm.Record{Attempts: domainFailedAttempts}), StateFailed},
132 {"rate-limited on the first attempt", fresh(certwarm.Record{Attempts: 1, Class: certwarm.ClassRateLimited}), StateFailed},
133 {"stale record from a wedged issuer", &certwarm.Record{
134 Attempts: 40, WrittenAt: now.Add(-30 * 24 * time.Hour).Unix(),
135 NextAttemptAt: now.Add(-30 * 24 * time.Hour).Unix(),
136 }, StateIssuingCert},
137 } {
138 if got, _, _ := domainState(pointing, true, tc.warm, now); got != tc.want {
139 t.Errorf("%s: state = %q, want %q", tc.name, got, tc.want)
140 }
141 }
142 // A live certificate outranks any record. The record is evidence about
143 // an ORDER; the cert is evidence about the hostname, and a hostname that
144 // is being served must never be told it has failed.
145 served := status.AliasReading{DNSStatus: "pointing", CertExpiresAt: now.Unix() + 86400}
146 if got, _, _ := domainState(served, true, fresh(certwarm.Record{Attempts: 99}), now); got != StateLive {
147 t.Errorf("a served hostname with a stale failure record = %q, want %q", got, StateLive)
148 }
149 // A customer-side condition is never overwritten by our cert trouble:
150 // telling somebody their certificate failed when their DNS points at
151 // their old host would send them looking in the wrong place.
152 elsewhere := status.AliasReading{DNSStatus: "elsewhere"}
153 if got, _, _ := domainState(elsewhere, true, fresh(certwarm.Record{Attempts: 99}), now); got != StateMisdirected {
154 t.Errorf("a misdirected hostname with a failure record = %q, want %q", got, StateMisdirected)
155 }
156}
157
158// The structured records and the English sentence are the same advice. If
159// they disagree, an app creates records that contradict what the person
160// operating it was told.
161func TestDomainRecordsMatchTheGuidanceSentence(t *testing.T) {
162 steering := DNSTargets{EdgeIPs: []string{"203.0.113.7", "198.51.100.9"}, SteeringHost: "edge.oncarlos.test"}
163
164 recs := domainRecords("www.customer.test", steering, false, "")
165 sentence := domainGuidance("www.customer.test", steering)
166 if !strings.Contains(sentence, "CNAME") {
167 t.Fatal("precondition: the steering sentence should offer a CNAME")
168 }
169 var cname, a []dnsRecord
170 for _, r := range recs {
171 switch r.Set {
172 case "cname":
173 cname = append(cname, r)
174 case "a":
175 a = append(a, r)
176 }
177 }
178 if len(cname) != 1 || cname[0].Value != "edge.oncarlos.test" {
179 t.Errorf("CNAME alternative = %+v", cname)
180 }
181 if len(a) != 2 {
182 t.Errorf("A alternative = %+v, want one per edge IP", a)
183 }
184 // The two must be distinguishable AS alternatives: a client creating
185 // every row would publish a CNAME beside A records for one name, which
186 // is illegal and is exactly what the sentence's "works too" avoids.
187 if cname[0].Set == a[0].Set {
188 t.Error("the CNAME and A records share a set — a client cannot tell they are alternatives")
189 }
190 for _, r := range recs {
191 if r.TTL != domainRecordTTL {
192 t.Errorf("record %+v: TTL %d, want %d — steering withdraws a dead edge in ~90s and a long TTL nullifies that", r, r.TTL, domainRecordTTL)
193 }
194 }
195}
196
197// An apex cannot carry a CNAME (RFC 1034), and the sentence already says so.
198// The records must agree.
199func TestDomainRecordsOfferNoCNAMEForAnApex(t *testing.T) {
200 steering := DNSTargets{EdgeIPs: []string{"203.0.113.7"}, SteeringHost: "edge.oncarlos.test"}
201 for _, r := range domainRecords("customer.test", steering, false, "") {
202 if r.Type == "CNAME" {
203 t.Fatalf("offered an apex a CNAME: %+v", r)
204 }
205 }
206}
207
208// Without steering, edge.<apps-domain> has no records behind it, so naming
209// it would send a customer at a hostname that does not resolve.
210func TestDomainRecordsNameNoAliasTargetWithoutSteering(t *testing.T) {
211 plain := DNSTargets{EdgeIPs: []string{"203.0.113.7"}}
212 recs := domainRecords("www.customer.test", plain, false, "")
213 if len(recs) != 1 || recs[0].Type != "A" {
214 t.Fatalf("records = %+v, want A records only", recs)
215 }
216}
217
218// A wildcard-enabled claim needs its delegation CNAME, and it is required
219// whichever alternative the customer picks — the platform already instructs
220// it at the attach door and stamps delegation_status from it.
221func TestDomainRecordsIncludeTheACMEChallengeForWildcardClaims(t *testing.T) {
222 targets := DNSTargets{EdgeIPs: []string{"203.0.113.7"}, SteeringHost: "edge.oncarlos.test"}
223 recs := domainRecords("www.customer.test", targets, true, "acme.oncarlos.test")
224 var found *dnsRecord
225 for i, r := range recs {
226 if strings.HasPrefix(r.Name, "_acme-challenge.") {
227 found = &recs[i]
228 }
229 }
230 if found == nil {
231 t.Fatal("a wildcard claim was given no _acme-challenge record")
232 }
233 if !found.Required {
234 t.Error("the challenge record is not marked required — it is needed whichever alternative is chosen")
235 }
236 if found.Value != "_acme-challenge.www.customer.test.acme.oncarlos.test" {
237 t.Errorf("challenge target = %q", found.Value)
238 }
239 // With no delegation zone configured there is nothing to delegate to.
240 for _, r := range domainRecords("www.customer.test", targets, true, "") {
241 if strings.HasPrefix(r.Name, "_acme-challenge.") {
242 t.Error("emitted a challenge record with no delegation zone configured")
243 }
244 }
245}
246
247// `status` is LEGACY and its semantics are frozen: it means "some non-stale
248// box reported ANY reading", nothing more. That is why a row can carry
249// status "live" while its state is "misdirected" — the hostname resolves
250// somewhere else entirely, and only one of those two words says so.
251//
252// This test exists to stop a well-meaning change from "fixing" status toward
253// state. Anything that reads status as a health verdict is reading it wrong;
254// the field is kept only so clients that predate state keep working.
255func TestLegacyStatusStaysDecoupledFromState(t *testing.T) {
256 now := time.Unix(1_000_000, 0)
257 // The exact divergence: a box HAS reported (so status is "live"), and
258 // what it reported is that the hostname points somewhere else.
259 state, _, _ := domainState(status.AliasReading{DNSStatus: "elsewhere"}, true, nil, now)
260 if state != StateMisdirected {
261 t.Fatalf("state = %q, want %q", state, StateMisdirected)
262 }
263 // If this ever fails, someone has redefined status. The list handler
264 // sets it to "live" for any seen reading; that is the frozen meaning.
265 const legacyStatusForAnySeenReading = "live"
266 if legacyStatusForAnySeenReading == state {
267 t.Fatal("status and state have converged — the legacy field's frozen meaning has been changed")
268 }
269}
270