const Fastify = require('fastify'); const { AppError } = require('../../../src/utils/AppError'); const assetsRoutes = require('../../../src/routes/assets'); function buildAssetRecord({ id = 'asset-1', assetNo = 'ZC-001', serialNo = 'SN-001', assigneeEmployeeId = 'employee-1', name = '测试笔记本', } = {}) { const now = new Date('2026-03-11T00:00:00.000Z'); return { id, asset_no: assetNo, name, category_id: 'cat-1', legal_entity_id: 'legal-1', cost_center_id: 'cc-1', purchase_request_id: null, serial_no: serialNo, location: '深圳办公室', status: 'in_use', assignee_employee_id: assigneeEmployeeId, activated_at: now, retired_at: null, created_at: now, updated_at: now, category: null, legal_entity: { id: 'legal-1', code: 'HY', name: '海韵科技', }, cost_center: { id: 'cc-1', code: 'CC001', name: '研发中心', legal_entity_id: 'legal-1', }, purchase_request: null, assignee: { id: assigneeEmployeeId, employee_no: 'E001', name: '张三', department: null, }, }; } function buildPrisma({ items = [] } = {}) { return { assetCard: { findMany: vi.fn().mockResolvedValue(items), }, }; } async function buildApp({ employee, prisma }) { const app = Fastify(); app.decorate('prisma', prisma); app.decorate('config', {}); app.decorate('authenticate', async (request) => { if (!request.headers.authorization) { throw new AppError({ code: 'AUTH_001', message: '未登录', statusCode: 401, }); } request.employee = employee; }); app.decorate('hasPermission', (actor, permissionCode) => { return (actor?.permissionCodes ?? []).includes('*') || (actor?.permissionCodes ?? []).includes(permissionCode); }); app.decorate('requirePermission', (permissionCode) => async (request) => { await app.authenticate(request); if (!app.hasPermission(request.employee, permissionCode)) { throw new AppError({ code: 'PERM_001', message: '无权限', statusCode: 403, details: { permission: permissionCode }, }); } }); 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(assetsRoutes, { prefix: '/api' }); return app; } describe('assets search route', () => { it('adds q matcher for assetNo serialNo and name fallback', async () => { const prisma = buildPrisma({ items: [buildAssetRecord()], }); const app = await buildApp({ employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] }, prisma, }); const response = await app.inject({ method: 'GET', url: '/api/assets?q=ZC-001', headers: { authorization: 'Bearer token' }, }); expect(response.statusCode).toBe(200); expect(response.json().data.items[0]).toEqual( expect.objectContaining({ assetNo: 'ZC-001', serialNo: 'SN-001', }), ); expect(prisma.assetCard.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ OR: [ { asset_no: 'ZC-001' }, { serial_no: 'ZC-001' }, { name: { contains: 'ZC-001', mode: 'insensitive' } }, ], }), }), ); await app.close(); }); it('forces own-scope lookup even when caller passes mine=0', async () => { const prisma = buildPrisma({ items: [buildAssetRecord({ assigneeEmployeeId: 'employee-1', assetNo: 'ZC-002', serialNo: 'SN-002' })], }); const app = await buildApp({ employee: { id: 'employee-1', permissionCodes: ['asset.view_own'] }, prisma, }); const response = await app.inject({ method: 'GET', url: '/api/assets?q=SN-002&mine=0', headers: { authorization: 'Bearer token' }, }); expect(response.statusCode).toBe(200); expect(response.json().data.mine).toBe(true); expect(prisma.assetCard.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ assignee_employee_id: 'employee-1', OR: [ { asset_no: 'SN-002' }, { serial_no: 'SN-002' }, { name: { contains: 'SN-002', mode: 'insensitive' } }, ], }), }), ); await app.close(); }); });