Files
0pt/tests/http/tenant-guard.test.ts

49 lines
2.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { NextRequest } from "next/server";
import { assertTenantMatchesVerified, VERIFIED_TENANT_HEADER } from "@/src/http/tenant-guard";
function req(headers: Record<string, string> = {}): NextRequest {
return new NextRequest("http://localhost/api/0pt/sources", { headers });
}
describe("assertTenantMatchesVerified", () => {
it("returns 403 when the client-supplied tenant_id disagrees with the verified header", () => {
const res = assertTenantMatchesVerified(req({ [VERIFIED_TENANT_HEADER]: "t_a" }), "t_b");
expect(res).not.toBeNull();
expect(res!.status).toBe(403);
});
it("the 403 body names both the requested and verified tenant, without leaking which is 'right'", async () => {
const res = assertTenantMatchesVerified(req({ [VERIFIED_TENANT_HEADER]: "t_a" }), "t_b");
const body = await res!.json();
expect(body.error).toBe("OPT_ACCESS_DENIED");
expect(body.details).toEqual({ requested_tenant: "t_b", verified_tenant: "t_a" });
});
it("returns null (proceed) when the tenant matches", () => {
const res = assertTenantMatchesVerified(req({ [VERIFIED_TENANT_HEADER]: "t_a" }), "t_a");
expect(res).toBeNull();
});
it("returns null (proceed) when no verified-tenant header is present (local-dev escape hatch)", () => {
const res = assertTenantMatchesVerified(req({}), "t_b");
expect(res).toBeNull();
});
it("returns null when tenantId itself is missing/empty (handlers 400 on that separately)", () => {
const res = assertTenantMatchesVerified(req({ [VERIFIED_TENANT_HEADER]: "t_a" }), null);
expect(res).toBeNull();
});
it("a spoofed verified-tenant header cannot be used to APPROVE a mismatched request -- only middleware.ts ever sets this header for real", () => {
// This is a documentation-by-test of the trust boundary: the guard trusts
// whatever is in the header, which is exactly why every route sits behind
// middleware.ts (matcher /api/0pt/:path*) that overwrites it with the
// real verified value -- a caller hitting the route directly, outside
// Next's routing, cannot reach the handler without going through
// middleware first in a real deployment.
const res = assertTenantMatchesVerified(req({ [VERIFIED_TENANT_HEADER]: "t_attacker" }), "t_attacker");
expect(res).toBeNull(); // matches -- proves the guard is a pure equality check, no extra magic
});
});