7ff8caf9d7
#1298 and #1303 merged together. #1298 taught the creation paths to resolve a fragmentation_level default; #1303 removed everything that consumed it. Neither conflicted textually, so main ended up validating the field on create and update, copying it on duplicate, resolving default_fragmentation_level for it, and advertising it in the v1 API docs — for a value nothing reads and a setting the Image Security tab no longer exposes. Inert rather than broken, which is exactly why it needed removing on purpose: dead code that contradicts the PR that just deleted the feature is how the next reader concludes fragmentation still works. The events.fragmentation_level column and the app_settings row stay, as #1303 decided — dropping a column is irreversible and the stored values are harmless once nothing reads them. Refs #1300
221 lines
9.1 KiB
JavaScript
221 lines
9.1 KiB
JavaScript
/**
|
|
* Image-security settings applied as creation defaults (#1296).
|
|
*
|
|
* Four controls in Settings → Image security were written, reloaded and
|
|
* rendered as toggles, and read by nothing:
|
|
*
|
|
* default_protection_level, default_image_quality,
|
|
* enable_canvas_rendering
|
|
*
|
|
* Each maps onto an `events` column migration 038 already created, and each
|
|
* is labelled "… by default". `enable_devtools_protection` was the only one
|
|
* of the five ever wired.
|
|
*
|
|
* The load-bearing constraint is that this is CREATION-time only. Applying
|
|
* these to existing events would silently change live galleries on upgrade —
|
|
* an install with enable_canvas_rendering already on would flip every grid to
|
|
* canvas rendering, which is the memory profile under investigation in #1287.
|
|
*/
|
|
|
|
const { bootCrmDb } = require('./helpers/crmDb');
|
|
|
|
describe('image-security creation defaults', () => {
|
|
let db;
|
|
let cleanup;
|
|
let getImageSecurityDefaults;
|
|
let resolveImageSecurityColumns;
|
|
let readBooleanSetting;
|
|
|
|
beforeAll(async () => {
|
|
({ db, cleanup } = await bootCrmDb());
|
|
({ getImageSecurityDefaults, resolveImageSecurityColumns, readBooleanSetting } =
|
|
require('../../src/routes/adminEvents/helpers'));
|
|
}, 120000);
|
|
|
|
afterAll(async () => {
|
|
if (cleanup) await cleanup();
|
|
});
|
|
|
|
const setSetting = async (key, value) => {
|
|
await db('app_settings')
|
|
.insert({ setting_key: key, setting_value: JSON.stringify(value), setting_type: 'security' })
|
|
.onConflict('setting_key')
|
|
.merge();
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
await db('app_settings').whereIn('setting_key', [
|
|
'default_protection_level', 'default_image_quality',
|
|
'enable_canvas_rendering',
|
|
]).del();
|
|
});
|
|
|
|
it('returns nothing when no settings are configured', async () => {
|
|
// Every key absent must fall through to the column defaults, which is
|
|
// exactly the behaviour before this existed.
|
|
expect(await getImageSecurityDefaults()).toEqual({});
|
|
});
|
|
|
|
it('maps each setting onto its events column', async () => {
|
|
await setSetting('default_protection_level', 'enhanced');
|
|
await setSetting('default_image_quality', 72);
|
|
await setSetting('enable_canvas_rendering', true);
|
|
|
|
expect(await getImageSecurityDefaults()).toEqual({
|
|
protection_level: 'enhanced',
|
|
image_quality: 72,
|
|
use_canvas_rendering: true,
|
|
});
|
|
});
|
|
|
|
it('carries a false canvas setting through, rather than dropping it', async () => {
|
|
// `false` is a real choice — dropping it as falsy would leave the column
|
|
// default in place and make "off" unreachable.
|
|
await setSetting('enable_canvas_rendering', false);
|
|
expect(await getImageSecurityDefaults()).toEqual({ use_canvas_rendering: false });
|
|
});
|
|
|
|
it.each([
|
|
['an unknown protection level', 'default_protection_level', 'paranoid'],
|
|
['a non-enum protection level', 'default_protection_level', 42],
|
|
['image quality above 100', 'default_image_quality', 250],
|
|
['image quality of zero', 'default_image_quality', 0],
|
|
['a non-numeric quality', 'default_image_quality', 'high'],
|
|
['a non-boolean canvas value', 'enable_canvas_rendering', 'yes'],
|
|
// parseInt would have rescued each of these into a valid-looking
|
|
// integer. The settings PUT stores values without validating them, so
|
|
// they can genuinely be in the table.
|
|
['a numeric prefix with trailing junk', 'default_image_quality', '72oops'],
|
|
['a fractional quality', 'default_image_quality', 72.5],
|
|
['a single-element array', 'default_image_quality', [72]],
|
|
])('ignores %s and falls through to the column default', async (_label, key, value) => {
|
|
await setSetting(key, value);
|
|
expect(await getImageSecurityDefaults()).toEqual({});
|
|
});
|
|
|
|
it('applies only the keys that are configured', async () => {
|
|
await setSetting('default_protection_level', 'maximum');
|
|
expect(await getImageSecurityDefaults()).toEqual({ protection_level: 'maximum' });
|
|
});
|
|
|
|
it('never throws, so a settings failure cannot block event creation', async () => {
|
|
await setSetting('default_image_quality', { nonsense: true });
|
|
await expect(getImageSecurityDefaults()).resolves.toEqual({});
|
|
});
|
|
|
|
describe('double-encoded settings (the settings tab round trip)', () => {
|
|
// GET returns setting_value undecoded and the tab PUTs the whole object
|
|
// back through JSON.stringify, so on SQLite one visit to the tab turns
|
|
// every value it read into a doubly-encoded string. A single parse left
|
|
// a string behind, the type checks rejected it, and the defaults went
|
|
// silently dead again.
|
|
const setRaw = async (key, raw) => {
|
|
await db('app_settings')
|
|
.insert({ setting_key: key, setting_value: raw, setting_type: 'security' })
|
|
.onConflict('setting_key').merge();
|
|
};
|
|
|
|
it('reads a double-encoded boolean', async () => {
|
|
await setRaw('enable_canvas_rendering', JSON.stringify(JSON.stringify(true)));
|
|
expect(await getImageSecurityDefaults()).toEqual({ use_canvas_rendering: true });
|
|
});
|
|
|
|
it('reads a double-encoded protection level', async () => {
|
|
await setRaw('default_protection_level', JSON.stringify(JSON.stringify('enhanced')));
|
|
expect(await getImageSecurityDefaults()).toEqual({ protection_level: 'enhanced' });
|
|
});
|
|
|
|
it('reads a double-encoded integer', async () => {
|
|
await setRaw('default_image_quality', JSON.stringify(JSON.stringify(72)));
|
|
expect(await getImageSecurityDefaults()).toEqual({ image_quality: 72 });
|
|
});
|
|
|
|
it('reads a value buried under many saves, not just one', async () => {
|
|
// Each visit to the settings tab used to add a layer, so the depth is
|
|
// however many times someone opened it — not a number to cap.
|
|
let raw = JSON.stringify('maximum');
|
|
for (let i = 0; i < 8; i += 1) raw = JSON.stringify(raw);
|
|
await setRaw('default_protection_level', raw);
|
|
expect(await getImageSecurityDefaults()).toEqual({ protection_level: 'maximum' });
|
|
});
|
|
|
|
it('still rejects a malformed value however many times it was encoded', async () => {
|
|
await setRaw('default_image_quality', JSON.stringify(JSON.stringify('72oops')));
|
|
expect(await getImageSecurityDefaults()).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('readBooleanSetting shares the same decoder', () => {
|
|
// Every reader of app_settings has to agree, or the settings tab shows
|
|
// protection disabled while newly created galleries turn it on.
|
|
const setRaw2 = async (key, raw) => {
|
|
await db('app_settings')
|
|
.insert({ setting_key: key, setting_value: raw, setting_type: 'security' })
|
|
.onConflict('setting_key').merge();
|
|
};
|
|
|
|
it('reads a double-encoded false as false, not as absent', async () => {
|
|
await setRaw2('enable_devtools_protection', JSON.stringify(JSON.stringify(false)));
|
|
expect(await readBooleanSetting('enable_devtools_protection')).toBe(false);
|
|
});
|
|
|
|
it('still reads a singly-encoded value', async () => {
|
|
await setRaw2('enable_devtools_protection', JSON.stringify(true));
|
|
expect(await readBooleanSetting('enable_devtools_protection')).toBe(true);
|
|
});
|
|
|
|
it('returns undefined for a non-boolean, so the caller keeps its default', async () => {
|
|
await setRaw2('enable_devtools_protection', JSON.stringify('sometimes'));
|
|
expect(await readBooleanSetting('enable_devtools_protection')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('resolveImageSecurityColumns', () => {
|
|
it('omits every column when neither the request nor the settings supply one', () => {
|
|
expect(resolveImageSecurityColumns({}, {})).toEqual({});
|
|
});
|
|
|
|
it('uses the global default when the request says nothing', () => {
|
|
expect(resolveImageSecurityColumns({}, { protection_level: 'maximum' }))
|
|
.toEqual({ protection_level: 'maximum' });
|
|
});
|
|
|
|
it('lets an explicit request value win over the global default', () => {
|
|
expect(resolveImageSecurityColumns(
|
|
{ protection_level: 'basic' },
|
|
{ protection_level: 'maximum' },
|
|
)).toEqual({ protection_level: 'basic' });
|
|
});
|
|
|
|
it('keeps an explicit false canvas value instead of reading it as absent', () => {
|
|
const columns = resolveImageSecurityColumns(
|
|
{ use_canvas_rendering: false },
|
|
{ use_canvas_rendering: true },
|
|
);
|
|
expect(columns.use_canvas_rendering).toBeFalsy();
|
|
});
|
|
|
|
it('keeps a zero-ish explicit value rather than falling through', () => {
|
|
// 0 is out of range for the column, but the guard is `!== undefined`,
|
|
// not truthiness — the validator is what rejects out-of-range input.
|
|
expect(resolveImageSecurityColumns({ image_quality: 0 }, { image_quality: 85 }))
|
|
.toEqual({ image_quality: 0 });
|
|
});
|
|
|
|
it('resolves each column independently', () => {
|
|
expect(resolveImageSecurityColumns(
|
|
{ image_quality: 60 },
|
|
{ protection_level: 'enhanced' },
|
|
)).toEqual({
|
|
protection_level: 'enhanced',
|
|
image_quality: 60,
|
|
});
|
|
});
|
|
|
|
it('tolerates a missing body, which is what an empty API request looks like', () => {
|
|
expect(resolveImageSecurityColumns(undefined, { image_quality: 90 }))
|
|
.toEqual({ image_quality: 90 });
|
|
});
|
|
});
|
|
});
|