//go:build unix package hostbox import ( "context" "database/sql" "encoding/json" "errors" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // admissionFixture is a bucket holding one record (instance or sidecar) and // the adopted hostclaim for its host, plus the identity a claim hook would // stamp on the route. type admissionFixture struct { store release.Store data string account, host, key string family, name string holder hostclaim.Holder owner registry.ClaimOwnership pass func(context.Context, release.Store, *registry.Registry, provision.ReconcileConfig) error } func newAdmissionFixture(t *testing.T, sidecar bool) admissionFixture { t.Helper() ctx := t.Context() store := release.NewDirStore(t.TempDir()) account := domains.Sqid(2) raw, err := json.Marshal(map[string]any{"id": 2, "sqid": account, "name": "fixture", "created_at": "2026-08-02T09:00:00Z"}) if err != nil { t.Fatal(err) } if _, err := store.Put(ctx, "accounts/"+account+"/account.json", raw, ""); err != nil { t.Fatal(err) } data := t.TempDir() if err := os.Mkdir(filepath.Join(data, "hostguard"), 0770); err != nil { t.Fatal(err) } created := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC) f := admissionFixture{store: store, data: data, account: account, host: "shop.example", family: "instance"} f.key = provision.RecordKey(account, "app", f.host) f.pass = provision.Reconcile if sidecar { f.host = provision.SidecarHost("worker", "app", account) f.family, f.name = "sidecar", "worker" f.key = provision.SidecarRecordKey(account, "app", "worker") f.pass = provision.ReconcileSidecars if err := provision.PutSidecarManifest(ctx, store, account, "app", provision.SidecarManifest{Schema: 1, App: "app", Args: []string{"run"}, Channel: "stable"}); err != nil { t.Fatal(err) } if err := provision.CreateSidecarRecord(ctx, store, provision.SidecarRecord{Schema: 1, Name: "worker", App: "app", Account: account, Host: f.host, Region: "ie", Channel: "stable", CreatedAt: created}); err != nil { t.Fatal(err) } } else { if err := provision.PutManifest(ctx, store, account, "app", provision.Manifest{Schema: 1, App: "app", Domain: "example"}); err != nil { t.Fatal(err) } if err := provision.CreateRecord(ctx, store, provision.Record{Schema: 1, Host: f.host, Slug: "shop", App: "app", Account: account, Region: "ie", Channel: "stable", CreatedAt: created}); err != nil { t.Fatal(err) } } o, err := hostclaim.Read(ctx, store, f.host) if err != nil { t.Fatal(err) } o, err = hostclaim.Update(ctx, store, o, hostclaim.Adopt(hostclaim.NewHolder{Family: f.family, Name: f.name, Key: f.key, Account: account, App: "app", Host: f.host, Region: "ie", CreatedAt: created}, "box", nil, nil, "adopt", func() string { return "id" })) if err != nil { t.Fatal(err) } f.holder = *o.Claim.Holder f.owner = registry.ClaimOwnership{ClaimIncarnation: f.holder.Incarnation, ClaimStamp: f.holder.Stamp, ClaimOwner: "owner", ClaimRowID: "row"} return f } func TestClaimRefusalPreservesDurableGuardAndInventory(t *testing.T) { type env struct { cfg *provision.ReconcileConfig reg *registry.Registry f admissionFixture } allow := func(gates *int) func(context.Context, registry.Route) error { return func(context.Context, registry.Route) error { *gates++; return nil } } refuse := func(gates *int) func(context.Context, registry.Route) error { return func(context.Context, registry.Route) error { *gates++; return errors.New("config refused") } } upsert := func(t *testing.T, e env, rt registry.Route) { t.Helper() if err := e.reg.Upsert(rt); err != nil { t.Fatal(err) } } cases := []struct { name string sidecar bool reached bool // the claim hook ran: guard and inventory written gates int // ConfigGate calls this pass makes apply func(*testing.T, env, *int) }{ {name: "inbound", apply: func(_ *testing.T, e env, _ *int) { e.cfg.Inbound = provision.InboundNone }}, {name: "instance config", gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = refuse(gates) }}, {name: "sidecar config", sidecar: true, gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = refuse(gates) }}, {name: "co-spawn", sidecar: true, apply: func(t *testing.T, e env, _ *int) { if err := os.WriteFile(filepath.Join(e.f.data, e.f.host+"-sidecar.env"), []byte("A=1\n"), 0600); err != nil { t.Fatal(err) } }}, {name: "instance shape mismatch", apply: func(t *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) upsert(t, e, registry.Route{Host: e.f.host, Kind: "instance", App: "other", ClaimOwnership: e.f.owner}) }}, {name: "instance row without claim owner", apply: func(t *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) upsert(t, e, registry.Route{Host: e.f.host, Kind: "instance", App: "app", Account: e.f.account, Provisioned: true}) }}, {name: "sidecar row not owned by this pass", sidecar: true, apply: func(t *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) upsert(t, e, registry.Route{Host: e.f.host, Kind: provision.SidecarKind, Slug: "worker", App: "app", Account: e.f.account, ClaimOwnership: e.f.owner}) }}, {name: "sidecar row without claim owner", sidecar: true, apply: func(t *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) upsert(t, e, registry.Route{Host: e.f.host, Kind: provision.SidecarKind, Slug: "worker", App: "app", Account: e.f.account, Provisioned: true}) }}, // Positive controls: the same fixtures reach the hook when nothing refuses. {name: "instance accepted", reached: true, gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) }}, {name: "sidecar accepted", sidecar: true, reached: true, gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) }}, {name: "instance owned row kept", reached: true, apply: func(t *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) upsert(t, e, registry.Route{Host: e.f.host, Kind: "instance", App: "app", Account: e.f.account, Provisioned: true, ClaimOwnership: e.f.owner}) }}, {name: "sidecar owned row kept", sidecar: true, reached: true, apply: func(t *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) upsert(t, e, registry.Route{Host: e.f.host, Kind: provision.SidecarKind, Slug: "worker", App: "app", Account: e.f.account, Provisioned: true, ClaimOwnership: e.f.owner}) }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ctx := t.Context() f := newAdmissionFixture(t, tc.sidecar) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() inventory, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db")) if err != nil { t.Fatal(err) } defer inventory.Close() guard := hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.store}} h := f.holder in := StopInventory{Host: f.host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: "box", Enrollment: "en", DirectoryRevision: "1", Complete: true} calls, gates := 0, 0 cfg := provision.ReconcileConfig{DataDir: f.data, ClaimEnabled: true, Region: "ie"} cfg.ClaimInstall = func(ctx context.Context, selected provision.ClaimRecord, write func(registry.ClaimOwnership) error) error { calls++ if err := guard.Activate(ctx, selected.Host, "box", nil); err != nil { return err } if err := inventory.WriteStopInventory(ctx, in); err != nil { return err } return write(f.owner) } tc.apply(t, env{cfg: &cfg, reg: reg, f: f}, &gates) before, err := reg.Get(f.host) if err != nil { t.Fatal(err) } if err := f.pass(ctx, f.store, reg, cfg); err != nil { t.Fatal(err) } if gates != tc.gates { t.Fatalf("config gate asked %d times, want %d", gates, tc.gates) } _, guardErr := os.ReadFile(filepath.Join(f.data, "hostguard", f.host+".json")) _, inventoryErr := inventory.ReadInstallation(ctx, f.host, h.Incarnation, h.Stamp) after, err := reg.Get(f.host) if err != nil { t.Fatal(err) } if tc.reached { if calls != 1 || guardErr != nil || inventoryErr != nil { t.Fatalf("positive control: calls=%d guard=%v inventory=%v", calls, guardErr, inventoryErr) } if after == nil || after.ClaimOwnership != f.owner { t.Fatalf("positive control lost ownership: %+v", after) } return } if calls != 0 || !errors.Is(guardErr, os.ErrNotExist) || !errors.Is(inventoryErr, sql.ErrNoRows) { t.Fatalf("refusal changed durable state: calls=%d guard=%v inventory=%v", calls, guardErr, inventoryErr) } if (before == nil) != (after == nil) || (before != nil && *before != *after) { t.Fatalf("refusal changed the registry: before=%+v after=%+v", before, after) } }) } }