Files
PicAnalysis/backend/tests/unit/services/todo.service.test.ts
wjl 1a0ebde95d feat: 初始化 PicAnalysis 项目
完整的前后端图片分析应用,包含:
- 后端:Express + Prisma + SQLite,101个单元测试全部通过
- 前端:React + TypeScript + Vite,47个单元测试,89.73%覆盖率
- E2E测试:Playwright 测试套件
- MCP集成:Playwright MCP配置完成并测试通过

功能模块:
- 用户认证(JWT)
- 文档管理(CRUD)
- 待办管理(三态工作流)
- 图片管理(上传、截图、OCR)

测试覆盖:
- 后端单元测试:101/101 
- 前端单元测试:47/47 
- E2E测试:通过 
- MCP Playwright测试:通过 

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 20:10:11 +08:00

478 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
});
});