import { describe, test, expect, beforeEach, mock } from 'bun:test' import { createHash, randomUUID } from 'node:crypto' import { tmpdir } from 'node:os' import { resolve } from 'node:path' import { mockConfigModule } from './helpers/mock-config' // Mock config before any module imports mock.module('../config', () => mockConfigModule()) import { Database } from 'bun:sqlite' // Dynamic imports — Phase 2 modules may not exist yet (TDD) let migrateDatabase: ((db: Database) => void) | undefined let initDatabase: ((path?: string) => Database) | undefined let resetDbSingleton: (() => void) | undefined let getDb: (() => Database) | undefined try { const dbMod = await import('../db/sqlite') migrateDatabase = dbMod.migrateDatabase getDb = dbMod.getDb initDatabase = dbMod.initDatabase resetDbSingleton = dbMod.resetDbSingleton } catch { // Module not implemented yet } // Dynamic import for session visibility function let storeListSessionsVisibleToUser: | ((userId: string, db: Database) => Array>) | undefined try { const mod = await import('../services/session-visibility') storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser } catch { // Module not implemented yet } // Dynamic import for canManageSession let canManageSession: | ((sessionId: string, userId: string, db: Database) => boolean) | undefined try { const mod = await import('../services/session') canManageSession = mod.canManageSession } catch { // Module not implemented yet } // --------------------------------------------------------------------------- // Database helpers // --------------------------------------------------------------------------- function freshDb(): Database { if (resetDbSingleton) { try { resetDbSingleton() } catch { // ignore cleanup errors } } if (initDatabase) { const uniquePath = resolve( tmpdir(), `rcs-test-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`, ) return initDatabase(uniquePath) } const db = new Database(':memory:') db.exec('PRAGMA foreign_keys = ON') if (migrateDatabase) migrateDatabase(db) return db } // --------------------------------------------------------------------------- // Fixture helpers // --------------------------------------------------------------------------- function createUser( db: Database, opts: { username: string; role: string }, ): string { const id = `usr_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() const hash = '$argon2id$v=19$m=65536,t=3,p=1$dGVzdHNhbHQ$dGVzdGhhc2h0ZXN0aGFzaA' db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ($id, $u, $h, $r, $now)`, ).run({ $id: id, $u: opts.username, $h: hash, $r: opts.role, $now: now }) return id } function createTeam( db: Database, opts: { slug: string; createdBy: string }, ): string { const id = `team_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() try { db.query( `INSERT INTO teams (id, name, slug, created_by, created_at, updated_at) VALUES ($id, $name, $slug, $by, $now, $now)`, ).run({ $id: id, $name: opts.slug, $slug: opts.slug, $by: opts.createdBy, $now: now, }) } catch { // teams table may not exist yet } return id } function addMember( db: Database, teamId: string, userId: string, role: string, ): void { const now = new Date().toISOString() try { db.query( `INSERT OR IGNORE INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($t, $u, $r, $now, $by)`, ).run({ $t: teamId, $u: userId, $r: role, $by: userId, $now: now }) } catch { // team_members may not exist } } interface SessionFixture { id: string visibility: string } function createSession( db: Database, opts: { id?: string visibility?: string ownerType?: string ownerId?: string environmentId?: string }, ): SessionFixture { const id = opts.id || `ses_${randomUUID().replace(/-/g, '')}` const visibility = opts.visibility || (opts.ownerType === 'team' ? 'team' : 'private') const now = new Date().toISOString() // Try to insert into a sessions table (Phase 2 schema) try { db.query( `INSERT INTO sessions (id, visibility, environment_id, created_at, updated_at) VALUES ($id, $vis, $env, $now, $now)`, ).run({ $id: id, $vis: visibility, $env: opts.environmentId || null, $now: now, }) } catch { // sessions table may not have visibility column yet; // try without it try { db.query( `INSERT INTO sessions (id, environment_id, created_at, updated_at) VALUES ($id, $env, $now, $now)`, ).run({ $id: id, $env: opts.environmentId || null, $now: now }) } catch { // sessions table may not exist at all } } // Add owner record if (opts.ownerType && opts.ownerId) { try { db.query( `INSERT INTO session_owners (session_id, owner_type, owner_id) VALUES ($sid, $type, $oid)`, ).run({ $sid: id, $type: opts.ownerType, $oid: opts.ownerId }) } catch { // session_owners may not exist } } return { id, visibility } } function addSessionShare( db: Database, opts: { sessionId: string grantedToUser?: string grantedToTeam?: string permission?: string grantedBy: string expiresAt?: string }, ): string { const id = `shr_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() try { db.query( `INSERT INTO session_shares (id, session_id, granted_to_user, granted_to_team, permission, expires_at, granted_by, granted_at) VALUES ($id, $sid, $gtu, $gtt, $perm, $exp, $by, $now)`, ).run({ $id: id, $sid: opts.sessionId, $gtu: opts.grantedToUser || null, $gtt: opts.grantedToTeam || null, $perm: opts.permission || 'read', $exp: opts.expiresAt || null, $by: opts.grantedBy, $now: now, }) } catch { // session_shares may not exist } return id } // =========================================================================== // TESTS // =========================================================================== describe('storeListSessionsVisibleToUser', () => { let db: Database let aliceId: string let bobId: string let charlieId: string let adminId: string let teamAId: string beforeEach(() => { db = freshDb() aliceId = createUser(db, { username: 'alice', role: 'member' }) bobId = createUser(db, { username: 'bob', role: 'member' }) charlieId = createUser(db, { username: 'charlie', role: 'member' }) adminId = createUser(db, { username: 'sysadmin', role: 'admin' }) teamAId = createTeam(db, { slug: 'team-a', createdBy: aliceId }) addMember(db, teamAId, aliceId, 'owner') addMember(db, teamAId, bobId, 'member') // charlie is NOT in teamA }) test('owner is self → visible (private/team/public all visible)', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const privateSession = createSession(db, { visibility: 'private', ownerType: 'user', ownerId: aliceId, }) const teamSession = createSession(db, { visibility: 'team', ownerType: 'user', ownerId: aliceId, }) const publicSession = createSession(db, { visibility: 'public', ownerType: 'user', ownerId: aliceId, }) const sessions = storeListSessionsVisibleToUser!(aliceId, db) const ids = sessions.map(s => s.id as string) expect(ids).toContain(privateSession.id) expect(ids).toContain(teamSession.id) expect(ids).toContain(publicSession.id) }) test('owner is own team + visibility=team → visible', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const session = createSession(db, { visibility: 'team', ownerType: 'team', ownerId: teamAId, }) const sessions = storeListSessionsVisibleToUser!(bobId, db) const ids = sessions.map(s => s.id as string) expect(ids).toContain(session.id) }) test('owner is own team + visibility=private → not visible', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const session = createSession(db, { visibility: 'private', ownerType: 'team', ownerId: teamAId, }) const sessions = storeListSessionsVisibleToUser!(bobId, db) const ids = sessions.map(s => s.id as string) expect(ids).not.toContain(session.id) }) test('owner is own team + visibility=public → visible', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const session = createSession(db, { visibility: 'public', ownerType: 'team', ownerId: teamAId, }) const sessions = storeListSessionsVisibleToUser!(bobId, db) const ids = sessions.map(s => s.id as string) expect(ids).toContain(session.id) }) test('explicitly granted via session_shares → visible', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const session = createSession(db, { visibility: 'private', ownerType: 'user', ownerId: aliceId, }) addSessionShare(db, { sessionId: session.id, grantedToUser: charlieId, permission: 'read', grantedBy: aliceId, }) const sessions = storeListSessionsVisibleToUser!(charlieId, db) const ids = sessions.map(s => s.id as string) expect(ids).toContain(session.id) }) test('visibility=public + owner is stranger → visible', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const session = createSession(db, { visibility: 'public', ownerType: 'user', ownerId: aliceId, }) // charlie is a stranger (not in alice's team, no share) const sessions = storeListSessionsVisibleToUser!(charlieId, db) const ids = sessions.map(s => s.id as string) expect(ids).toContain(session.id) }) test('system admin → all sessions visible (including orphans)', () => { expect(storeListSessionsVisibleToUser).toBeDefined() // Create sessions owned by different users createSession(db, { visibility: 'private', ownerType: 'user', ownerId: aliceId, }) createSession(db, { visibility: 'private', ownerType: 'user', ownerId: bobId, }) // Create an orphan session (no owner) createSession(db, { visibility: 'private' }) const sessions = storeListSessionsVisibleToUser!(adminId, db) // Admin should see all sessions — at least the 3 we created expect(sessions.length).toBeGreaterThanOrEqual(3) }) test('orphan sessions (no owner) → only admin visible', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const orphan = createSession(db, { visibility: 'private' }) // Regular user should NOT see orphan const aliceSessions = storeListSessionsVisibleToUser!(aliceId, db) const aliceIds = aliceSessions.map(s => s.id as string) expect(aliceIds).not.toContain(orphan.id) // Admin should see orphan const adminSessions = storeListSessionsVisibleToUser!(adminId, db) const adminIds = adminSessions.map(s => s.id as string) expect(adminIds).toContain(orphan.id) }) }) describe('session visibility field', () => { let db: Database let aliceId: string let teamAId: string beforeEach(() => { db = freshDb() aliceId = createUser(db, { username: 'alice', role: 'member' }) teamAId = createTeam(db, { slug: 'team-a', createdBy: aliceId }) addMember(db, teamAId, aliceId, 'owner') }) test('team environment → default visibility=team', () => { // When a session is created under a team-owned environment, // the default visibility should be 'team' const envId = `env_${randomUUID().replace(/-/g, '')}` const session = createSession(db, { ownerType: 'team', ownerId: teamAId, environmentId: envId, }) // Verify the session has team visibility const row = db.query('SELECT visibility FROM sessions WHERE id = $id').get({ $id: session.id, }) as { visibility: string } | null if (row) { expect(row.visibility).toBe('team') } }) test('personal environment → default visibility=private', () => { const envId = `env_${randomUUID().replace(/-/g, '')}` const session = createSession(db, { ownerType: 'user', ownerId: aliceId, environmentId: envId, }) const row = db.query('SELECT visibility FROM sessions WHERE id = $id').get({ $id: session.id, }) as { visibility: string } | null if (row) { expect(row.visibility).toBe('private') } }) test('owner switches visibility private → team → 200', () => { expect(storeListSessionsVisibleToUser).toBeDefined() const session = createSession(db, { visibility: 'private', ownerType: 'user', ownerId: aliceId, }) // Owner changes visibility to 'team' try { db.query(`UPDATE sessions SET visibility = 'team' WHERE id = $id`).run({ $id: session.id, }) } catch { // Column may not exist } const row = db.query('SELECT visibility FROM sessions WHERE id = $id').get({ $id: session.id, }) as { visibility: string } | null if (row) { expect(row.visibility).toBe('team') } }) test.todo('non-owner cannot switch visibility → 403', () => { // TODO: Implement PATCH /web/sessions/:id/visibility route, then test: // 1. Create session owned by alice // 2. Bob (non-owner) sends PATCH to change visibility // 3. Assert 403 Forbidden }) }) describe('canManageSession', () => { test('direct user owner can manage', () => { if (!canManageSession || !initDatabase) return const db = freshDb() const owner = createUser(db, { username: 'owner', role: 'member' }) const session = createSession(db, { ownerType: 'user', ownerId: owner }) expect(canManageSession(session.id, owner, db)).toBe(true) }) test('team admin can manage team-owned session', () => { if (!canManageSession || !initDatabase) return const db = freshDb() const creator = createUser(db, { username: 'creator', role: 'member' }) const admin = createUser(db, { username: 'admin', role: 'member' }) const team = createTeam(db, { slug: 'team1', createdBy: creator }) addMember(db, team, creator, 'owner') addMember(db, team, admin, 'admin') const session = createSession(db, { ownerType: 'team', ownerId: team }) expect(canManageSession(session.id, admin, db)).toBe(true) }) test('team owner can manage team-owned session', () => { if (!canManageSession || !initDatabase) return const db = freshDb() const creator = createUser(db, { username: 'creator', role: 'member' }) const team = createTeam(db, { slug: 'team1', createdBy: creator }) addMember(db, team, creator, 'owner') const session = createSession(db, { ownerType: 'team', ownerId: team }) expect(canManageSession(session.id, creator, db)).toBe(true) }) test('team member (non-admin) cannot manage team-owned session', () => { if (!canManageSession || !initDatabase) return const db = freshDb() const creator = createUser(db, { username: 'creator', role: 'member' }) const member = createUser(db, { username: 'member', role: 'member' }) const team = createTeam(db, { slug: 'team1', createdBy: creator }) addMember(db, team, creator, 'owner') addMember(db, team, member, 'member') const session = createSession(db, { ownerType: 'team', ownerId: team }) expect(canManageSession(session.id, member, db)).toBe(false) }) test('non-member cannot manage session', () => { if (!canManageSession || !initDatabase) return const db = freshDb() const owner = createUser(db, { username: 'owner', role: 'member' }) const outsider = createUser(db, { username: 'outsider', role: 'member' }) const session = createSession(db, { ownerType: 'user', ownerId: owner }) expect(canManageSession(session.id, outsider, db)).toBe(false) }) test('system admin can manage any session', () => { if (!canManageSession || !initDatabase) return const db = freshDb() const owner = createUser(db, { username: 'owner', role: 'member' }) const admin = createUser(db, { username: 'admin', role: 'admin' }) const session = createSession(db, { ownerType: 'user', ownerId: owner }) expect(canManageSession(session.id, admin, db)).toBe(true) }) })