431 lines
14 KiB
JavaScript
431 lines
14 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
|
|
require('../src/common');
|
|
|
|
const { buildCanvasSizePayload, MAX_CANVAS_SIZE_DIMENSION } = require('../src/data');
|
|
const { buildCanvasSizeFormViewModel } = require('../src/web/routes/signage/canvas-sizes/form-view-model');
|
|
const { buildDuplicateCanvasSizeName, buildDuplicateCanvasSize } = require('../src/web/routes/signage/canvas-sizes/duplicate');
|
|
const registerContentRoutes = require('../src/web/routes/admin/content');
|
|
const adminPageScript = fs.readFileSync(require.resolve('../src/web/public/js/admin/admin-page.js'), 'utf8');
|
|
const canvasSizeFormTemplate = fs.readFileSync(require.resolve('../src/web/views/signage/canvas-sizes/form.hbs'), 'utf8');
|
|
const canvasSizeListTemplate = fs.readFileSync(require.resolve('../src/web/views/signage/canvas-sizes/list.hbs'), 'utf8');
|
|
|
|
test('canvas size payload rejects dimensions above the shared maximum', () => {
|
|
assert.throws(() => {
|
|
buildCanvasSizePayload({ body: { name: 'Oversized', width: MAX_CANVAS_SIZE_DIMENSION + 1, height: 1080 } }, null);
|
|
}, {
|
|
message: 'Canvas size dimensions must be 16384 or less.'
|
|
});
|
|
});
|
|
|
|
test('canvas size edit form locks dimensions while the size is in use', () => {
|
|
const viewModel = buildCanvasSizeFormViewModel({
|
|
id: 7,
|
|
name: 'Full HD',
|
|
width: 1920,
|
|
height: 1080,
|
|
inUse: true
|
|
}, '', null, true);
|
|
|
|
assert.equal(viewModel.dimensionsLocked, true);
|
|
assert.equal(viewModel.deleteDisabled, true);
|
|
assert.equal(viewModel.maxDimension, MAX_CANVAS_SIZE_DIMENSION);
|
|
assert.ok(adminPageScript.includes('function initLimitedInputValues()'));
|
|
assert.ok(adminPageScript.includes('function initLimitedTextInputValues()'));
|
|
assert.ok(canvasSizeFormTemplate.includes('id="canvas-size-name" name="name" type="text" maxlength="255" data-limit-text-length'));
|
|
assert.ok(canvasSizeFormTemplate.includes('data-limit-input-value'));
|
|
});
|
|
|
|
test('buildDuplicateCanvasSize copies canvas size fields for the create page', () => {
|
|
const duplicate = buildDuplicateCanvasSize({
|
|
id: 7,
|
|
name: 'Full HD',
|
|
width: 1920,
|
|
height: 1080
|
|
}, buildDuplicateCanvasSizeName('Full HD'));
|
|
|
|
assert.equal(duplicate.id, null);
|
|
assert.equal(duplicate.name, 'Copy of Full HD');
|
|
assert.equal(duplicate.width, 1920);
|
|
assert.equal(duplicate.height, 1080);
|
|
});
|
|
|
|
test('canvas size list template includes duplicate action', () => {
|
|
assert.match(canvasSizeListTemplate, /\/canvas-sizes\/\{\{id\}\}\/duplicate/);
|
|
assert.match(canvasSizeListTemplate, />Dupe<\/a>/);
|
|
});
|
|
|
|
test('canvas size update returns a warning when an in-use canvas changes dimensions', async () => {
|
|
const handlers = {};
|
|
const app = {
|
|
get(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
},
|
|
post(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
}
|
|
};
|
|
|
|
const queries = [];
|
|
const pool = {
|
|
async query(sql) {
|
|
queries.push(sql);
|
|
if (sql.includes('FROM c_canvas_sizes WHERE id = ?')) {
|
|
return [[{ id: 7, name: 'Full HD', width: 1920, height: 1080 }]];
|
|
}
|
|
if (sql.includes('SELECT COUNT(*) AS ref_count FROM c_templates WHERE canvas_size_id = ?')) {
|
|
return [[{ ref_count: 1 }]];
|
|
}
|
|
throw new Error('Unexpected query: ' + sql);
|
|
}
|
|
};
|
|
|
|
const deps = {
|
|
pool,
|
|
common: {
|
|
fetchCanvasSizesPage: async () => ({}),
|
|
getSearchQuery: () => '',
|
|
getSortQuery: () => '',
|
|
getSortDirectionQuery: () => 'asc',
|
|
fetchCanvasSizeById: async (connection, id) => {
|
|
const [rows] = await connection.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [id]);
|
|
return rows[0] || null;
|
|
},
|
|
buildCanvasSizePayload: buildCanvasSizePayload,
|
|
fetchDuplicateName: async () => null
|
|
},
|
|
pages: {
|
|
renderCanvasSizesPage() { return ''; },
|
|
renderCanvasSizeEditPage() { return ''; },
|
|
renderCanvasSizeAddPage() { return ''; }
|
|
},
|
|
upload: {
|
|
any() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
single() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
},
|
|
setAuthMessageCookie() {},
|
|
fetchScreensBySlideId: async () => [],
|
|
fetchScreensByTemplateId: async () => [],
|
|
collectUploadReferencesFromSlide: () => [],
|
|
collectUploadReferencesFromTemplate: () => [],
|
|
collectUploadReferencesFromPayload: () => [],
|
|
removeUnusedUploadFiles: async () => {},
|
|
syncPlaylistUploadsOnChange: async () => {},
|
|
getAuditUserId: () => 1,
|
|
redirectAfterSave: () => {},
|
|
notifyPlayerScreens: async () => 0,
|
|
broadcastDashboardState: async () => {},
|
|
backgroundTaskQueue: { enqueueTask: async () => null },
|
|
getSlideDeleteBlockMessage: async () => '',
|
|
getTemplateDeleteBlockMessage: async () => '',
|
|
getCanvasSizeDeleteBlockMessage: async () => '',
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
hasAnyPermission: () => false,
|
|
uploadDir: 'e:\\Projects Git\\pulse-signage\\media\\uploads'
|
|
};
|
|
|
|
registerContentRoutes(app, deps);
|
|
|
|
const routeHandlers = handlers['/canvas-sizes/:id'];
|
|
assert.ok(Array.isArray(routeHandlers));
|
|
|
|
const req = {
|
|
params: { id: '7' },
|
|
body: { name: 'Full HD', width: 2560, height: 1440 }
|
|
};
|
|
const res = {
|
|
statusCode: 0,
|
|
body: '',
|
|
redirectedTo: '',
|
|
redirect(url) {
|
|
this.redirectedTo = url;
|
|
},
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
send(body) {
|
|
this.body = body;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await routeHandlers[1](req, res, () => {});
|
|
|
|
assert.equal(res.statusCode, 400);
|
|
assert.equal(res.body, 'This canvas size is still used by one or more templates, so its dimensions cannot be changed.');
|
|
assert.equal(res.redirectedTo, '');
|
|
assert.equal(queries.length, 2);
|
|
});
|
|
|
|
test('slide upload cleanup route removes pending media assets', async () => {
|
|
const uploadRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-slide-upload-cleanup-'));
|
|
const uploadDir = path.join(uploadRoot, 'media', 'uploads');
|
|
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
await fs.promises.writeFile(path.join(uploadDir, 'test-file.png'), Buffer.from('temporary upload'));
|
|
const handlers = {};
|
|
const app = {
|
|
get(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
},
|
|
post(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
}
|
|
};
|
|
|
|
const deps = {
|
|
pool: {
|
|
async query(sql) {
|
|
if (String(sql).includes('is_published = 0')) {
|
|
return [[{ id: 1 }]];
|
|
}
|
|
if (String(sql).includes('DELETE FROM c_media_assets')) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
return [[{ ref_count: 0 }]];
|
|
}
|
|
},
|
|
common: {
|
|
fetchTemplatesData: async () => ({}),
|
|
fetchRssFeedsData: async () => ({ rssFeeds: [] }),
|
|
fetchApiSourcesData: async () => ({ apiSources: [] }),
|
|
fetchTimetablesData: async () => ({ timetableGroups: [] }),
|
|
parseJsonSafe: () => null,
|
|
fetchRssFeedItemsByFeedId: async () => [],
|
|
normalizeRssFeedItem: (item) => item,
|
|
fetchSlidesPage: async () => ({}),
|
|
fetchSlideById: async () => null,
|
|
fetchTemplatesPage: async () => ({}),
|
|
fetchTemplateById: async () => null,
|
|
fetchCanvasSizesPage: async () => ({}),
|
|
fetchCanvasSizeById: async () => null,
|
|
getSearchQuery: () => '',
|
|
getSortQuery: () => '',
|
|
getSortDirectionQuery: () => 'asc',
|
|
fetchDuplicateName: async () => null,
|
|
buildCanvasSizePayload
|
|
},
|
|
pages: {
|
|
renderCanvasSizesPage() { return ''; },
|
|
renderCanvasSizeEditPage() { return ''; },
|
|
renderCanvasSizeAddPage() { return ''; },
|
|
renderSlideAddPage() { return ''; },
|
|
renderSlideEditPage() { return ''; },
|
|
renderTemplatesPage() { return ''; },
|
|
renderTemplateAddPage() { return ''; },
|
|
renderTemplateEditPage() { return ''; }
|
|
},
|
|
upload: {
|
|
any() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
single() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
},
|
|
setAuthMessageCookie() {},
|
|
fetchScreensBySlideId: async () => [],
|
|
fetchScreensByTemplateId: async () => [],
|
|
collectUploadReferencesFromSlide: () => [],
|
|
collectUploadReferencesFromTemplate: () => [],
|
|
collectUploadReferencesFromPayload: () => [],
|
|
removeUnusedUploadFiles: async () => {},
|
|
syncPlaylistUploadsOnChange: async () => {},
|
|
getAuditUserId: () => 1,
|
|
redirectAfterSave: () => {},
|
|
notifyPlayerScreens: async () => 0,
|
|
broadcastDashboardState: async () => {},
|
|
backgroundTaskQueue: { enqueueTask: async () => null },
|
|
getSlideDeleteBlockMessage: async () => '',
|
|
getTemplateDeleteBlockMessage: async () => '',
|
|
getCanvasSizeDeleteBlockMessage: async () => '',
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
hasAnyPermission: () => true,
|
|
uploadDir
|
|
};
|
|
|
|
registerContentRoutes(app, deps);
|
|
|
|
const routeHandlers = handlers['/slides/uploads/cleanup'];
|
|
assert.ok(Array.isArray(routeHandlers));
|
|
|
|
const req = {
|
|
body: { uploadPaths: ['/media/uploads/test-file.png'] },
|
|
currentUser: { id: 1, permissions: ['slides.create'] }
|
|
};
|
|
const res = {
|
|
statusCode: 0,
|
|
body: '',
|
|
sendStatus(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(body) {
|
|
this.body = body;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
try {
|
|
await routeHandlers[1](req, res, () => {});
|
|
|
|
assert.equal(res.statusCode, 204);
|
|
assert.equal(fs.existsSync(path.join(uploadDir, 'test-file.png')), false);
|
|
assert.equal(res.statusCode, 204);
|
|
} finally {
|
|
await fs.promises.rm(uploadRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('wysiwyg image uploads are capped below the dedicated image region limit', async () => {
|
|
const handlers = {};
|
|
const app = {
|
|
get(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
},
|
|
post(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
}
|
|
};
|
|
|
|
const deps = {
|
|
pool: {
|
|
async query() {
|
|
return [[]];
|
|
}
|
|
},
|
|
common: {
|
|
fetchTemplatesData: async () => ({}),
|
|
fetchRssFeedsData: async () => ({ rssFeeds: [] }),
|
|
fetchApiSourcesData: async () => ({ apiSources: [] }),
|
|
fetchTimetablesData: async () => ({ timetableGroups: [] }),
|
|
parseJsonSafe: () => null,
|
|
fetchRssFeedItemsByFeedId: async () => [],
|
|
normalizeRssFeedItem: (item) => item,
|
|
fetchSlidesPage: async () => ({}),
|
|
fetchSlideById: async () => null,
|
|
fetchTemplatesPage: async () => ({}),
|
|
fetchTemplateById: async () => null,
|
|
fetchCanvasSizesPage: async () => ({}),
|
|
fetchCanvasSizeById: async () => null,
|
|
getSearchQuery: () => '',
|
|
getSortQuery: () => '',
|
|
getSortDirectionQuery: () => 'asc',
|
|
fetchDuplicateName: async () => null,
|
|
buildCanvasSizePayload
|
|
},
|
|
pages: {
|
|
renderCanvasSizesPage() { return ''; },
|
|
renderCanvasSizeEditPage() { return ''; },
|
|
renderCanvasSizeAddPage() { return ''; },
|
|
renderSlideAddPage() { return ''; },
|
|
renderSlideEditPage() { return ''; },
|
|
renderTemplatesPage() { return ''; },
|
|
renderTemplateAddPage() { return ''; },
|
|
renderTemplateEditPage() { return ''; }
|
|
},
|
|
upload: {
|
|
any() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
single() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
},
|
|
setAuthMessageCookie() {},
|
|
fetchScreensBySlideId: async () => [],
|
|
fetchScreensByTemplateId: async () => [],
|
|
collectUploadReferencesFromSlide: () => [],
|
|
collectUploadReferencesFromTemplate: () => [],
|
|
collectUploadReferencesFromPayload: () => [],
|
|
removeUnusedUploadFiles: async () => {},
|
|
syncPlaylistUploadsOnChange: async () => {},
|
|
getAuditUserId: () => 1,
|
|
redirectAfterSave: () => {},
|
|
notifyPlayerScreens: async () => 0,
|
|
broadcastDashboardState: async () => {},
|
|
backgroundTaskQueue: { enqueueTask: async () => null },
|
|
getSlideDeleteBlockMessage: async () => '',
|
|
getTemplateDeleteBlockMessage: async () => '',
|
|
getCanvasSizeDeleteBlockMessage: async () => '',
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
hasAnyPermission: () => true,
|
|
uploadDir: 'e:\\Projects Git\\pulse-signage\\media\\uploads'
|
|
};
|
|
|
|
registerContentRoutes(app, deps);
|
|
|
|
const routeHandlers = handlers['/slides/uploads'];
|
|
assert.ok(Array.isArray(routeHandlers));
|
|
|
|
const req = {
|
|
file: {
|
|
filename: 'wysiwyg-large.png',
|
|
originalname: 'wysiwyg-large.png',
|
|
mimetype: 'image/png',
|
|
size: 11 * 1024 * 1024
|
|
},
|
|
get(headerName) {
|
|
return headerName === 'X-Upload-Context' ? 'wysiwyg' : '';
|
|
},
|
|
currentUser: { id: 1, permissions: ['slides.create'] }
|
|
};
|
|
const res = {
|
|
statusCode: 0,
|
|
body: '',
|
|
json(body) {
|
|
this.body = body;
|
|
return this;
|
|
},
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
}
|
|
};
|
|
let nextError = null;
|
|
|
|
await routeHandlers[2](req, res, (error) => {
|
|
nextError = error || null;
|
|
});
|
|
|
|
assert.ok(nextError);
|
|
assert.equal(nextError.statusCode, 400);
|
|
assert.equal(nextError.message, 'Image must be 2 MB or smaller. Larger images should use the dedicated Image region.');
|
|
});
|