Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
/**
|
|
* Unit tests for src/auth/context.ts `resolveAuthContext` -- the flag-gated
|
|
* auth seam. Verification is injected (opts.verify) so these tests are pure
|
|
* and network-free; they exercise the decision logic in both AUTH_ENFORCE
|
|
* modes.
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { resolveAuthContext, type OptRequestContext } from '@/src/auth/context';
|
|
import type { VerifiedPrincipal } from '@/src/auth/zitadel';
|
|
|
|
const principal: VerifiedPrincipal = {
|
|
sub: 'user_1',
|
|
tenantId: 'tenant_x',
|
|
claims: { sub: 'user_1', iss: 'https://id-customers.siax.io' },
|
|
};
|
|
|
|
const okVerify = vi.fn(async () => principal);
|
|
const failVerify = vi.fn(async () => {
|
|
throw new Error('token_verification_failed');
|
|
});
|
|
|
|
function h(init: Record<string, string>): Headers {
|
|
return new Headers(init);
|
|
}
|
|
|
|
describe('resolveAuthContext -- enforce mode', () => {
|
|
it('401s when no bearer token is present', async () => {
|
|
const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_x' }), { enforce: true, verify: okVerify });
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) {
|
|
expect(d.status).toBe(401);
|
|
expect(d.code).toBe('OPT_UNAUTHENTICATED');
|
|
}
|
|
});
|
|
|
|
it('401s when the token fails verification', async () => {
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), {
|
|
enforce: true,
|
|
verify: failVerify,
|
|
});
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) expect(d.status).toBe(401);
|
|
});
|
|
|
|
it('attaches the verified principal and derives tenant from the TOKEN', async () => {
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer good.tok.en' }), {
|
|
enforce: true,
|
|
verify: okVerify,
|
|
});
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) {
|
|
expect(d.ctx.principal).toEqual(principal);
|
|
expect(d.ctx.tenantId).toBe('tenant_x');
|
|
expect(d.ctx.actorId).toBe('user_1');
|
|
}
|
|
});
|
|
|
|
it('403s when X-Tenant-Id disagrees with the token tenant', async () => {
|
|
const d = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_evil' }),
|
|
{ enforce: true, verify: okVerify },
|
|
);
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) {
|
|
expect(d.status).toBe(403);
|
|
expect(d.code).toBe('OPT_ACCESS_DENIED');
|
|
expect(d.details).toMatchObject({ header_tenant: 'tenant_evil', token_tenant: 'tenant_x' });
|
|
}
|
|
});
|
|
|
|
it('allows a matching X-Tenant-Id', async () => {
|
|
const d = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_x' }),
|
|
{ enforce: true, verify: okVerify },
|
|
);
|
|
expect(d.ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resolveAuthContext -- non-enforce (default) mode', () => {
|
|
it('never rejects when no token is present', async () => {
|
|
const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_header' }), {
|
|
enforce: false,
|
|
verify: okVerify,
|
|
});
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) {
|
|
expect((d.ctx as OptRequestContext).principal).toBeUndefined();
|
|
expect(d.ctx.tenantId).toBe('tenant_header');
|
|
}
|
|
});
|
|
|
|
it('never rejects even when a present token fails verification (logs a warning)', async () => {
|
|
const logger = { warn: vi.fn() };
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), {
|
|
enforce: false,
|
|
verify: failVerify,
|
|
logger,
|
|
});
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) expect(d.ctx.principal).toBeUndefined();
|
|
expect(logger.warn).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('attaches the principal when a present token verifies, without overriding a supplied tenant', async () => {
|
|
const d = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_header' }),
|
|
{ enforce: false, verify: okVerify },
|
|
);
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) {
|
|
expect(d.ctx.principal).toEqual(principal);
|
|
// non-breaking: caller-supplied header tenant is preserved
|
|
expect(d.ctx.tenantId).toBe('tenant_header');
|
|
}
|
|
});
|
|
});
|