Files
PicAnalysis/backend/tests/unit/services/document.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

412 lines
12 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.
/**
* 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);
});
});
});