const Fastify = require('fastify'); const meRoutes = require('../../../src/routes/me'); const { AppError } = require('../../../src/utils/AppError'); const { encryptStringOrThrow } = require('../../../src/services/crypto'); const FIELD_ENCRYPTION_KEY = Buffer.alloc(32, 7).toString('base64'); function createPrismaState(overrides = {}) { const auditEntries = []; const employeeRecord = { id: 'employee-1', employee_no: 'E001', name: '测试员工', phone: '13800000000', email: 'employee@test.com', department_id: 'department-1', direct_manager_id: 'manager-1', status: 'active', emergency_contact_name: null, emergency_contact_phone: null, bank_account_encrypted: '', bank_name: '', bank_institution: null, avatar_path: null, id_number_encrypted: null, notification_push_enabled: true, roles: [{ role_code: 'employee' }], wecom_identity_binding: null, ...overrides, }; return { prisma: { employee: { findUnique: vi.fn().mockResolvedValue(employeeRecord), update: vi.fn().mockImplementation(async ({ data }) => { employeeRecord.notification_push_enabled = data.notification_push_enabled; return { id: employeeRecord.id, notification_push_enabled: employeeRecord.notification_push_enabled, }; }), }, auditLog: { create: vi.fn().mockImplementation(async ({ data }) => { auditEntries.push(data); return { id: `audit-${auditEntries.length}` }; }), }, }, employeeRecord, auditEntries, }; } async function buildApp(prisma) { const app = Fastify(); app.decorate('prisma', prisma); app.decorate('config', { fieldEncryptionKey: FIELD_ENCRYPTION_KEY }); app.decorate('authenticate', async (request) => { if (!request.headers.authorization) { throw new AppError({ code: 'AUTH_001', message: '未登录', statusCode: 401, }); } request.employee = { id: 'employee-1', permissionCodes: [], }; }); app.setErrorHandler((error, request, reply) => { if (error instanceof AppError) { return reply.status(error.statusCode).send({ requestId: request.id, error: { code: error.code, message: error.message, details: error.details ?? {}, }, }); } return reply.status(500).send({ requestId: request.id, error: { code: 'SYS_001', message: '服务器内部错误', details: {}, }, }); }); await app.register(meRoutes, { prefix: '/api' }); return app; } describe('me routes', () => { afterEach(() => { vi.clearAllMocks(); }); it('returns notificationPushEnabled in /me response', async () => { const { prisma } = createPrismaState({ notification_push_enabled: false }); const app = await buildApp(prisma); const response = await app.inject({ method: 'GET', url: '/api/me', headers: { authorization: 'Bearer test-token' }, }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual( expect.objectContaining({ id: 'employee-1', notificationPushEnabled: false, profileCompleteness: { hasEmergencyContact: false, hasBankAccount: false, hasAvatar: false, hasIdNumber: false, score: 0, }, }), ); await app.close(); }); it('calculates profileCompleteness score from completed profile fields', async () => { const { prisma } = createPrismaState({ emergency_contact_name: '联系人', emergency_contact_phone: '13900000000', bank_account_encrypted: encryptStringOrThrow( { fieldEncryptionKey: FIELD_ENCRYPTION_KEY }, '6222000012345678', ), bank_name: '中国建设银行', bank_institution: { id: 'bank-1', code: 'CCB', name: '中国建设银行' }, avatar_path: null, id_number_encrypted: null, }); const app = await buildApp(prisma); const response = await app.inject({ method: 'GET', url: '/api/me', headers: { authorization: 'Bearer test-token' }, }); expect(response.statusCode).toBe(200); expect(response.json().data.profileCompleteness).toEqual({ hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50, }); await app.close(); }); it('updates notification preferences for current employee', async () => { const { prisma, employeeRecord, auditEntries } = createPrismaState(); const app = await buildApp(prisma); const response = await app.inject({ method: 'PATCH', url: '/api/me/notification-preferences', headers: { authorization: 'Bearer test-token' }, payload: { notificationPushEnabled: false, }, }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual({ notificationPushEnabled: false }); expect(employeeRecord.notification_push_enabled).toBe(false); expect(auditEntries[0]).toEqual( expect.objectContaining({ action: 'EMPLOYEE_NOTIFICATION_PREFERENCE_UPDATED', }), ); await app.close(); }); it('explains invalid notification settings without changing preferences', async () => { const { prisma, auditEntries } = createPrismaState(); const app = await buildApp(prisma); try { const response = await app.inject({ method: 'PATCH', url: '/api/me/notification-preferences', headers: { authorization: 'Bearer test-token' }, payload: { notificationPushEnabled: 'false' } }); expect(response.statusCode).toBe(400); expect(response.json().error.details.reasonCode).toBe('profile.notification_preferences_invalid'); expect(prisma.employee.update).not.toHaveBeenCalled(); expect(auditEntries).toHaveLength(0); } finally { await app.close(); } }); });