import { describe, it, expect, beforeEach } from 'vitest'; import { appRouter } from './index'; import { getTestDb, resetTestDb, globalSqlite } from './test-setup'; import type { TRPCContext } from './types'; describe('tRPC API Layer', () => { let ctx: TRPCContext; let caller: ReturnType; let projectId: number; beforeEach(async () => { await resetTestDb(); const db = await getTestDb(); ctx = { clerkUserId: 'user_test', db }; caller = appRouter.createCaller(ctx); }); describe('Project CRUD', () => { it('should create a project', async () => { const project = await caller.project.createProject({ name: 'Test Project', description: 'A test project', }); expect(project).toMatchObject({ name: 'Test Project', description: 'A test project', ownerId: 1, }); expect(project.id).toBeDefined(); expect(project.id).toBeGreaterThan(0); expect(project.createdAt).toBeInstanceOf(Date); expect(project.updatedAt).toBeInstanceOf(Date); }); it('should list projects', async () => { await caller.project.createProject({ name: 'Test Project' }); const projects = await caller.project.listProjects(); expect(Array.isArray(projects)).toBe(true); expect(projects.length).toBeGreaterThan(0); }); it('should get a specific project', async () => { const created = await caller.project.createProject({ name: 'Get Test' }); const project = await caller.project.getProject({ id: created.id }); expect(project.id).toBe(created.id); expect(project.name).toBe('Get Test'); }); it('should update a project', async () => { const created = await caller.project.createProject({ name: 'Update Test' }); const updated = await caller.project.updateProject({ id: created.id, name: 'Updated Test', description: 'Updated description', }); expect(updated.name).toBe('Updated Test'); expect(updated.description).toBe('Updated description'); }); it('should delete a project', async () => { const created = await caller.project.createProject({ name: 'Delete Test' }); const result = await caller.project.deleteProject({ id: created.id }); expect(result).toEqual({ success: true }); }); }); describe('Character CRUD', () => { beforeEach(async () => { const project = await caller.project.createProject({ name: 'Character Test Project', }); projectId = project.id; }); it('should create a character', async () => { const character = await caller.project.createCharacter({ name: 'John Doe', description: 'Main character', projectId, }); expect(character).toMatchObject({ name: 'John Doe', description: 'Main character', projectId, }); }); it('should list characters for a project', async () => { await caller.project.createCharacter({ name: 'Char 1', projectId }); const characters = await caller.project.listCharacters({ projectId }); expect(characters.length).toBeGreaterThan(0); }); }); describe('Scene CRUD', () => { beforeEach(async () => { const project = await caller.project.createProject({ name: 'Scene Test Project', }); projectId = project.id; }); it('should create a scene', async () => { const scene = await caller.project.createScene({ title: 'INT. OFFICE - DAY', content: 'John sits at his desk.', projectId, order: 1, }); expect(scene).toMatchObject({ title: 'INT. OFFICE - DAY', content: 'John sits at his desk.', projectId, order: 1, }); }); it('should list scenes for a project', async () => { await caller.project.createScene({ title: 'Scene 1', projectId, order: 1 }); const scenes = await caller.project.listScenes({ projectId }); expect(scenes.length).toBeGreaterThan(0); }); it('should update scene order', async () => { const scene = await caller.project.createScene({ title: 'Reorder Scene', projectId, order: 1, }); const updated = await caller.project.updateScene({ id: scene.id, order: 5 }); expect(updated.order).toBe(5); }); }); describe('Error Handling', () => { it('should throw error when getting non-existent project', async () => { await expect( caller.project.getProject({ id: 99999 }) ).rejects.toThrow('not found'); }); it('should throw error when deleting non-existent project', async () => { await expect( caller.project.deleteProject({ id: 99999 }) ).rejects.toThrow('not found'); }); }); describe('Project Sharing', () => { let sharedProjectId: number; beforeEach(async () => { const project = await caller.project.createProject({ name: 'Shared Project', }); sharedProjectId = project.id; // Insert a second user globalSqlite!.exec("INSERT INTO users (id, clerk_id, email, name) VALUES (2, 'user2_test', 'user2@test.com', 'User Two');"); }); it('should share a project with another user', async () => { const member = await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'editor', }); expect(member).toMatchObject({ projectId: sharedProjectId, userId: 2, role: 'editor', }); }); it('should list project members including owner', async () => { await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'viewer', }); const members = await caller.project.listMembers({ projectId: sharedProjectId }); expect(members.length).toBeGreaterThanOrEqual(2); const owner = members.find((m: any) => m.userId === 1 && m.role === 'owner'); const member = members.find((m: any) => m.userId === 2 && m.role === 'viewer'); expect(owner).toBeDefined(); expect(member).toBeDefined(); }); it('should update a member role', async () => { await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'viewer', }); const updated = await caller.project.updateMemberRole({ projectId: sharedProjectId, userId: 2, role: 'admin', }); expect(updated.role).toBe('admin'); }); it('should remove a member', async () => { await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'editor', }); const result = await caller.project.removeMember({ projectId: sharedProjectId, userId: 2, }); expect(result).toEqual({ success: true }); const members = await caller.project.listMembers({ projectId: sharedProjectId }); const removed = members.find((m: any) => m.userId === 2); expect(removed).toBeUndefined(); }); it('should throw error when sharing with yourself', async () => { await expect( caller.project.shareProject({ projectId: sharedProjectId, userId: 1, role: 'editor', }) ).rejects.toThrow('yourself'); }); it('should throw error when sharing duplicate user', async () => { await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'editor', }); await expect( caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'viewer', }) ).rejects.toThrow('already a member'); }); it('should allow shared members to access project', async () => { await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'editor', }); // Create caller for user 2 const db = await getTestDb(); const ctx2: TRPCContext = { clerkUserId: 'user2_test', db }; const caller2 = appRouter.createCaller(ctx2); const project = await caller2.project.getProject({ id: sharedProjectId }); expect(project.id).toBe(sharedProjectId); }); it('should include shared projects in listProjects for member', async () => { await caller.project.shareProject({ projectId: sharedProjectId, userId: 2, role: 'viewer', }); const db = await getTestDb(); const ctx2: TRPCContext = { clerkUserId: 'user2_test', db }; const caller2 = appRouter.createCaller(ctx2); const projects = await caller2.project.listProjects(); const found = projects.find((p: any) => p.id === sharedProjectId); expect(found).toBeDefined(); }); }); });