const Fastify = require('fastify'); const { AppError } = require('../../../src/utils/AppError'); const assetsRoutes = require('../../../src/routes/assets'); function buildAssetRecord({ id = 'asset-1', assetNo = 'FA-2026-000001', serialNo = 'SN-001', assetType = 'fixed_asset', sensitivityLevel = 'internal', assigneeEmployeeId = 'employee-1', ownerEmployeeId = null, keeperEmployeeId = null, archiveLocation = null, externalRef = null, sourceHash = null, confidentialityNote = null, metadata = null, name = '测试笔记本', } = {}) { const now = new Date('2026-03-11T00:00:00.000Z'); return { id, asset_no: assetNo, name, asset_type: assetType, sensitivity_level: sensitivityLevel, 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, owner_employee_id: ownerEmployeeId, keeper_employee_id: keeperEmployeeId, archive_location: archiveLocation, external_ref: externalRef, source_hash: sourceHash, verification_no: null, confidentiality_note: confidentialityNote, metadata, last_verified_at: null, expires_at: null, 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, }, owner: null, keeper: 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('uses one scoped OR query for assetNo, serialNo, VER/hash/ref/location and name lookup', async () => { const prisma = buildPrisma({ items: [buildAssetRecord()], }); const app = await buildApp({ employee: { id: 'admin-1', permissionCodes: ['asset.manage'] }, prisma, }); const response = await app.inject({ method: 'GET', url: '/api/assets?q=FA-2026-000001', headers: { authorization: 'Bearer token' }, }); expect(response.statusCode).toBe(200); expect(response.json().data.items[0]).toEqual( expect.objectContaining({ assetNo: 'FA-2026-000001', serialNo: 'SN-001', }), ); expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(1); expect(prisma.assetCard.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ AND: expect.arrayContaining([ expect.objectContaining({}), expect.objectContaining({ OR: expect.arrayContaining([ { asset_no: 'FA-2026-000001' }, { serial_no: 'FA-2026-000001' }, { verification_no: 'FA-2026-000001' }, { source_hash: 'FA-2026-000001' }, { name: { contains: 'FA-2026-000001', 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: 'FA-2026-000002', 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).toHaveBeenCalledTimes(1); expect(prisma.assetCard.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ AND: expect.arrayContaining([ expect.objectContaining({ OR: [ { assignee_employee_id: 'employee-1' }, { owner_employee_id: 'employee-1' }, { keeper_employee_id: 'employee-1' }, ], }), expect.objectContaining({ OR: expect.arrayContaining([{ serial_no: 'SN-002' }]), }), ]), }), }), ); await app.close(); }); it('redacts sensitive electronic asset fields for asset.view_all callers that are not related employees', async () => { const prisma = buildPrisma({ items: [ buildAssetRecord({ id: 'asset-3', assetNo: 'EA-DOC-2026-000001', assetType: 'electronic_document', sensitivityLevel: 'confidential', archiveLocation: '离线档案柜-A', externalRef: 'DOC-001', sourceHash: 'sha256:abcdef', confidentialityNote: '仅负责人可查看', metadata: { source: 'redacted_index' }, }), ], }); const app = await buildApp({ employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] }, prisma, }); const response = await app.inject({ method: 'GET', url: '/api/assets?q=EA-DOC-2026-000001', headers: { authorization: 'Bearer token' }, }); expect(response.statusCode).toBe(200); expect(response.json().data.items[0]).toEqual( expect.objectContaining({ assetNo: 'EA-DOC-2026-000001', archiveLocation: null, externalRef: null, sourceHash: null, confidentialityNote: null, metadata: null, sensitiveFieldsRedacted: true, }), ); await app.close(); }); it('rejects redacted index import items that still contain full local paths', async () => { const prisma = buildPrisma(); const app = await buildApp({ employee: { id: 'admin-1', permissionCodes: ['asset.manage'] }, prisma, }); const response = await app.inject({ method: 'POST', url: '/api/assets/import/redacted-doc-index', headers: { authorization: 'Bearer token' }, payload: { dryRun: true, items: [ { name: '合同索引', archiveLocation: '/home/shunli/Projects/faculty-daily-assistant/公司/合同.pdf', }, ], }, }); expect(response.statusCode).toBe(400); expect(response.json().error.details).toEqual( expect.objectContaining({ 'items.0.archiveLocation': 'local_absolute_path', }), ); await app.close(); }); });