// Set before requiring the auth layer (token.js reads JWT_SECRET at load) and // db.js (the users model, pulled in via the utils/auth facade, builds the pool // at load). Pointing the DB at a closed port stops idle connections from keeping // this process alive — none of these tests touch the database. process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret' process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, after } = require('node:test') const assert = require('node:assert/strict') const sessionService = require('../src/auth/session.service') const authFacade = require('../src/utils/auth') const db = require('../src/utils/db') after(() => db.close()) const USER = { id: 7, username: 'alice', role: 'admin' } // Build a request double carrying a token, either as a cookie or a Bearer header. function reqWithCookie(token) { return { cookies: { [authFacade.COOKIE_NAME]: token }, headers: {} } } function reqWithBearer(token) { return { cookies: {}, headers: { authorization: `Bearer ${token}` } } } test('createSession → validateSession round-trips a Session object', () => { const { token, session } = sessionService.createSession(USER, 'local') assert.equal(typeof token, 'string') // The returned session object carries the canonical shape. assert.equal(session.userId, USER.id) assert.equal(session.username, USER.username) assert.equal(session.role, USER.role) assert.equal(session.authMethod, 'local') assert.ok(session.sessionId, 'sessionId (jti) is present') assert.equal(typeof session.createdAt, 'number') // Validating the same token off a request yields the same identity. const validated = sessionService.validateSession(reqWithCookie(token)) assert.ok(validated) assert.equal(validated.userId, USER.id) assert.equal(validated.username, USER.username) assert.equal(validated.role, USER.role) assert.equal(validated.authMethod, 'local') assert.equal(validated.sessionId, session.sessionId) }) test('validateSession accepts a Bearer token as well as a cookie', () => { const { token } = sessionService.createSession(USER, 'mobile') const validated = sessionService.validateSession(reqWithBearer(token)) assert.ok(validated) assert.equal(validated.userId, USER.id) assert.equal(validated.authMethod, 'mobile') }) test('authMethod defaults to local when an unknown method is passed', () => { const { session } = sessionService.createSession(USER, 'bogus') assert.equal(session.authMethod, 'local') }) test('a partial (TOTP challenge) token is NOT a valid session', () => { const challenge = sessionService.createPartialSession(USER) assert.equal(typeof challenge, 'string') // Stage-tagged tokens must never validate as a full session. assert.equal(sessionService.validateSession(reqWithCookie(challenge)), null) assert.equal(sessionService.decodeIdentity(challenge), null) }) test('upgradeSessionAfterTotp accepts a challenge and rejects a session token', () => { const challenge = sessionService.createPartialSession(USER) const decoded = sessionService.upgradeSessionAfterTotp(challenge) assert.ok(decoded) assert.equal(decoded.id, USER.id) assert.equal(decoded.stage, 'totp') // A normal session token is not a TOTP challenge — must be rejected here. const { token } = sessionService.createSession(USER, 'local') assert.equal(sessionService.upgradeSessionAfterTotp(token), null) }) test('validateSession / decodeIdentity return null for missing or garbage input', () => { assert.equal(sessionService.validateSession({ cookies: {}, headers: {} }), null) assert.equal(sessionService.decodeIdentity(null), null) assert.equal(sessionService.decodeIdentity('not-a-jwt'), null) }) test('revoke / invalidate stubs report success without throwing', () => { assert.equal(sessionService.revokeSession('sid-1'), true) assert.equal(sessionService.invalidateSession('sid-1'), true) assert.equal(sessionService.invalidateAllUserSessions(USER.id), true) }) test('sessionMeta derives ip / userAgent / deviceHash from the request', () => { const meta = sessionService.sessionMeta({ ip: '203.0.113.5', headers: { 'user-agent': 'jest' } }) assert.equal(meta.ip, '203.0.113.5') assert.equal(meta.userAgent, 'jest') assert.equal(typeof meta.deviceHash, 'string') assert.ok(meta.deviceHash.length > 0) }) test('backward-compat: utils/auth facade still exports the original API', () => { for (const name of [ 'isLoggedIn', 'requireRole', 'signToken', 'verifyToken', 'signTotpChallenge', 'verifyTotpChallenge', 'setAuthCookie', 'clearAuthCookie', 'getUserFromRequest', ]) { assert.equal(typeof authFacade[name], 'function', `${name} is exported as a function`) } assert.equal(typeof authFacade.COOKIE_NAME, 'string') // getUserFromRequest still returns the historical { id, username, role } shape. const { token } = sessionService.createSession(USER, 'local') const decoded = authFacade.getUserFromRequest(reqWithCookie(token)) assert.deepEqual(decoded, { id: USER.id, username: USER.username, role: USER.role }) assert.equal(authFacade.getUserFromRequest({ cookies: {}, headers: {} }), null) })