import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test' import { randomUUID, createHash } from 'node:crypto' import { tmpdir } from 'node:os' import { resolve } from 'node:path' import { Database } from 'bun:sqlite' import { mockConfigModule } from './helpers/mock-config' // Mock config before module imports mock.module('../config', () => mockConfigModule()) import { migrateDatabase, initDatabase, resetDbSingleton, getDb, } from '../db/sqlite' import { storeReset, storeCreateSession, storeCreateEnvironment, storeUpdateSession, storeDeleteSession, storeUpdateEnvironment, storeGetSession, storeGetEnvironment, storeBindSession, storeIsSessionOwner, } from '../store' import { isSessionVisibleToUser, resolveOwnedWebSessionId, } from '../services/session' // --------------------------------------------------------------------------- // Database helpers // --------------------------------------------------------------------------- function freshDb(): Database { try { resetDbSingleton() } catch { // ignore } const path = resolve( tmpdir(), `rcs-r2-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`, ) return initDatabase(path) } // Several tests below call db.close() to force a write failure. Without // afterAll cleanup, the closed singleton leaks to subsequent test files // (bun test without --isolate shares module state), causing // "Cannot use a closed database" in downstream files. Reset on exit. afterAll(() => { try { resetDbSingleton() } catch { // ignore } }) function seedUser( db: Database, opts: { id?: string; username?: string; role?: string }, ): string { const id = opts.id || `usr_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ($id, $username, 'hash', $role, $now)`, ).run({ $id: id, $username: opts.username || `user_${id.slice(0, 8)}`, $role: opts.role || 'member', $now: now, }) return id } // =========================================================================== // Task 1: Store dual-write consistency // =========================================================================== describe('Task 1: store dual-write consistency', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) describe('storeUpdateSession throws on DB failure', () => { test('throws when DB write fails', () => { const session = storeCreateSession({}) // Close DB to force a write failure db.close() expect(() => { storeUpdateSession(session.id, { title: 'Updated' }) }).toThrow() }) test('succeeds when DB is available', () => { const session = storeCreateSession({}) const result = storeUpdateSession(session.id, { title: 'Updated' }) expect(result).toBe(true) expect(storeGetSession(session.id)?.title).toBe('Updated') }) }) describe('storeDeleteSession throws on DB failure', () => { test('throws when DB write fails', () => { const session = storeCreateSession({}) // Close DB to force a write failure db.close() expect(() => { storeDeleteSession(session.id) }).toThrow() }) test('succeeds when DB is available', () => { const session = storeCreateSession({}) const result = storeDeleteSession(session.id) expect(result).toBe(true) }) }) describe('storeUpdateEnvironment writes to DB', () => { test('persists status change to DB', () => { const env = storeCreateEnvironment({ secret: 'test-secret' }) storeUpdateEnvironment(env.id, { status: 'offline' }) // Verify DB was updated const row = db .query('SELECT status FROM environments WHERE id = $id') .get({ $id: env.id }) as { status: string } | null expect(row).not.toBeNull() expect(row!.status).toBe('offline') }) test('persists machineName to DB', () => { const env = storeCreateEnvironment({ secret: 'test-secret' }) storeUpdateEnvironment(env.id, { machineName: 'my-machine' }) const row = db .query('SELECT machine_name FROM environments WHERE id = $id') .get({ $id: env.id }) as { machine_name: string } | null expect(row).not.toBeNull() expect(row!.machine_name).toBe('my-machine') }) test('persists lastPollAt to DB', () => { const env = storeCreateEnvironment({ secret: 'test-secret' }) const newDate = new Date('2026-06-15T12:00:00Z') storeUpdateEnvironment(env.id, { lastPollAt: newDate }) const row = db .query('SELECT last_poll_at FROM environments WHERE id = $id') .get({ $id: env.id }) as { last_poll_at: string } | null expect(row).not.toBeNull() expect(row!.last_poll_at).toBe(newDate.toISOString()) }) test('persists multiple fields at once', () => { const env = storeCreateEnvironment({ secret: 'test-secret' }) storeUpdateEnvironment(env.id, { status: 'offline', machineName: 'mac1', }) const row = db .query('SELECT status, machine_name FROM environments WHERE id = $id') .get({ $id: env.id }) as { status: string machine_name: string } | null expect(row).not.toBeNull() expect(row!.status).toBe('offline') expect(row!.machine_name).toBe('mac1') }) test('returns false for non-existent environment', () => { expect(storeUpdateEnvironment('env_no', { status: 'active' })).toBe(false) }) test('throws on DB failure', () => { const env = storeCreateEnvironment({ secret: 'test-secret' }) db.close() expect(() => { storeUpdateEnvironment(env.id, { status: 'offline' }) }).toThrow() }) }) }) // =========================================================================== // Task 2: isSessionVisibleToUser + resolveOwnedWebSessionId with shares/teams // =========================================================================== describe('Task 2: session visibility with shares and teams', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) describe('isSessionVisibleToUser', () => { test('returns owner for direct session owner', () => { const userId = seedUser(db, { role: 'member' }) const session = storeCreateSession({}) storeBindSession(session.id, userId) const result = isSessionVisibleToUser(session.id, userId, db) expect(result).toBe('owner') }) test('returns write for user with write share', () => { const ownerId = seedUser(db, { role: 'member' }) const shareUserId = seedUser(db, { username: 'shareuser', role: 'member', }) const session = storeCreateSession({}) storeBindSession(session.id, ownerId) // Create a write share const shareId = `share_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at) VALUES ($id, $sessionId, $userId, 'write', $grantedBy, $now)`, ).run({ $id: shareId, $sessionId: session.id, $userId: shareUserId, $grantedBy: ownerId, $now: now, }) const result = isSessionVisibleToUser(session.id, shareUserId, db) expect(result).toBe('write') }) test('returns read for user with read share', () => { const ownerId = seedUser(db, { role: 'member' }) const shareUserId = seedUser(db, { username: 'reader', role: 'member', }) const session = storeCreateSession({}) storeBindSession(session.id, ownerId) const shareId = `share_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at) VALUES ($id, $sessionId, $userId, 'read', $grantedBy, $now)`, ).run({ $id: shareId, $sessionId: session.id, $userId: shareUserId, $grantedBy: ownerId, $now: now, }) const result = isSessionVisibleToUser(session.id, shareUserId, db) expect(result).toBe('read') }) test('returns null for expired share', () => { const ownerId = seedUser(db, { role: 'member' }) const shareUserId = seedUser(db, { username: 'expired', role: 'member', }) const session = storeCreateSession({}) storeBindSession(session.id, ownerId) const shareId = `share_${randomUUID().replace(/-/g, '')}` const pastDate = new Date('2020-01-01T00:00:00Z').toISOString() const now = new Date().toISOString() db.query( `INSERT INTO session_shares (id, session_id, granted_to_user, permission, expires_at, granted_by, granted_at) VALUES ($id, $sessionId, $userId, 'write', $exp, $grantedBy, $now)`, ).run({ $id: shareId, $sessionId: session.id, $userId: shareUserId, $exp: pastDate, $grantedBy: ownerId, $now: now, }) const result = isSessionVisibleToUser(session.id, shareUserId, db) expect(result).toBeNull() }) test('returns owner for team owner when session owned by team', () => { const userId = seedUser(db, { role: 'member' }) const teamId = `team_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() // Create team and add user as owner db.query( `INSERT INTO teams (id, name, slug, created_by, created_at, updated_at) VALUES ($id, 'Test Team', 'test-team-r2', $userId, $now, $now)`, ).run({ $id: teamId, $userId: userId, $now: now }) db.query( `INSERT INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($teamId, $userId, 'owner', $now, $userId)`, ).run({ $teamId: teamId, $userId: userId, $now: now }) // Create session owned by team const session = storeCreateSession({}) storeBindSession(session.id, teamId, 'team') const result = isSessionVisibleToUser(session.id, userId, db) expect(result).toBe('owner') }) test('returns read for team share', () => { const ownerId = seedUser(db, { role: 'member' }) const userId = seedUser(db, { username: 'teammember', role: 'member', }) const teamId = `team_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() // Create team and add user as member db.query( `INSERT INTO teams (id, name, slug, created_by, created_at, updated_at) VALUES ($id, 'Share Team', 'share-team-r2', $ownerId, $now, $now)`, ).run({ $id: teamId, $ownerId: ownerId, $now: now }) db.query( `INSERT INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($teamId, $userId, 'member', $now, $ownerId)`, ).run({ $teamId: teamId, $userId: userId, $now: now, $ownerId: ownerId }) // Create session and share to team const session = storeCreateSession({}) storeBindSession(session.id, ownerId) const shareId = `share_${randomUUID().replace(/-/g, '')}` db.query( `INSERT INTO session_shares (id, session_id, granted_to_team, permission, granted_by, granted_at) VALUES ($id, $sessionId, $teamId, 'read', $grantedBy, $now)`, ).run({ $id: shareId, $sessionId: session.id, $teamId: teamId, $grantedBy: ownerId, $now: now, }) const result = isSessionVisibleToUser(session.id, userId, db) expect(result).toBe('read') }) test('returns null for user with no access', () => { const ownerId = seedUser(db, { role: 'member' }) const otherUserId = seedUser(db, { username: 'outsider', role: 'member', }) const session = storeCreateSession({}) storeBindSession(session.id, ownerId) const result = isSessionVisibleToUser(session.id, otherUserId, db) expect(result).toBeNull() }) }) describe('resolveOwnedWebSessionId with shares', () => { test('resolves session for user with write share', () => { const ownerId = seedUser(db, { role: 'member' }) const shareUserId = seedUser(db, { username: 'writer', role: 'member', }) const session = storeCreateSession({}) storeBindSession(session.id, ownerId) const shareId = `share_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at) VALUES ($id, $sessionId, $userId, 'write', $grantedBy, $now)`, ).run({ $id: shareId, $sessionId: session.id, $userId: shareUserId, $grantedBy: ownerId, $now: now, }) // Write-level check: should resolve for write share const resolved = resolveOwnedWebSessionId(session.id, shareUserId, true) expect(resolved).toBe(session.id) }) test('resolves session for user with read share (read-only)', () => { const ownerId = seedUser(db, { role: 'member' }) const shareUserId = seedUser(db, { username: 'reader2', role: 'member', }) const session = storeCreateSession({}) storeBindSession(session.id, ownerId) const shareId = `share_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at) VALUES ($id, $sessionId, $userId, 'read', $grantedBy, $now)`, ).run({ $id: shareId, $sessionId: session.id, $userId: shareUserId, $grantedBy: ownerId, $now: now, }) // Read-level check: should resolve const resolved = resolveOwnedWebSessionId(session.id, shareUserId, false) expect(resolved).toBe(session.id) // Write-level check: should NOT resolve (read-only share) const writeResolved = resolveOwnedWebSessionId( session.id, shareUserId, true, ) expect(writeResolved).toBeNull() }) test('resolves session for team member when team owns session', () => { const userId = seedUser(db, { role: 'member' }) const teamId = `team_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO teams (id, name, slug, created_by, created_at, updated_at) VALUES ($id, 'Resolve Team', 'resolve-team-r2', $userId, $now, $now)`, ).run({ $id: teamId, $userId: userId, $now: now }) db.query( `INSERT INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($teamId, $userId, 'owner', $now, $userId)`, ).run({ $teamId: teamId, $userId: userId, $now: now }) const session = storeCreateSession({}) storeBindSession(session.id, teamId, 'team') const resolved = resolveOwnedWebSessionId(session.id, userId, true) expect(resolved).toBe(session.id) }) }) }) // =========================================================================== // Task 3: teams.ts admin created_by determinism // =========================================================================== describe('Task 3: admin created_by for team creation', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('admin query uses ORDER BY created_at ASC for determinism', () => { // Create two admins with different creation times const now1 = new Date('2026-01-01T00:00:00Z').toISOString() const now2 = new Date('2026-06-01T00:00:00Z').toISOString() db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ('usr_second_admin', 'admin2', 'hash', 'admin', $now2)`, ).run({ $now2: now2 }) db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ('usr_first_admin', 'admin1', 'hash', 'admin', $now1)`, ).run({ $now1: now1 }) // The query used in teams.ts should pick the earliest admin const adminUser = db .query( "SELECT id FROM users WHERE role = 'admin' ORDER BY created_at ASC LIMIT 1", ) .get() as { id: string } | null expect(adminUser).not.toBeNull() expect(adminUser!.id).toBe('usr_first_admin') }) test('no admin user returns null (for 400 response)', () => { // No admin users in DB const adminUser = db .query( "SELECT id FROM users WHERE role = 'admin' ORDER BY created_at ASC LIMIT 1", ) .get() as { id: string } | null expect(adminUser).toBeNull() }) }) // =========================================================================== // Task 4: API key session creation should not bind __system__ // =========================================================================== describe('Task 4: API key session creation skips __system__ binding', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('session created without binding when userId is __system__', () => { const session = storeCreateSession({ source: 'web' }) // Simulate the fixed logic: skip bind when userId is __system__ const userId = '__system__' if (userId !== '__system__') { storeBindSession(session.id, userId) } // Verify no ownership was created const isOwner = storeIsSessionOwner(session.id, '__system__') expect(isOwner).toBe(false) }) test('session is bound when X-User-Id header provides real user', () => { const realUserId = seedUser(db, { role: 'member' }) const session = storeCreateSession({ source: 'web' }) // Simulate: API key auth + X-User-Id header const userId = '__system__' const headerUserId = realUserId if (userId === '__system__' && headerUserId) { storeBindSession(session.id, headerUserId) } else if (userId !== '__system__') { storeBindSession(session.id, userId) } expect(storeIsSessionOwner(session.id, realUserId)).toBe(true) expect(storeIsSessionOwner(session.id, '__system__')).toBe(false) }) test('session is orphan when API key without X-User-Id', () => { const session = storeCreateSession({ source: 'web' }) // Simulate: API key auth without X-User-Id const userId = '__system__' const headerUserId: string | undefined = undefined if (userId === '__system__' && headerUserId) { storeBindSession(session.id, headerUserId) } else if (userId !== '__system__') { storeBindSession(session.id, userId) } expect(storeIsSessionOwner(session.id, '__system__')).toBe(false) }) })