Files
FrenoCorp/server/trpc/project-router.test.ts
Michael Freno 79d153f75a FRE-592: Fix remaining code review blockers and add tests
- Replace in-memory Maps with Drizzle ORM queries for all CRUD operations
- Use integer IDs matching SQLite schema instead of UUIDs
- Fix scriptId to projectId inconsistency in characters and scenes
- Add project ownership verification on all mutation procedures
- Make getCharacter/getScene procedures protected (not public)
- Proper JWT-based userId validation via context
- Add cascade delete for characters/relationships/scenes on project deletion
- Add verifyProjectOwnership helper for authorization checks
- Rewrite tests with createCallerFactory pattern for tRPC v11
- Use better-sqlite3 for in-memory test database
- Split vitest config into separate file from vite config
2026-04-24 08:31:42 -04:00

166 lines
4.7 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { appRouter } from './index';
import { getTestDb, resetTestDb } from './test-setup';
import type { TRPCContext } from './types';
describe('tRPC API Layer', () => {
let ctx: TRPCContext;
let caller: ReturnType<typeof appRouter.createCaller>;
let projectId: number;
beforeEach(async () => {
await resetTestDb();
const db = await getTestDb();
ctx = { userId: 1, 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: ctx.userId,
});
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');
});
});
});