完整的前后端图片分析应用,包含: - 后端: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>
412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
/**
|
||
* Document Service Unit Tests
|
||
* TDD: Test-Driven Development
|
||
*/
|
||
|
||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||
import { DocumentService } from '../../../src/services/document.service';
|
||
import { prisma } from '../../../src/lib/prisma';
|
||
|
||
describe('DocumentService', () => {
|
||
// @ralph 我要测试什么?
|
||
// - 创建文档
|
||
// - 更新文档内容
|
||
// - 删除文档
|
||
// - 获取用户文档列表
|
||
// - 按分类筛选文档
|
||
// - 边界情况:空内容、特殊字符、长文本
|
||
|
||
let userId: string;
|
||
let categoryId: string;
|
||
|
||
beforeEach(async () => {
|
||
// Create test user and category
|
||
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: 'Test Category',
|
||
type: 'document',
|
||
},
|
||
});
|
||
categoryId = category.id;
|
||
});
|
||
|
||
afterEach(async () => {
|
||
// Clean up
|
||
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 document', async () => {
|
||
// @ralph 正常路径是否覆盖?
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Test document content',
|
||
title: 'Test Document',
|
||
category_id: categoryId,
|
||
});
|
||
|
||
expect(document).toBeDefined();
|
||
expect(document.id).toBeDefined();
|
||
expect(document.content).toBe('Test document content');
|
||
expect(document.title).toBe('Test Document');
|
||
expect(document.user_id).toBe(userId);
|
||
expect(document.category_id).toBe(categoryId);
|
||
});
|
||
|
||
it('should create document with minimal required fields', async () => {
|
||
// @ralph 最小输入是否处理?
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Minimal content',
|
||
});
|
||
|
||
expect(document).toBeDefined();
|
||
expect(document.content).toBe('Minimal content');
|
||
expect(document.title).toBeNull();
|
||
expect(document.category_id).toBeNull();
|
||
});
|
||
|
||
it('should reject empty content', async () => {
|
||
// @ralph 无效输入是否拒绝?
|
||
await expect(
|
||
DocumentService.create({
|
||
user_id: userId,
|
||
content: '',
|
||
})
|
||
).rejects.toThrow('content');
|
||
});
|
||
|
||
it('should handle long content', async () => {
|
||
// @ralph 边界条件是否测试?
|
||
const longContent = 'A'.repeat(10000);
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: longContent,
|
||
});
|
||
|
||
expect(document.content).toBe(longContent);
|
||
});
|
||
|
||
it('should handle special characters in content', async () => {
|
||
// @ralph 特殊字符是否正确处理?
|
||
const specialContent = 'Test with 中文 and émojis 🎉 and "quotes"';
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: specialContent,
|
||
});
|
||
|
||
expect(document.content).toBe(specialContent);
|
||
});
|
||
|
||
it('should reject non-existent category', async () => {
|
||
// @ralph 外键约束是否验证?
|
||
const fakeCategoryId = '00000000-0000-0000-0000-000000000000';
|
||
|
||
await expect(
|
||
DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Test',
|
||
category_id: fakeCategoryId,
|
||
})
|
||
).rejects.toThrow();
|
||
});
|
||
|
||
it('should reject non-existent user', async () => {
|
||
// @ralph 用户验证是否正确?
|
||
const fakeUserId = '00000000-0000-0000-0000-000000000000';
|
||
|
||
await expect(
|
||
DocumentService.create({
|
||
user_id: fakeUserId,
|
||
content: 'Test',
|
||
})
|
||
).rejects.toThrow();
|
||
});
|
||
});
|
||
|
||
describe('findById', () => {
|
||
it('should find document by id', async () => {
|
||
// @ralph 查找功能是否正常?
|
||
const created = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Test content',
|
||
});
|
||
|
||
const found = await DocumentService.findById(created.id, userId);
|
||
|
||
expect(found).toBeDefined();
|
||
expect(found!.id).toBe(created.id);
|
||
expect(found!.content).toBe('Test content');
|
||
});
|
||
|
||
it('should return null for non-existent document', async () => {
|
||
// @ralph 未找到时是否返回null?
|
||
const found = await DocumentService.findById('00000000-0000-0000-0000-000000000000', userId);
|
||
expect(found).toBeNull();
|
||
});
|
||
|
||
it('should not return document from different user', async () => {
|
||
// @ralph 数据隔离是否正确?
|
||
const otherUser = await prisma.user.create({
|
||
data: {
|
||
username: 'otheruser',
|
||
password_hash: '$2a$10$test',
|
||
},
|
||
});
|
||
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Private document',
|
||
});
|
||
|
||
const found = await DocumentService.findById(document.id, otherUser.id);
|
||
expect(found).toBeNull();
|
||
|
||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||
});
|
||
});
|
||
|
||
describe('update', () => {
|
||
it('should update document content', async () => {
|
||
// @ralph 更新功能是否正常?
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Original content',
|
||
});
|
||
|
||
const updated = await DocumentService.update(document.id, userId, {
|
||
content: 'Updated content',
|
||
});
|
||
|
||
expect(updated.content).toBe('Updated content');
|
||
expect(updated.id).toBe(document.id);
|
||
});
|
||
|
||
it('should update title', async () => {
|
||
// @ralph 部分更新是否支持?
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Content',
|
||
});
|
||
|
||
const updated = await DocumentService.update(document.id, userId, {
|
||
title: 'New Title',
|
||
});
|
||
|
||
expect(updated.title).toBe('New Title');
|
||
expect(updated.content).toBe('Content');
|
||
});
|
||
|
||
it('should update category', async () => {
|
||
// @ralph 分类更新是否正确?
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Content',
|
||
});
|
||
|
||
const updated = await DocumentService.update(document.id, userId, {
|
||
category_id: categoryId,
|
||
});
|
||
|
||
expect(updated.category_id).toBe(categoryId);
|
||
});
|
||
|
||
it('should reject update for non-existent document', async () => {
|
||
// @ralph 错误处理是否正确?
|
||
await expect(
|
||
DocumentService.update('00000000-0000-0000-0000-000000000000', userId, {
|
||
content: 'Updated',
|
||
})
|
||
).rejects.toThrow();
|
||
});
|
||
|
||
it('should reject update from different user', async () => {
|
||
// @ralph 权限控制是否正确?
|
||
const otherUser = await prisma.user.create({
|
||
data: {
|
||
username: 'otheruser',
|
||
password_hash: '$2a$10$test',
|
||
},
|
||
});
|
||
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Content',
|
||
});
|
||
|
||
await expect(
|
||
DocumentService.update(document.id, otherUser.id, {
|
||
content: 'Hacked',
|
||
})
|
||
).rejects.toThrow();
|
||
|
||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||
});
|
||
});
|
||
|
||
describe('delete', () => {
|
||
it('should delete document', async () => {
|
||
// @ralph 删除功能是否正常?
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Content',
|
||
});
|
||
|
||
await DocumentService.delete(document.id, userId);
|
||
|
||
const found = await DocumentService.findById(document.id, userId);
|
||
expect(found).toBeNull();
|
||
});
|
||
|
||
it('should reject delete for non-existent document', async () => {
|
||
// @ralph 错误处理是否正确?
|
||
await expect(
|
||
DocumentService.delete('00000000-0000-0000-0000-000000000000', userId)
|
||
).rejects.toThrow();
|
||
});
|
||
|
||
it('should reject delete from different user', async () => {
|
||
// @ralph 权限控制是否正确?
|
||
const otherUser = await prisma.user.create({
|
||
data: {
|
||
username: 'otheruser',
|
||
password_hash: '$2a$10$test',
|
||
},
|
||
});
|
||
|
||
const document = await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Content',
|
||
});
|
||
|
||
await expect(
|
||
DocumentService.delete(document.id, otherUser.id)
|
||
).rejects.toThrow();
|
||
|
||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||
});
|
||
});
|
||
|
||
describe('findByUser', () => {
|
||
beforeEach(async () => {
|
||
// Create multiple documents
|
||
await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Document 1',
|
||
title: 'First',
|
||
category_id: categoryId,
|
||
});
|
||
await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Document 2',
|
||
title: 'Second',
|
||
});
|
||
await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Document 3',
|
||
title: 'Third',
|
||
category_id: categoryId,
|
||
});
|
||
});
|
||
|
||
it('should return all user documents', async () => {
|
||
// @ralph 列表查询是否正确?
|
||
const documents = await DocumentService.findByUser(userId);
|
||
|
||
expect(documents).toHaveLength(3);
|
||
expect(documents.every(d => d.user_id === userId)).toBe(true);
|
||
});
|
||
|
||
it('should filter by category', async () => {
|
||
// @ralph 筛选功能是否正确?
|
||
const documents = await DocumentService.findByUser(userId, { category_id: categoryId });
|
||
|
||
expect(documents).toHaveLength(2);
|
||
expect(documents.every(d => d.category_id === categoryId)).toBe(true);
|
||
});
|
||
|
||
it('should support pagination', async () => {
|
||
// @ralph 分页功能是否支持?
|
||
const page1 = await DocumentService.findByUser(userId, { page: 1, limit: 2 });
|
||
expect(page1).toHaveLength(2);
|
||
|
||
const page2 = await DocumentService.findByUser(userId, { page: 2, limit: 2 });
|
||
expect(page2).toHaveLength(1);
|
||
});
|
||
|
||
it('should return empty for user with no documents', async () => {
|
||
// @ralph 空结果是否正确处理?
|
||
const otherUser = await prisma.user.create({
|
||
data: {
|
||
username: 'emptyuser',
|
||
password_hash: '$2a$10$test',
|
||
},
|
||
});
|
||
|
||
const documents = await DocumentService.findByUser(otherUser.id);
|
||
expect(documents).toHaveLength(0);
|
||
|
||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||
});
|
||
});
|
||
|
||
describe('search', () => {
|
||
beforeEach(async () => {
|
||
await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'This is about programming with JavaScript',
|
||
title: 'Programming Guide',
|
||
});
|
||
await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'Cooking recipes for beginners',
|
||
title: 'Cooking Book',
|
||
});
|
||
await DocumentService.create({
|
||
user_id: userId,
|
||
content: 'JavaScript best practices',
|
||
title: 'Advanced Programming',
|
||
});
|
||
});
|
||
|
||
it('should search in content', async () => {
|
||
// @ralph 内容搜索是否正常?
|
||
const results = await DocumentService.search(userId, 'JavaScript');
|
||
|
||
expect(results).toHaveLength(2);
|
||
expect(results.every(d => d.content.includes('JavaScript') || d.title?.includes('JavaScript'))).toBe(true);
|
||
});
|
||
|
||
it('should search in title', async () => {
|
||
// @ralph 标题搜索是否正常?
|
||
const results = await DocumentService.search(userId, 'Programming');
|
||
|
||
expect(results).toHaveLength(2);
|
||
});
|
||
|
||
it('should return empty for no matches', async () => {
|
||
// @ralph 无结果时是否正确?
|
||
const results = await DocumentService.search(userId, 'NonExistentTerm');
|
||
expect(results).toHaveLength(0);
|
||
});
|
||
|
||
it('should handle empty search term', async () => {
|
||
// @ralph 空搜索词是否处理?
|
||
const results = await DocumentService.search(userId, '');
|
||
expect(results).toHaveLength(3);
|
||
});
|
||
});
|
||
});
|