// Pure-logic tests for the mobile SSO bridge MODEL. The .db layer is stubbed // (plain object exports → mutable in-process), so these are DB-free and only // exercise the model's gating, hashing, and code-generation logic. 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, beforeEach, after } = require('node:test') const assert = require('node:assert/strict') const crypto = require('crypto') const bridge = require('../src/model/mobileAuthBridge/mobileAuthBridge.model') const bridgeDb = require('../src/model/mobileAuthBridge/mobileAuthBridge.db') const db = require('../src/utils/db') after(() => db.close()) const sha256 = (s) => crypto.createHash('sha256').update(String(s)).digest('hex') let calls beforeEach(() => { calls = { insertSession: [], insertCode: [], completeSession: [], markCodeUsed: [], findValidCode: [] } bridgeDb.insertSession = async (a) => { calls.insertSession.push(a); return 1 } bridgeDb.insertCode = async (a) => { calls.insertCode.push(a); return 1 } bridgeDb.completeSession = async (sid, uid) => { calls.completeSession.push([sid, uid]); return 1 } bridgeDb.consumeSession = async () => 1 bridgeDb.getSession = async () => null bridgeDb.findValidCode = async (h) => { calls.findValidCode.push(h); return { code_hash: h, user_id: 9, session_id: 's' } } bridgeDb.markCodeUsed = async () => 1 bridgeDb.pruneExpired = async () => 0 }) test('startSession generates a uuid session_id and persists the row', async () => { const { sessionId } = await bridge.startSession({ provider: 'google', codeChallenge: 'chal', redirectUri: 'runicgateway://auth/callback', state: 'st', }) assert.match(sessionId, /^[0-9a-f-]{36}$/) assert.equal(calls.insertSession.length, 1) assert.equal(calls.insertSession[0].sessionId, sessionId) assert.equal(calls.insertSession[0].provider, 'google') assert.ok(calls.insertSession[0].expiresAt instanceof Date) assert.ok(calls.insertSession[0].expiresAt.getTime() > Date.now()) }) test('issueAuthCode: completes the session, stores only the code HASH, returns the raw code once', async () => { const out = await bridge.issueAuthCode({ sessionId: 's-1', userId: 42 }) assert.ok(out && typeof out.code === 'string') // >=128-bit, url-safe, opaque. assert.ok(out.code.length >= 22, 'at least 128 bits of base64url entropy') assert.match(out.code, /^[A-Za-z0-9_-]+$/) // Session was marked completed for THIS user. assert.deepEqual(calls.completeSession[0], ['s-1', 42]) // Only the sha256 hash of the code is persisted; the raw code never is. assert.equal(calls.insertCode[0].codeHash, sha256(out.code)) assert.equal(calls.insertCode[0].userId, 42) assert.equal(calls.insertCode[0].sessionId, 's-1') }) test('issueAuthCode returns null (mints no code) when the session is not pending/eligible', async () => { bridgeDb.completeSession = async () => 0 // not pending / expired / already used const out = await bridge.issueAuthCode({ sessionId: 's-1', userId: 42 }) assert.equal(out, null) assert.equal(calls.insertCode.length, 0, 'no code row when the session could not be completed') }) test('findRedeemableCode looks up by the code HASH, not the raw code', async () => { const row = await bridge.findRedeemableCode('raw-code-value') assert.equal(calls.findValidCode[0], sha256('raw-code-value')) assert.equal(row.user_id, 9) }) test('consumeCode is single-use: true only when a row was actually flipped', async () => { bridgeDb.markCodeUsed = async () => 1 assert.equal(await bridge.consumeCode('c'), true) bridgeDb.markCodeUsed = async () => 0 // already used / raced assert.equal(await bridge.consumeCode('c'), false) }) test('two issued codes are distinct (fresh entropy each time)', async () => { const a = await bridge.issueAuthCode({ sessionId: 's', userId: 1 }) const b = await bridge.issueAuthCode({ sessionId: 's', userId: 1 }) assert.notEqual(a.code, b.code) })