/** * Todo Service Unit Tests * TDD: Test-Driven Development */ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { TodoService } from '../../../src/services/todo.service'; import { prisma } from '../../../src/lib/prisma'; describe('TodoService', () => { // @ralph 我要测试什么? // - 创建待办事项 // - 更新待办状态 (pending -> completed -> confirmed) // - 软删除待办 // - 按状态筛选 // - 优先级排序 // - 到期日期处理 let userId: string; let categoryId: string; let documentId: string; beforeEach(async () => { // Create test user, category and document const user = await prisma.user.create({ data: { username: `testuser_${Date.now()}`, email: `test_${Date.now()}@example.com`, password_hash: '$2a$10$test', }, }); userId = user.id; const category = await prisma.category.create({ data: { user_id: userId, name: 'Work', type: 'todo', }, }); categoryId = category.id; const document = await prisma.document.create({ data: { user_id: userId, content: 'Related document', }, }); documentId = document.id; }); afterEach(async () => { // Clean up await prisma.todo.deleteMany({ where: { user_id: userId } }); await prisma.document.deleteMany({ where: { user_id: userId } }); await prisma.category.deleteMany({ where: { user_id: userId } }); await prisma.user.delete({ where: { id: userId } }); }); describe('create', () => { it('should create a new todo with required fields', async () => { // @ralph 正常路径是否覆盖? const todo = await TodoService.create({ user_id: userId, title: 'Test todo', }); expect(todo).toBeDefined(); expect(todo.id).toBeDefined(); expect(todo.title).toBe('Test todo'); expect(todo.status).toBe('pending'); expect(todo.priority).toBe('medium'); expect(todo.user_id).toBe(userId); }); it('should create todo with all fields', async () => { // @ralph 完整创建是否支持? const dueDate = new Date('2025-12-31'); const todo = await TodoService.create({ user_id: userId, title: 'Complete task', description: 'Task description', priority: 'high', due_date: dueDate, category_id: categoryId, document_id: documentId, }); expect(todo.title).toBe('Complete task'); expect(todo.description).toBe('Task description'); expect(todo.priority).toBe('high'); expect(new Date(todo.due_date!)).toEqual(dueDate); expect(todo.category_id).toBe(categoryId); expect(todo.document_id).toBe(documentId); }); it('should reject empty title', async () => { // @ralph 验证是否正确? await expect( TodoService.create({ user_id: userId, title: '', }) ).rejects.toThrow('title'); }); it('should reject invalid priority', async () => { // @ralph 枚举验证是否正确? await expect( TodoService.create({ user_id: userId, title: 'Test', priority: 'invalid' as any, }) ).rejects.toThrow(); }); it('should reject invalid status', async () => { // @ralph 状态验证是否正确? await expect( TodoService.create({ user_id: userId, title: 'Test', status: 'invalid' as any, }) ).rejects.toThrow(); }); }); describe('findById', () => { it('should find todo by id', async () => { // @ralph 查找功能是否正常? const created = await TodoService.create({ user_id: userId, title: 'Find me', }); const found = await TodoService.findById(created.id, userId); expect(found).toBeDefined(); expect(found!.id).toBe(created.id); expect(found!.title).toBe('Find me'); }); it('should return null for non-existent todo', async () => { // @ralph 未找到时是否返回null? const found = await TodoService.findById('00000000-0000-0000-0000-000000000000', userId); expect(found).toBeNull(); }); it('should not return todo from different user', async () => { // @ralph 数据隔离是否正确? const otherUser = await prisma.user.create({ data: { username: 'otheruser', password_hash: '$2a$10$test', }, }); const todo = await TodoService.create({ user_id: userId, title: 'Private todo', }); const found = await TodoService.findById(todo.id, otherUser.id); expect(found).toBeNull(); await prisma.user.delete({ where: { id: otherUser.id } }); }); }); describe('update', () => { it('should update todo title', async () => { // @ralph 更新功能是否正常? const todo = await TodoService.create({ user_id: userId, title: 'Old title', }); const updated = await TodoService.update(todo.id, userId, { title: 'New title', }); expect(updated.title).toBe('New title'); }); it('should update todo description', async () => { // @ralph 部分更新是否支持? const todo = await TodoService.create({ user_id: userId, title: 'Test', }); const updated = await TodoService.update(todo.id, userId, { description: 'New description', }); expect(updated.description).toBe('New description'); }); it('should update status and set completed_at', async () => { // @ralph 状态转换是否正确? const todo = await TodoService.create({ user_id: userId, title: 'Task', status: 'pending', }); const updated = await TodoService.update(todo.id, userId, { status: 'completed', }); expect(updated.status).toBe('completed'); expect(updated.completed_at).toBeDefined(); }); it('should set confirmed_at when status changes to confirmed', async () => { // @ralph 三态流程是否正确? const todo = await TodoService.create({ user_id: userId, title: 'Task', status: 'completed', }); const updated = await TodoService.update(todo.id, userId, { status: 'confirmed', }); expect(updated.status).toBe('confirmed'); expect(updated.confirmed_at).toBeDefined(); }); it('should clear completed_at when reverting to pending', async () => { // @ralph 状态回退是否正确? const todo = await TodoService.create({ user_id: userId, title: 'Task', status: 'completed', }); const updated = await TodoService.update(todo.id, userId, { status: 'pending', }); expect(updated.status).toBe('pending'); expect(updated.completed_at).toBeNull(); }); it('should update priority', async () => { // @ralph 优先级更新是否正确? const todo = await TodoService.create({ user_id: userId, title: 'Task', }); const updated = await TodoService.update(todo.id, userId, { priority: 'urgent', }); expect(updated.priority).toBe('urgent'); }); it('should update due_date', async () => { // @ralph 到期日期更新是否正确? const todo = await TodoService.create({ user_id: userId, title: 'Task', }); const newDate = new Date('2025-12-31'); const updated = await TodoService.update(todo.id, userId, { due_date: newDate, }); expect(new Date(updated.due_date!)).toEqual(newDate); }); it('should reject update for non-existent todo', async () => { // @ralph 错误处理是否正确? await expect( TodoService.update('00000000-0000-0000-0000-000000000000', userId, { title: 'Updated', }) ).rejects.toThrow(); }); }); describe('delete', () => { it('should delete todo', async () => { // @ralph 删除功能是否正常? const todo = await TodoService.create({ user_id: userId, title: 'Delete me', }); await TodoService.delete(todo.id, userId); const found = await TodoService.findById(todo.id, userId); expect(found).toBeNull(); }); it('should reject delete for non-existent todo', async () => { // @ralph 错误处理是否正确? await expect( TodoService.delete('00000000-0000-0000-0000-000000000000', userId) ).rejects.toThrow(); }); }); describe('findByUser', () => { beforeEach(async () => { // Create multiple todos await TodoService.create({ user_id: userId, title: 'Pending task 1', status: 'pending', priority: 'high', }); await TodoService.create({ user_id: userId, title: 'Completed task', status: 'completed', priority: 'medium', }); await TodoService.create({ user_id: userId, title: 'Confirmed task', status: 'confirmed', priority: 'low', }); await TodoService.create({ user_id: userId, title: 'Pending task 2', status: 'pending', priority: 'urgent', }); }); it('should return all user todos', async () => { // @ralph 列表查询是否正确? const todos = await TodoService.findByUser(userId); expect(todos).toHaveLength(4); expect(todos.every(t => t.user_id === userId)).toBe(true); }); it('should filter by status', async () => { // @ralph 状态筛选是否正确? const pending = await TodoService.findByUser(userId, { status: 'pending' }); expect(pending).toHaveLength(2); expect(pending.every(t => t.status === 'pending')).toBe(true); const completed = await TodoService.findByUser(userId, { status: 'completed' }); expect(completed).toHaveLength(1); const confirmed = await TodoService.findByUser(userId, { status: 'confirmed' }); expect(confirmed).toHaveLength(1); }); it('should filter by priority', async () => { // @ralph 优先级筛选是否正确? const high = await TodoService.findByUser(userId, { priority: 'high' }); expect(high).toHaveLength(1); expect(high[0].priority).toBe('high'); const urgent = await TodoService.findByUser(userId, { priority: 'urgent' }); expect(urgent).toHaveLength(1); expect(urgent[0].priority).toBe('urgent'); }); it('should filter by category', async () => { // @ralph 分类筛选是否正确? const todoWithCategory = await TodoService.create({ user_id: userId, title: 'Categorized task', category_id: categoryId, }); const categorized = await TodoService.findByUser(userId, { category_id: categoryId }); expect(categorized.length).toBeGreaterThanOrEqual(1); expect(categorized.some(t => t.id === todoWithCategory.id)).toBe(true); }); it('should filter by document', async () => { // @ralph 文档筛选是否正确? const todoWithDocument = await TodoService.create({ user_id: userId, title: 'Document task', document_id: documentId, }); const withDocument = await TodoService.findByUser(userId, { document_id: documentId }); expect(withDocument.length).toBeGreaterThanOrEqual(1); expect(withDocument.some(t => t.id === todoWithDocument.id)).toBe(true); }); it('should support pagination', async () => { // @ralph 分页是否支持? const page1 = await TodoService.findByUser(userId, { page: 1, limit: 2 }); expect(page1).toHaveLength(2); }); it('should sort by priority by default', async () => { // @ralph 排序是否正确? const todos = await TodoService.findByUser(userId, { limit: 10 }); // Check that urgent comes before high, and high before medium const priorities = todos.map(t => { const order = { urgent: 0, high: 1, medium: 2, low: 3 }; return order[t.priority as keyof typeof order]; }); for (let i = 1; i < priorities.length; i++) { expect(priorities[i]).toBeGreaterThanOrEqual(priorities[i - 1]); } }); it('should return overdue todos', async () => { // @ralph 过期筛选是否正确? const pastDate = new Date(); pastDate.setDate(pastDate.getDate() - 10); await TodoService.create({ user_id: userId, title: 'Overdue task', due_date: pastDate, status: 'pending', }); const overdue = await TodoService.findByUser(userId, { overdue: true }); expect(overdue.length).toBeGreaterThanOrEqual(1); expect(overdue.every(t => t.due_date && new Date(t.due_date) < new Date())).toBe(true); }); }); describe('getPendingTodos', () => { it('should return only pending todos', async () => { // @ralph 待办列表是否正确? await TodoService.create({ user_id: userId, title: 'Pending 1', status: 'pending' }); await TodoService.create({ user_id: userId, title: 'Pending 2', status: 'pending' }); await TodoService.create({ user_id: userId, title: 'Completed', status: 'completed' }); await TodoService.create({ user_id: userId, title: 'Confirmed', status: 'confirmed' }); const pending = await TodoService.getPendingTodos(userId); expect(pending).toHaveLength(2); expect(pending.every(t => t.status === 'pending')).toBe(true); }); }); describe('getCompletedTodos', () => { it('should return only completed todos', async () => { // @ralph 已完成列表是否正确? await TodoService.create({ user_id: userId, title: 'Pending', status: 'pending' }); await TodoService.create({ user_id: userId, title: 'Completed 1', status: 'completed' }); await TodoService.create({ user_id: userId, title: 'Completed 2', status: 'completed' }); await TodoService.create({ user_id: userId, title: 'Confirmed', status: 'confirmed' }); const completed = await TodoService.getCompletedTodos(userId); expect(completed).toHaveLength(2); expect(completed.every(t => t.status === 'completed')).toBe(true); }); }); describe('getConfirmedTodos', () => { it('should return only confirmed todos', async () => { // @ralph 已确认列表是否正确? await TodoService.create({ user_id: userId, title: 'Pending', status: 'pending' }); await TodoService.create({ user_id: userId, title: 'Completed', status: 'completed' }); await TodoService.create({ user_id: userId, title: 'Confirmed 1', status: 'confirmed' }); await TodoService.create({ user_id: userId, title: 'Confirmed 2', status: 'confirmed' }); const confirmed = await TodoService.getConfirmedTodos(userId); expect(confirmed).toHaveLength(2); expect(confirmed.every(t => t.status === 'confirmed')).toBe(true); }); }); });