完整的前后端图片分析应用,包含: - 后端: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>
478 lines
15 KiB
TypeScript
478 lines
15 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|
||
});
|