fix: 完善动态错误指引与发票日期校验(09-05-error-explainability-payment-readiness)
这个提交包含在:
@@ -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();
|
||||
|
||||
在新工单中引用
屏蔽一个用户