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

109 行
5.9 KiB
JavaScript

const Fastify = require("fastify");
const paymentAccountsRoutes = require("../../../src/routes/paymentAccounts");
const { encryptStringOrThrow } = require("../../../src/services/crypto");
const { AppError } = require("../../../src/utils/AppError");
const ACTOR_ID = "11111111-1111-4111-8111-111111111111";
const ACCOUNT_ID = "22222222-2222-4222-8222-222222222222";
const LEGAL_ENTITY_ID = "33333333-3333-4333-8333-333333333333";
const BANK_ID = "44444444-4444-4444-8444-444444444444";
const FIELD_ENCRYPTION_KEY = Buffer.alloc(32, 7).toString("base64");
const SYNTHETIC_ACCOUNT = "6217000000000012345";
function buildRecord(overrides = {}) {
return {
id: ACCOUNT_ID,
legal_entity_id: LEGAL_ENTITY_ID,
bank_institution_id: BANK_ID,
account_name: "合成测试公司",
account_number_encrypted: encryptStringOrThrow({ fieldEncryptionKey: FIELD_ENCRYPTION_KEY }, SYNTHETIC_ACCOUNT),
enabled: true,
archived_at: null,
created_at: new Date("2026-08-02T00:00:00.000Z"),
updated_at: new Date("2026-08-02T00:00:00.000Z"),
legal_entity: { id: LEGAL_ENTITY_ID, code: "TEST", name: "合成测试公司" },
bank_institution: { id: BANK_ID, code: "CCB", name: "中国建设银行" },
...overrides,
};
}
async function buildApp(permissionCodes) {
const app = Fastify();
const actor = { id: ACTOR_ID, roleCodes: [], permissionCodes };
const record = buildRecord();
const auditCreate = vi.fn(async () => ({ id: "audit" }));
const notificationCreateMany = vi.fn(async ({ data }) => ({ count: data.length }));
const paymentAccountCreate = vi.fn(async ({ data }) => buildRecord({ ...data }));
const tx = {
legalEntity: { findFirst: vi.fn(async () => ({ id: LEGAL_ENTITY_ID, name: "合成测试公司" })) },
bankInstitution: { findFirst: vi.fn(async () => ({ id: BANK_ID, code: "CCB", name: "中国建设银行" })) },
paymentAccount: {
create: paymentAccountCreate,
update: vi.fn(),
findFirst: vi.fn(async () => record),
},
auditLog: { create: auditCreate },
employeeRole: { findMany: vi.fn(async () => [{ employee_id: ACTOR_ID }]) },
notification: { createMany: notificationCreateMany },
};
app.decorate("config", { fieldEncryptionKey: FIELD_ENCRYPTION_KEY });
app.decorate("prisma", {
bankInstitution: { findMany: vi.fn(async () => [{ id: BANK_ID, code: "CCB", name: "中国建设银行" }]) },
paymentAccount: {
findMany: vi.fn(async () => [record]),
findUnique: vi.fn(async () => ({ id: ACCOUNT_ID, account_number_encrypted: record.account_number_encrypted })),
},
auditLog: { create: auditCreate },
$transaction: vi.fn(async (callback) => callback(tx)),
});
app.decorate("authenticate", async (request) => { request.employee = actor; });
app.decorate("hasPermission", (employee, permission) => employee.permissionCodes.includes("*") || employee.permissionCodes.includes(permission));
app.decorate("requirePermission", (permission) => async (request) => {
request.employee = actor;
if (!app.hasPermission(actor, permission)) throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
});
app.setErrorHandler((error, request, reply) => reply.status(error.statusCode ?? 500).send({ requestId: request.id, error: { code: error.code ?? "SYS_001", message: error.message, details: error.details ?? {} } }));
await app.register(paymentAccountsRoutes, { prefix: "/api" });
return { app, auditCreate, notificationCreateMany, paymentAccountCreate };
}
describe("payment account routes", () => {
it("returns only a masked account on normal reads", async () => {
const testApp = await buildApp(["payment.account.read"]);
const response = await testApp.app.inject({ method: "GET", url: "/api/payment-accounts" });
expect(response.statusCode).toBe(200);
expect(response.json().data.items[0].accountNumberMasked).toBe("6217***********2345");
expect(JSON.stringify(response.json())).not.toContain(SYNTHETIC_ACCOUNT);
await testApp.app.close();
});
it("requires a reason before revealing the full account and audits the successful view", async () => {
const testApp = await buildApp(["payment.account.read_full"]);
const missingReason = await testApp.app.inject({ method: "POST", url: `/api/payment-accounts/${ACCOUNT_ID}/reveal`, payload: {} });
expect(missingReason.statusCode).toBe(400);
const response = await testApp.app.inject({ method: "POST", url: `/api/payment-accounts/${ACCOUNT_ID}/reveal`, payload: { reason: "银行制单核对" } });
expect(response.statusCode).toBe(200);
expect(response.json().data.accountNumber).toBe(SYNTHETIC_ACCOUNT);
expect(testApp.auditCreate).toHaveBeenCalledWith({ data: expect.objectContaining({ action: "PAYMENT_ACCOUNT_SENSITIVE_VIEWED", extra: { reason: "银行制单核对" } }) });
await testApp.app.close();
});
it("creates an encrypted account with masked audit and notifies administrators", async () => {
const testApp = await buildApp(["payment.account.manage"]);
const response = await testApp.app.inject({ method: "POST", url: "/api/payment-accounts", payload: { legalEntityId: LEGAL_ENTITY_ID, bankInstitutionId: BANK_ID, accountName: "合成测试公司", accountNumber: SYNTHETIC_ACCOUNT } });
expect(response.statusCode).toBe(201);
expect(testApp.paymentAccountCreate.mock.calls[0][0].data.account_number_encrypted).toMatch(/^v1:/);
expect(JSON.stringify(testApp.auditCreate.mock.calls)).not.toContain(SYNTHETIC_ACCOUNT);
expect(testApp.notificationCreateMany).toHaveBeenCalledOnce();
await testApp.app.close();
});
it("does not grant payment-account access to a system administrator by role implication", async () => {
const testApp = await buildApp(["system.settings", "system.accounts.manage"]);
const response = await testApp.app.inject({ method: "GET", url: "/api/payment-accounts" });
expect(response.statusCode).toBe(403);
await testApp.app.close();
});
});