/** * UserCredential Model Tests * * Tests the UserCredential Prisma model including: * - Model structure and constraints * - Enum validation * - Unique constraints * - Foreign key relationships * - Default values * * Note: RLS policy tests are in user-credential-rls.spec.ts * Note: Encryption tests are in user-credential-encryption.middleware.spec.ts */ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { PrismaClient, CredentialType, CredentialScope } from "@prisma/client"; const shouldRunDbIntegrationTests = process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL); const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip; describeFn("UserCredential Model", () => { let prisma: PrismaClient; let testUserId: string; let testWorkspaceId: string; beforeAll(async () => { // Note: These tests require a running database // They will be skipped in CI if DATABASE_URL is not set if (!shouldRunDbIntegrationTests) { console.warn("Skipping UserCredential model tests (set RUN_DB_TESTS=true and DATABASE_URL)"); return; } prisma = new PrismaClient(); // Create test user and workspace const user = await prisma.user.create({ data: { email: `test-${Date.now()}@example.com`, name: "Test User", emailVerified: true, }, }); testUserId = user.id; const workspace = await prisma.workspace.create({ data: { name: `Test Workspace ${Date.now()}`, ownerId: testUserId, }, }); testWorkspaceId = workspace.id; }); afterAll(async () => { if (!prisma) return; // Clean up test data await prisma.userCredential.deleteMany({ where: { userId: testUserId }, }); await prisma.workspace.deleteMany({ where: { id: testWorkspaceId }, }); await prisma.user.deleteMany({ where: { id: testUserId }, }); await prisma.$disconnect(); }); describe("Model Structure", () => { it("should create a user-scoped credential with all required fields", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Test API Key", provider: "github", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "test:encrypted:value", maskedValue: "****1234", }, }); expect(credential.id).toBeDefined(); expect(credential.userId).toBe(testUserId); expect(credential.workspaceId).toBeNull(); expect(credential.name).toBe("Test API Key"); expect(credential.provider).toBe("github"); expect(credential.type).toBe(CredentialType.API_KEY); expect(credential.scope).toBe(CredentialScope.USER); expect(credential.encryptedValue).toBe("test:encrypted:value"); expect(credential.maskedValue).toBe("****1234"); expect(credential.isActive).toBe(true); // Default value expect(credential.metadata).toEqual({}); // Default value expect(credential.createdAt).toBeInstanceOf(Date); expect(credential.updatedAt).toBeInstanceOf(Date); }); it("should create a workspace-scoped credential", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: testWorkspaceId, name: "Workspace Token", provider: "openai", type: CredentialType.ACCESS_TOKEN, scope: CredentialScope.WORKSPACE, encryptedValue: "workspace:encrypted:value", }, }); expect(credential.scope).toBe(CredentialScope.WORKSPACE); expect(credential.workspaceId).toBe(testWorkspaceId); }); it("should support all CredentialType enum values", async () => { if (!prisma) return; const types = [ CredentialType.API_KEY, CredentialType.OAUTH_TOKEN, CredentialType.ACCESS_TOKEN, CredentialType.SECRET, CredentialType.PASSWORD, CredentialType.CUSTOM, ]; for (const type of types) { const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: `Test ${type}`, provider: "test", type, scope: CredentialScope.USER, encryptedValue: `encrypted:${type}:value`, }, }); expect(credential.type).toBe(type); } }); it("should support all CredentialScope enum values", async () => { if (!prisma) return; const scopes = [CredentialScope.USER, CredentialScope.WORKSPACE, CredentialScope.SYSTEM]; for (const scope of scopes) { const credential = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: scope === CredentialScope.WORKSPACE ? testWorkspaceId : null, name: `Test ${scope}`, provider: "test", type: CredentialType.API_KEY, scope, encryptedValue: `encrypted:${scope}:value`, }, }); expect(credential.scope).toBe(scope); } }); }); describe("Optional Fields", () => { it("should allow optional fields to be null or undefined", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Minimal Credential", provider: "custom", type: CredentialType.CUSTOM, scope: CredentialScope.USER, encryptedValue: "encrypted:minimal:value", // All optional fields omitted }, }); expect(credential.workspaceId).toBeNull(); expect(credential.maskedValue).toBeNull(); expect(credential.description).toBeNull(); expect(credential.expiresAt).toBeNull(); expect(credential.lastUsedAt).toBeNull(); expect(credential.rotatedAt).toBeNull(); }); it("should allow all optional fields to be set", async () => { if (!prisma) return; const now = new Date(); const expiresAt = new Date(Date.now() + 86400000); // 24 hours const credential = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: testWorkspaceId, name: "Full Credential", provider: "github", type: CredentialType.OAUTH_TOKEN, scope: CredentialScope.WORKSPACE, encryptedValue: "encrypted:full:value", maskedValue: "****xyz", description: "This is a test credential", expiresAt, lastUsedAt: now, rotatedAt: now, metadata: { customField: "customValue" }, isActive: false, }, }); expect(credential.workspaceId).toBe(testWorkspaceId); expect(credential.maskedValue).toBe("****xyz"); expect(credential.description).toBe("This is a test credential"); expect(credential.expiresAt).toEqual(expiresAt); expect(credential.lastUsedAt).toEqual(now); expect(credential.rotatedAt).toEqual(now); expect(credential.metadata).toEqual({ customField: "customValue" }); expect(credential.isActive).toBe(false); }); }); describe("Unique Constraints", () => { it("should enforce unique constraint on (userId, workspaceId, provider, name)", async () => { if (!prisma) return; // Create first credential await prisma.userCredential.create({ data: { userId: testUserId, name: "Duplicate Test", provider: "github", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:value:1", }, }); // Attempt to create duplicate await expect( prisma.userCredential.create({ data: { userId: testUserId, name: "Duplicate Test", provider: "github", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:value:2", }, }) ).rejects.toThrow(/Unique constraint/); }); it("should allow same name for different providers", async () => { if (!prisma) return; const name = "API Key"; const github = await prisma.userCredential.create({ data: { userId: testUserId, name, provider: "github", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:github:value", }, }); const openai = await prisma.userCredential.create({ data: { userId: testUserId, name, provider: "openai", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:openai:value", }, }); expect(github.provider).toBe("github"); expect(openai.provider).toBe("openai"); }); it("should allow same name for different workspaces", async () => { if (!prisma) return; const name = "Workspace Token"; const workspace1 = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: testWorkspaceId, name, provider: "test", type: CredentialType.ACCESS_TOKEN, scope: CredentialScope.WORKSPACE, encryptedValue: "encrypted:ws1:value", }, }); // Create second workspace for test const workspace2 = await prisma.workspace.create({ data: { name: `Test Workspace 2 ${Date.now()}`, ownerId: testUserId, }, }); const credential2 = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: workspace2.id, name, provider: "test", type: CredentialType.ACCESS_TOKEN, scope: CredentialScope.WORKSPACE, encryptedValue: "encrypted:ws2:value", }, }); expect(workspace1.workspaceId).toBe(testWorkspaceId); expect(credential2.workspaceId).toBe(workspace2.id); // Cleanup await prisma.workspace.delete({ where: { id: workspace2.id } }); }); }); describe("Foreign Key Relations", () => { it("should cascade delete when user is deleted", async () => { if (!prisma) return; // Create temporary user const tempUser = await prisma.user.create({ data: { email: `temp-${Date.now()}@example.com`, name: "Temp User", }, }); // Create credential for temp user const credential = await prisma.userCredential.create({ data: { userId: tempUser.id, name: "Temp Credential", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:temp:value", }, }); // Delete user await prisma.user.delete({ where: { id: tempUser.id } }); // Credential should be deleted const deletedCredential = await prisma.userCredential.findUnique({ where: { id: credential.id }, }); expect(deletedCredential).toBeNull(); }); it("should cascade delete when workspace is deleted", async () => { if (!prisma) return; // Create temporary workspace const tempWorkspace = await prisma.workspace.create({ data: { name: `Temp Workspace ${Date.now()}`, ownerId: testUserId, }, }); // Create workspace-scoped credential const credential = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: tempWorkspace.id, name: "Workspace Credential", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.WORKSPACE, encryptedValue: "encrypted:workspace:value", }, }); // Delete workspace await prisma.workspace.delete({ where: { id: tempWorkspace.id } }); // Credential should be deleted const deletedCredential = await prisma.userCredential.findUnique({ where: { id: credential.id }, }); expect(deletedCredential).toBeNull(); }); it("should include user relation", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Relation Test", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:relation:value", }, include: { user: true, }, }); expect(credential.user).toBeDefined(); expect(credential.user.id).toBe(testUserId); }); it("should include workspace relation when workspace-scoped", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, workspaceId: testWorkspaceId, name: "Workspace Relation Test", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.WORKSPACE, encryptedValue: "encrypted:workspace:relation:value", }, include: { workspace: true, }, }); expect(credential.workspace).toBeDefined(); expect(credential.workspace?.id).toBe(testWorkspaceId); }); }); describe("Timestamps", () => { it("should auto-set createdAt and updatedAt on create", async () => { if (!prisma) return; const before = new Date(); const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Timestamp Test", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:timestamp:value", }, }); const after = new Date(); expect(credential.createdAt.getTime()).toBeGreaterThanOrEqual(before.getTime()); expect(credential.createdAt.getTime()).toBeLessThanOrEqual(after.getTime()); expect(credential.updatedAt.getTime()).toBeGreaterThanOrEqual(before.getTime()); expect(credential.updatedAt.getTime()).toBeLessThanOrEqual(after.getTime()); }); it("should auto-update updatedAt on update", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Update Test", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:update:value", }, }); const originalUpdatedAt = credential.updatedAt; // Wait a bit to ensure timestamp difference await new Promise((resolve) => setTimeout(resolve, 10)); const updated = await prisma.userCredential.update({ where: { id: credential.id }, data: { description: "Updated description" }, }); expect(updated.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime()); expect(updated.createdAt).toEqual(credential.createdAt); // createdAt unchanged }); }); describe("Metadata JSONB", () => { it("should store and retrieve JSON metadata", async () => { if (!prisma) return; const metadata = { scopes: ["repo", "user"], tokenType: "bearer", expiresIn: 3600, customField: { nested: "value" }, }; const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Metadata Test", provider: "github", type: CredentialType.OAUTH_TOKEN, scope: CredentialScope.USER, encryptedValue: "encrypted:metadata:value", metadata, }, }); expect(credential.metadata).toEqual(metadata); }); it("should allow empty metadata object", async () => { if (!prisma) return; const credential = await prisma.userCredential.create({ data: { userId: testUserId, name: "Empty Metadata", provider: "test", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: "encrypted:empty:value", }, }); expect(credential.metadata).toEqual({}); }); }); });