240 lines
7.9 KiB
TypeScript
240 lines
7.9 KiB
TypeScript
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
import {
|
|
getSlipConfiguration,
|
|
isSlipProvider,
|
|
type SlipProvider,
|
|
verifySlip,
|
|
} from "./slip-checker";
|
|
|
|
const env = {
|
|
SLIPOK_API_URL: "https://slipok.example/verify",
|
|
SLIPOK_API_KEY: "slipok-test-key",
|
|
SLIP2GO_VERIFY_URL: "https://slip2go.example/api/verify-slip/qr-image/info",
|
|
SLIP2GO_API_SECRET: "slip2go-test-key",
|
|
PROMPTPAY_TYPE: "mobile",
|
|
PROMPTPAY_IDENTIFIER: "+66-81-234-5678",
|
|
};
|
|
const buffer = Buffer.from("test slip image");
|
|
const transaction = { transRef: "BANK-REF-1", amount: 12.5 };
|
|
let fetchSpy: ReturnType<typeof spyOn<typeof globalThis, "fetch">> | undefined;
|
|
afterEach(() => fetchSpy?.mockRestore());
|
|
|
|
function respond(raw: unknown, status = 200) {
|
|
fetchSpy = spyOn(globalThis, "fetch").mockResolvedValue(
|
|
Response.json(raw, { status }),
|
|
);
|
|
}
|
|
|
|
describe("slip provider configuration", () => {
|
|
test("allows only the two supported providers", () => {
|
|
expect(isSlipProvider("slipok")).toBe(true);
|
|
expect(isSlipProvider("slip2go")).toBe(true);
|
|
expect(isSlipProvider("other")).toBe(false);
|
|
});
|
|
|
|
test.each([
|
|
["mobile", "+66-81-234-5678", "02001", "0812345678"],
|
|
["mobile", "0066812345678", "02001", "0812345678"],
|
|
["nationalId", "1-2345-67890-12-3", "02003", "1234567890123"],
|
|
["ewallet", "012 345 678 901 234", "02004", "012345678901234"],
|
|
])("normalizes %s %s", (type, value, accountType, accountNumber) => {
|
|
expect(
|
|
getSlipConfiguration("slip2go", {
|
|
...env,
|
|
PROMPTPAY_TYPE: type,
|
|
PROMPTPAY_IDENTIFIER: value,
|
|
}).receiver,
|
|
).toEqual({ accountType, accountNumber });
|
|
});
|
|
|
|
test.each([
|
|
{ SLIP2GO_API_SECRET: undefined },
|
|
{ SLIP2GO_VERIFY_URL: undefined },
|
|
{ SLIP2GO_VERIFY_URL: "invalid" },
|
|
{ SLIP2GO_VERIFY_URL: "file:///tmp/slip" },
|
|
{ PROMPTPAY_TYPE: "bankAccount" },
|
|
{ PROMPTPAY_IDENTIFIER: undefined },
|
|
{ PROMPTPAY_IDENTIFIER: "0812abc" },
|
|
])("rejects invalid Slip2Go configuration %p", (override) => {
|
|
expect(() =>
|
|
getSlipConfiguration("slip2go", { ...env, ...override }),
|
|
).toThrow();
|
|
});
|
|
|
|
test("SlipOK does not require PromptPay configuration", () => {
|
|
expect(
|
|
getSlipConfiguration("slipok", {
|
|
SLIPOK_API_URL: env.SLIPOK_API_URL,
|
|
SLIPOK_API_KEY: env.SLIPOK_API_KEY,
|
|
}).receiver,
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("slip verification requests", () => {
|
|
test("preserves SlipOK upload format and retains the full response", async () => {
|
|
const raw = {
|
|
success: true,
|
|
data: { ...transaction, sender: { name: "Sender" } },
|
|
};
|
|
respond(raw);
|
|
expect(await verifySlip("slipok", buffer, "image/png", 12.5, env)).toEqual({
|
|
success: true,
|
|
provider: "slipok",
|
|
data: transaction,
|
|
raw,
|
|
});
|
|
const [url, init] = fetchSpy!.mock.calls[0];
|
|
expect(url).toBe(env.SLIPOK_API_URL);
|
|
expect(init!.headers).toEqual({ "x-authorization": env.SLIPOK_API_KEY });
|
|
expect(init!.method).toBe("POST");
|
|
const form = init!.body as FormData;
|
|
expect(Array.from(form.keys())).toEqual(["files", "amount"]);
|
|
expect(form.get("amount")).toBe("12.5");
|
|
const file = form.get("files") as File;
|
|
expect(file.type).toBe("image/png");
|
|
expect(await file.text()).toBe("test slip image");
|
|
expect(init!.signal).toBeInstanceOf(AbortSignal);
|
|
});
|
|
|
|
test.each([
|
|
"200000",
|
|
"200200",
|
|
])("Slip2Go success %s checks receiver and amount", async (code) => {
|
|
const raw = { code, message: "Slip found.", data: transaction };
|
|
respond(raw);
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/jpeg", 12.5, env),
|
|
).toEqual({ success: true, provider: "slip2go", data: transaction, raw });
|
|
const [url, init] = fetchSpy!.mock.calls[0];
|
|
expect(url).toBe(env.SLIP2GO_VERIFY_URL);
|
|
expect(init!.headers).toEqual({ Authorization: env.SLIP2GO_API_SECRET });
|
|
const form = init!.body as FormData;
|
|
expect(Array.from(form.keys())).toEqual(["file", "payload"]);
|
|
expect((form.get("file") as File).type).toBe("image/jpeg");
|
|
expect(JSON.parse(form.get("payload") as string)).toEqual({
|
|
checkAmount: { type: "eq", amount: "12.5" },
|
|
checkReceiver: [{ accountType: "02001", accountNumber: "0812345678" }],
|
|
});
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("failed verification", () => {
|
|
test.each([
|
|
"200401",
|
|
"200402",
|
|
"200403",
|
|
"200404",
|
|
"200500",
|
|
"200501",
|
|
"200502",
|
|
"200202",
|
|
"200001",
|
|
"unexpected",
|
|
])("rejects Slip2Go code %s even with HTTP 200 and transaction data", async (code) => {
|
|
const raw = { code, message: "Rejected", data: transaction };
|
|
respond(raw);
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/png", 12.5, env),
|
|
).toMatchObject({ success: false, code, message: "Rejected", raw });
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("preserves SlipOK failure code and message", async () => {
|
|
respond({ success: false, code: 1010, message: "Invalid slip" });
|
|
expect(
|
|
await verifySlip("slipok", buffer, "image/png", 12.5, env),
|
|
).toMatchObject({ success: false, code: 1010, message: "Invalid slip" });
|
|
});
|
|
|
|
for (const provider of ["slipok", "slip2go"] satisfies SlipProvider[]) {
|
|
const success =
|
|
provider === "slipok" ? { success: true } : { code: "200200" };
|
|
test.each([
|
|
{},
|
|
{ transRef: "", amount: 12.5 },
|
|
{ transRef: "REF" },
|
|
{ transRef: "REF", amount: "12.5" },
|
|
{ transRef: "REF", amount: 0 },
|
|
])(`${provider} rejects incomplete transaction %p`, async (data) => {
|
|
respond({ ...success, data });
|
|
expect(
|
|
await verifySlip(provider, buffer, "image/png", 12.5, env),
|
|
).toMatchObject({ success: false, code: "INVALID_RESPONSE" });
|
|
});
|
|
|
|
test(`${provider} rejects amount mismatches`, async () => {
|
|
respond({ ...success, data: { ...transaction, amount: 10 } });
|
|
expect(
|
|
await verifySlip(provider, buffer, "image/png", 12.5, env),
|
|
).toMatchObject({ success: false, code: "AMOUNT_MISMATCH" });
|
|
});
|
|
|
|
test(`${provider} rejects HTTP errors despite a success body`, async () => {
|
|
respond({ ...success, data: transaction }, 500);
|
|
expect(
|
|
(await verifySlip(provider, buffer, "image/png", 12.5, env)).success,
|
|
).toBe(false);
|
|
});
|
|
}
|
|
|
|
test.each([
|
|
[null],
|
|
[[]],
|
|
["invalid"],
|
|
])("rejects malformed envelopes %p", async (raw) => {
|
|
respond(raw);
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/png", 12.5, env),
|
|
).toMatchObject({ success: false, code: "INVALID_RESPONSE" });
|
|
});
|
|
|
|
test("rejects non-JSON responses", async () => {
|
|
fetchSpy = spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response("<html>error</html>"),
|
|
);
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/png", 12.5, env),
|
|
).toMatchObject({ success: false, code: "INVALID_RESPONSE" });
|
|
});
|
|
|
|
test.each([
|
|
401, 429, 503,
|
|
])("preserves provider failure on HTTP %s", async (status) => {
|
|
respond({ code: "401005", message: "Insufficient Token" }, status);
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/png", 12.5, env),
|
|
).toMatchObject({
|
|
success: false,
|
|
code: "401005",
|
|
message: "Insufficient Token",
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
"TimeoutError",
|
|
"AbortError",
|
|
"TypeError",
|
|
])("handles %s without retry", async (name) => {
|
|
fetchSpy = spyOn(globalThis, "fetch").mockRejectedValue(
|
|
new DOMException("Failed", name),
|
|
);
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/png", 12.5, env),
|
|
).toMatchObject({
|
|
success: false,
|
|
code: name === "TypeError" ? "NETWORK_ERROR" : "TIMEOUT",
|
|
});
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("missing configuration never contacts a provider", async () => {
|
|
respond({});
|
|
expect(
|
|
await verifySlip("slip2go", buffer, "image/png", 12.5, {}),
|
|
).toMatchObject({ success: false, code: "CONFIGURATION_ERROR" });
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|