Apply RLS context at task service boundaries, harden orchestrator/web integration and session startup behavior, re-enable targeted frontend tests, and lock vulnerable transitive dependencies so QA and security gates pass cleanly.
550 lines
16 KiB
TypeScript
550 lines
16 KiB
TypeScript
/**
|
|
* 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({});
|
|
});
|
|
});
|
|
});
|