feat: 增加测试覆盖率并优化代码质量

test: 添加单元测试和端到端测试
refactor: 重构登录页面和上传模块
ci: 更新测试覆盖率阈值至42%
build: 添加测试相关依赖
docs: 更新测试文档
style: 修复代码格式问题
This commit is contained in:
张翔
2026-03-11 11:14:37 +08:00
parent 8fd7ed84ed
commit b207bfa7af
58 changed files with 14494 additions and 655 deletions
+169
View File
@@ -0,0 +1,169 @@
import { GET, POST, PUT } from './route';
import { NextRequest } from 'next/server';
jest.mock('@/lib/auth', () => ({
auth: jest.fn(),
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: jest.fn(),
}));
jest.mock('@/db', () => ({
db: {
select: jest.fn().mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
limit: jest.fn().mockResolvedValue([]),
orderBy: jest.fn().mockResolvedValue([]),
}),
}),
}),
insert: jest.fn().mockReturnValue({
values: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([{
id: 'test-id',
key: 'test_key',
value: 'test_value',
category: 'general',
}]),
}),
}),
},
}));
describe('/api/admin/config', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET', () => {
it('should return 401 if not authenticated', async () => {
const request = new NextRequest('http://localhost/api/admin/config');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 if no permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
const request = new NextRequest('http://localhost/api/admin/config');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return configs if authenticated and has permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
const request = new NextRequest('http://localhost/api/admin/config');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.configs).toBeDefined();
expect(data.flat).toBeDefined();
});
});
describe('POST', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/admin/config', {
method: 'POST',
body: JSON.stringify({ key: 'test', value: {} }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 400 if missing required fields', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
const request = new NextRequest('http://localhost/api/admin/config', {
method: 'POST',
body: JSON.stringify({ key: 'test' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('缺少必要字段');
});
});
describe('PUT', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/admin/config', {
method: 'PUT',
body: JSON.stringify({ configs: [] }),
});
const response = await PUT(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 if no permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
const request = new NextRequest('http://localhost/api/admin/config', {
method: 'PUT',
body: JSON.stringify({ configs: [] }),
});
const response = await PUT(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return 400 if configs is not an array', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
const request = new NextRequest('http://localhost/api/admin/config', {
method: 'PUT',
body: JSON.stringify({ configs: 'not-array' }),
});
const response = await PUT(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('无效的数据格式');
});
});
});
@@ -0,0 +1,183 @@
import { NextRequest, NextResponse } from 'next/server';
jest.mock('@/db', () => ({
db: {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
},
}));
jest.mock('@/lib/auth', () => ({
auth: jest.fn(),
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: jest.fn(),
}));
jest.mock('@/lib/audit', () => ({
createAuditLog: jest.fn().mockResolvedValue({}),
}));
const { db } = require('@/db');
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
describe('GET /api/admin/content/[id]', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return 401 if not authenticated', async () => {
auth.mockResolvedValue(null);
const { GET } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123');
const params = Promise.resolve({ id: '123' });
const response = await GET(request, { params });
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 if no permission', async () => {
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
const { GET } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123');
const params = Promise.resolve({ id: '123' });
const response = await GET(request, { params });
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return 404 if content not found', async () => {
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
db.limit.mockResolvedValue([]);
const { GET } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123');
const params = Promise.resolve({ id: '123' });
const response = await GET(request, { params });
const data = await response.json();
expect(response.status).toBe(404);
expect(data.error).toBe('内容不存在');
});
it('should return content if found', async () => {
const mockContent = {
id: '123',
title: 'Test Content',
status: 'published',
};
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
db.limit.mockResolvedValue([mockContent]);
db.orderBy.mockResolvedValue([]);
const { GET } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123');
const params = Promise.resolve({ id: '123' });
const response = await GET(request, { params });
const data = await response.json();
expect(response.status).toBe(200);
expect(data.title).toBe('Test Content');
});
});
describe('PUT /api/admin/content/[id]', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return 401 if not authenticated', async () => {
auth.mockResolvedValue(null);
const { PUT } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123', {
method: 'PUT',
body: JSON.stringify({ title: 'Updated' }),
});
const params = Promise.resolve({ id: '123' });
const response = await PUT(request, { params });
const data = await response.json();
expect(response.status).toBe(401);
});
it('should return 403 if no permission', async () => {
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
const { PUT } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123', {
method: 'PUT',
body: JSON.stringify({ title: 'Updated' }),
});
const params = Promise.resolve({ id: '123' });
const response = await PUT(request, { params });
const data = await response.json();
expect(response.status).toBe(403);
});
});
describe('DELETE /api/admin/content/[id]', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return 401 if not authenticated', async () => {
auth.mockResolvedValue(null);
const { DELETE } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123', {
method: 'DELETE',
});
const params = Promise.resolve({ id: '123' });
const response = await DELETE(request, { params });
const data = await response.json();
expect(response.status).toBe(401);
});
it('should return 403 if no permission', async () => {
auth.mockResolvedValue({ user: { role: 'editor' } });
hasPermission.mockReturnValue(false);
const { DELETE } = require('./route');
const request = new NextRequest('http://localhost/api/admin/content/123', {
method: 'DELETE',
});
const params = Promise.resolve({ id: '123' });
const response = await DELETE(request, { params });
const data = await response.json();
expect(response.status).toBe(403);
});
});
+142
View File
@@ -0,0 +1,142 @@
import { describe, it, expect, jest, beforeAll, beforeEach } from '@jest/globals';
import { NextRequest } from 'next/server';
import '@testing-library/jest-dom';
const mockAuth = jest.fn();
const mockHasPermission = jest.fn();
const mockDbSelect = jest.fn();
const mockDbInsert = jest.fn();
jest.mock('@/lib/auth', () => ({
auth: mockAuth,
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: mockHasPermission,
}));
jest.mock('@/db', () => ({
db: {
select: () => ({
from: () => ({
where: () => ({
orderBy: () => ({
limit: () => ({
offset: mockDbSelect,
}),
}),
}),
}),
}),
insert: () => ({
values: () => ({
returning: mockDbInsert,
}),
}),
},
}));
jest.mock('drizzle-orm', () => ({
eq: jest.fn(),
desc: jest.fn(),
and: jest.fn(),
like: jest.fn(),
sql: jest.fn(),
}));
jest.mock('nanoid', () => ({
nanoid: () => 'test-id-123',
}));
jest.mock('@/lib/audit', () => ({
createAuditLog: jest.fn(),
}));
jest.mock('@/db/schema', () => ({
content: {},
}));
import { GET, POST } from './route';
describe('/api/admin/content', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET', () => {
it('should return 401 when not authenticated', async () => {
mockAuth.mockResolvedValueOnce(null);
const request = new NextRequest('http://localhost/api/admin/content');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 when user lacks permission', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'viewer' },
});
mockHasPermission.mockReturnValueOnce(false);
const request = new NextRequest('http://localhost/api/admin/content');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return content list when authorized', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'admin' },
});
mockHasPermission.mockReturnValueOnce(true);
mockDbSelect.mockResolvedValueOnce([]);
mockDbSelect.mockResolvedValueOnce([{ count: 0 }]);
const request = new NextRequest('http://localhost/api/admin/content');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.items).toEqual([]);
expect(data.pagination).toBeDefined();
});
});
describe('POST', () => {
it('should return 401 when not authenticated', async () => {
mockAuth.mockResolvedValueOnce(null);
const request = new NextRequest('http://localhost/api/admin/content', {
method: 'POST',
body: JSON.stringify({ type: 'news', title: 'Test', slug: 'test' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 400 when missing required fields', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'admin' },
});
mockHasPermission.mockReturnValueOnce(true);
const request = new NextRequest('http://localhost/api/admin/content', {
method: 'POST',
body: JSON.stringify({ type: 'news' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('缺少必要字段');
});
});
});
+98
View File
@@ -0,0 +1,98 @@
import { POST, DELETE } from './route';
import { NextRequest } from 'next/server';
jest.mock('@/lib/auth', () => ({
auth: jest.fn(),
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: jest.fn(),
}));
jest.mock('@/lib/audit', () => ({
createAuditLog: jest.fn(),
}));
jest.mock('@/lib/upload', () => ({
uploadFile: jest.fn().mockResolvedValue({
id: 'test-id',
name: 'test.jpg',
type: 'image',
size: 1024,
url: 'https://example.com/test.jpg',
}),
deleteFile: jest.fn(),
}));
describe('/api/admin/upload', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('POST', () => {
it('should return 401 if not authenticated', async () => {
const formData = new FormData();
formData.append('file', new File(['test'], 'test.jpg', { type: 'image/jpeg' }));
const request = new NextRequest('http://localhost/api/admin/upload', {
method: 'POST',
body: formData,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 if no permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
const request = new NextRequest('http://localhost/api/admin/upload', {
method: 'POST',
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return 400 if no file', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'admin', id: 'test-user' } });
hasPermission.mockReturnValue(true);
const request = {
formData: jest.fn().mockResolvedValue(new FormData()),
} as any;
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('未找到文件');
});
});
describe('DELETE', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/admin/upload?url=test.jpg', {
method: 'DELETE',
});
const response = await DELETE(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
});
});
+121
View File
@@ -0,0 +1,121 @@
import { GET, PUT, DELETE } from './route';
import { NextRequest } from 'next/server';
jest.mock('@/lib/auth', () => ({
auth: jest.fn(),
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: jest.fn(),
}));
jest.mock('@/db', () => ({
db: {
select: jest.fn().mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
limit: jest.fn().mockResolvedValue([{
id: 'test-user-id',
email: 'test@example.com',
name: 'Test User',
role: 'admin',
}]),
}),
}),
}),
update: jest.fn().mockReturnValue({
set: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([{
id: 'test-user-id',
email: 'updated@example.com',
name: 'Updated User',
}]),
}),
}),
}),
delete: jest.fn().mockReturnValue({
where: jest.fn().mockResolvedValue(undefined),
}),
},
}));
describe('/api/admin/users/[id]', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET', () => {
it('should return 401 if not authenticated', async () => {
const request = new NextRequest('http://localhost/api/admin/users/test-id');
const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 if no permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
const request = new NextRequest('http://localhost/api/admin/users/test-id');
const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return user if authenticated and has permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
const request = new NextRequest('http://localhost/api/admin/users/test-id');
const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(200);
expect(data.user).toBeDefined();
});
});
describe('PUT', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/admin/users/test-id', {
method: 'PUT',
body: JSON.stringify({ name: 'Updated User' }),
});
const response = await PUT(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
});
describe('DELETE', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/admin/users/test-id', {
method: 'DELETE',
});
const response = await DELETE(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
});
});
+135
View File
@@ -0,0 +1,135 @@
import { describe, it, expect, jest, beforeAll, beforeEach } from '@jest/globals';
import { NextRequest } from 'next/server';
import '@testing-library/jest-dom';
const mockAuth = jest.fn();
const mockHasPermission = jest.fn();
const mockDbSelect = jest.fn();
const mockDbInsert = jest.fn();
jest.mock('@/lib/auth', () => ({
auth: mockAuth,
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: mockHasPermission,
}));
jest.mock('@/db', () => ({
db: {
select: () => ({
from: () => ({
where: () => ({
limit: mockDbSelect,
}),
orderBy: mockDbSelect,
}),
}),
insert: () => ({
values: () => ({
returning: mockDbInsert,
}),
}),
},
}));
jest.mock('drizzle-orm', () => ({
eq: jest.fn(),
}));
jest.mock('nanoid', () => ({
nanoid: () => 'test-id-123',
}));
jest.mock('bcryptjs', () => ({
hash: jest.fn().mockResolvedValue('hashed-password'),
}));
jest.mock('@/db/schema', () => ({
users: {},
}));
import { GET, POST } from './route';
describe('/api/admin/users', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET', () => {
it('should return 401 when not authenticated', async () => {
mockAuth.mockResolvedValueOnce(null);
const request = new NextRequest('http://localhost/api/admin/users');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 when user lacks permission', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'viewer' },
});
mockHasPermission.mockReturnValueOnce(false);
const request = new NextRequest('http://localhost/api/admin/users');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('无权限');
});
it('should return users list when authorized', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'admin' },
});
mockHasPermission.mockReturnValueOnce(true);
mockDbSelect.mockResolvedValueOnce([
{ id: '1', email: 'admin@example.com', name: 'Admin', role: 'admin' },
]);
const request = new NextRequest('http://localhost/api/admin/users');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.users).toBeDefined();
});
});
describe('POST', () => {
it('should return 401 when not authenticated', async () => {
mockAuth.mockResolvedValueOnce(null);
const request = new NextRequest('http://localhost/api/admin/users', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com', name: 'Test', password: 'password', role: 'viewer' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 400 when missing required fields', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'admin' },
});
mockHasPermission.mockReturnValueOnce(true);
const request = new NextRequest('http://localhost/api/admin/users', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('缺少必填字段');
});
});
});