package host import ( "context" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) const adminTokenHost = "shop.bab.oncarlos.com" type adminTokenFixture struct { t *testing.T claims release.Store data string backend *activator.ExecBackend tokens claimAdminTokens rt registry.Route actor hostbox.Actor verify error } // newAdminTokenFixture is a first unscheduled wake on box-1: a serving claim // whose executor is execBox, no local hostguard record yet (Activate runs at // Admit, after selection), a staged live release and no .env at all. func newAdminTokenFixture(t *testing.T, execBox string) *adminTokenFixture { t.Helper() ctx := t.Context() st := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) in := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + adminTokenHost + ".json", Account: "bab", App: "jam", Host: adminTokenHost, Region: "ie", RecordRegion: "ie", CreatedAt: now} body := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-06T12:00:00Z"}`) absent, err := hostclaim.Read(ctx, st, adminTokenHost) if err != nil { t.Fatal(err) } o, err := hostclaim.Update(ctx, st, absent, hostclaim.BeginCreate(in, body, "create", "console", now)) if err != nil { t.Fatal(err) } if _, err = hostclaim.Update(ctx, st, o, hostclaim.CompleteCreate("create", execBox)); err != nil { t.Fatal(err) } root := t.TempDir() data, opt := filepath.Join(root, "data"), filepath.Join(root, "opt") for _, dir := range []string{filepath.Join(data, "hostguard"), filepath.Join(opt, "versions", "v1", "bab"), filepath.Join(opt, "live")} { if err = os.MkdirAll(dir, 0o770); err != nil { t.Fatal(err) } } bin := filepath.Join(opt, "versions", "v1", "bab", "jam") if err = os.WriteFile(bin, []byte("#!/bin/sh\n"), 0o755); err != nil { t.Fatal(err) } if err = os.Symlink(bin, filepath.Join(opt, "live", adminTokenHost)); err != nil { t.Fatal(err) } reg, err := registry.Open(filepath.Join(root, "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) f := &adminTokenFixture{t: t, claims: st, data: data, backend: &activator.ExecBackend{DataDir: data, OptDir: opt}, actor: hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enr-1"}} f.rt = registry.Route{Host: adminTokenHost, Socket: filepath.Join(root, "run", "app.sock"), DBPath: filepath.Join(data, adminTokenHost+".db")} local := &productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: data}, slots: map[string]*claimParticipation{}} f.tokens = claimAdminTokens{ Guard: hostguard.Guard{DataDir: data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: st}}, Claims: st, Verify: f.verifyActor, Local: func(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection) error { a, err := f.verifyActor(ctx) if err != nil { return err } if s.Box != a.Box || s.Region != a.Region { return hostbox.ErrChanged } return local.Check(ctx, a, c, s) }, Policy: hostbox.Policy{Held: hostbox.RegistryHoldPolicy(reg)}.Check, Now: time.Now, } return f } func (f *adminTokenFixture) verifyActor(context.Context) (hostbox.Actor, error) { return f.actor, f.verify } func (f *adminTokenFixture) envPath() string { return filepath.Join(f.data, adminTokenHost+".env") } func (f *adminTokenFixture) token() string { f.t.Helper() raw, err := os.ReadFile(f.envPath()) if err != nil { f.t.Fatal(err) } var tok string for _, line := range strings.Split(string(raw), "\n") { if v, ok := strings.CutPrefix(line, "CARLOS_ADMIN_TOKEN="); ok { if tok != "" { f.t.Fatalf("env file holds two tokens: %q", raw) } tok = v } } return tok } func envHasToken(env []string, tok string) bool { for _, kv := range env { if kv == "CARLOS_ADMIN_TOKEN="+tok { return true } } return false } // B3 regression: the claim path bypasses the legacy launch preparation that // mints CARLOS_ADMIN_TOKEN. The production serving launch source must // initialize the token under the claim's own serving authority before the // read-only snapshot, and every later selection must see the same token. func TestClaimProductionFirstWakeInitializesAdminToken(t *testing.T) { f := newAdminTokenFixture(t, "box-1") launch := claimServingLaunchSource(f.backend, f.tokens) first, err := launch(t.Context(), f.rt) if err != nil { t.Fatalf("first unscheduled wake: launch selection failed: %v", err) } tok := f.token() if len(tok) != 64 { t.Fatalf("minted token %q, want 64 hex characters", tok) } if !envHasToken(first.Env, tok) || first.Identity.Config == "" || first.Identity.Release == "" { t.Fatalf("snapshot does not carry the minted token: %+v", first) } // Retry selection, still before Admit: no rotation, identical identity. again, err := launch(t.Context(), f.rt) if err != nil { t.Fatal(err) } if f.token() != tok || again.Identity != first.Identity { t.Fatalf("retry rotated the token or changed the launch identity: %q→%q %+v→%+v", tok, f.token(), first.Identity, again.Identity) } // After Admit activates the local record, the token is still stable. if err = f.tokens.Guard.Activate(t.Context(), adminTokenHost, "box-1", nil); err != nil { t.Fatal(err) } after, err := launch(t.Context(), f.rt) if err != nil { t.Fatal(err) } if f.token() != tok || after.Identity != first.Identity { t.Fatalf("post-admission selection rotated the token: %q→%q", tok, f.token()) } } // With a local record in place and the token removed, the token is minted // again only through the guarded installation path (Guard.With). func TestClaimProductionAdminTokenUnderInstalledRecord(t *testing.T) { f := newAdminTokenFixture(t, "box-1") if err := f.tokens.Guard.Activate(t.Context(), adminTokenHost, "box-1", nil); err != nil { t.Fatal(err) } if _, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt); err != nil { t.Fatalf("installed serving holder: %v", err) } if len(f.token()) != 64 { t.Fatal("no token minted under the installed record") } } // Missing or stale authority refuses the wake before any file is written. func TestClaimProductionAdminTokenRefusesWithoutAuthority(t *testing.T) { t.Parallel() for _, tc := range []struct { name string setup func(*adminTokenFixture) }{ {"other executor", func(f *adminTokenFixture) {}}, {"identity unverifiable", func(f *adminTokenFixture) { f.verify = errors.New("enrollment revoked") }}, {"no authorizer", func(f *adminTokenFixture) { f.tokens = claimAdminTokens{} }}, {"stale local record", func(f *adminTokenFixture) { // A record from an earlier incarnation is not this holder's installation. if err := os.WriteFile(filepath.Join(f.data, "hostguard", adminTokenHost+".json"), []byte(`{"v":1,"host":"`+adminTokenHost+`","incarnation":"old","active_stamp":1,"retired_stamps":[]}`), 0o600); err != nil { f.t.Fatal(err) } }}, } { t.Run(tc.name, func(t *testing.T) { box := "box-1" if tc.name == "other executor" { box = "box-2" } f := newAdminTokenFixture(t, box) tc.setup(f) if _, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt); err == nil { t.Fatal("launch selection succeeded without serving authority") } if _, err := os.Stat(f.envPath()); !errors.Is(err, os.ErrNotExist) { t.Fatalf("token initialization wrote %s without authority: %v", f.envPath(), err) } }) } } // The snapshot itself stays read-only: it refuses a missing token and never // creates one, so the InputPolicy re-check under hostguard cannot mint. func TestSnapshotClaimLaunchNeverMintsAdminToken(t *testing.T) { f := newAdminTokenFixture(t, "box-1") if _, err := f.backend.SnapshotClaimLaunch(t.Context(), f.rt); err == nil { t.Fatal("snapshot accepted a missing token") } if _, err := os.Stat(f.envPath()); !errors.Is(err, os.ErrNotExist) { t.Fatalf("snapshot wrote %s: %v", f.envPath(), err) } }