文件
hyb-oa/server/tests/unit/routes/assets.search.test.js
T

284 行
7.5 KiB
JavaScript

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('prefers exact assetNo matching before serial or fuzzy lookup', 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).toHaveBeenCalledTimes(1);
expect(prisma.assetCard.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
asset_no: 'ZC-001',
}),
}),
);
await app.close();
});
it('falls back to serialNo exact lookup when assetNo misses', async () => {
const prisma = {
assetCard: {
findMany: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([buildAssetRecord({ id: 'asset-2', assetNo: 'ZC-003', serialNo: 'SN-003' })]),
},
};
const app = await buildApp({
employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] },
prisma,
});
const response = await app.inject({
method: 'GET',
url: '/api/assets?q=SN-003',
headers: { authorization: 'Bearer token' },
});
expect(response.statusCode).toBe(200);
expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(2);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
where: expect.objectContaining({
asset_no: 'SN-003',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: expect.objectContaining({
serial_no: 'SN-003',
}),
}),
);
await app.close();
});
it('falls back to fuzzy name search only after both exact lookups miss', async () => {
const prisma = {
assetCard: {
findMany: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([buildAssetRecord({ id: 'asset-3', assetNo: 'ZC-004', serialNo: 'SN-004', name: '研发测试笔记本' })]),
},
};
const app = await buildApp({
employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] },
prisma,
});
const response = await app.inject({
method: 'GET',
url: '/api/assets?q=测试笔记本',
headers: { authorization: 'Bearer token' },
});
expect(response.statusCode).toBe(200);
expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(3);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
where: expect.objectContaining({
asset_no: '测试笔记本',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: expect.objectContaining({
serial_no: '测试笔记本',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
where: expect.objectContaining({
name: { contains: '测试笔记本', mode: 'insensitive' },
}),
}),
);
await app.close();
});
it('forces own-scope lookup even when caller passes mine=0', async () => {
const prisma = {
assetCard: {
findMany: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([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).toHaveBeenCalledTimes(2);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
where: expect.objectContaining({
assignee_employee_id: 'employee-1',
asset_no: 'SN-002',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: expect.objectContaining({
assignee_employee_id: 'employee-1',
serial_no: 'SN-002',
}),
}),
);
await app.close();
});
});