package console import ( "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/certwarm" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/status" ) func TestDomainStateVocabulary(t *testing.T) { now := time.Unix(1_000_000, 0) future, past := now.Unix()+86400, now.Unix()-86400 for name, tc := range map[string]struct { reading status.AliasReading seen bool wantState string wantReason string }{ "no box has reported": {seen: false, wantState: StateChecking, wantReason: "no-reading"}, "our lookup failed": {reading: status.AliasReading{DNSStatus: "unresolved"}, seen: true, wantState: StateUnknown, wantReason: "lookup-failed"}, "no record yet": {reading: status.AliasReading{DNSStatus: ""}, seen: true, wantState: StatePendingDNS, wantReason: "no-record"}, "points elsewhere": {reading: status.AliasReading{DNSStatus: "elsewhere"}, seen: true, wantState: StateMisdirected, wantReason: "points-elsewhere"}, "pointing, no cert": {reading: status.AliasReading{DNSStatus: "pointing"}, seen: true, wantState: StateIssuingCert, wantReason: "awaiting-certificate"}, "pointing, dead cert": {reading: status.AliasReading{DNSStatus: "pointing", CertExpiresAt: past}, seen: true, wantState: StateIssuingCert, wantReason: "awaiting-certificate"}, "pointing, live cert": {reading: status.AliasReading{DNSStatus: "pointing", CertExpiresAt: future}, seen: true, wantState: StateLive, wantReason: "serving"}, "a status we don't know": {reading: status.AliasReading{DNSStatus: "quantum"}, seen: true, wantState: StateUnknown, wantReason: "unrecognised-status"}, } { state, reason, detail := domainState(tc.reading, tc.seen, nil, now) if state != tc.wantState || reason != tc.wantReason { t.Errorf("%s: (%q, %q), want (%q, %q)", name, state, reason, tc.wantState, tc.wantReason) } if strings.TrimSpace(detail) == "" { t.Errorf("%s: no human sentence", name) } } } // "we could not check" must never be phrased as "you have not done your // part". A resolver failure on our side and a genuinely missing record are // the two cases most likely to be collapsed, and collapsing them blames a // customer for our outage. func TestDomainStateNeverBlamesTheCustomerForOurOutage(t *testing.T) { now := time.Unix(1_000_000, 0) ourFault, _, ourDetail := domainState(status.AliasReading{DNSStatus: "unresolved"}, true, nil, now) theirs, _, _ := domainState(status.AliasReading{DNSStatus: ""}, true, nil, now) if ourFault == theirs { t.Fatalf("a failed lookup and a missing record both read %q", ourFault) } low := strings.ToLower(ourDetail) if !strings.Contains(low, "our side") { t.Errorf("the lookup-failure sentence does not own the problem: %q", ourDetail) } for _, blame := range []string{"create", "you have not", "your record is missing"} { if strings.Contains(low, blame) { t.Errorf("the lookup-failure sentence asks the customer to act (%q): %q", blame, ourDetail) } } } // Every state the capability block advertises must be reachable, and every // state the deriver can return must be advertised. A vocabulary that names a // state nothing produces is the dishonesty that got "verifying" renamed. func TestAdvertisedStatesAreExactlyTheReachableOnes(t *testing.T) { now := time.Unix(1_000_000, 0) produced := map[string]bool{} // The warm inputs are as real as the readings: a fresh record with the // attempt count the threshold names is exactly what the issuer writes, // so "failed" earns its place in the vocabulary the same way every other // state does — by being produced from an input the platform can produce. failing := &certwarm.Record{ Attempts: domainFailedAttempts, Class: certwarm.ClassOther, WrittenAt: now.Unix(), NextAttemptAt: now.Unix() + 3600, } for _, tc := range []struct { r status.AliasReading seen bool warm *certwarm.Record }{ {seen: false}, {r: status.AliasReading{DNSStatus: "unresolved"}, seen: true}, {r: status.AliasReading{DNSStatus: ""}, seen: true}, {r: status.AliasReading{DNSStatus: "elsewhere"}, seen: true}, {r: status.AliasReading{DNSStatus: "pointing"}, seen: true}, {r: status.AliasReading{DNSStatus: "pointing", CertExpiresAt: now.Unix() + 86400}, seen: true}, {r: status.AliasReading{DNSStatus: "pointing"}, seen: true, warm: failing}, } { s, _, _ := domainState(tc.r, tc.seen, tc.warm, now) produced[s] = true } // "unverified" comes from the claim, not the readings (#215): a claim // stored pending is exactly what a member's unproven attach writes. s, _, _ := claimState(domains.Claim{Pending: true}, status.AliasReading{DNSStatus: "pointing"}, true, nil, now) produced[s] = true for _, s := range domainStates { if !produced[s] { t.Errorf("state %q is advertised but nothing produces it", s) } } for s := range produced { if !slices.Contains(domainStates, s) { t.Errorf("state %q is produced but not advertised", s) } } } // "failed" is a claim about HISTORY — that the platform has been trying and // failing for hours — so the only thing that may produce it is a record of // that history. Three ways of having no such record must all still read as // "issuing-cert", and each of them was a live bug shape before §3e: no // record at all (the ordinary case), a record from an issuer that has since // wedged, and a record that has not yet reached the threshold. func TestFailedIsOnlyEverSaidOnFreshEnoughEvidence(t *testing.T) { now := time.Unix(1_000_000, 0) pointing := status.AliasReading{DNSStatus: "pointing"} fresh := func(r certwarm.Record) *certwarm.Record { r.WrittenAt, r.NextAttemptAt = now.Unix(), now.Unix()+3600 return &r } for _, tc := range []struct { name string warm *certwarm.Record want string }{ {"no record at all", nil, StateIssuingCert}, {"below the threshold", fresh(certwarm.Record{Attempts: domainFailedAttempts - 1}), StateIssuingCert}, {"at the threshold", fresh(certwarm.Record{Attempts: domainFailedAttempts}), StateFailed}, {"rate-limited on the first attempt", fresh(certwarm.Record{Attempts: 1, Class: certwarm.ClassRateLimited}), StateFailed}, {"stale record from a wedged issuer", &certwarm.Record{ Attempts: 40, WrittenAt: now.Add(-30 * 24 * time.Hour).Unix(), NextAttemptAt: now.Add(-30 * 24 * time.Hour).Unix(), }, StateIssuingCert}, } { if got, _, _ := domainState(pointing, true, tc.warm, now); got != tc.want { t.Errorf("%s: state = %q, want %q", tc.name, got, tc.want) } } // A live certificate outranks any record. The record is evidence about // an ORDER; the cert is evidence about the hostname, and a hostname that // is being served must never be told it has failed. served := status.AliasReading{DNSStatus: "pointing", CertExpiresAt: now.Unix() + 86400} if got, _, _ := domainState(served, true, fresh(certwarm.Record{Attempts: 99}), now); got != StateLive { t.Errorf("a served hostname with a stale failure record = %q, want %q", got, StateLive) } // A customer-side condition is never overwritten by our cert trouble: // telling somebody their certificate failed when their DNS points at // their old host would send them looking in the wrong place. elsewhere := status.AliasReading{DNSStatus: "elsewhere"} if got, _, _ := domainState(elsewhere, true, fresh(certwarm.Record{Attempts: 99}), now); got != StateMisdirected { t.Errorf("a misdirected hostname with a failure record = %q, want %q", got, StateMisdirected) } } // The structured records and the English sentence are the same advice. If // they disagree, an app creates records that contradict what the person // operating it was told. func TestDomainRecordsMatchTheGuidanceSentence(t *testing.T) { steering := DNSTargets{EdgeIPs: []string{"203.0.113.7", "198.51.100.9"}, SteeringHost: "edge.oncarlos.test"} recs := domainRecords("www.customer.test", steering, false, "") sentence := domainGuidance("www.customer.test", steering) if !strings.Contains(sentence, "CNAME") { t.Fatal("precondition: the steering sentence should offer a CNAME") } var cname, a []dnsRecord for _, r := range recs { switch r.Set { case "cname": cname = append(cname, r) case "a": a = append(a, r) } } if len(cname) != 1 || cname[0].Value != "edge.oncarlos.test" { t.Errorf("CNAME alternative = %+v", cname) } if len(a) != 2 { t.Errorf("A alternative = %+v, want one per edge IP", a) } // The two must be distinguishable AS alternatives: a client creating // every row would publish a CNAME beside A records for one name, which // is illegal and is exactly what the sentence's "works too" avoids. if cname[0].Set == a[0].Set { t.Error("the CNAME and A records share a set — a client cannot tell they are alternatives") } for _, r := range recs { if r.TTL != domainRecordTTL { 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) } } } // An apex cannot carry a CNAME (RFC 1034), and the sentence already says so. // The records must agree. func TestDomainRecordsOfferNoCNAMEForAnApex(t *testing.T) { steering := DNSTargets{EdgeIPs: []string{"203.0.113.7"}, SteeringHost: "edge.oncarlos.test"} for _, r := range domainRecords("customer.test", steering, false, "") { if r.Type == "CNAME" { t.Fatalf("offered an apex a CNAME: %+v", r) } } } // Without steering, edge. has no records behind it, so naming // it would send a customer at a hostname that does not resolve. func TestDomainRecordsNameNoAliasTargetWithoutSteering(t *testing.T) { plain := DNSTargets{EdgeIPs: []string{"203.0.113.7"}} recs := domainRecords("www.customer.test", plain, false, "") if len(recs) != 1 || recs[0].Type != "A" { t.Fatalf("records = %+v, want A records only", recs) } } // A wildcard-enabled claim needs its delegation CNAME, and it is required // whichever alternative the customer picks — the platform already instructs // it at the attach door and stamps delegation_status from it. func TestDomainRecordsIncludeTheACMEChallengeForWildcardClaims(t *testing.T) { targets := DNSTargets{EdgeIPs: []string{"203.0.113.7"}, SteeringHost: "edge.oncarlos.test"} recs := domainRecords("www.customer.test", targets, true, "acme.oncarlos.test") var found *dnsRecord for i, r := range recs { if strings.HasPrefix(r.Name, "_acme-challenge.") { found = &recs[i] } } if found == nil { t.Fatal("a wildcard claim was given no _acme-challenge record") } if !found.Required { t.Error("the challenge record is not marked required — it is needed whichever alternative is chosen") } if found.Value != "_acme-challenge.www.customer.test.acme.oncarlos.test" { t.Errorf("challenge target = %q", found.Value) } // With no delegation zone configured there is nothing to delegate to. for _, r := range domainRecords("www.customer.test", targets, true, "") { if strings.HasPrefix(r.Name, "_acme-challenge.") { t.Error("emitted a challenge record with no delegation zone configured") } } } // `status` is LEGACY and its semantics are frozen: it means "some non-stale // box reported ANY reading", nothing more. That is why a row can carry // status "live" while its state is "misdirected" — the hostname resolves // somewhere else entirely, and only one of those two words says so. // // This test exists to stop a well-meaning change from "fixing" status toward // state. Anything that reads status as a health verdict is reading it wrong; // the field is kept only so clients that predate state keep working. func TestLegacyStatusStaysDecoupledFromState(t *testing.T) { now := time.Unix(1_000_000, 0) // The exact divergence: a box HAS reported (so status is "live"), and // what it reported is that the hostname points somewhere else. state, _, _ := domainState(status.AliasReading{DNSStatus: "elsewhere"}, true, nil, now) if state != StateMisdirected { t.Fatalf("state = %q, want %q", state, StateMisdirected) } // If this ever fails, someone has redefined status. The list handler // sets it to "live" for any seen reading; that is the frozen meaning. const legacyStatusForAnySeenReading = "live" if legacyStatusForAnySeenReading == state { t.Fatal("status and state have converged — the legacy field's frozen meaning has been changed") } }