/** @jest-environment node */ import { getServerSession } from 'next-auth' import { GET, POST } from '@/app/api/performance-survey/[...path]/route' import { getPerformanceSurveyDetail, previewPerformanceSurveyEmployeeProfiles, submitPerformanceSurveyResponse, syncPerformanceSurveyEmployeeProfiles, } from '@/lib/performance-survey-service' import { verifyDingtalkPerformanceSessionToken } from '@/lib/dingtalk-performance-session' jest.mock('next-auth', () => ({ getServerSession: jest.fn(), })) jest.mock('@/lib/auth', () => ({ authOptions: {}, })) jest.mock('@/lib/dingtalk-performance-session', () => ({ DINGTALK_PERFORMANCE_SESSION_HEADER: 'x-performance-token', verifyDingtalkPerformanceSessionToken: jest.fn(), })) jest.mock('@/lib/performance-survey-service', () => ({ PerformanceSurveyApiError: class PerformanceSurveyApiError extends Error { status: number constructor(message: string, status = 400) { super(message) this.status = status } }, getPerformanceSurveyDetail: jest.fn(), previewPerformanceSurveyEmployeeProfiles: jest.fn(), submitPerformanceSurveyResponse: jest.fn(), syncPerformanceSurveyEmployeeProfiles: jest.fn(), })) describe('performance survey employee sync route', () => { beforeEach(() => { jest.clearAllMocks() ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'admin-1' }, expires: '2099-01-01T00:00:00.000Z', }) ;(verifyDingtalkPerformanceSessionToken as jest.Mock).mockReturnValue(null) }) test('钉钉评分凭证优先于浏览器普通会话并绑定指定评价对象', async () => { ;(verifyDingtalkPerformanceSessionToken as jest.Mock).mockReturnValue({ userId: 'external-1', email: 'external@example.com', name: '跨部门同事', avatar: null, role: 'USER', department: '运营中心', managedDepartment: null, isSuperAdmin: false, exp: Math.floor(Date.now() / 1000) + 3600, scope: { kind: 'assignment', surveyId: 'survey-1', targetId: 'target-1' }, }) ;(getPerformanceSurveyDetail as jest.Mock).mockResolvedValue({ id: 'survey-1' }) const response = await GET( new Request('https://example.test/api/performance-survey/surveys/survey-1?targetId=target-1', { headers: { 'x-performance-token': 'signed-token' }, }), { params: { path: ['surveys', 'survey-1'] } }, ) expect(response.status).toBe(200) expect(getServerSession).not.toHaveBeenCalled() expect(getPerformanceSurveyDetail).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ id: 'external-1' }) }), 'survey-1', 'target-1', ) }) test('钉钉评分凭证不能读取通知以外的评价对象', async () => { ;(verifyDingtalkPerformanceSessionToken as jest.Mock).mockReturnValue({ userId: 'external-1', email: 'external@example.com', name: '跨部门同事', role: 'USER', exp: Math.floor(Date.now() / 1000) + 3600, scope: { kind: 'assignment', surveyId: 'survey-1', targetId: 'target-1' }, }) const response = await GET( new Request('https://example.test/api/performance-survey/surveys/survey-1?targetId=target-other', { headers: { 'x-performance-token': 'signed-token' }, }), { params: { path: ['surveys', 'survey-1'] } }, ) expect(response.status).toBe(403) expect(getPerformanceSurveyDetail).not.toHaveBeenCalled() }) test('钉钉评分凭证提交时把通知评价对象传给服务层复核', async () => { ;(verifyDingtalkPerformanceSessionToken as jest.Mock).mockReturnValue({ userId: 'external-1', email: 'external@example.com', name: '跨部门同事', role: 'USER', exp: Math.floor(Date.now() / 1000) + 3600, scope: { kind: 'assignment', surveyId: 'survey-1', targetId: 'target-1' }, }) ;(submitPerformanceSurveyResponse as jest.Mock).mockResolvedValue({ id: 'response-1' }) const response = await POST( new Request('https://example.test/api/performance-survey/surveys/survey-1/responses', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-performance-token': 'signed-token', }, body: JSON.stringify({ targetId: 'target-1', scores: {} }), }), { params: { path: ['surveys', 'survey-1', 'responses'] } }, ) expect(response.status).toBe(200) expect(submitPerformanceSurveyResponse).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ id: 'external-1' }) }), 'survey-1', { targetId: 'target-1', scores: {} }, 'target-1', ) }) test('preview 子路径只执行只读预检', async () => { ;(previewPerformanceSurveyEmployeeProfiles as jest.Mock).mockResolvedValue({ mode: 'preview', dryRun: true, }) const response = await POST( new Request('https://example.test/api/performance-survey/admin/sync-employees/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ limit: 200 }), }), { params: { path: ['admin', 'sync-employees', 'preview'] } }, ) const payload = await response.json() expect(response.status).toBe(200) expect(payload).toEqual({ success: true, data: { mode: 'preview', dryRun: true }, }) expect(previewPerformanceSurveyEmployeeProfiles).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ id: 'admin-1' }) }), { limit: 200 }, ) expect(syncPerformanceSurveyEmployeeProfiles).not.toHaveBeenCalled() }) test('原同步路径继续执行正式同步,契约不变', async () => { ;(syncPerformanceSurveyEmployeeProfiles as jest.Mock).mockResolvedValue({ scannedAccounts: 10, upserted: 8, }) const response = await POST( new Request('https://example.test/api/performance-survey/admin/sync-employees', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ limit: 3000 }), }), { params: { path: ['admin', 'sync-employees'] } }, ) const payload = await response.json() expect(response.status).toBe(200) expect(payload).toEqual({ success: true, data: { scannedAccounts: 10, upserted: 8 }, }) expect(syncPerformanceSurveyEmployeeProfiles).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ id: 'admin-1' }) }), { limit: 3000 }, ) expect(previewPerformanceSurveyEmployeeProfiles).not.toHaveBeenCalled() }) })