package credentialstore import ( "context" "path/filepath" "testing" "github.com/stretchr/testify/require" "iop/packages/go/config" ) func setupRouteTestFixture(t *testing.T) (*Store, *IssuedPrincipal, *CredentialSlotRecord, *CredentialSlotRecord) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test_routes.db") ctx := context.Background() reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) t.Cleanup(func() { _ = store.Close() }) p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "route-principal"}) require.NoError(t, err) env := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher1"), } openaiSlot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "openai-slot-1", Envelope: env, }) require.NoError(t, err) anthropicSlot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "anthropic", CredentialKind: CredentialKindAPIKey, Alias: "anthropic-slot-1", Envelope: env, }) require.NoError(t, err) return store, p, openaiSlot, anthropicSlot } func TestRoutesDisambiguateSameUpstreamModelBySlot(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) dbPath := filepath.Join(t.TempDir(), "disambig.db") ctx := context.Background() store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) defer store.Close() p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "p-disambig"}) require.NoError(t, err) env := SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("cipher1"), } slotA, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "slot-a", Envelope: env, }) require.NoError(t, err) slotB, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "slot-b", Envelope: env, }) require.NoError(t, err) // Create Route 1 referencing slotA for gpt-4o route1, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: slotA.ID, Alias: "route-gpt4o-a", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) require.Equal(t, slotA.ID, route1.SlotID) // Create Route 2 referencing slotB for gpt-4o route2, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: slotB.ID, Alias: "route-gpt4o-b", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) require.Equal(t, slotB.ID, route2.SlotID) routes, err := store.ListRoutes(ctx, p.Principal.ID) require.NoError(t, err) require.Len(t, routes, 2) } func TestSlotSupportsMultipleCompatibleRoutes(t *testing.T) { store, p, openaiSlot, _ := setupRouteTestFixture(t) ctx := context.Background() r1, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: openaiSlot.ID, Alias: "route-gpt4o", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) r2, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: openaiSlot.ID, Alias: "route-gpt4o-mini", ProfileID: "openai", UpstreamModel: "gpt-4o-mini", }) require.NoError(t, err) slotRoutes, err := store.ListRoutesBySlot(ctx, p.Principal.ID, openaiSlot.ID) require.NoError(t, err) require.Len(t, slotRoutes, 2) require.Equal(t, r1.ID, slotRoutes[0].ID) require.Equal(t, r2.ID, slotRoutes[1].ID) } func TestRouteRejectsCrossPrincipalSlot(t *testing.T) { store, _, openaiSlotP1, _ := setupRouteTestFixture(t) ctx := context.Background() p2, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "principal-2"}) require.NoError(t, err) // Attempting to create a route for P2 using P1's slot must fail _, err = store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p2.Principal.ID, SlotID: openaiSlotP1.ID, Alias: "p2-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.Error(t, err) require.ErrorIs(t, err, ErrCrossPrincipalSlot) } func TestRouteRejectsIncompatibleProfile(t *testing.T) { store, p, openaiSlot, anthropicSlot := setupRouteTestFixture(t) ctx := context.Background() // OpenAI slot with Anthropic profile should fail _, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: openaiSlot.ID, Alias: "bad-route-1", ProfileID: "anthropic", UpstreamModel: "claude-3-5-sonnet", }) require.Error(t, err) require.ErrorIs(t, err, ErrIncompatibleProfile) // Anthropic slot with OpenAI profile should fail _, err = store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: anthropicSlot.ID, Alias: "bad-route-2", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.Error(t, err) require.ErrorIs(t, err, ErrIncompatibleProfile) } func TestRouteAliasCollisionFailsClosed(t *testing.T) { store, p, openaiSlot, _ := setupRouteTestFixture(t) ctx := context.Background() _, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: openaiSlot.ID, Alias: "my-route-alias", ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) _, err = store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: openaiSlot.ID, Alias: "my-route-alias", ProfileID: "openai", UpstreamModel: "gpt-4o-mini", }) require.Error(t, err) require.ErrorIs(t, err, ErrRouteAliasAlreadyExists) } func TestSlotSupportsCompatibleChatAndMessagesProfiles(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) ctx := context.Background() slot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "minimax", CredentialKind: CredentialKindBearer, Alias: "minimax", Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}, }) require.NoError(t, err) chat, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "mini-chat", ProfileID: "minimax_chat", UpstreamModel: "MiniMax-M2"}) require.NoError(t, err) messages, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "mini-messages", ProfileID: "minimax_messages", UpstreamModel: "MiniMax-M2"}) require.NoError(t, err) require.NotEqual(t, chat.ProfileID, messages.ProfileID) routes, err := store.ListRoutesBySlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) require.Len(t, routes, 2) } // TestGLMSlotSupportsGeneralAndCodingProfiles verifies that a single // glm/bearer credential slot can bind two distinct routes — one for General // API and one for Coding Plan — each with its own alias, profile, and // resource selector, while a different vendor slot rejects glm_coding. // Official endpoint distinction rechecked on 2026-08-02: // https://docs.z.ai/api-reference/introduction // https://docs.z.ai/scenario-example/develop-tools/others func TestGLMSlotSupportsGeneralAndCodingProfiles(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-glm"), } slot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "glm", CredentialKind: CredentialKindBearer, Alias: "glm-slot", Envelope: env, }) require.NoError(t, err) apiRoute, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "glm-5.1-api", ProfileID: "glm", UpstreamModel: "glm-5.1", ResourceSelector: "glm-api", }) require.NoError(t, err) require.Equal(t, "glm", apiRoute.ProfileID) require.Equal(t, "glm-5.1-api", apiRoute.Alias) require.Equal(t, "glm-api", apiRoute.ResourceSelector) codingRoute, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "glm-5.1-coding", ProfileID: "glm_coding", UpstreamModel: "glm-5.1", ResourceSelector: "glm-coding", }) require.NoError(t, err) require.Equal(t, "glm_coding", codingRoute.ProfileID) require.Equal(t, "glm-5.1-coding", codingRoute.Alias) require.Equal(t, "glm-coding", codingRoute.ResourceSelector) require.NotEqual(t, apiRoute.ID, codingRoute.ID, "two routes must have distinct IDs") routes, err := store.ListRoutes(ctx, p.Principal.ID) require.NoError(t, err) require.Len(t, routes, 2) routesBySlot, err := store.ListRoutesBySlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) require.Len(t, routesBySlot, 2) // A different-vendor slot must reject glm_coding. otherSlot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "openai-slot", Envelope: env, }) require.NoError(t, err) _, err = store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: otherSlot.ID, Alias: "openai-glm-coding", ProfileID: "glm_coding", UpstreamModel: "glm-5.1", }) require.ErrorIs(t, err, ErrIncompatibleProfile) } func TestRouteRejectsUnknownVendorCredentialTuple(t *testing.T) { reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, p := setupTestStoreWithRegistry(t, reg) slot, err := store.CreateSlot(context.Background(), CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "unknown-provider", CredentialKind: CredentialKindBearer, Alias: "unknown", Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}, }) require.NoError(t, err) _, err = store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "model"}) require.ErrorIs(t, err, ErrIncompatibleProfile) mismatched, err := store.CreateSlot(context.Background(), CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindAPIKey, Alias: "mismatched", Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque-2")}, }) require.NoError(t, err) _, err = store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: mismatched.ID, ProfileID: "openai", UpstreamModel: "model"}) require.ErrorIs(t, err, ErrIncompatibleProfile) } func TestRouteResolutionUsesImmutableProfileCatalog(t *testing.T) { store, p, slot, _ := setupRouteTestFixture(t) ctx := context.Background() original, ok := config.BuiltInProtocolProfiles["openai"] require.True(t, ok) delete(config.BuiltInProtocolProfiles, "openai") t.Cleanup(func() { config.BuiltInProtocolProfiles["openai"] = original }) route, err := store.CreateRoute(ctx, CreateRouteInput{ PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o", }) require.NoError(t, err) require.Equal(t, "openai", route.ProfileID) } func TestRouteRejectsInactiveSlot(t *testing.T) { store, p, slot, _ := setupRouteTestFixture(t) ctx := context.Background() first, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o"}) require.NoError(t, err) loaded, err := store.GetSlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) disabled, err := store.DisableSlot(ctx, p.Principal.ID, slot.ID, loaded.Revision) require.NoError(t, err) _, err = store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o-mini"}) require.ErrorIs(t, err, ErrSlotNotActive) revoked, err := store.RevokeSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision) require.NoError(t, err) _, err = store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4.1"}) require.ErrorIs(t, err, ErrSlotRevoked) require.NotEmpty(t, first.ID) require.Equal(t, StatusRevoked, revoked.Status) } func TestEnableRouteRejectsInactiveSlot(t *testing.T) { store, p, slot, _ := setupRouteTestFixture(t) ctx := context.Background() route, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o"}) require.NoError(t, err) disabledRoute, err := store.DisableRoute(ctx, p.Principal.ID, route.ID, route.Revision) require.NoError(t, err) loaded, err := store.GetSlot(ctx, p.Principal.ID, slot.ID) require.NoError(t, err) disabledSlot, err := store.DisableSlot(ctx, p.Principal.ID, slot.ID, loaded.Revision) require.NoError(t, err) _, err = store.EnableRoute(ctx, p.Principal.ID, route.ID, disabledRoute.Revision) require.ErrorIs(t, err, ErrSlotNotActive) _, err = store.EnableSlot(ctx, p.Principal.ID, slot.ID, disabledSlot.Revision) require.NoError(t, err) enabledRoute, err := store.EnableRoute(ctx, p.Principal.ID, route.ID, disabledRoute.Revision) require.NoError(t, err) require.Equal(t, StatusActive, enabledRoute.Status) } func TestUpdateRouteUsesRevisionCAS(t *testing.T) { store, p, slot, _ := setupRouteTestFixture(t) ctx := context.Background() route, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "before", ProfileID: "openai", UpstreamModel: "gpt-4o"}) require.NoError(t, err) updated, err := store.UpdateRoute(ctx, UpdateRouteInput{PrincipalID: p.Principal.ID, RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: " after ", ProfileID: "openai", UpstreamModel: "gpt-4.1", ResourceSelector: "preferred"}) require.NoError(t, err) require.Equal(t, int64(1), updated.Revision) require.Equal(t, "after", updated.Alias) require.Equal(t, "gpt-4.1", updated.UpstreamModel) require.Equal(t, "preferred", updated.ResourceSelector) _, err = store.UpdateRoute(ctx, UpdateRouteInput{PrincipalID: p.Principal.ID, RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: "stale", ProfileID: "openai", UpstreamModel: "gpt-4o"}) require.ErrorIs(t, err, ErrRevisionMismatch) } func TestRouteAliasCannotShadowRouteID(t *testing.T) { store, p, slot, _ := setupRouteTestFixture(t) first, err := store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "first", ProfileID: "openai", UpstreamModel: "gpt-4o"}) require.NoError(t, err) _, err = store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: first.ID, ProfileID: "openai", UpstreamModel: "gpt-4.1"}) require.ErrorIs(t, err, ErrRouteAliasAlreadyExists) } func TestRouteLifecyclePersistsAcrossReopen(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "route-lifecycle.db") reg := newFakeKeyRegistry() reg.RegisterKey("k1", 1) store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "route-reopen"}) require.NoError(t, err) slot, err := store.CreateSlot(ctx, CreateSlotInput{ PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "reopen-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: p.Principal.ID, SlotID: slot.ID, Alias: "reopen-route", ProfileID: "openai", UpstreamModel: "gpt-4o", ResourceSelector: "primary"}) require.NoError(t, err) disabled, err := store.DisableRoute(ctx, p.Principal.ID, route.ID, route.Revision) require.NoError(t, err) require.NoError(t, store.Close()) reopened, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg)) require.NoError(t, err) defer reopened.Close() loaded, err := reopened.GetRoute(ctx, p.Principal.ID, route.ID) require.NoError(t, err) require.Equal(t, StatusDisabled, loaded.Status) require.Equal(t, disabled.Revision, loaded.Revision) require.Equal(t, "reopen-route", loaded.Alias) require.Equal(t, "primary", loaded.ResourceSelector) }