package credentialstore import ( "context" "database/sql" "path/filepath" "testing" "github.com/google/uuid" "github.com/stretchr/testify/require" ) type fakeKeyRegistry struct { keys map[string]map[uint64]bool } func newFakeKeyRegistry() *fakeKeyRegistry { return &fakeKeyRegistry{ keys: make(map[string]map[uint64]bool), } } func (f *fakeKeyRegistry) RegisterKey(keyID string, version uint64) { if f.keys[keyID] == nil { f.keys[keyID] = make(map[uint64]bool) } f.keys[keyID][version] = true } func (f *fakeKeyRegistry) RemoveKey(keyID string, version uint64) { if versions := f.keys[keyID]; versions != nil { delete(versions, version) } } func (f *fakeKeyRegistry) HasEnvelopeKey(_ context.Context, keyID string, keyVersion uint64) (bool, error) { if versions, ok := f.keys[keyID]; ok { return versions[keyVersion], nil } return false, nil } func setupTestStoreWithRegistry(t *testing.T, registry EnvelopeKeyRegistry) (*Store, *IssuedPrincipal) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test_slots.db") ctx := context.Background() opts := []Option{} if registry != nil { opts = append(opts, WithEnvelopeKeyRegistry(registry)) } store, err := Open(ctx, dbPath, opts...) require.NoError(t, err) t.Cleanup(func() { _ = store.Close() }) p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "test-principal"}) require.NoError(t, err) return store, p } func TestCredentialSlotsAllowSameVendorMultipleSlots(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env1 := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher1"), } slot1, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "slot-1", Envelope: env1, }) require.NoError(t, err) require.Equal(t, "openai", slot1.Vendor) require.Equal(t, "slot-1", slot1.Alias) env2 := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher2"), } slot2, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "slot-2", Envelope: env2, }) require.NoError(t, err) require.Equal(t, "openai", slot2.Vendor) require.Equal(t, "slot-2", slot2.Alias) slots, err := store.ListSlots(ctx, p.Principal.ID) require.NoError(t, err) require.Len(t, slots, 2) } func TestCredentialSlotAcceptsCallerSuppliedSlotID(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env := SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")} callerID := uuid.New().String() created, err := store.CreateSlot(ctx, CreateSlotInput{ SlotID: callerID, PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "caller-id", Envelope: env, }) require.NoError(t, err) require.Equal(t, callerID, created.ID) // The persisted row and its initial revision both use the caller id. loaded, err := store.GetSlot(ctx, p.Principal.ID, callerID) require.NoError(t, err) require.Equal(t, callerID, loaded.ID) revs, err := store.ListSlotRevisions(ctx, p.Principal.ID, callerID) require.NoError(t, err) require.Len(t, revs, 1) require.Equal(t, callerID, revs[0].SlotID) // A non-UUID caller id is rejected before any row is written. _, err = store.CreateSlot(ctx, CreateSlotInput{ SlotID: "not-a-uuid", PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "bad-id", Envelope: env, }) require.Error(t, err) require.Contains(t, err.Error(), "slot_id") // The legacy zero value still yields a store-generated id. legacy, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "legacy-id", Envelope: env, }) require.NoError(t, err) require.NotEmpty(t, legacy.ID) require.NotEqual(t, callerID, legacy.ID) } func TestCredentialSlotRejectsUnknownEnvelopeKey(t *testing.T) { reg := newFakeKeyRegistry() // Do not register "unknown-key" store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "unknown-key", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher"), } _, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "slot-unknown", Envelope: env, }) require.Error(t, err) require.ErrorIs(t, err, ErrUnknownEnvelopeKey) // Test unconfigured store (unavailable key registry) unconfiguredStore, p2 := setupTestStoreWithRegistry(t, nil) envRegistered := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher"), } _, err = unconfiguredStore.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p2.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "slot-unavail", Envelope: envRegistered, }) require.Error(t, err) require.ErrorIs(t, err, ErrEnvelopeKeyUnavailable) } func TestCredentialSlotRotationRequiresRegisteredKey(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env1 := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher1"), } slot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "rotate-slot", Envelope: env1, }) require.NoError(t, err) // Attempt rotation with unregistered key "k2" env2 := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k2", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher2"), } _, err = store.RotateSlotSecret(ctx, RotateSlotSecretInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, CurrentRevision: slot.Revision, Envelope: env2, }) require.Error(t, err) require.ErrorIs(t, err, ErrUnknownEnvelopeKey) // Register k2 and rotate successfully reg.RegisterKey("k2", 1) rotated, err := store.RotateSlotSecret(ctx, RotateSlotSecretInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, CurrentRevision: slot.Revision, Envelope: env2, }) require.NoError(t, err) require.Equal(t, int64(1), rotated.Revision) require.Equal(t, "k2", rotated.Envelope.KeyID) } func TestCredentialSlotAliasUniqueWithinPrincipal(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher1"), } _, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "dup-alias", Envelope: env, }) require.NoError(t, err) _, err = store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "anthropic", CredentialKind: CredentialKindAPIKey, Alias: "dup-alias", Envelope: env, }) require.Error(t, err) require.ErrorIs(t, err, ErrSlotAliasAlreadyExists) } func TestCredentialSlotLifecyclePersists(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "lifecycle.db") ctx := context.Background() reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) reg.RegisterKey("k1", 2) store1, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) p, err := store1.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "life-principal"}) require.NoError(t, err) env1 := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher-v1"), } slot, err := store1.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "life-slot", Envelope: env1, }) require.NoError(t, err) require.Equal(t, StatusDraft, slot.Status) activatedRoute, err := store1.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "life-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) _ = activatedRoute slot, err = store1.GetSlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) require.Equal(t, StatusActive, slot.Status) // Disable slot disabled, err := store1.DisableSlot(ctx, p.Principal.ID, slot.ID, slot.Revision) require.NoError(t, err) require.Equal(t, StatusDisabled, disabled.Status) require.Equal(t, int64(2), disabled.Revision) // Enable slot enabled, err := store1.EnableSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision) require.NoError(t, err) require.Equal(t, StatusActive, enabled.Status) require.Equal(t, int64(3), enabled.Revision) // Rotate secret env2 := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 2, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher-v2"), } rotated, err := store1.RotateSlotSecret(ctx, RotateSlotSecretInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, CurrentRevision: enabled.Revision, Envelope: env2, }) require.NoError(t, err) require.Equal(t, int64(4), rotated.Revision) require.Equal(t, uint64(2), rotated.Envelope.KeyVersion) // Close store and reopen from dbPath require.NoError(t, store1.Close()) store2, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) defer store2.Close() reloaded, err := store2.GetSlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) require.Equal(t, StatusActive, reloaded.Status) require.Equal(t, int64(4), reloaded.Revision) require.Equal(t, []byte("cipher-v2"), reloaded.Envelope.Ciphertext) revisions, err := store2.ListSlotRevisions(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) require.Len(t, revisions, 2) require.Equal(t, int64(0), revisions[0].Revision) require.Equal(t, int64(4), revisions[1].Revision) // Revoke slot revoked, err := store2.RevokeSlot(ctx, p.Principal.ID, slot.ID, reloaded.Revision) require.NoError(t, err) require.Equal(t, StatusRevoked, revoked.Status) require.NotNil(t, revoked.RevokedAt) } func TestCredentialSlotLifecycleReopensWithInactiveBindings(t *testing.T) { ctx := context.Background() for _, tc := range []struct { name string transition func(*Store, string, string, int64) (*CredentialSlotRecord, error) }{ { name: "disabled", transition: func(store *Store, principalID, slotID string, revision int64) (*CredentialSlotRecord, error) { return store.DisableSlot(ctx, principalID, slotID, revision) }, }, { name: "revoked", transition: func(store *Store, principalID, slotID string, revision int64) (*CredentialSlotRecord, error) { return store.RevokeSlot(ctx, principalID, slotID, revision) }, }, } { t.Run(tc.name, func(t *testing.T) { dbPath := filepath.Join(t.TempDir(), tc.name+".db") reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) principal, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: tc.name + "-principal"}) require.NoError(t, err) slot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: principal.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: tc.name + "-slot", Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}, }) require.NoError(t, err) route, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: principal.Principal.ID, SlotID: slot.ID, Alias: tc.name + "-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) slot, err = store.GetSlot(ctx, principal.Principal.ID, slot.ID) require.NoError(t, err) inactive, err := tc.transition(store, principal.Principal.ID, slot.ID, slot.Revision) require.NoError(t, err) require.NoError(t, store.Close()) reopened, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) defer reopened.Close() reloaded, err := reopened.GetSlot(ctx, principal.Principal.ID, slot.ID) require.NoError(t, err) require.Equal(t, inactive.Status, reloaded.Status) retainedRoute, err := reopened.GetRoute(ctx, principal.Principal.ID, route.ID) require.NoError(t, err) require.Equal(t, StatusActive, retainedRoute.Status) if tc.name == "disabled" { enabled, err := reopened.EnableSlot(ctx, principal.Principal.ID, slot.ID, reloaded.Revision) require.NoError(t, err) require.Equal(t, StatusActive, enabled.Status) } else { _, err := reopened.EnableSlot(ctx, principal.Principal.ID, slot.ID, reloaded.Revision) require.Error(t, err) } }) } } func TestCredentialSlotOptionalAliasDoesNotCollide(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env := SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")} first, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: " OPENAI ", CredentialKind: " BEARER ", Envelope: env}) require.NoError(t, err) require.Empty(t, first.Alias) require.Equal(t, "openai", first.Vendor) require.Equal(t, CredentialKindBearer, first.CredentialKind) second, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: " ", Envelope: env}) require.NoError(t, err) require.Empty(t, second.Alias) aliased, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: " primary ", Envelope: env}) require.NoError(t, err) require.Equal(t, "primary", aliased.Alias) byAlias, err := store.GetSlotByAlias(ctx, p.Principal.ID, " primary ") require.NoError(t, err) require.Equal(t, aliased.ID, byAlias.ID) _, err = store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "primary", Envelope: env}) require.ErrorIs(t, err, ErrSlotAliasAlreadyExists) } func TestCredentialSlotRequiresKnownCredentialKind(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) _, err := store.CreateSlot(context.Background(), CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: "session_cookie", Alias: "bad-kind", Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}, }) require.Error(t, err) require.Contains(t, err.Error(), "credential_kind") } func TestCredentialSlotActivationRequiresRegisteredKey(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() env := SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")} slot, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "key-gated", Envelope: env}) require.NoError(t, err) _, err = store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o"}) require.NoError(t, err) slot, err = store.GetSlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) disabled, err := store.DisableSlot(ctx, p.Principal.ID, slot.ID, slot.Revision) require.NoError(t, err) reg.RemoveKey("k1", 1) _, err = store.EnableSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision) require.ErrorIs(t, err, ErrUnknownEnvelopeKey) reg.RegisterKey("k1", 1) enabled, err := store.EnableSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision) require.NoError(t, err) require.Equal(t, StatusActive, enabled.Status) } func TestCredentialSlotMigrationPreservesIntermediateCatalog(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "intermediate.db") db, err := sql.Open(dialectSQLite, dbPath) require.NoError(t, err) _, err = db.Exec(` CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE credential_slots ( id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id)); CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision)); CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT); INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'); INSERT INTO credential_slots VALUES ('slot-1', 'principal-1', 'openai', 'legacy-slot', 'active', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL); INSERT INTO credential_slots VALUES ('slot-2', 'principal-1', 'anthropic', 'legacy-anthropic', 'active', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL); INSERT INTO credential_slot_revisions VALUES ('revision-1', 'slot-1', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z'); INSERT INTO credential_slot_revisions VALUES ('revision-2', 'slot-2', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z'); INSERT INTO routes VALUES ('route-1', 'principal-1', 'slot-1', 'legacy-openai', 'openai', 'gpt-4o', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL); INSERT INTO routes VALUES ('route-2', 'principal-1', 'slot-2', 'legacy-anthropic-route', 'anthropic', 'claude-3-5-sonnet', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`) require.NoError(t, err) require.NoError(t, db.Close()) reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) defer store.Close() legacy, err := store.GetSlot(ctx, "principal-1", "slot-1") require.NoError(t, err) require.Equal(t, CredentialKindBearer, legacy.CredentialKind) require.Equal(t, "legacy-slot", legacy.Alias) legacyAnthropic, err := store.GetSlot(ctx, "principal-1", "slot-2") require.NoError(t, err) require.Equal(t, CredentialKindAPIKey, legacyAnthropic.CredentialKind) created, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: "principal-1", Vendor: "openai", CredentialKind: CredentialKindBearer, Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("new")}}) require.NoError(t, err) require.Empty(t, created.Alias) require.NoError(t, store.Close()) reopened, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) defer reopened.Close() loaded, err := reopened.GetSlot(ctx, "principal-1", created.ID) require.NoError(t, err) require.Empty(t, loaded.Alias) } func TestCredentialSlotMigrationRejectsIncompatibleActiveBinding(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "incompatible-intermediate.db") db, err := sql.Open(dialectSQLite, dbPath) require.NoError(t, err) _, err = db.Exec(` CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE credential_slots ( id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id)); CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision)); CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT); INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'); INSERT INTO credential_slots VALUES ('slot-1', 'principal-1', 'unknown-provider', 'legacy-slot', 'active', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL); INSERT INTO credential_slot_revisions VALUES ('revision-1', 'slot-1', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z'); INSERT INTO routes VALUES ('route-1', 'principal-1', 'slot-1', 'legacy-route', 'openai', 'model', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`) require.NoError(t, err) require.NoError(t, db.Close()) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(newFakeKeyRegistry())) require.Nil(t, store) require.ErrorIs(t, err, ErrIncompatibleProfile) require.NotContains(t, err.Error(), "opaque") } func TestCredentialSlotMigrationRejectsIncompatibleInactiveBinding(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "incompatible-inactive-intermediate.db") db, err := sql.Open(dialectSQLite, dbPath) require.NoError(t, err) _, err = db.Exec(` CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE credential_slots ( id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id)); CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision)); CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT); INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'); INSERT INTO credential_slots VALUES ('slot-1', 'principal-1', 'unknown-provider', 'legacy-slot', 'disabled', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL); INSERT INTO credential_slot_revisions VALUES ('revision-1', 'slot-1', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z'); INSERT INTO routes VALUES ('route-1', 'principal-1', 'slot-1', 'legacy-route', 'openai', 'model', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`) require.NoError(t, err) require.NoError(t, db.Close()) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(newFakeKeyRegistry())) require.Nil(t, store) require.ErrorIs(t, err, ErrIncompatibleProfile) require.NotContains(t, err.Error(), "opaque") } func TestCredentialSlotMigrationRejectsActiveRouteWithoutSlot(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "orphan-route-intermediate.db") db, err := sql.Open(dialectSQLite, dbPath) require.NoError(t, err) _, err = db.Exec(` CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE credential_slots ( id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id)); CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision)); CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT); INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'); INSERT INTO routes VALUES ('route-1', 'principal-1', 'nonexistent-slot', 'legacy-route', 'openai', 'model', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`) require.NoError(t, err) require.NoError(t, db.Close()) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(newFakeKeyRegistry())) require.Nil(t, store) require.ErrorIs(t, err, ErrIncompatibleProfile) require.NotContains(t, err.Error(), "opaque") }