package host import ( "context" "encoding/json" "errors" "os" "testing" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/rootstate" ) func plan14UnitBindingFixture(t *testing.T, unit, backing, socket string) (*claimUnitBindings, hostguard.RootInstallation, claimUnitShape) { t.Helper() in := hostguard.RootInstallation{Host: "shop.example", Account: "bab", App: "shop", Record: "apps/bab/shop/instances/shop.example.json", Incarnation: "inc", Stamp: 1, Migration: 1, InventoryRevision: 1, Box: "box", Enrollment: "enrollment"} shape := claimUnitShape{Host: in.Host, Account: in.Account, App: in.App, Kind: "instance", Unit: unit, Backing: backing, Socket: socket, DBPath: "/data/instances/shop/data.db"} if socket == "" { shape.Addr = "127.0.0.1:8090" } b := &claimUnitBindings{state: &rollbackMemory{files: map[string][]byte{}}, installation: func(context.Context, string) (hostguard.RootInstallation, error) { return in, nil }, canonical: func(context.Context, string) (claimUnitShape, error) { return shape, nil }} return b, in, shape } func TestPlan14CanonicalUnitBinding(t *testing.T) { for _, tc := range []struct{ name, unit, backing, socket string }{ {"named", "console.service", "", ""}, {"socket", "woodstar-atbridge.service", "", "/run/carlos/woodstar.sock"}, {"whole-app", "carlos-app@shop.example.service", "unit", ""}, } { t.Run(tc.name, func(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, tc.unit, tc.backing, tc.socket) saved, err := b.establish(t.Context(), in.Host) if err != nil { t.Fatal(err) } row := shape.route() if _, err := b.check(t.Context(), in.Host, &row); err != nil { t.Fatal(err) } raw, _ := b.state.Read(in.Host + ".json") for _, mutate := range []func(*registry.Route){ func(r *registry.Route) { r.Unit = "other.service" }, func(r *registry.Route) { r.Backing = "exec" }, func(r *registry.Route) { r.Socket = "/run/other.sock" }, func(r *registry.Route) { r.Addr = "127.0.0.1:9000" }, func(r *registry.Route) { r.DBPath = "/data/sibling.db" }, func(r *registry.Route) { r.Account = "other" }, func(r *registry.Route) { r.Tunnel = "other" }, func(r *registry.Route) { r.Kind = "sidecar" }, } { forged := row mutate(&forged) if _, err := b.check(t.Context(), in.Host, &forged); err == nil { t.Fatal("accepted changed shape", forged) } } b.canonical = func(context.Context, string) (claimUnitShape, error) { changed := shape changed.Unit = "other.service" return changed, nil } if _, err := b.establish(t.Context(), in.Host); err == nil { t.Fatal("rebound a live installation") } if _, err := b.check(t.Context(), in.Host, &row); err == nil { t.Fatal("accepted changed canonical configuration") } now, _ := b.state.Read(in.Host + ".json") if string(now) != string(raw) { t.Fatal("replaced binding") } b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } successor := in successor.Stamp++ b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return successor, nil } if _, err := b.check(t.Context(), in.Host, &row); err == nil { t.Fatal("adopted successor") } if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { return os.ErrPermission }); err == nil { t.Fatal("retired without stop") } pending, _ := b.read(in.Host) if !pending.Retiring || pending.Retired { t.Fatal("missing durable retirement intent") } if _, err := b.establish(t.Context(), in.Host); err == nil { t.Fatal("overwrote predecessor retirement") } called := false if err := b.retire(t.Context(), saved, func(got claimUnitBinding) error { called = true if got.Owner != saved.Owner || got.Shape != shape { t.Fatal("retirement retargeted") } return nil }); err != nil { t.Fatal(err) } if !called { t.Fatal("no stop verification") } if _, err := b.check(t.Context(), in.Host, &row); err == nil { t.Fatal("retired binding granted work") } if _, err := b.establish(t.Context(), in.Host); err == nil { t.Fatal("implicit successor grant") } }) } } func TestPlan14CanonicalUnitBindingRefusals(t *testing.T) { b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") for _, raw := range []string{"", "{}", `{"Schema":1,"unknown":1}`} { b.state.Write(in.Host+".json", []byte(raw)) if _, err := b.establish(t.Context(), in.Host); err == nil { t.Fatal("corrupt history became permission") } } b.state.Remove(in.Host + ".json") b.canonical = func(context.Context, string) (claimUnitShape, error) { return claimUnitShape{}, os.ErrNotExist } if _, err := b.establish(t.Context(), in.Host); !errors.Is(err, os.ErrNotExist) { t.Fatal(err) } if _, err := b.state.Read(in.Host + ".json"); !errors.Is(err, os.ErrNotExist) { t.Fatal("created binding without root config", err) } } func TestPlan14UnitBindingRetirementCrash(t *testing.T) { b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") saved, err := b.establish(t.Context(), in.Host) if err != nil { t.Fatal(err) } if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { return os.ErrPermission }); err == nil { t.Fatal("lost stop accepted") } restarted := *b if _, err := restarted.check(t.Context(), in.Host, nil); err == nil { t.Fatal("crash reopened admission") } if err := restarted.retire(t.Context(), saved, func(claimUnitBinding) error { return nil }); err != nil { t.Fatal(err) } raw, _ := b.state.Read(in.Host + ".json") var record claimUnitBinding if err := json.Unmarshal(raw, &record); err != nil || !record.Retired { t.Fatal(record, err) } } func TestPlan14UnitBindingConfig(t *testing.T) { _, _, s := plan14UnitBindingFixture(t, "console.service", "", "") raw, _ := json.Marshal(struct { Schema int Units []claimUnitShape }{1, []claimUnitShape{s}}) if got, err := decodeClaimUnitShapes(raw); err != nil || got[s.Host] != s { t.Fatal(got, err) } for _, kind := range []string{"duplicate host", "duplicate unit", "tunnel", "unknown field", "trailing", "unsafe addr", "template", "backing"} { t.Run(kind, func(t *testing.T) { other := s other.Host = "sibling.example" config := struct { Schema int Units []claimUnitShape }{1, []claimUnitShape{s}} switch kind { case "duplicate host": config.Units = append(config.Units, s) case "duplicate unit": config.Units = append(config.Units, other) case "unsafe addr": config.Units[0].Addr = "0.0.0.0:8090" case "template": config.Units[0].Unit = "x@.service" case "backing": config.Units[0].Backing = "unit" } body, _ := json.Marshal(config) switch kind { case "tunnel": body = []byte(`{"Schema":1,"Units":[{"Tunnel":"x"}]}`) case "unknown field": body = []byte(`{"Schema":1,"Units":[],"Authority":true}`) case "trailing": body = append(body, []byte(`{}`)...) } if _, err := decodeClaimUnitShapes(body); err == nil { t.Fatal("accepted invalid config") } }) } } type plan14UnitBindingFault struct { rootstate.Store fail bool writes int } func (s *plan14UnitBindingFault) Write(name string, body []byte) error { s.writes++ if err := s.Store.Write(name, body); err != nil { return err } if s.fail { return os.ErrInvalid } return nil } func TestPlan14UnitBindingDurability(t *testing.T) { b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") fault := &plan14UnitBindingFault{Store: b.state, fail: true} b.state = fault if _, err := b.establish(t.Context(), in.Host); err == nil { t.Fatal("reported unsynced creation") } if _, err := b.establish(t.Context(), in.Host); err == nil { t.Fatal("duplicate creation skipped durability") } fault.fail = false saved, err := b.establish(t.Context(), in.Host) if err != nil { t.Fatal(err) } fault.fail = true effects := 0 if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err == nil { t.Fatal("reported unsynced retirement intent") } if effects != 0 { t.Fatal("effects before durable intent") } if _, err := b.check(t.Context(), in.Host, nil); err == nil { t.Fatal("visible retirement intent reopened") } fault.fail = false if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err != nil { t.Fatal(err) } if effects != 1 { t.Fatal(effects) } fault.fail = true if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err == nil { t.Fatal("duplicate completion skipped durability") } if effects != 1 { t.Fatal("replayed completed effect") } }