| index 19f6aebba..95bcf388f 100644 |
| --- a/internal/provision/reconcile_test.go |
| +++ b/internal/provision/reconcile_test.go |
| @@ -129,6 +129,70 @@ func TestReconcileMints(t *testing.T) { |
| } |
| } |
| |
| +// Every shape the record can now describe, minted in one pass. The three |
| +// cases are the production routes the old vocabulary could not express: |
| +// a console (addr-backed, unit-owned, never hibernating), a whole-app TCP |
| +// tenant, and a static site. |
| +func TestReconcileMintsEveryShape(t *testing.T) { |
| + s, sqid := setup(t) |
| + off := false |
| + recs := []Record{ |
| + {Schema: 1, Host: "console.go.tito.io", Slug: "console", App: "tito", Account: sqid, |
| + Pool: "tito", Region: "ie", Channel: "stable", Kind: "instance", |
| + Addr: "127.0.0.1:8090", Unit: "console.service", CreatedAt: time.Now()}, |
| + {Schema: 1, Host: "whole.go.tito.io", Slug: "whole", App: "tito", Account: sqid, |
| + Pool: "tito", Region: "ie", Channel: "stable", Addr: "127.0.0.1:9443", |
| + Hibernate: &off, CreatedAt: time.Now()}, |
| + {Schema: 1, Host: "site.go.tito.io", Slug: "site", App: "tito", Account: sqid, |
| + Pool: "tito", Region: "ie", Channel: "stable", Kind: "static", |
| + StatusNamed: true, CreatedAt: time.Now()}, |
| + } |
| + for _, rec := range recs { |
| + if err := CreateRecord(ctx, s, rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + reg := regWith(t) |
| + cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"} |
| + if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + console := *mustGet(t, reg, "console.go.tito.io") |
| + console.CreatedAt = 0 |
| + wantConsole := registry.Route{ |
| + Host: "console.go.tito.io", Slug: "console", Kind: "instance", |
| + Addr: "127.0.0.1:8090", Unit: "console.service", |
| + App: "tito", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", |
| + Provisioned: true, Seed: `["seed"]`, Status: "ready", |
| + } |
| + if console != wantConsole { |
| + t.Errorf("console route = %+v, want %+v", console, wantConsole) |
| + } |
| + |
| + whole := *mustGet(t, reg, "whole.go.tito.io") |
| + if whole.Socket != "" || whole.DBPath != "" { |
| + t.Errorf("an addr-backed tenant derived filesystem paths: socket %q db %q", whole.Socket, whole.DBPath) |
| + } |
| + if whole.Hibernate { |
| + t.Error("an explicitly non-hibernating record minted a hibernating route") |
| + } |
| + |
| + site := *mustGet(t, reg, "site.go.tito.io") |
| + if site.Kind != "static" { |
| + t.Errorf("static route Kind = %q", site.Kind) |
| + } |
| + if site.Socket != "" || site.DBPath != "" { |
| + t.Errorf("a static route derived filesystem paths: socket %q db %q", site.Socket, site.DBPath) |
| + } |
| + if site.Hibernate { |
| + t.Error("a static route was armed for hibernation — it has no process to wake") |
| + } |
| + if !site.StatusNamed { |
| + t.Error("StatusNamed did not reach the row") |
| + } |
| +} |
| + |
| // 2. Scope. |
| func TestReconcileScope(t *testing.T) { |
| s, sqid := setup(t) |
| @@ -436,6 +500,55 @@ func TestReconcileSkipsRecordWithInvalidHostShape(t *testing.T) { |
| mustGet(t, reg, bad) |
| } |
| |
| +// ValidateShape is a safety net, not just the console door's policy check: |
| +// a record can reach the bucket by paths other than the console (an older |
| +// or future writer, or direct bucket access), and this pass must not |
| +// materialize one whose Kind/Addr/Unit/Environment combination is |
| +// malformed. Skipped loudly, and protected from removal for the same |
| +// reason a corrupted body or an invalid host shape is — an unusable record |
| +// is not a retired one. |
| +func TestReconcileSkipsRecordWithInvalidShape(t *testing.T) { |
| + s, sqid := setup(t) |
| + // kind "service" with no addr: ValidateShape refuses this combination |
| + // (the platform only proxies to a service, so it must name its |
| + // upstream), but it agrees with its own key path and its host shape is |
| + // fine — the only thing wrong is the Kind/Addr combination itself. |
| + bad := Record{Schema: 1, Host: "bad-service.go.tito.io", Slug: "bad-service", App: "tito", |
| + Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", Kind: "service", |
| + CreatedAt: time.Now()} |
| + body, err := json.Marshal(bad) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := s.Put(ctx, RecordKey(sqid, "tito", bad.Host), body, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil { |
| + t.Fatal(err) |
| + } |
| + reg := regWith(t) |
| + cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"} |
| + if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + if rt, err := reg.Get(bad.Host); err != nil || rt != nil { |
| + t.Errorf("minted a route from a shape-invalid record: %+v %v", rt, err) |
| + } |
| + // The good neighbour in the same pass is untouched by the skip. |
| + mustGet(t, reg, "acme.go.tito.io") |
| + |
| + // And a route that somehow already carries that host is not treated as |
| + // retired: the record is there, it just cannot be used. |
| + if err := reg.Upsert(registry.Route{Host: bad.Host, Kind: "instance", App: "tito", |
| + Status: "ready", Provisioned: true, Region: "ie", Pool: "tito", Account: sqid}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + mustGet(t, reg, bad.Host) |
| +} |
| + |
| // CRITICAL regression (code review finding): an existing route's record |
| // going unparseable — not gone, just unreadable this pass — must never read |
| // as "the bucket retired it". Before the fix, json.Unmarshal failure and |