import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { createAssetService, validateUploadRequest } from './mindspace-assets.mjs'; function createMockPool(state) { return { async getConnection() { return { async beginTransaction() {}, async commit() {}, async rollback() {}, release() {}, async query(sql, params = []) { if (sql.includes('FROM h5_space_categories') && sql.includes('FOR UPDATE')) { const category = state.categories.find( (item) => item.id === params[0] && item.user_id === params[1], ); return [category ? [category] : []]; } if (sql.includes('FROM h5_user_spaces') && sql.includes('FOR UPDATE')) { const space = state.spaces.find( (item) => item.id === params[0] && item.user_id === params[1], ); return [space ? [space] : []]; } if (sql.includes('INSERT INTO h5_upload_sessions')) { state.uploads.push({ id: params[0], user_id: params[1], space_id: params[2], category_id: params[3], filename: params[4], expected_size: params[5], reserved_bytes: params[7], temporary_storage_key: params[8], status: 'reserved', expires_at: params[9], }); return [[]]; } if (sql.includes('UPDATE h5_user_spaces') && sql.includes('reserved_bytes = reserved_bytes +')) { const space = state.spaces.find((item) => item.id === params[2]); space.reserved_bytes += params[0]; return [[]]; } if (sql.includes('FROM h5_upload_sessions') && sql.includes('WHERE id = ?')) { const upload = state.uploads.find( (item) => item.id === params[0] && item.user_id === params[1], ); return [upload ? [upload] : []]; } if (sql.includes('UPDATE h5_upload_sessions') && sql.includes("status = 'uploaded'")) { const upload = state.uploads.find( (item) => item.id === params[3] && item.user_id === params[4], ); upload.actual_size = params[0]; upload.detected_mime_type = params[1]; upload.checksum = params[2]; upload.status = 'uploaded'; return [[]]; } if (sql.includes('FROM h5_upload_sessions u') && sql.includes('FOR UPDATE')) { const upload = state.uploads.find( (item) => item.id === params[0] && item.user_id === params[1], ); if (!upload) return [[]]; const category = state.categories.find((item) => item.id === upload.category_id); return [[{ ...upload, category_code: category.category_code }]]; } if (sql.includes('INSERT INTO h5_assets')) { state.assets.push({ id: params[0], user_id: params[1], status: params[13], risk_level: params[11], current_version_id: params[8], original_filename: params[6], mime_type: params[5], size_bytes: params[9], }); return [[]]; } if (sql.includes('INSERT INTO h5_asset_versions')) { state.versions.push({ id: params[0], asset_id: params[1], storage_key: params[3], scan_status: params[8], }); return [[]]; } if (sql.includes('UPDATE h5_user_spaces') && sql.includes('used_bytes = used_bytes +')) { const space = state.spaces.find((item) => item.id === params[3]); space.reserved_bytes -= params[0]; space.used_bytes += params[1]; return [[]]; } if (sql.includes("status = 'completed'")) { const upload = state.uploads.find( (item) => item.id === params[2] && item.user_id === params[3], ); upload.status = 'completed'; upload.completed_asset_id = params[1]; return [[]]; } if (sql.includes('JOIN h5_asset_versions v')) { const asset = state.assets.find( (item) => item.id === params[0] && item.user_id === params[1], ); if (!asset) return [[]]; const version = state.versions.find((item) => item.id === asset.current_version_id); const category = state.categories.find((item) => item.id === asset.category_id); return [[ { ...asset, category_code: category?.category_code ?? 'oa', storage_key: version.storage_key, scan_status: version.scan_status, category_id: category?.id, display_name: asset.original_filename, asset_type: 'file', visibility: 'private', source_type: 'upload', checksum: 'abc', created_at: 1, updated_at: 1, }, ]]; } if (sql.includes("status IN ('reserved', 'uploaded') AND expires_at")) { return [state.uploads.filter((item) => item.expires_at <= params[0])]; } return [[]]; }, }; }, async query(sql, params = []) { if (sql.includes('FROM h5_upload_sessions') && sql.includes('WHERE id = ?')) { const upload = state.uploads.find( (item) => item.id === params[0] && item.user_id === params[1], ); return [upload ? [upload] : []]; } if (sql.includes('UPDATE h5_upload_sessions') && sql.includes("status = 'uploaded'")) { const upload = state.uploads.find( (item) => item.id === params[3] && item.user_id === params[4], ); upload.actual_size = params[0]; upload.detected_mime_type = params[1]; upload.checksum = params[2]; upload.status = 'uploaded'; return [[]]; } return [[]]; }, }; } test('validateUploadRequest rejects traversal filenames', () => { assert.throws( () => validateUploadRequest({ filename: '../secret.pdf', sizeBytes: 10 }), (error) => error.code === 'invalid_filename', ); }); test('completeUpload marks safe files ready and blocks script payloads', async () => { const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-')); const state = { categories: [ { id: 'cat-1', user_id: 'user-1', space_id: 'space-1', category_code: 'oa' }, { id: 'cat-2', user_id: 'user-2', space_id: 'space-2', category_code: 'oa' }, ], spaces: [ { id: 'space-1', user_id: 'user-1', quota_bytes: 5 * 1024 * 1024, used_bytes: 0, reserved_bytes: 0, status: 'active', }, ], uploads: [], assets: [], versions: [], }; let nextId = 0; const service = createAssetService(createMockPool(state), { storageRoot, idFactory: () => `id-${++nextId}`, }); const payload = Buffer.from(''); const upload = await service.createUpload('user-1', { categoryId: 'cat-1', filename: 'note.txt', sizeBytes: payload.length, }); await service.writeUploadContent('user-1', upload.id, payload); const asset = await service.completeUpload('user-1', upload.id); assert.equal(asset.status, 'quarantined'); assert.equal(asset.scanStatus, 'blocked'); }); test('createUpload rejects another users category id', async () => { const state = { categories: [ { id: 'cat-1', user_id: 'user-1', space_id: 'space-1', category_code: 'oa' }, { id: 'cat-2', user_id: 'user-2', space_id: 'space-2', category_code: 'oa' }, ], spaces: [ { id: 'space-1', user_id: 'user-1', quota_bytes: 5 * 1024 * 1024, used_bytes: 0, reserved_bytes: 0, status: 'active', }, ], uploads: [], assets: [], versions: [], }; const service = createAssetService(createMockPool(state), { storageRoot: await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-')), }); await assert.rejects( () => service.createUpload('user-1', { categoryId: 'cat-2', filename: 'note.txt', sizeBytes: 4, }), (error) => error.code === 'category_not_found', ); });