Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { FakePaymentProvider } from "../../tests/setup/fake-provider.js";
import { resetPaymentInstruments } from "../otel/instruments.js";
import { createPaymentService } from "../services/payment-service.js";
import { MemoryPaymentIntentStore } from "../store/memory-store.js";
import { buildPaymentRoutes } from "./build-routes.js";
function makeEntries() {
resetPaymentInstruments();
const provider = new FakePaymentProvider();
const store = new MemoryPaymentIntentStore();
const service = createPaymentService({ provider, store });
return { entries: buildPaymentRoutes({ service }), service, store };
}
describe("buildPaymentRoutes", () => {
it("returns exactly 7 route entries", { timeout: 5000 }, () => {
const { entries } = makeEntries();
expect(entries).toHaveLength(7);
});
it("every route is tagged 'payments'", { timeout: 5000 }, () => {
const { entries } = makeEntries();
for (const e of entries) {
expect(e.route.tags).toContain("payments");
}
});
it("every route paths to an /intents subtree", { timeout: 5000 }, () => {
const { entries } = makeEntries();
for (const e of entries) {
expect(e.route.path.startsWith("/intents")).toBe(true);
}
});
it("every route has a handler function", { timeout: 5000 }, () => {
const { entries } = makeEntries();
for (const e of entries) {
expect(typeof e.handler).toBe("function");
}
});
it("includes create, list, get, confirm, capture, cancel, refund", {
timeout: 5000,
}, () => {
const { entries } = makeEntries();
const paths = entries.map(
(e) => `${e.route.method.toUpperCase()} ${e.route.path}`
);
expect(paths).toContain("POST /intents");
expect(paths).toContain("GET /intents");
expect(paths).toContain("GET /intents/{id}");
expect(paths).toContain("POST /intents/{id}/confirm");
expect(paths).toContain("POST /intents/{id}/capture");
expect(paths).toContain("POST /intents/{id}/cancel");
expect(paths).toContain("POST /intents/{id}/refund");
});
it("create handler forwards description + metadata to the service", {
timeout: 5000,
}, async () => {
const { entries, store } = makeEntries();
const createEntry = entries.find(
(e) => e.route.method === "post" && e.route.path === "/intents"
);
if (!createEntry) throw new Error("missing create entry");
const c = {
get: () => ({
id: "u1",
emailVerified: true,
roles: [],
tenantId: "t1",
}),
req: {
param: () => ({}),
query: () => ({}),
json: async () => ({
amount: { amount: "100", currency: "USD" },
description: "Order #42",
metadata: { order: "42" },
}),
raw: new Request("http://x/intents", { method: "POST" }),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await createEntry.handler(c)) as Response;
expect(res.status).toBe(201);
const stored = await store.listByOwner("u1");
expect(stored[0]?.tenantId).toBe("t1");
});
it("capture handler forwards an explicit amount", {
timeout: 5000,
}, async () => {
const { entries, service } = makeEntries();
const created = await service.createIntent({
ownerId: "u1",
input: { amount: { amount: 500n, currency: "USD" } },
});
const capEntry = entries.find(
(e) =>
e.route.method === "post" &&
e.route.path === "/intents/{id}/capture"
);
if (!capEntry) throw new Error("missing capture entry");
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({ id: created.id }),
query: () => ({}),
json: async () => ({
amount: { amount: "300", currency: "USD" },
}),
raw: new Request("http://x", { method: "POST" }),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await capEntry.handler(c)) as Response;
expect(res.status).toBe(200);
});
it("refund handler forwards full body (amount, reason, metadata)", {
timeout: 5000,
}, async () => {
const { entries, service } = makeEntries();
const created = await service.createIntent({
ownerId: "u1",
input: { amount: { amount: 1000n, currency: "USD" } },
});
await service.captureIntent({ id: created.id, ownerId: "u1" });
const refundEntry = entries.find(
(e) =>
e.route.method === "post" &&
e.route.path === "/intents/{id}/refund"
);
if (!refundEntry) throw new Error("missing refund entry");
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({ id: created.id }),
query: () => ({}),
json: async () => ({
amount: { amount: "200", currency: "USD" },
reason: "fraudulent",
metadata: { ticket: "T-99" },
}),
raw: new Request("http://x", { method: "POST" }),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await refundEntry.handler(c)) as Response;
expect(res.status).toBe(200);
});
it("chain returns 500 when PaymentPluginError has no statusCode", {
timeout: 5000,
}, async () => {
const { entries, service } = makeEntries();
const created = await service.createIntent({
ownerId: "u1",
input: { amount: { amount: 1n, currency: "USD" } },
});
const getEntry = entries.find(
(e) => e.route.method === "get" && e.route.path === "/intents/{id}"
);
if (!getEntry) throw new Error("missing get entry");
const { PaymentPluginError, PAYMENT_PLUGIN_CODES } = await import(
"../error-codes/error-codes.js"
);
const stubService = service as unknown as {
getIntent: (a: unknown) => Promise<unknown>;
};
const original = stubService.getIntent.bind(stubService);
stubService.getIntent = async () => {
throw new PaymentPluginError(
PAYMENT_PLUGIN_CODES.PROVIDER_ERROR,
"boom"
);
};
try {
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({ id: created.id }),
query: () => ({}),
json: async () => ({}),
raw: new Request("http://x"),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await getEntry.handler(c)) as Response;
expect(res.status).toBe(500);
} finally {
stubService.getIntent = original;
}
});
it("handler returns 404 when ownerScope finds no principal", {
timeout: 5000,
}, async () => {
const { entries } = makeEntries();
// Pick a route that does NOT call requireAuth's throw path first
// (we want ownerScope to be the layer that fires) — supply a
// pre-set `user: { id: '...' }` to pass requireAuth, then null
// it for ownerScope by overriding `get`.
const getEntry = entries.find(
(e) => e.route.method === "get" && e.route.path === "/intents/{id}"
);
if (!getEntry) throw new Error("missing get entry");
let toggle = true;
const c = {
// First call: requireAuth sees a non-null principal.
// Second call (inside ownerScope): returns null to hit the
// defensive branch.
get: () => {
if (toggle) {
toggle = false;
return { id: "u1", emailVerified: true, roles: [] };
}
return null;
},
req: {
param: () => ({ id: "pi_x" }),
query: () => ({}),
json: async () => ({}),
raw: new Request("http://x/intents/pi_x"),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await getEntry.handler(c)) as Response;
expect(res.status).toBe(404);
});
it("handler propagates unknown errors from inner call", {
timeout: 5000,
}, async () => {
const { entries } = makeEntries();
const entry = entries.find(
(e) => e.route.method === "post" && e.route.path === "/intents"
);
if (!entry) throw new Error("missing post entry");
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({}),
query: () => ({}),
json: async () => {
throw new TypeError("kaboom");
},
raw: new Request("http://x/intents", { method: "POST" }),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
let caught: unknown;
try {
await entry.handler(c);
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(TypeError);
});
it("list handler filters by status when query supplies one", {
timeout: 5000,
}, async () => {
const { entries, service } = makeEntries();
await service.createIntent({
ownerId: "u1",
input: { amount: { amount: 1n, currency: "USD" } },
});
const listEntry = entries.find(
(e) => e.route.method === "get" && e.route.path === "/intents"
);
if (!listEntry) throw new Error("missing list entry");
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({}),
query: () => ({ status: "captured" }),
json: async () => ({}),
raw: new Request("http://x/intents?status=captured"),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await listEntry.handler(c)) as Response;
expect(res.status).toBe(200);
const body = (await res.json()) as { intents: unknown[] };
expect(body.intents).toEqual([]);
});
it("capture handler tolerates malformed JSON body", {
timeout: 5000,
}, async () => {
const { entries, service } = makeEntries();
const created = await service.createIntent({
ownerId: "u1",
input: { amount: { amount: 1n, currency: "USD" } },
});
const capEntry = entries.find(
(e) =>
e.route.method === "post" &&
e.route.path === "/intents/{id}/capture"
);
if (!capEntry) throw new Error("missing capture entry");
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({ id: created.id }),
query: () => ({}),
json: async () => {
throw new SyntaxError("bad json");
},
raw: new Request("http://x", { method: "POST" }),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await capEntry.handler(c)) as Response;
expect(res.status).toBe(200);
});
it("refund handler tolerates malformed JSON body", {
timeout: 5000,
}, async () => {
const { entries, service } = makeEntries();
const created = await service.createIntent({
ownerId: "u1",
input: { amount: { amount: 1n, currency: "USD" } },
});
await service.captureIntent({ id: created.id, ownerId: "u1" });
const refundEntry = entries.find(
(e) =>
e.route.method === "post" &&
e.route.path === "/intents/{id}/refund"
);
if (!refundEntry) throw new Error("missing refund entry");
const c = {
get: () => ({ id: "u1", emailVerified: true, roles: [] }),
req: {
param: () => ({ id: created.id }),
query: () => ({}),
json: async () => {
throw new SyntaxError("bad json");
},
raw: new Request("http://x", { method: "POST" }),
},
json: (body: unknown, status: number) =>
new Response(JSON.stringify(body), { status }),
} as never;
const res = (await refundEntry.handler(c)) as Response;
expect(res.status).toBe(200);
});
});