package steering import ( "context" "encoding/json" "strings" "testing" "github.com/carlosframework/platform/internal/registry" ) // edgesBody renders one box's edges file the way internal/host's // edgesFilePass writes it (schema v1) — the bytes the housekeeper duty reads // out of control/edges/.json and hands to ConvergeDeployment verbatim. func edgesBody(t *testing.T, box, eip, region string) []byte { t.Helper() body, err := json.Marshal(map[string]any{ "v": 1, "box": box, "eip": eip, "region": region, "generation": "2026-08-23T09:00:00Z", "hosts": map[string]any{"app.example.com": map[string]any{"role": "home"}}, }) if err != nil { t.Fatal(err) } return body } // TestConvergeDeploymentFoldsAndConverges is the entry point's whole job: two // boxes' edges files plus one steered route become the four latency records // (2 names x 2 edges) the zone should hold. func TestConvergeDeploymentFoldsAndConverges(t *testing.T) { captureLog(t) f := newFake() err := ConvergeDeployment(context.Background(), f, testZone, []registry.Route{steeredRoute("app.example.com")}, [][]byte{ edgesBody(t, "dublin", "1.2.3.4", "eu-west-1"), edgesBody(t, "oregon", "5.6.7.8", "us-west-2"), }, "example.com") if err != nil { t.Fatalf("ConvergeDeployment: %v", err) } want := []string{ "app.example.com.|A|dublin", "app.example.com.|A|oregon", "edge.example.com.|A|dublin", "edge.example.com.|A|oregon", } got := recordNames(f.records) if len(got) != len(want) { t.Fatalf("records = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("records = %v, want %v", got, want) } } if f.nCreateCheck != 2 { t.Errorf("health checks created = %d, want one per distinct EIP (2)", f.nCreateCheck) } } // TestConvergeDeploymentSkipsUnparseableEdgesFiles: a file that LISTS and // READS but does not PARSE is a content problem the fold tolerates — it is // skipped, named in the log, and the pass converges on the rest. (A file that // fails to READ is a different class entirely and never reaches here: the // duty fails closed before calling this function at all — see // internal/host/steering_loop.go's loadEdgesFileBodies.) func TestConvergeDeploymentSkipsUnparseableEdgesFiles(t *testing.T) { logs := captureLog(t) f := newFake() err := ConvergeDeployment(context.Background(), f, testZone, []registry.Route{steeredRoute("app.example.com")}, [][]byte{ []byte("{not json at all"), edgesBody(t, "dublin", "1.2.3.4", "eu-west-1"), []byte(`{"box":"ancient","eip":"9.9.9.9","region":"eu-west-1"}`), // no "v" — unversioned }, "example.com") if err != nil { t.Fatalf("ConvergeDeployment: %v", err) } // Only dublin folded in: 2 names x 1 edge. if got := recordNames(f.records); len(got) != 2 { t.Fatalf("records = %v, want the 2 dublin records only", got) } out := logs.String() if !strings.Contains(out, "skipping edges file") { t.Fatalf("log = %q, want a skip line naming the unparseable files", out) } if strings.Count(out, "skipping edges file") != 2 { t.Fatalf("log = %q, want exactly 2 skip lines (bad json + unversioned)", out) } } // TestConvergeDeploymentAllEdgesUnparseableRefusesTeardown is the composition // that matters most: every edges file unreadable-as-content folds to an EMPTY // desired set, and converge's empty-set refusal must catch it. A pass that // obeyed it would delete the deployment's entire managed record set because // of a schema problem. func TestConvergeDeploymentAllEdgesUnparseableRefusesTeardown(t *testing.T) { logs := captureLog(t) f := newFake() // Seed the zone with a managed record so a teardown would be visible. mustConverge(t, f, []desiredRecord{dublinRec}) before := len(f.records) writesBefore := f.writes() err := ConvergeDeployment(context.Background(), f, testZone, []registry.Route{steeredRoute("app.example.com")}, [][]byte{[]byte("garbage"), []byte("{}")}, "example.com") if err != nil { t.Fatalf("ConvergeDeployment: %v", err) } if len(f.records) != before { t.Fatalf("records = %d, want %d — an empty fold must never tear the zone down", len(f.records), before) } if f.writes() != writesBefore { t.Fatalf("writes = %d, want %d (zero new writes)", f.writes(), writesBefore) } if !strings.Contains(logs.String(), "refusing to remove any record") { t.Fatalf("log = %q, want the empty-set refusal", logs.String()) } } // TestConvergeDeploymentRefusesBadZoneID: the zone-id gate is converge's, and // the entry point must not paper over it — a malformed id returns an error // (which the duty logs) rather than converging against something else. func TestConvergeDeploymentRefusesBadZoneID(t *testing.T) { captureLog(t) f := newFake() err := ConvergeDeployment(context.Background(), f, "/hostedzone/Z0ABC", []registry.Route{steeredRoute("app.example.com")}, [][]byte{edgesBody(t, "dublin", "1.2.3.4", "eu-west-1")}, "example.com") if err == nil { t.Fatal("ConvergeDeployment accepted a non-bare hosted-zone id") } if f.writes() != 0 { t.Fatalf("writes = %d, want 0", f.writes()) } }