gongxiangp2/lib/__tests__/performance-survey-sync-route.test.ts
2026-09-04 11:50:50 +08:00

191 lines
6.5 KiB
TypeScript

/** @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()
})
})