Files
chat/api/strategies/iam.spec.js
zooqueen 33c6d948d5 Pin the request path: bearer in, principal out
Six cases, and the refusals are the point — a forged token has to be a 401 and
not a 500, a request with no bearer must never reach the verifier, and a
provider conflict stays a 403 rather than being reported as a bad token. Also
pins that the token the caller presented is the one handed on for the calls made
on their behalf, which is what keeps the two principals the same by construction.
2026-08-14 14:37:43 -07:00

75 lines
2.8 KiB
JavaScript

jest.mock('@hanzochat/data-schemas', () => ({
logger: { error: jest.fn(), warn: jest.fn(), debug: jest.fn(), info: jest.fn() },
}));
const mockVerify = jest.fn();
const mockReconcile = jest.fn();
jest.mock('~/server/services/iamToken', () => ({ verifyIamToken: mockVerify }));
jest.mock('~/server/services/iamUser', () => ({ reconcileUser: mockReconcile }));
const iamStrategy = require('./iam');
/**
* The request path, end to end: bearer in, principal out. Passport supplies
* `success` and `fail` on the strategy it calls, so they are stubbed here the
* same way.
*/
const run = (headers) => {
const strategy = iamStrategy();
const result = new Promise((resolve) => {
strategy.success = (user) => resolve({ ok: true, user });
strategy.fail = (info, status) => resolve({ ok: false, info, status });
strategy.error = (err) => resolve({ ok: false, error: err });
});
strategy.authenticate({ headers });
return result;
};
describe('the iam strategy', () => {
beforeEach(() => {
jest.clearAllMocks();
mockVerify.mockResolvedValue({ sub: 'hanzo/alice', email: '[email protected]' });
mockReconcile.mockResolvedValue({ _id: { toString: () => 'abc123' }, email: '[email protected]' });
});
it('identifies the holder of a valid token', async () => {
const res = await run({ authorization: 'Bearer good-token' });
expect(res.ok).toBe(true);
expect(res.user.id).toBe('abc123');
expect(mockVerify).toHaveBeenCalledWith('good-token');
});
it('passes the caller their own token, so cloud runs as the same principal', async () => {
await run({ authorization: 'Bearer good-token' });
expect(mockReconcile).toHaveBeenCalledWith(expect.objectContaining({ sub: 'hanzo/alice' }), 'good-token');
});
it('refuses a request with no bearer', async () => {
const res = await run({});
expect(res).toMatchObject({ ok: false, status: 401 });
expect(mockVerify).not.toHaveBeenCalled();
});
it('refuses a token IAM rejects, rather than failing the request', async () => {
mockVerify.mockRejectedValue(new Error('invalid signature'));
const res = await run({ authorization: 'Bearer forged' });
expect(res).toMatchObject({ ok: false, status: 401 });
expect(res.error).toBeUndefined();
});
it('refuses a verified token carrying no subject', async () => {
mockVerify.mockResolvedValue({ email: '[email protected]' });
expect(await run({ authorization: 'Bearer nosub' })).toMatchObject({ ok: false, status: 401 });
});
it('keeps a provider conflict a 403, not a bad token', async () => {
const err = new Error('already registered another way');
err.status = 403;
mockReconcile.mockRejectedValue(err);
expect(await run({ authorization: 'Bearer good-token' })).toMatchObject({
ok: false,
status: 403,
});
});
});