// Regression test: GET /api/v1/assets must be scoped to the caller's accessible // projects. A pre-fix bug returned every asset across every project to any // authenticated user, defeating RBAC v2. // // Like project-access.test.js, the assets router uses the singleton pool, so we // point DATABASE_URL at TEST_DATABASE_URL and seed through the same pool. // Skips when TEST_DATABASE_URL is unset. import { test } from 'node:test'; import assert from 'node:assert/strict'; import express from 'express'; import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js'; const SKIP = !isTestDbConfigured() && 'TEST_DATABASE_URL not set'; async function appWithAssets(user) { process.env.DATABASE_URL = process.env.TEST_DATABASE_URL; process.env.AUTH_ENABLED = 'true'; // Importing the assets router constructs BullMQ queues; they connect lazily, // and the list route only touches Postgres, so no Redis is needed here. const { default: assetsRouter } = await import('../../src/routes/assets.js?v=' + Date.now()); const app = express(); app.use(express.json()); app.use((req, _res, next) => { req.user = user; next(); }); app.use('/api/v1/assets', assetsRouter); return new Promise(r => { const srv = app.listen(0, '127.0.0.1', () => r({ baseUrl: 'http://127.0.0.1:' + srv.address().port, close: () => new Promise(rs => srv.close(rs)) })); }); } async function seed(pool) { const proj = async (n) => (await pool.query(`INSERT INTO projects (name, s3_prefix) VALUES ($1,$1) RETURNING id`, [n])).rows[0].id; const alpha = await proj('Alpha'); const beta = await proj('Beta'); const asset = (pid, name) => pool.query( `INSERT INTO assets (project_id, filename, display_name, media_type, status) VALUES ($1, $2, $2, 'video', 'ready')`, [pid, name]); await asset(alpha, 'a1'); await asset(alpha, 'a2'); await asset(beta, 'b1'); const admin = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('adm','x','admin') RETURNING id`)).rows[0].id, role: 'admin' }; const scoped = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('vu','x','viewer') RETURNING id`)).rows[0].id, role: 'viewer' }; await pool.query( `INSERT INTO project_access (project_id, subject_type, subject_id, level) VALUES ($1,'user',$2,'view')`, [alpha, scoped.id]); return { alpha, beta, admin, scoped }; } test('GET /assets returns only assets in granted projects for scoped users', { skip: SKIP }, async () => { const pool = await setupTestDb(); process.env.DATABASE_URL = process.env.TEST_DATABASE_URL; try { const { admin, scoped } = await seed(pool); // Admin sees all three. let a = await appWithAssets(admin); let body = await (await fetch(a.baseUrl + '/api/v1/assets')).json(); assert.equal(body.assets.length, 3, 'admin should see every asset'); await a.close(); // Scoped viewer (granted only Alpha) sees the two Alpha assets, not Beta's. let s = await appWithAssets(scoped); body = await (await fetch(s.baseUrl + '/api/v1/assets')).json(); assert.equal(body.assets.length, 2, 'scoped user should see only granted-project assets'); assert.ok(body.assets.every(x => x.display_name !== 'b1'), 'must not leak ungranted-project asset'); await s.close(); } finally { await pool.end(); } }); test('GET /assets returns nothing for a user with no grants', { skip: SKIP }, async () => { const pool = await setupTestDb(); process.env.DATABASE_URL = process.env.TEST_DATABASE_URL; try { await seed(pool); const nobody = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('no','x','viewer') RETURNING id`)).rows[0].id, role: 'viewer' }; const s = await appWithAssets(nobody); const body = await (await fetch(s.baseUrl + '/api/v1/assets')).json(); assert.deepEqual(body, { assets: [], total: 0 }); await s.close(); } finally { await pool.end(); } });