Files
edr-platform/integration/src/authz.it.ts
2026-08-03 10:51:43 +00:00

93 lines
3.4 KiB
TypeScript

/**
* Who is allowed to touch a payment. Cheap to run (no booking chain), and the
* failures here are the expensive kind: a tenant reading another tenant's
* invoice, or an unauthenticated caller marking one paid.
*/
import { afterAll, describe, expect, it } from "vitest";
import request from "supertest";
import {
API,
PAYMENT_API,
api,
closeDb,
customerA,
customerB,
db,
login,
payment,
} from "./client";
describe("payment authorization boundaries", () => {
afterAll(closeDb);
it("hides one tenant's invoice from the other", async () => {
const rows = await db<{ id: string; company_id: string }>(
`SELECT i.id, i.company_id FROM freight.invoices i
JOIN freight.companies c ON c.id = i.company_id
WHERE c.tin = '0102030405' AND i.deleted_at IS NULL
ORDER BY i.created_at DESC LIMIT 1`,
);
if (!rows[0]) return; // nothing billed yet in this run — payment files cover it
const res = await api(customerB, "get", `/api/billing/my-invoices/${rows[0].id}`);
expect([403, 404]).toContain(res.status);
});
it("refuses to let one tenant pay the other's invoice", async () => {
const rows = await db<{ id: string }>(
`SELECT i.id FROM freight.invoices i
JOIN freight.companies c ON c.id = i.company_id
WHERE c.tin = '0102030405' AND i.status <> 'PAID' AND i.deleted_at IS NULL
ORDER BY i.created_at DESC LIMIT 1`,
);
if (!rows[0]) return;
const res = await api(customerB, "post", `/api/billing/my-invoices/${rows[0].id}/pay`, {
method: "CBE_BIRR",
platform: "web",
});
expect(res.status).toBeGreaterThanOrEqual(400);
});
it("keeps a portal customer out of backoffice payment operations", async () => {
const res = await api(customerA, "get", "/api/billing/invoices");
expect(res.status).toBeGreaterThanOrEqual(400);
});
it("rejects a portal account on the backoffice login audience", async () => {
const res = await login(customerA, "12345678", "backoffice");
expect(res.status).toBeGreaterThanOrEqual(400);
});
it("requires the service token on freight's mark-paid callback", async () => {
const body = {
version: 1,
eventId: "authz-probe",
eventType: "payment.succeeded",
occurredAt: new Date().toISOString(),
service: "FREIGHT",
intentId: "00000000-0000-0000-0000-000000000000",
referenceType: "SHIPMENT",
referenceId: "00000000-0000-0000-0000-000000000000",
provider: "CBE_BIRR",
amountMinor: 1,
currency: "ETB",
};
const res = await request(API).post("/api/internal/payments/mark-paid").send(body);
expect([401, 403]).toContain(res.status);
});
it("requires the service token on the payment API's internal surface", async () => {
const res = await request(PAYMENT_API).get("/payments/intents?service=FREIGHT");
expect([400, 401, 403]).toContain(res.status);
// …and accepts it when present (400 = bad query, not an auth failure).
const withToken = await payment("get", "/payments/intents?service=FREIGHT");
expect([401, 403]).not.toContain(withToken.status);
});
it("leaves the provider webhook surface public — trust is the signature", async () => {
// A garbage payload must be acked, not 401'd: providers do not authenticate.
const res = await request(PAYMENT_API).post("/webhooks/cbe-birr").send({ nonsense: true });
expect(res.status).toBe(200);
});
});