112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import { afterEach, expect, test } from "bun:test";
|
|
import { closeDatabases, setDb } from "@wrnexus/db";
|
|
import { connectFromConfig } from "@wrnexus/db/connect";
|
|
import { createPluginRunner } from "@wrnexus/plugin";
|
|
import { parse } from "@wrnexus/syntax";
|
|
import { readFileSync, readdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { databasePaymentStore, paymentPlugin } from "../src/index.ts";
|
|
|
|
afterEach(() => closeDatabases());
|
|
test("payment plugin contributes its schema only when configured", async () => {
|
|
const metadata = new Map<string, unknown>();
|
|
const context = {
|
|
root: process.cwd(),
|
|
mode: "development" as const,
|
|
command: "dev" as const,
|
|
metadata,
|
|
warn() {},
|
|
};
|
|
const runner = createPluginRunner(paymentPlugin(), context);
|
|
await runner.configure({ payment: { default: "sandbox", sandbox: true } } as any);
|
|
const contributions = await runner.contributions();
|
|
expect(contributions.migrations.map((row) => row.id)).toEqual([
|
|
"wrnexus-payment-001",
|
|
"wrnexus-payment-002-event-order",
|
|
]);
|
|
expect(contributions.componentDirs).toHaveLength(1);
|
|
expect(contributions.routes.map((row) => row.path)).toEqual(["/api/payments/webhook/:gateway"]);
|
|
const off = createPluginRunner(paymentPlugin(), { ...context, metadata: new Map() });
|
|
await off.configure({} as any);
|
|
expect((await off.contributions()).migrations).toHaveLength(0);
|
|
expect((await off.contributions()).routes).toHaveLength(0);
|
|
});
|
|
|
|
test("database store persists append-only events and atomically caps refunds", async () => {
|
|
const db = connectFromConfig({ driver: "sqlite", url: ":memory:" });
|
|
setDb(db);
|
|
const metadata = new Map<string, unknown>();
|
|
const runner = createPluginRunner(paymentPlugin(), {
|
|
root: process.cwd(),
|
|
mode: "development",
|
|
command: "dev",
|
|
metadata,
|
|
warn() {},
|
|
});
|
|
await runner.configure({ payment: { default: "sandbox" } } as any);
|
|
for (const migration of (await runner.contributions()).migrations) {
|
|
const up = migration.source!.split("-- +down")[0].replace("-- +up", "");
|
|
for (const statement of up
|
|
.split(";")
|
|
.map((x) => x.trim())
|
|
.filter(Boolean))
|
|
await db.exec(statement);
|
|
}
|
|
const store = databasePaymentStore();
|
|
const intent = {
|
|
id: "pay-1",
|
|
subject: "u",
|
|
gateway: "sandbox",
|
|
gatewayRef: "sb-1",
|
|
amount: 100,
|
|
currency: "USD",
|
|
idempotencyKey: "once",
|
|
createdAt: 1,
|
|
client: {},
|
|
metadata: {},
|
|
};
|
|
await store.putIntent(intent);
|
|
expect((await store.findIntentByKey("once"))?.id).toBe("pay-1");
|
|
expect(
|
|
await store.appendEvent({
|
|
id: "e1",
|
|
intentId: "pay-1",
|
|
type: "captured",
|
|
status: "captured",
|
|
at: 1,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
await store.appendEvent({
|
|
id: "e1",
|
|
intentId: "pay-1",
|
|
type: "captured",
|
|
status: "captured",
|
|
at: 1,
|
|
}),
|
|
).toBe(false);
|
|
const base = {
|
|
intentId: "pay-1",
|
|
gatewayRef: "",
|
|
currency: "USD",
|
|
reason: "test",
|
|
status: "pending" as const,
|
|
createdAt: 1,
|
|
};
|
|
const results = await Promise.all([
|
|
store.reserveRefund({ id: "r1", amount: 60, idempotencyKey: "r1", ...base }, 100),
|
|
store.reserveRefund({ id: "r2", amount: 60, idempotencyKey: "r2", ...base }, 100),
|
|
]);
|
|
expect(results.filter(Boolean)).toHaveLength(1);
|
|
});
|
|
|
|
test("all payment components parse and never collect raw card data", () => {
|
|
for (const file of readdirSync(join(import.meta.dir, "../components")).filter((x) =>
|
|
x.endsWith(".wrn"),
|
|
)) {
|
|
const source = readFileSync(join(import.meta.dir, "../components", file), "utf8");
|
|
expect(() => parse(source)).not.toThrow();
|
|
expect(source).not.toMatch(/cardNumber|cvv|\bpan\b/i);
|
|
}
|
|
});
|