import { describe, test, expect, beforeEach, mock } from 'bun:test' import { mockConfigModule } from './helpers/mock-config' mock.module('../config', () => mockConfigModule()) import { Hono } from 'hono' import { Database } from 'bun:sqlite' import { randomUUID } from 'node:crypto' import { tmpdir } from 'node:os' import { resolve } from 'node:path' import { unlinkSync, existsSync } from 'node:fs' import webSessions from '../routes/web/sessions' import webEnvironments from '../routes/web/environments' import { storeReset, storeCreateEnvironment, storeBindSession } from '../store' import { initDatabase, resetDbSingleton, getDb } from '../db/sqlite' import { issueSessionToken } from '../auth/session' function freshDb(): Database { try { resetDbSingleton() } catch {} const path = resolve( tmpdir(), `rcs-team-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`, ) return initDatabase(path) } function createUser( db: Database, role = 'member', ): { id: string; username: string } { const id = `usr_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ($id, $u, 'h', $r, $now)`, ).run({ $id: id, $u: `user_${id.slice(0, 6)}`, $r: role, $now: now }) return { id, username: `user_${id.slice(0, 6)}` } } function createTeam(db: Database, createdBy: string): string { const id = `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, $n, $s, $by, $now, $now)`, ).run({ $id: id, $n: 'Eng', $s: `eng-${id.slice(0, 6)}`, $by: createdBy, $now: now, }) db.query( `INSERT INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($t, $u, 'member', $now, $by)`, ).run({ $t: id, $u: createdBy, $now: now, $by: createdBy }) return id } function linkEnvToTeam(db: Database, envId: string, teamId: string): void { db.query( `INSERT OR IGNORE INTO team_environments (team_id, environment_id) VALUES ($t, $e)`, ).run({ $t: teamId, $e: envId }) } function authHeader(token: string): Record { return { Authorization: `Bearer ${token}` } } function createApp(): Hono { const app = new Hono() app.route('/web', webSessions) app.route('/web', webEnvironments) return app } describe('团队域 session 创建与 environment 可见性', () => { let db: Database let app: Hono let user: { id: string; username: string } let accessToken: string let teamId: string let envId: string beforeEach(() => { db = freshDb() app = createApp() user = createUser(db) teamId = createTeam(db, user.id) // Register an environment and link it to the team const env = storeCreateEnvironment({ secret: 'secret', machineName: 'dev', username: user.id, }) envId = env.id linkEnvToTeam(db, envId, teamId) // Issue a session token for the user const { accessToken: at } = issueSessionToken(user.id, db) accessToken = at }) test('团队域新建 session 应设 visibility=team 并绑 team owner', async () => { const res = await app.request('/web/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader(accessToken), }, body: JSON.stringify({ title: 'Team Session', environment_id: envId, team_id: teamId, }), }) expect(res.status).toBe(200) const session = await res.json() // Bug A: visibility 应为 'team',不是 'private' expect(session.visibility).toBe('team') // session_owners 应有 owner_type='team', owner_id=teamId const owners = db .query( 'SELECT owner_type, owner_id FROM session_owners WHERE session_id = $sid', ) .all({ $sid: session.id }) as Array<{ owner_type: string owner_id: string }> const teamOwner = owners.find(o => o.owner_type === 'team') expect(teamOwner).toBeDefined() expect(teamOwner!.owner_id).toBe(teamId) }) test('团队域 session 应被 team 成员可见', async () => { // Create a session in team context const createRes = await app.request('/web/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader(accessToken), }, body: JSON.stringify({ title: 'Team Session', environment_id: envId, team_id: teamId, }), }) const session = await createRes.json() // Add another team member const member2 = createUser(db) db.query( `INSERT INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($t, $u, 'member', $now, $by)`, ).run({ $t: teamId, $u: member2.id, $now: new Date().toISOString(), $by: user.id, }) // Member2 should be able to resolve the session (not 403) const { accessToken: at2 } = issueSessionToken(member2.id, db) const getRes = await app.request(`/web/sessions/${session.id}`, { headers: authHeader(at2), }) expect(getRes.status).not.toBe(403) // Should be 200 (session visible to team member) expect(getRes.status).toBe(200) }) test('团队域应能看到属于该 team 的 environment', async () => { const res = await app.request('/web/environments', { headers: authHeader(accessToken), }) expect(res.status).toBe(200) const envs = await res.json() expect(Array.isArray(envs)).toBe(true) expect(envs.length).toBeGreaterThanOrEqual(1) // The team-linked env should have team_id and team_name const teamEnv = envs.find((e: { team_id?: string }) => e.team_id === teamId) expect(teamEnv).toBeDefined() expect(teamEnv.team_name).toBe('Eng') }) test('个人域不应看到 team 的 environment', async () => { // Create a user with no team membership const solo = createUser(db) const { accessToken: at } = issueSessionToken(solo.id, db) const res = await app.request('/web/environments', { headers: authHeader(at), }) expect(res.status).toBe(200) const envs = await res.json() // Solo user should NOT see the team's environment (only admin sees all) const teamEnv = envs.find((e: { team_id?: string }) => e.team_id === teamId) expect(teamEnv).toBeUndefined() }) })