Files
chat/api/cache/banViolation.spec.js
zooqueen 3c15370f7d Verify the caller's Hanzo IAM token instead of issuing a session
Chat was minting its own credentials on top of an identity IAM had already
established: the browser ran the PKCE flow, posted the resulting IAM token to a
bridge, and got back a Chat JWT, a refresh cookie and a Session row. Everything
after login was authenticated by that second credential, and IAM's was filed
away server-side for calls to cloud.

The token IAM issues is now the only one. It rides every request as the bearer,
is verified against IAM's JWKS on the spot, and is not stored — so there is one
credential, and the principal that authenticates here is by construction the
principal cloud bills. `resolveTenantBearer` loses the machinery that existed to
keep a stored copy honest (principal binding, expiry checks, cookie fallback,
the renewal middleware in front of the completion route); a token that IS the
request cannot name a different user or outlive the request it arrived on.

The audience is now checked. One issuer serves every Hanzo relying party, so a
token addressed to another app would otherwise authenticate here on the strength
of a shared signature.

Removed, because IAM owns them: two-factor enrolment and verification, email
verification, password reset, the admin panel's OAuth code exchange, and the
server-initiated OpenID redirect. Sign-out now ends the session where it lives —
revoking both tokens at the issuer — rather than dropping the local copy and
leaving the browser recognised.

A guest holds nothing at all. Anonymous chat used to mint a signed guest token,
which proved nothing: minting was unauthenticated and the free-message quota
keyed on the client address regardless. A request without a bearer is now the
guest, identified by that same address, hashed so it stays out of job metadata
and logs.
2026-08-14 14:25:01 -07:00

116 lines
3.8 KiB
JavaScript

const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const banViolation = require('./banViolation');
describe('banViolation', () => {
let mongoServer;
let req, res, errorMessage;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(() => {
req = { ip: '127.0.0.1' };
res = {};
errorMessage = {
type: 'someViolation',
user_id: new mongoose.Types.ObjectId().toString(), // Use valid ObjectId
prev_count: 0,
violation_count: 0,
};
process.env.BAN_VIOLATIONS = 'true';
process.env.BAN_DURATION = '7200000'; // 2 hours in ms
process.env.BAN_INTERVAL = '20';
});
afterEach(() => {
jest.clearAllMocks();
});
it('should not ban if BAN_VIOLATIONS are not enabled', async () => {
process.env.BAN_VIOLATIONS = 'false';
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeFalsy();
});
it('should not ban if errorMessage is not provided', async () => {
await banViolation(req, res, null);
expect(errorMessage.ban).toBeFalsy();
});
it('[1/3] should ban if violation_count crosses the interval threshold: 19 -> 39', async () => {
errorMessage.prev_count = 19;
errorMessage.violation_count = 39;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeTruthy();
});
it('[2/3] should ban if violation_count crosses the interval threshold: 19 -> 20', async () => {
errorMessage.prev_count = 19;
errorMessage.violation_count = 20;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeTruthy();
});
const randomValueAbove = Math.floor(20 + Math.random() * 100);
it(`[3/3] should ban if violation_count crosses the interval threshold: 19 -> ${randomValueAbove}`, async () => {
errorMessage.prev_count = 19;
errorMessage.violation_count = randomValueAbove;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeTruthy();
});
it('should handle invalid BAN_INTERVAL and default to 20', async () => {
process.env.BAN_INTERVAL = 'invalid';
errorMessage.prev_count = 19;
errorMessage.violation_count = 39;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeTruthy();
});
it('should ban if BAN_DURATION is invalid as default is 2 hours', async () => {
process.env.BAN_DURATION = 'invalid';
errorMessage.prev_count = 19;
errorMessage.violation_count = 39;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeTruthy();
});
it('should not ban if violation_count does not change', async () => {
errorMessage.prev_count = 0;
errorMessage.violation_count = 0;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeFalsy();
});
it('[1/2] should not ban if violation_count does not cross the interval threshold: 0 -> 19', async () => {
errorMessage.prev_count = 0;
errorMessage.violation_count = 19;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeFalsy();
});
const randomValueUnder = Math.floor(1 + Math.random() * 19);
it(`[2/2] should not ban if violation_count does not cross the interval threshold: 0 -> ${randomValueUnder}`, async () => {
errorMessage.prev_count = 0;
errorMessage.violation_count = randomValueUnder;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeFalsy();
});
it('[EDGE CASE] should not ban if violation_count is lower', async () => {
errorMessage.prev_count = 0;
errorMessage.violation_count = -10;
await banViolation(req, res, errorMessage);
expect(errorMessage.ban).toBeFalsy();
});
});