import { describe, test, expect, beforeEach } from 'bun:test' import { Database } from 'bun:sqlite' // Dynamic imports — modules may not exist yet (TDD) let issueSessionToken: | (( userId: string, db: Database, ) => { accessToken: string; refreshToken: string }) | undefined let resolveSessionToken: | ((token: string, db: Database) => string | null) | undefined let revokeSessionToken: ((token: string, db: Database) => void) | undefined let revokeAllUserTokens: ((userId: string, db: Database) => void) | undefined let migrateDatabase: ((db: Database) => void) | undefined try { const sessionMod = await import('../auth/session') issueSessionToken = sessionMod.issueSessionToken resolveSessionToken = sessionMod.resolveSessionToken revokeSessionToken = sessionMod.revokeSessionToken revokeAllUserTokens = sessionMod.revokeAllUserTokens } catch { // Module not implemented yet } try { const dbMod = await import('../db/sqlite') migrateDatabase = dbMod.migrateDatabase } catch { // Module not implemented yet } // Helper: create a fresh in-memory database with schema function freshDb(): Database { const db = new Database(':memory:') if (migrateDatabase) { migrateDatabase(db) } // Ensure users table has a test user db.exec( "INSERT OR IGNORE INTO users (id, username, password_hash, role, created_at) VALUES ('usr_test1', 'testuser', 'hash', 'admin', '2026-01-01T00:00:00Z')", ) return db } describe('issueSessionToken', () => { test('returns access token with rct_ prefix and refresh token', () => { expect(issueSessionToken).toBeDefined() const db = freshDb() const result = issueSessionToken!('usr_test1', db) expect(result.accessToken).toBeDefined() expect(result.refreshToken).toBeDefined() expect(result.accessToken.startsWith('rct_')).toBe(true) expect(typeof result.refreshToken).toBe('string') expect(result.refreshToken.length).toBeGreaterThan(0) }) test('access token expires in approximately 1 hour', () => { expect(issueSessionToken).toBeDefined() const db = freshDb() const result = issueSessionToken!('usr_test1', db) // Check the token record in the database const row = db .query( "SELECT expires_at, kind FROM session_tokens WHERE token_hash = (SELECT token_hash FROM session_tokens WHERE kind = 'access' AND user_id = 'usr_test1' ORDER BY created_at DESC LIMIT 1)", ) .get() as { expires_at: string; kind: string } | null expect(row).not.toBeNull() expect(row!.kind).toBe('access') const expiresAt = new Date(row!.expires_at).getTime() const now = Date.now() // Should expire in roughly 1 hour (3600s ± 60s tolerance) const diffSeconds = (expiresAt - now) / 1000 expect(diffSeconds).toBeGreaterThan(3500) expect(diffSeconds).toBeLessThan(3700) }) test('refresh token expires in approximately 30 days', () => { expect(issueSessionToken).toBeDefined() const db = freshDb() issueSessionToken!('usr_test1', db) const row = db .query( "SELECT expires_at, kind FROM session_tokens WHERE kind = 'refresh' AND user_id = 'usr_test1' ORDER BY created_at DESC LIMIT 1", ) .get() as { expires_at: string; kind: string } | null expect(row).not.toBeNull() expect(row!.kind).toBe('refresh') const expiresAt = new Date(row!.expires_at).getTime() const now = Date.now() // Should expire in roughly 30 days (2592000s ± 3600s tolerance) const diffSeconds = (expiresAt - now) / 1000 expect(diffSeconds).toBeGreaterThan(2588400) expect(diffSeconds).toBeLessThan(2595600) }) test('writes tokens to session_tokens table in SQLite', () => { expect(issueSessionToken).toBeDefined() const db = freshDb() issueSessionToken!('usr_test1', db) const rows = db .query("SELECT * FROM session_tokens WHERE user_id = 'usr_test1'") .all() as Array<{ token_hash: string; kind: string }> expect(rows.length).toBeGreaterThanOrEqual(2) const kinds = rows.map(r => r.kind).sort() expect(kinds).toContain('access') expect(kinds).toContain('refresh') }) }) describe('resolveSessionToken', () => { test('returns userId for valid access token', () => { expect(issueSessionToken).toBeDefined() expect(resolveSessionToken).toBeDefined() const db = freshDb() const { accessToken } = issueSessionToken!('usr_test1', db) const userId = resolveSessionToken!(accessToken, db) expect(userId).toBe('usr_test1') }) test('returns null for invalid token', () => { expect(resolveSessionToken).toBeDefined() const db = freshDb() const userId = resolveSessionToken!('rct_nonexistent_token', db) expect(userId).toBeNull() }) test('returns null for expired token', () => { expect(issueSessionToken).toBeDefined() expect(resolveSessionToken).toBeDefined() const db = freshDb() const { accessToken } = issueSessionToken!('usr_test1', db) // Manually set the token's expiry to the past db.exec( "UPDATE session_tokens SET expires_at = '2020-01-01T00:00:00Z' WHERE user_id = 'usr_test1' AND kind = 'access'", ) const userId = resolveSessionToken!(accessToken, db) expect(userId).toBeNull() }) test('returns null for revoked token', () => { expect(issueSessionToken).toBeDefined() expect(resolveSessionToken).toBeDefined() expect(revokeSessionToken).toBeDefined() const db = freshDb() const { accessToken } = issueSessionToken!('usr_test1', db) revokeSessionToken!(accessToken, db) const userId = resolveSessionToken!(accessToken, db) expect(userId).toBeNull() }) }) describe('revokeSessionToken', () => { test('after revocation resolveSessionToken returns null', () => { expect(issueSessionToken).toBeDefined() expect(resolveSessionToken).toBeDefined() expect(revokeSessionToken).toBeDefined() const db = freshDb() const { accessToken } = issueSessionToken!('usr_test1', db) // Token should resolve before revocation expect(resolveSessionToken!(accessToken, db)).toBe('usr_test1') revokeSessionToken!(accessToken, db) // Token should not resolve after revocation expect(resolveSessionToken!(accessToken, db)).toBeNull() }) test('revoking an already-revoked token does not throw', () => { expect(issueSessionToken).toBeDefined() expect(revokeSessionToken).toBeDefined() const db = freshDb() const { accessToken } = issueSessionToken!('usr_test1', db) revokeSessionToken!(accessToken, db) // Second revocation should not throw expect(() => revokeSessionToken!(accessToken, db)).not.toThrow() }) }) describe('revokeAllUserTokens', () => { test('revokes all tokens for a user', () => { expect(issueSessionToken).toBeDefined() expect(resolveSessionToken).toBeDefined() expect(revokeAllUserTokens).toBeDefined() const db = freshDb() // Issue multiple sessions const session1 = issueSessionToken!('usr_test1', db) const session2 = issueSessionToken!('usr_test1', db) // Both should resolve expect(resolveSessionToken!(session1.accessToken, db)).toBe('usr_test1') expect(resolveSessionToken!(session2.accessToken, db)).toBe('usr_test1') revokeAllUserTokens!('usr_test1', db) // Neither should resolve expect(resolveSessionToken!(session1.accessToken, db)).toBeNull() expect(resolveSessionToken!(session2.accessToken, db)).toBeNull() expect(resolveSessionToken!(session1.refreshToken, db)).toBeNull() expect(resolveSessionToken!(session2.refreshToken, db)).toBeNull() }) })