Files
picpeak/backend/__tests__/integration/imageSecurityDefaults.test.js
T
Paul Nothaft 7ff8caf9d7 fix: remove the fragmentation handling stranded by #1303
#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
2026-09-05 23:41:17 +02:00

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