const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const crypto = require('crypto'); const sharp = require('sharp'); require('../src/common'); const mediaLibrary = require('../src/web/lib/media/library'); const registerMediaLibraryRoutes = require('../src/web/routes/settings/media-library'); const { normalizeMediaPath, createImageVariants, removeImageVariants, optimizeUploadedImage, registerMediaAssets, syncMediaAssetsFromDirectory, countMediaAssetReferences } = require('../src/web/lib/media/library'); test('media library normalizes only media URLs', () => { assert.equal(normalizeMediaPath('/media/uploads/photo.png'), '/media/uploads/photo.png'); assert.equal(normalizeMediaPath(' /media/video.mp4 '), '/media/video.mp4'); assert.equal(normalizeMediaPath('/assets/photo.png'), null); }); test('media library optimizes large raster uploads in place', async () => { const uploadDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-media-optimize-')); const filePath = path.join(uploadDir, 'large.jpg'); const sourceBuffer = await sharp(crypto.randomBytes(5000 * 100 * 3), { raw: { width: 5000, height: 100, channels: 3 } }).jpeg({ quality: 100 }).toBuffer(); await fs.promises.writeFile(filePath, sourceBuffer); try { const file = { path: filePath, size: sourceBuffer.length }; await optimizeUploadedImage(file, { maxDimension: 8192 }); const optimizedStats = await fs.promises.stat(filePath); assert.equal(file.path, filePath); assert.equal(file.size, optimizedStats.size); assert.ok(optimizedStats.size < sourceBuffer.length); const metadata = await sharp(filePath).metadata(); assert.equal(metadata.format, 'jpeg'); assert.equal(metadata.width, 5000); } finally { await fs.promises.rm(uploadDir, { recursive: true, force: true }); } }); test('media library creates only smaller image variants and preserves the original', async () => { const uploadDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-media-variants-')); const filePath = path.join(uploadDir, 'large.jpg'); const sourceBuffer = await sharp(crypto.randomBytes(5000 * 100 * 3), { raw: { width: 5000, height: 100, channels: 3 } }).jpeg({ quality: 100 }).toBuffer(); await fs.promises.writeFile(filePath, sourceBuffer); try { const variants = await createImageVariants({ path: filePath }); assert.deepEqual(variants.map(function (variant) { return variant.width; }), [2048, 4096]); assert.equal((await fs.promises.stat(filePath)).size, sourceBuffer.length); for (const variant of variants) { const variantPath = path.join(uploadDir, path.basename(variant.path)); assert.ok((await fs.promises.stat(variantPath)).size < sourceBuffer.length); } await removeImageVariants(filePath); assert.equal((await fs.promises.readdir(uploadDir)).filter(function (name) { return name.includes('.variant-'); }).length, 0); } finally { await fs.promises.rm(uploadDir, { recursive: true, force: true }); } }); test('media library registers uploaded files with their metadata', async () => { const queries = []; const pool = { async query(sql, params) { queries.push({ sql, params }); return [{ affectedRows: 1 }]; } }; const registered = await registerMediaAssets(pool, [{ filename: 'upload.png', originalname: 'Photo.png', mimetype: 'image/png', size: 42 }], file => '/media/uploads/' + file.filename, () => 'image', 7); assert.deepEqual(registered, ['/media/uploads/upload.png']); assert.match(queries[0].sql, /INSERT INTO c_media_assets/); assert.deepEqual(queries[0].params, [ '/media/uploads/upload.png', 'Photo.png', 'image', 'image/png', 42, 1, 7, 7 ]); }); test('media library backfills existing image and video uploads', async () => { const uploadDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-media-library-')); await fs.promises.writeFile(path.join(uploadDir, 'legacy.png'), Buffer.from('image')); await fs.promises.writeFile(path.join(uploadDir, 'legacy.mp4'), Buffer.from('video')); await fs.promises.writeFile(path.join(uploadDir, 'ignore.txt'), Buffer.from('other')); const queries = []; const pool = { async query(sql, params) { queries.push({ sql, params }); return [{ affectedRows: 1 }]; } }; try { assert.equal(await syncMediaAssetsFromDirectory(pool, uploadDir), 2); assert.deepEqual(queries.map(query => query.params[0]).sort(), [ '/media/uploads/legacy.mp4', '/media/uploads/legacy.png' ]); assert.match(queries[0].sql, /original_name = original_name/); assert.match(queries[0].sql, /is_published = is_published/); } finally { await fs.promises.rm(uploadDir, { recursive: true, force: true }); } }); test('media library counts slide and template references', async () => { const queries = []; const pool = { async query(sql, params) { queries.push({ sql, params }); if (sql.includes('FROM c_slides')) { return [[{ ref_count: 2 }]]; } return [[{ ref_count: 1 }]]; } }; assert.equal(await countMediaAssetReferences(pool, '/media/uploads/shared.png'), 3); assert.equal(queries.length, 2); assert.equal(queries[0].params[0], '/media/uploads/shared.png'); assert.equal(queries[1].params[0], '/media/uploads/shared.png'); assert.match(queries[0].sql, /LOCATE/); assert.match(queries[0].sql, /CAST\(COALESCE\(content_json, ''\) AS CHAR\)/); }); test('media library upload and delete routes audit successful asset changes', async () => { const originalCreateVariants = mediaLibrary.createImageVariants; const originalRegisterAsset = mediaLibrary.registerMediaAsset; const originalCountReferences = mediaLibrary.countMediaAssetReferences; const originalRemoveVariants = mediaLibrary.removeImageVariants; const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-media-audit-')); const uploadDir = path.join(root, 'media', 'uploads'); const filePath = path.join(uploadDir, 'photo.png'); await fs.promises.mkdir(uploadDir, { recursive: true }); await fs.promises.writeFile(filePath, 'image'); const routeHandlers = {}; const events = []; const app = { get(route, ...handlers) { routeHandlers['GET ' + route] = handlers; }, post(route, ...handlers) { routeHandlers['POST ' + route] = handlers; } }; const pool = { async query(sql) { if (sql.includes('FROM o_app_settings')) return [[]]; if (sql.includes('FROM c_media_assets')) { return [[{ id: 9, media_path: '/media/uploads/photo.png', original_name: 'Photo.png', media_type: 'image', file_size: 5 }]]; } return [[]]; } }; mediaLibrary.createImageVariants = async function () {}; mediaLibrary.registerMediaAsset = async function () {}; mediaLibrary.countMediaAssetReferences = async function () { return 0; }; mediaLibrary.removeImageVariants = async function () {}; try { registerMediaLibraryRoutes(app, { pool: pool, pages: {}, upload: { single() { return function (_req, _res, next) { next(); }; } }, uploadDir: uploadDir, getAuditUserId(req) { return req.currentUser.id; }, recordRequestAuditEvent(_pool, _req, event) { events.push(event); }, requirePermission() { return function (_req, _res, next) { next(); }; } }); const response = { redirect(value) { this.location = value; return this; } }; const next = function (error) { if (error) throw error; }; const request = { currentUser: { id: 4 }, body: {}, file: { path: filePath, filename: 'photo.png', originalname: 'Photo.png', mimetype: 'image/png', size: 5 } }; await routeHandlers['POST /media-library'].at(-1)(request, response, next); await routeHandlers['POST /media-library/:id/delete'].at(-1)( { currentUser: { id: 4 }, params: { id: '9' } }, response, next ); assert.deepEqual(events.map(function (event) { return event.eventType; }), ['media.uploaded', 'media.deleted']); assert.equal(events[0].targetLabel, 'Photo.png'); assert.equal(events[1].targetId, 9); assert.ok(events.every(function (event) { return event.category === 'media-assets' && event.actorUserId === 4; })); } finally { mediaLibrary.createImageVariants = originalCreateVariants; mediaLibrary.registerMediaAsset = originalRegisterAsset; mediaLibrary.countMediaAssetReferences = originalCountReferences; mediaLibrary.removeImageVariants = originalRemoveVariants; await fs.promises.rm(root, { recursive: true, force: true }); } });