fix: 完善动态错误指引与发票日期校验(09-05-error-explainability-payment-readiness)

这个提交包含在:
2026-09-16 12:56:27 +08:00
父节点 b7d2285d8d
当前提交 3516d7ec04
共修改 28 个文件,包含 791 行新增和 616 行删除
+41 -6
查看文件
@@ -214,6 +214,41 @@ async function buildApp(passwordHash) {
}
describe("auth login route", () => {
it.each([
["/login", {}, "login_input_invalid"],
["/email-login/code", { email: "invalid" }, "email_input_invalid"],
["/email-login/verify", {}, "email_code_input_invalid"],
["/change-password", {}, "password_change_input_invalid"],
["/forgot-password", { email: "invalid" }, "email_input_invalid"],
["/reset-password", {}, "password_reset_input_invalid"],
["/change-password", { oldPassword: "SyntheticOld1234", newPassword: "aaaaaaaaaaaa" }, "password_policy_failed"],
])("explains invalid input at %s without authentication side effects", async (url, payload, reason) => {
const state = await buildApp("unused");
try {
const response = await state.app.inject({ method: "POST", url: `/api/auth${url}`, payload,
headers: { "x-device-fingerprint": "synthetic-input-validation" } });
expect(response.statusCode).toBe(400);
expect(response.json().error.details.reasonCode).toBe(`auth.${reason}`);
expect(state.employeeUpdate).not.toHaveBeenCalled();
expect(state.authSessionCreate).not.toHaveBeenCalled();
expect(state.emailLoginCodeCreate).not.toHaveBeenCalled();
expect(state.passwordResetCodeCreate).not.toHaveBeenCalled();
expect(state.auditCreate).not.toHaveBeenCalled();
expect(JSON.stringify(response.json())).not.toContain("SyntheticOld1234");
} finally { await state.app.close(); }
});
it("explains missing device context before looking up an account", async () => {
const state = await buildApp("unused");
try {
const response = await state.app.inject({ method: "POST", url: "/api/auth/login",
payload: { identifier: "[email protected]", password: "SyntheticPassword1234" } });
expect(response.statusCode).toBe(400);
expect(response.json().error.details.reasonCode).toBe("auth.device_context_missing");
expect(state.employeeFindFirst).not.toHaveBeenCalled();
expect(state.authSessionCreate).not.toHaveBeenCalled();
} finally { await state.app.close(); }
});
it("keeps signed deeplink usable after legacy exp and redirects normal browsers to target", async () => {
const passwordHash = await hashPassword("AdminPassword1234");
const { app } = await buildApp(passwordHash);
@@ -268,7 +303,7 @@ describe("auth login route", () => {
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ targetPath: "invalid_target_path" });
expect(response.json().error.details).toEqual({ targetPath: "invalid_target_path", reasonCode: "auth.deeplink_target_invalid" });
await app.close();
});
@@ -293,7 +328,7 @@ describe("auth login route", () => {
});
expect(absoluteUrlResponse.statusCode).toBe(400);
expect(absoluteUrlResponse.json().error.details).toEqual({ targetPath: "invalid_target_path" });
expect(absoluteUrlResponse.json().error.details).toEqual({ targetPath: "invalid_target_path", reasonCode: "auth.deeplink_target_invalid" });
await app.close();
});
@@ -566,7 +601,7 @@ describe("auth login route", () => {
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ code: "invalid_or_expired" });
expect(response.json().error.details).toEqual({ code: "invalid_or_expired", reasonCode: "auth.code_invalid_or_expired" });
expect(emailLoginCodeUpdateMany).toHaveBeenCalledWith({
where: { id: "email-code-1", used_at: null },
data: { attempts_count: { increment: 1 } },
@@ -870,7 +905,7 @@ describe("auth login route", () => {
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ code: "invalid_or_expired" });
expect(response.json().error.details).toEqual({ code: "invalid_or_expired", reasonCode: "auth.code_invalid_or_expired" });
expect(employeeUpdate).not.toHaveBeenCalled();
expect(emailLoginCodeUpdateMany).not.toHaveBeenCalled();
expect(auditCreate).not.toHaveBeenCalled();
@@ -966,7 +1001,7 @@ describe("auth login route", () => {
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ code: "invalid_or_expired" });
expect(response.json().error.details).toEqual({ code: "invalid_or_expired", reasonCode: "auth.code_invalid_or_expired" });
expect(employeeUpdate).toHaveBeenCalledWith({
where: { id: EMPLOYEE_ID },
data: expect.objectContaining({
@@ -1008,7 +1043,7 @@ describe("auth login route", () => {
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ code: "invalid_or_expired" });
expect(response.json().error.details).toEqual({ code: "invalid_or_expired", reasonCode: "auth.code_invalid_or_expired" });
expect(employeeUpdate).not.toHaveBeenCalled();
expect(passwordResetCodeUpdateMany).not.toHaveBeenCalled();
expect(auditCreate).not.toHaveBeenCalled();