fix(cms): resolve admin dogfood findings on auth, content editing, and UX
修复 CMS / Admin 后台 dogfood 专项测试发现的阻塞性与体验性问题: - 登录后无限重定向(Secure Cookie + Edge Runtime JWT 兼容) - 内容新增/编辑保存 400/500(默认不加密请求体、自动生成唯一 slug) - 编辑时提交 status 导致验证错误 - 原生 confirm 阻塞自动化测试,改为 AlertDialog - 表单字段可访问性(id/htmlFor/fieldset) - JSON 数组标签字段新增 TagInput 组件 - 操作反馈统一使用 Sonner Toast - 后台隐藏营销 Cookie 横幅 - 媒体库空状态优化 添加 dogfood-cms-regression 与 dogfood-cms-output 报告、截图及工作流路由测试。
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
// ─── Mock @/lib/db ────────────────────────────────────────────────────────
|
||||
const mockContentItemFindUnique = jest.fn<(args: unknown) => Promise<unknown | null>>();
|
||||
const mockContentItemFindUniqueOrThrow = jest.fn<(args: unknown) => Promise<unknown>>();
|
||||
|
||||
jest.mock('@/lib/db', () => ({
|
||||
prisma: {
|
||||
contentItem: {
|
||||
findUnique: mockContentItemFindUnique,
|
||||
findUniqueOrThrow: mockContentItemFindUniqueOrThrow,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// ─── Mock workflow service ────────────────────────────────────────────────
|
||||
const mockSubmitForReview = jest.fn<(itemId: string, user: unknown) => Promise<unknown>>();
|
||||
const mockApprove = jest.fn<(itemId: string, user: unknown) => Promise<unknown>>();
|
||||
const mockReject = jest.fn<(itemId: string, user: unknown, reason?: string) => Promise<unknown>>();
|
||||
const mockArchive = jest.fn<(itemId: string, user: unknown) => Promise<unknown>>();
|
||||
|
||||
jest.mock('@/lib/cms/workflow', () => ({
|
||||
submitForReview: mockSubmitForReview,
|
||||
approve: mockApprove,
|
||||
reject: mockReject,
|
||||
archive: mockArchive,
|
||||
}));
|
||||
|
||||
// ─── Mock permissions ─────────────────────────────────────────────────────
|
||||
const mockRequirePermission = jest.fn<
|
||||
(request: NextRequest, modelCode: string, action: string) => Promise<unknown>
|
||||
>();
|
||||
|
||||
jest.mock('@/lib/permissions', () => ({
|
||||
requirePermission: mockRequirePermission,
|
||||
}));
|
||||
|
||||
jest.unmock('./route');
|
||||
|
||||
import { POST } from './route';
|
||||
|
||||
function createMockRequest(body: Record<string, unknown>): NextRequest {
|
||||
return {
|
||||
json: async () => body,
|
||||
} as unknown as NextRequest;
|
||||
}
|
||||
|
||||
function mockAuthorized(action: 'update' | 'publish' = 'update') {
|
||||
mockRequirePermission.mockResolvedValue({
|
||||
user: { userId: 'user-1', username: 'editor', role: action === 'publish' ? 'reviewer' : 'editor' },
|
||||
});
|
||||
}
|
||||
|
||||
const mockItem = {
|
||||
id: 'item-1',
|
||||
modelId: 'model-1',
|
||||
modelCode: 'news',
|
||||
title: '测试新闻',
|
||||
slug: 'test-news',
|
||||
locale: 'zh-CN',
|
||||
status: 'draft',
|
||||
data: '{}',
|
||||
version: 1,
|
||||
sortOrder: 0,
|
||||
createdBy: 'editor',
|
||||
updatedBy: 'editor',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockUpdatedItem = { ...mockItem, status: 'review', version: 2 };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockContentItemFindUnique.mockResolvedValue(mockItem);
|
||||
mockContentItemFindUniqueOrThrow.mockResolvedValue(mockUpdatedItem);
|
||||
});
|
||||
|
||||
describe('POST /api/admin/items/[id]/workflow', () => {
|
||||
it('returns 400 when action is missing', async () => {
|
||||
mockAuthorized();
|
||||
const request = createMockRequest({});
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'item-1' }) });
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body.error).toBe('缺少 action 参数');
|
||||
});
|
||||
|
||||
it('returns 400 for invalid action', async () => {
|
||||
mockAuthorized();
|
||||
const request = createMockRequest({ action: 'publish' });
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'item-1' }) });
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body.error).toContain('非法的 action');
|
||||
});
|
||||
|
||||
it('returns 404 when item not found', async () => {
|
||||
mockAuthorized();
|
||||
mockContentItemFindUnique.mockResolvedValue(null);
|
||||
const request = createMockRequest({ action: 'submit' });
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'missing' }) });
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(body.error).toBe('内容不存在');
|
||||
});
|
||||
|
||||
it('submit requires update permission', async () => {
|
||||
mockAuthorized('update');
|
||||
const request = createMockRequest({ action: 'submit' });
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'item-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockSubmitForReview).toHaveBeenCalledWith(
|
||||
'item-1',
|
||||
expect.objectContaining({ username: 'editor' })
|
||||
);
|
||||
expect(mockRequirePermission).toHaveBeenCalledWith(
|
||||
request,
|
||||
'news',
|
||||
'update'
|
||||
);
|
||||
});
|
||||
|
||||
it('approve requires publish permission', async () => {
|
||||
mockAuthorized('publish');
|
||||
const request = createMockRequest({ action: 'approve' });
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'item-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockApprove).toHaveBeenCalledWith(
|
||||
'item-1',
|
||||
expect.objectContaining({ username: 'editor' })
|
||||
);
|
||||
expect(mockRequirePermission).toHaveBeenCalledWith(
|
||||
request,
|
||||
'news',
|
||||
'publish'
|
||||
);
|
||||
});
|
||||
|
||||
it('reject passes reason to workflow service', async () => {
|
||||
mockAuthorized('publish');
|
||||
const request = createMockRequest({ action: 'reject', reason: '需要修改' });
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'item-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockReject).toHaveBeenCalledWith(
|
||||
'item-1',
|
||||
expect.objectContaining({ username: 'editor' }),
|
||||
'需要修改'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns validation error on illegal transition', async () => {
|
||||
mockAuthorized('publish');
|
||||
mockApprove.mockRejectedValue(new Error('当前状态 draft 不允许审核通过'));
|
||||
const request = createMockRequest({ action: 'approve' });
|
||||
const response = await POST(request, { params: Promise.resolve({ id: 'item-1' }) });
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body.error).toBe('当前状态 draft 不允许审核通过');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user