158 lines
7.8 KiB
JavaScript
158 lines
7.8 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const root = path.join(__dirname, "..");
|
|
const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-content-library-"));
|
|
const serviceDir = path.join(sandbox, "src", "services");
|
|
fs.mkdirSync(serviceDir, { recursive: true });
|
|
|
|
for (const file of [
|
|
"config.js",
|
|
"db.js",
|
|
"settings.js",
|
|
"upload-security.js",
|
|
"content-library.js",
|
|
"content-routes.js"
|
|
]) {
|
|
fs.copyFileSync(path.join(root, "src", "services", file), path.join(serviceDir, file));
|
|
}
|
|
|
|
let database = null;
|
|
(async () => {
|
|
try {
|
|
database = require(path.join(serviceDir, "db.js"));
|
|
// Core imports the Web server before main() runs migrate(). The content
|
|
// service must therefore be safe to load without touching database tables.
|
|
const library = require(path.join(serviceDir, "content-library.js"));
|
|
const routes = require(path.join(serviceDir, "content-routes.js"));
|
|
database.migrate();
|
|
const settings = require(path.join(serviceDir, "settings.js"));
|
|
settings.ensureDefaults();
|
|
settings.setSetting("session_secret", "content-library-verification-secret");
|
|
assert(database.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'content_resources'").get());
|
|
library.ensureContentLibrary();
|
|
assert(library.supportedExtensions().includes(".mp4"));
|
|
assert(library.supportedExtensions().includes(".mp3"));
|
|
assert(library.supportedExtensions().includes(".webm"));
|
|
assert(library.supportedExtensions().includes(".png"));
|
|
assert(library.supportedExtensions().includes(".woff2"));
|
|
|
|
const pngPath = path.join(library.INCOMING_DIR, "verify.png.upload");
|
|
fs.writeFileSync(
|
|
pngPath,
|
|
Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZK1sAAAAASUVORK5CYII=",
|
|
"base64"
|
|
)
|
|
);
|
|
const [created] = await library.importUploadedFiles([
|
|
{
|
|
path: pngPath,
|
|
originalname: "Alert Graphic.png",
|
|
mimetype: "image/png"
|
|
}
|
|
], { access_level: "locked", uploaded_by: "verify-admin" });
|
|
|
|
assert.strictEqual(created.access_level, "locked");
|
|
assert.strictEqual(created.category, "image");
|
|
assert.strictEqual(created.preview_kind, "image");
|
|
assert.strictEqual(library.publicUrl(created, "https://lumi.example"), null);
|
|
assert(fs.existsSync(library.resourceFilePath(created)));
|
|
assert.match(library.serializeResource(created, { base_url: "https://lumi.example" }).download_url, /\/admin\/resources\/[^/]+\/download\/Alert%20Graphic\.png$/);
|
|
|
|
const signed = new URL(library.createSignedUrl(created.id, {
|
|
base_url: "https://lumi.example",
|
|
ttl_seconds: 60
|
|
}));
|
|
const signedResource = library.verifySignedResource(
|
|
created.id,
|
|
signed.searchParams.get("expires"),
|
|
signed.searchParams.get("sig")
|
|
);
|
|
assert.strictEqual(signedResource.id, created.id);
|
|
|
|
const exposed = library.setResourceAccess(created.id, "exposed");
|
|
const publicUrl = new URL(library.publicUrl(exposed, "https://lumi.example"));
|
|
const publicToken = publicUrl.pathname.split("/")[2];
|
|
assert(publicToken.length >= 40);
|
|
assert.strictEqual(library.getResourceByExposedToken(publicToken).id, created.id);
|
|
|
|
const renamed = library.updateResourceName(created.id, "Follower alert");
|
|
assert.strictEqual(renamed.display_name, "Follower alert");
|
|
assert.strictEqual(library.listResources({ category: "image" }).length, 1);
|
|
|
|
assert.deepStrictEqual(routes.parseByteRange("bytes=0-9", 100), { start: 0, end: 9 });
|
|
assert.deepStrictEqual(routes.parseByteRange("bytes=-10", 100), { start: 90, end: 99 });
|
|
assert.strictEqual(routes.parseByteRange("bytes=200-300", 100).invalid, true);
|
|
assert.strictEqual(routes.parseByteRange("bytes=0-1,4-5", 100).invalid, true);
|
|
|
|
const stats = library.getStorageStats();
|
|
assert.strictEqual(stats.file_count, 1);
|
|
assert(stats.used_bytes > 0);
|
|
assert(stats.max_file_bytes > 0);
|
|
|
|
const configuredStorage = library.updateStorageSettings({
|
|
limit_gib: "2.5",
|
|
reserve_gib: "0.25",
|
|
max_file_mib: "32",
|
|
max_files: "7"
|
|
});
|
|
assert.strictEqual(configuredStorage.limit_bytes, Math.floor(2.5 * 1024 ** 3));
|
|
assert.strictEqual(configuredStorage.reserve_bytes, Math.floor(0.25 * 1024 ** 3));
|
|
assert.strictEqual(configuredStorage.max_file_bytes, 32 * 1024 ** 2);
|
|
assert.strictEqual(configuredStorage.max_files, 7);
|
|
assert.deepStrictEqual(library.uploadLimits(), { max_file_bytes: 32 * 1024 ** 2, max_files: 7 });
|
|
|
|
const lockedAgain = library.setResourceAccess(created.id, "locked");
|
|
assert.strictEqual(library.getResourceByExposedToken(publicToken), null);
|
|
assert.strictEqual(lockedAgain.public_token, "");
|
|
|
|
const storedPath = library.resourceFilePath(created);
|
|
library.deleteResource(created.id);
|
|
assert.strictEqual(fs.existsSync(storedPath), false);
|
|
assert.strictEqual(library.getResource(created.id), null);
|
|
assert.strictEqual(library.getStorageStats().file_count, 0);
|
|
|
|
const serverSource = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8");
|
|
assert(serverSource.includes("registerPublicContentRoutes(app)"));
|
|
assert(serverSource.includes("registerContentAdminRoutes(app"));
|
|
assert(serverSource.includes('path: "/admin/resources"'));
|
|
assert(serverSource.includes("global.lumiFrameworks.resources"));
|
|
|
|
const overlayRouteSource = fs.readFileSync(path.join(root, "src", "services", "overlay-routes.js"), "utf8");
|
|
assert(overlayRouteSource.includes("media-src https: http: data: blob:"), "OBS overlay CSP must allow configured media sources");
|
|
|
|
const viewSource = fs.readFileSync(path.join(root, "src", "web", "views", "admin-resources.ejs"), "utf8");
|
|
assert(viewSource.includes("data-resource-dropzone"));
|
|
assert(viewSource.includes("data-resource-preview-modal"));
|
|
assert(viewSource.includes("data-resource-media-toggle") && viewSource.includes("data-resource-media-seek"));
|
|
assert(viewSource.includes("data-resource-rename-submit") && viewSource.includes("resource-more-actions"));
|
|
assert(viewSource.includes("Available to Lumi"));
|
|
assert(viewSource.includes("data-resource-access-form") && viewSource.includes("data-confirm-mode=\"modal\"") && viewSource.includes("data-no-destructive-confirm"));
|
|
const contentRouteSource = fs.readFileSync(path.join(root, "src", "services", "content-routes.js"), "utf8");
|
|
assert(contentRouteSource.includes('/admin/resources/:id/revoke'));
|
|
assert(contentRouteSource.includes('/admin/resources/:id/download/:filename'));
|
|
|
|
const clientSource = fs.readFileSync(path.join(root, "src", "web", "public", "content-library.js"), "utf8");
|
|
assert(clientSource.includes("window.LumiConfirm?.destructiveToken"), "Resource actions must use the shared timed confirmation API");
|
|
assert(clientSource.includes('headers["X-Confirmation-Token"] = token'), "Resource actions must send the issued confirmation token directly");
|
|
assert(clientSource.includes("data-resource-upload-limit-copy") && clientSource.includes("selectedFilesError"), "Saved upload limits must be reflected and enforced in the UI");
|
|
|
|
const cssSource = fs.readFileSync(path.join(root, "src", "web", "public", "content-library.css"), "utf8");
|
|
assert(cssSource.includes(".resource-empty-state[hidden]") && cssSource.includes("display: none !important"));
|
|
assert(cssSource.includes(".resource-preview-tile:focus-within .resource-media-controls"));
|
|
|
|
const coreKnowledge = fs.readFileSync(path.join(root, "knowledge", "core", "lumi-core.md"), "utf8");
|
|
assert(coreKnowledge.includes("GET /admin/resources") && coreKnowledge.includes("GET /media/:token/:filename"));
|
|
|
|
console.log("Content-library verification passed.");
|
|
} finally {
|
|
try { database?.db?.close(); } catch {}
|
|
fs.rmSync(sandbox, { recursive: true, force: true });
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|