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
This commit is contained in:
@@ -1,71 +1,63 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { appRouter } from './index';
|
||||
import type { Project } from '../../server/types/project';
|
||||
import { getTestDb, resetTestDb } from './test-setup';
|
||||
import type { TRPCContext } from './types';
|
||||
|
||||
describe('tRPC API Layer', () => {
|
||||
let ctx: { userId: string };
|
||||
let ctx: TRPCContext;
|
||||
let caller: ReturnType<typeof appRouter.createCaller>;
|
||||
let projectId: number;
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = { userId: '123e4567-e89b-12d3-a456-426614174000' };
|
||||
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 appRouter.project.createProject.mutate({
|
||||
input: {
|
||||
name: 'Test Project',
|
||||
description: 'A test project',
|
||||
},
|
||||
ctx,
|
||||
const project = await caller.project.createProject({
|
||||
name: 'Test Project',
|
||||
description: 'A test project',
|
||||
});
|
||||
|
||||
expect(project).toMatchObject({
|
||||
name: 'Test Project',
|
||||
description: 'A test project',
|
||||
userId: ctx.userId,
|
||||
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 () => {
|
||||
const projects = await appRouter.project.listProjects.query({
|
||||
ctx: { userId: ctx.userId },
|
||||
});
|
||||
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 () => {
|
||||
// First create a project
|
||||
const created = await appRouter.project.createProject.mutate({
|
||||
input: { name: 'Get Test' },
|
||||
ctx,
|
||||
});
|
||||
const created = await caller.project.createProject({ name: 'Get Test' });
|
||||
|
||||
const project = await appRouter.project.getProject.query({
|
||||
input: { id: created.id },
|
||||
ctx,
|
||||
});
|
||||
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 appRouter.project.createProject.mutate({
|
||||
input: { name: 'Update Test' },
|
||||
ctx,
|
||||
});
|
||||
const created = await caller.project.createProject({ name: 'Update Test' });
|
||||
|
||||
const updated = await appRouter.project.updateProject.mutate({
|
||||
input: {
|
||||
id: created.id,
|
||||
name: 'Updated Test',
|
||||
description: 'Updated description',
|
||||
},
|
||||
ctx,
|
||||
const updated = await caller.project.updateProject({
|
||||
id: created.id,
|
||||
name: 'Updated Test',
|
||||
description: 'Updated description',
|
||||
});
|
||||
|
||||
expect(updated.name).toBe('Updated Test');
|
||||
@@ -73,39 +65,27 @@ describe('tRPC API Layer', () => {
|
||||
});
|
||||
|
||||
it('should delete a project', async () => {
|
||||
const created = await appRouter.project.createProject.mutate({
|
||||
input: { name: 'Delete Test' },
|
||||
ctx,
|
||||
});
|
||||
const created = await caller.project.createProject({ name: 'Delete Test' });
|
||||
|
||||
const result = await appRouter.project.deleteProject.mutate({
|
||||
input: { id: created.id },
|
||||
ctx,
|
||||
});
|
||||
const result = await caller.project.deleteProject({ id: created.id });
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Character CRUD', () => {
|
||||
let projectId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const project = await appRouter.project.createProject.mutate({
|
||||
input: { name: 'Character Test Project' },
|
||||
ctx,
|
||||
const project = await caller.project.createProject({
|
||||
name: 'Character Test Project',
|
||||
});
|
||||
projectId = project.id;
|
||||
});
|
||||
|
||||
it('should create a character', async () => {
|
||||
const character = await appRouter.project.createCharacter.mutate({
|
||||
input: {
|
||||
name: 'John Doe',
|
||||
description: 'Main character',
|
||||
projectId,
|
||||
},
|
||||
ctx,
|
||||
const character = await caller.project.createCharacter({
|
||||
name: 'John Doe',
|
||||
description: 'Main character',
|
||||
projectId,
|
||||
});
|
||||
|
||||
expect(character).toMatchObject({
|
||||
@@ -116,40 +96,28 @@ describe('tRPC API Layer', () => {
|
||||
});
|
||||
|
||||
it('should list characters for a project', async () => {
|
||||
await appRouter.project.createCharacter.mutate({
|
||||
input: { name: 'Char 1', projectId },
|
||||
ctx,
|
||||
});
|
||||
await caller.project.createCharacter({ name: 'Char 1', projectId });
|
||||
|
||||
const characters = await appRouter.project.listCharacters.query({
|
||||
input: { projectId },
|
||||
ctx,
|
||||
});
|
||||
const characters = await caller.project.listCharacters({ projectId });
|
||||
|
||||
expect(characters.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scene CRUD', () => {
|
||||
let projectId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const project = await appRouter.project.createProject.mutate({
|
||||
input: { name: 'Scene Test Project' },
|
||||
ctx,
|
||||
const project = await caller.project.createProject({
|
||||
name: 'Scene Test Project',
|
||||
});
|
||||
projectId = project.id;
|
||||
});
|
||||
|
||||
it('should create a scene', async () => {
|
||||
const scene = await appRouter.project.createScene.mutate({
|
||||
input: {
|
||||
title: 'INT. OFFICE - DAY',
|
||||
content: 'John sits at his desk.',
|
||||
projectId,
|
||||
order: 1,
|
||||
},
|
||||
ctx,
|
||||
const scene = await caller.project.createScene({
|
||||
title: 'INT. OFFICE - DAY',
|
||||
content: 'John sits at his desk.',
|
||||
projectId,
|
||||
order: 1,
|
||||
});
|
||||
|
||||
expect(scene).toMatchObject({
|
||||
@@ -161,29 +129,21 @@ describe('tRPC API Layer', () => {
|
||||
});
|
||||
|
||||
it('should list scenes for a project', async () => {
|
||||
await appRouter.project.createScene.mutate({
|
||||
input: { title: 'Scene 1', projectId, order: 1 },
|
||||
ctx,
|
||||
});
|
||||
await caller.project.createScene({ title: 'Scene 1', projectId, order: 1 });
|
||||
|
||||
const scenes = await appRouter.project.listScenes.query({
|
||||
input: { projectId },
|
||||
ctx,
|
||||
});
|
||||
const scenes = await caller.project.listScenes({ projectId });
|
||||
|
||||
expect(scenes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should update scene order', async () => {
|
||||
const scene = await appRouter.project.createScene.mutate({
|
||||
input: { title: 'Reorder Scene', projectId, order: 1 },
|
||||
ctx,
|
||||
const scene = await caller.project.createScene({
|
||||
title: 'Reorder Scene',
|
||||
projectId,
|
||||
order: 1,
|
||||
});
|
||||
|
||||
const updated = await appRouter.project.updateScene.mutate({
|
||||
input: { id: scene.id, order: 5 },
|
||||
ctx,
|
||||
});
|
||||
const updated = await caller.project.updateScene({ id: scene.id, order: 5 });
|
||||
|
||||
expect(updated.order).toBe(5);
|
||||
});
|
||||
@@ -192,20 +152,14 @@ describe('tRPC API Layer', () => {
|
||||
describe('Error Handling', () => {
|
||||
it('should throw error when getting non-existent project', async () => {
|
||||
await expect(
|
||||
appRouter.project.getProject.query({
|
||||
input: { id: '00000000-0000-0000-0000-000000000000' },
|
||||
ctx,
|
||||
})
|
||||
caller.project.getProject({ id: 99999 })
|
||||
).rejects.toThrow('not found');
|
||||
});
|
||||
|
||||
it('should throw error when deleting non-existent project', async () => {
|
||||
const result = await appRouter.project.deleteProject.mutate({
|
||||
input: { id: '00000000-0000-0000-0000-000000000000' },
|
||||
ctx,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false });
|
||||
await expect(
|
||||
caller.project.deleteProject({ id: 99999 })
|
||||
).rejects.toThrow('not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user