Files
0pt/tests/api/anonymous-routes-auth.test.ts
T
admin a1557b2a7d
CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 14s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / quality (push) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m31s
feat(payments): bind orders to verified tenant and buyer
2026-09-15 19:53:58 +02:00

101 lines
4.8 KiB
TypeScript

/**
* Regression tests for the anonymous-access finding on `/api/orders` (GET)
* and `/api/admin/metrics` (GET): a live compliance audit confirmed both
* responded HTTP 200 with zero authentication (the order queue + financial
* pipeline aggregates were readable by anyone). Both are outside
* middleware.ts's matcher (`/api/0pt/:path*` only), so each handler now
* calls `requireRouteAuth` (src/auth/require-route-auth.ts -> the same
* fail-closed `resolveAuthContext`/Zitadel verifier `/api/0pt/*` relies on)
* directly.
*
* No DATABASE_URL is set in this test env, so lib/orders-store.ts falls back
* to its in-memory store -- these tests only exercise the auth gate, not
* order persistence.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { NextRequest } from 'next/server';
import { GET as ordersGET } from '@/app/api/orders/route';
import { GET as orderGET } from '@/app/api/orders/[id]/route';
import { GET as adminMetricsGET } from '@/app/api/admin/metrics/route';
import { POST as depositPOST } from '@/app/api/payments/[order_id]/deposit/route';
function req(url: string, headers: Record<string, string> = {}): NextRequest {
return new NextRequest(new URL(url, 'http://localhost'), { headers });
}
describe('anonymous-access regression: /api/orders (GET) and /api/admin/metrics (GET)', () => {
const saved = { auth: process.env.AUTH_ENFORCE, node: process.env.NODE_ENV };
beforeEach(() => {
// Default posture: AUTH_ENFORCE unset => enforced (fail-closed), matching
// production and matching how resolveAuthContext's own tests run.
delete process.env.AUTH_ENFORCE;
});
afterEach(() => {
if (saved.auth === undefined) delete process.env.AUTH_ENFORCE;
else process.env.AUTH_ENFORCE = saved.auth;
if (saved.node === undefined) delete process.env.NODE_ENV;
else process.env.NODE_ENV = saved.node;
});
it('REGRESSION: GET /api/orders 401s with no Authorization header (was: 200 + full order list)', async () => {
const res = await ordersGET(req('/api/orders'));
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error).toBe('OPT_UNAUTHENTICATED');
});
it('GET /api/orders 401s on an invalid/unparseable bearer token', async () => {
const res = await ordersGET(req('/api/orders', { authorization: 'Bearer not.a.real.jwt' }));
expect(res.status).toBe(401);
});
it('GET /api/orders/{id} rejects anonymous and spoofed-tenant reads before loading an order', async () => {
const params = { params: Promise.resolve({ id: 'order-unknown' }) };
expect((await orderGET(req('/api/orders/order-unknown'), params)).status).toBe(401);
expect((await orderGET(req('/api/orders/order-unknown', {
'x-0pt-verified-tenant': 'tenant_attacker',
}), params)).status).toBe(401);
});
it('deposit checkout rejects anonymous callers before checking Stripe credentials or order id', async () => {
const request = req('/api/payments/order-unknown/deposit');
const result = await depositPOST(request, { params: Promise.resolve({ order_id: 'order-unknown' }) });
expect(result.status).toBe(401);
});
it('REGRESSION: GET /api/admin/metrics 401s with no Authorization header (was: 200 + financial aggregates)', async () => {
const res = await adminMetricsGET(req('/api/admin/metrics'));
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error).toBe('OPT_UNAUTHENTICATED');
});
it('a bare X-Tenant-Id header alone (no bearer token) does not grant access to either route', async () => {
// Guards against a shortcut implementation that trusts a client-supplied
// tenant/verified header instead of re-verifying the token.
const ordersRes = await ordersGET(req('/api/orders', { 'x-tenant-id': 'tenant_attacker' }));
expect(ordersRes.status).toBe(401);
const metricsRes = await adminMetricsGET(req('/api/admin/metrics', { 'x-tenant-id': 'tenant_attacker' }));
expect(metricsRes.status).toBe(401);
});
it('a spoofed x-0pt-verified-tenant header alone does not grant access (never trusted directly)', async () => {
// These routes sit outside middleware.ts's matcher, so nothing strips or
// overwrites this header before the handler sees it -- proving the
// handler re-verifies via the real JWT check rather than trusting it.
const res = await ordersGET(req('/api/orders', { 'x-0pt-verified-tenant': 'tenant_attacker' }));
expect(res.status).toBe(401);
});
it('AUTH_ENFORCE=false cannot mint/read payable orders without a verified buyer', async () => {
process.env.AUTH_ENFORCE = 'false';
const ordersRes = await ordersGET(req('/api/orders'));
expect(ordersRes.status).toBe(401);
const metricsRes = await adminMetricsGET(req('/api/admin/metrics'));
expect(metricsRes.status).toBe(200);
});
});