chore: sync marketing pages, CMS extensions, tests and project docs
同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
// ─── Mock @/lib/db ────────────────────────────────────────────────────────
|
||||
const mockContentModelFindMany = jest.fn<(args: unknown) => Promise<unknown[]>>();
|
||||
|
||||
jest.mock('@/lib/db', () => ({
|
||||
prisma: {
|
||||
contentModel: {
|
||||
findMany: mockContentModelFindMany,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// ─── Mock permissions ─────────────────────────────────────────────────────
|
||||
const mockRequirePermission = jest.fn<
|
||||
(request: NextRequest, modelCode: string, action: string) => Promise<unknown>
|
||||
>();
|
||||
|
||||
jest.mock('@/lib/permissions', () => ({
|
||||
requirePermission: mockRequirePermission,
|
||||
}));
|
||||
|
||||
jest.unmock('./route');
|
||||
|
||||
import { GET } from './route';
|
||||
|
||||
function createMockRequest(): NextRequest {
|
||||
return {
|
||||
url: 'http://localhost/api/admin/models',
|
||||
} as unknown as NextRequest;
|
||||
}
|
||||
|
||||
function mockAuthorized() {
|
||||
mockRequirePermission.mockResolvedValue({
|
||||
user: { userId: 'user-1', username: 'admin', role: 'content_admin' },
|
||||
});
|
||||
}
|
||||
|
||||
function mockUnauthorized() {
|
||||
mockRequirePermission.mockResolvedValue({
|
||||
response: new Response(JSON.stringify({ error: '未授权' }), { status: 401 }),
|
||||
});
|
||||
}
|
||||
|
||||
function mockForbidden() {
|
||||
mockRequirePermission.mockResolvedValue({
|
||||
response: new Response(JSON.stringify({ error: '无权限' }), { status: 403 }),
|
||||
});
|
||||
}
|
||||
|
||||
const mockModels = [
|
||||
{
|
||||
id: 'model-1',
|
||||
code: 'news',
|
||||
name: '新闻资讯',
|
||||
description: '新闻内容模型',
|
||||
fields: JSON.stringify([
|
||||
{ code: 'title', name: '标题', type: 'text', required: true },
|
||||
{ code: 'body', name: '正文', type: 'richtext', required: false },
|
||||
]),
|
||||
sortOrder: 1,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'model-2',
|
||||
code: 'product',
|
||||
name: '产品管理',
|
||||
description: '产品内容模型',
|
||||
fields: JSON.stringify([{ code: 'name', name: '产品名称', type: 'text', required: true }]),
|
||||
sortOrder: 2,
|
||||
createdAt: new Date('2026-01-02'),
|
||||
updatedAt: new Date('2026-01-02'),
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockContentModelFindMany.mockResolvedValue(mockModels);
|
||||
});
|
||||
|
||||
describe('GET /api/admin/models', () => {
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
mockUnauthorized();
|
||||
const response = await GET(createMockRequest());
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('returns 403 when authenticated but unauthorized', async () => {
|
||||
mockForbidden();
|
||||
const response = await GET(createMockRequest());
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('returns parsed model list ordered by sortOrder', async () => {
|
||||
mockAuthorized();
|
||||
const response = await GET(createMockRequest());
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body).toHaveLength(2);
|
||||
expect(body[0]).toMatchObject({
|
||||
code: 'news',
|
||||
name: '新闻资讯',
|
||||
fields: [
|
||||
{ code: 'title', name: '标题', type: 'text', required: true },
|
||||
{ code: 'body', name: '正文', type: 'richtext', required: false },
|
||||
],
|
||||
});
|
||||
expect(body[1]).toMatchObject({
|
||||
code: 'product',
|
||||
name: '产品管理',
|
||||
});
|
||||
expect(mockContentModelFindMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 500 on database error', async () => {
|
||||
mockAuthorized();
|
||||
mockContentModelFindMany.mockRejectedValue(new Error('db error'));
|
||||
|
||||
const response = await GET(createMockRequest());
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { authenticateRequest } from '@/lib/auth';
|
||||
import { success, unauthorized, internalError } from '@/lib/api-response';
|
||||
import { requirePermission } from '@/lib/permissions';
|
||||
import { success, internalError } from '@/lib/api-response';
|
||||
|
||||
// GET /api/admin/models - 获取所有内容模型
|
||||
export async function GET(request: NextRequest) {
|
||||
const payload = authenticateRequest(request);
|
||||
if (!payload) return unauthorized();
|
||||
const permission = await requirePermission(request, 'content-model', 'read');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
try {
|
||||
const models = await prisma.contentModel.findMany({
|
||||
|
||||
Reference in New Issue
Block a user