Files
pulse-signage/test/media-library.test.js
T
lzstealth 05c9341b08
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m50s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s
Release v2.15.4
2026-10-17 16:01:00 +01:00

215 lines
8.5 KiB
JavaScript

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 });
}
});