113 lines
4.6 KiB
JavaScript
113 lines
4.6 KiB
JavaScript
/**
|
|
* Unit test for the non-mutating isSessionExpired() helper added to
|
|
* middleware/sessionTimeout.js. Used by GET /auth/session to mirror the
|
|
* timeout enforcement that sessionTimeoutMiddleware applies to /api/admin
|
|
* endpoints — closing the asymmetry that surfaced as the redirect-loop
|
|
* recurrence on v3.39.1-beta.0 (issue #350).
|
|
*
|
|
* The helper has two branches:
|
|
* 1. In-memory `lastActivity` exists for this token → expired iff
|
|
* now - lastActivity > timeout.
|
|
* 2. No in-memory entry (post-restart, or first request) → expired
|
|
* iff token's iat is older than the timeout (post-restart guard
|
|
* that the existing middleware already implements at line ~101).
|
|
*
|
|
* Both branches must NOT mutate the in-memory `sessions` Map — the
|
|
* middleware is the only place that tracks activity. We assert that.
|
|
*/
|
|
|
|
jest.mock('../../src/database/db', () => ({
|
|
db: () => ({
|
|
where: () => ({
|
|
first: () => ({
|
|
timeout: () => Promise.resolve(null),
|
|
}),
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
// Speed up the cached-timeout reads. The module reads
|
|
// `security_session_timeout_minutes` from app_settings and falls back to
|
|
// DEFAULT_SESSION_TIMEOUT (60 min) when the row is null.
|
|
const SIXTY_MINUTES_MS = 60 * 60 * 1000;
|
|
|
|
const sessionTimeout = require('../../src/middleware/sessionTimeout');
|
|
const { isSessionExpired } = sessionTimeout;
|
|
|
|
function makeDecodedToken({ id = 1, iatSecondsAgo = 0 } = {}) {
|
|
return { id, iat: Math.floor((Date.now() - iatSecondsAgo * 1000) / 1000) };
|
|
}
|
|
|
|
describe('isSessionExpired (sessionTimeout helper)', () => {
|
|
it('returns false for a freshly-issued token with no in-memory record', async () => {
|
|
const decoded = makeDecodedToken({ id: 1, iatSecondsAgo: 60 });
|
|
expect(await isSessionExpired('fresh-token-1', decoded)).toBe(false);
|
|
});
|
|
|
|
it('returns true when iat is older than the timeout (post-restart guard)', async () => {
|
|
const decoded = makeDecodedToken({
|
|
id: 2,
|
|
// 90 minutes > 60 minute default timeout
|
|
iatSecondsAgo: 90 * 60,
|
|
});
|
|
expect(await isSessionExpired('stale-token-2', decoded)).toBe(true);
|
|
});
|
|
|
|
it('returns false / true based on lastActivity when one exists', async () => {
|
|
// Drive the in-memory map by running the actual middleware once to
|
|
// record activity for the token, then check the helper.
|
|
const decoded = makeDecodedToken({ id: 3 });
|
|
|
|
// Drive the actual middleware once with a real signed token so it
|
|
// records this token in the in-memory `sessions` Map. Then check the
|
|
// helper sees that recent activity and reports "not expired".
|
|
const res = { status: jest.fn(() => res), json: jest.fn() };
|
|
const jwt = require('jsonwebtoken');
|
|
process.env.JWT_SECRET = 'session-timeout-helper-test-secret';
|
|
const realToken = jwt.sign(decoded, process.env.JWT_SECRET, {
|
|
issuer: 'picpeak-auth',
|
|
});
|
|
const realReq = {
|
|
headers: { authorization: `Bearer ${realToken}` },
|
|
cookies: {},
|
|
};
|
|
await sessionTimeout.sessionTimeoutMiddleware(realReq, res, () => {});
|
|
|
|
const decodedReal = jwt.decode(realToken);
|
|
// Just-recorded → not expired
|
|
expect(await isSessionExpired(realToken, decodedReal)).toBe(false);
|
|
});
|
|
|
|
it('returns false when token / decoded is missing (defensive)', async () => {
|
|
expect(await isSessionExpired(null, { id: 1 })).toBe(false);
|
|
expect(await isSessionExpired('tok', null)).toBe(false);
|
|
expect(await isSessionExpired('tok', {})).toBe(false);
|
|
});
|
|
|
|
// Sanity: the helper must not poke the `sessions` Map. Indirectly check
|
|
// by counting active sessions before/after a call with a never-seen
|
|
// token — should not change.
|
|
it('does not mutate the in-memory sessions map', async () => {
|
|
const before = sessionTimeout.getActiveSessions();
|
|
await isSessionExpired('never-seen-token-99', makeDecodedToken({ id: 99 }));
|
|
const after = sessionTimeout.getActiveSessions();
|
|
expect(after).toBe(before);
|
|
});
|
|
|
|
it('uses the default 60-minute timeout when no DB setting exists', async () => {
|
|
// 59 minutes → not expired
|
|
const fresh = makeDecodedToken({ id: 4, iatSecondsAgo: 59 * 60 });
|
|
expect(await isSessionExpired('fresh-4', fresh)).toBe(false);
|
|
|
|
// 61 minutes → expired (just past the default)
|
|
const stale = makeDecodedToken({ id: 5, iatSecondsAgo: 61 * 60 });
|
|
expect(await isSessionExpired('stale-5', stale)).toBe(true);
|
|
});
|
|
|
|
// Document the constant the test relies on so a future timeout change
|
|
// makes this assertion explicit rather than mysterious.
|
|
it('default timeout is 60 minutes (constant under test)', () => {
|
|
expect(SIXTY_MINUTES_MS).toBe(60 * 60 * 1000);
|
|
});
|
|
});
|