Files
picpeak/backend/__tests__/services/feedbackDefaults.test.js
T
Luca e2844d1909 feat(gallery): colour labels for client proofing, and one global default per feedback type (#1044) (#1137)
Colour labels for client proofing, plus the photographer's own stars and colours in the admin grid.

- Guest colour labels alongside likes/reactions, opt-in per event (defaults off so live galleries do not change mid-proofing), with 'colors' and 'lightroom' keybind schemes.
- One global default per feedback type, replacing the per-type scatter.
- Admin marks live in their own table (photo_admin_marks) so they can never reach a guest-facing surface.
- XMP export prefers a real label, keeping the rating-derived mapping as a fallback.

Review: concurrent-write loss on the mark update path, migration index idempotency and error classification all fixed in 7139bcae; migrations renumbered to 182/183 in 8fecdfae after 180/181 were taken on main.

Merged with admin privileges: bypass-size-gate is a required check that fails on size alone for review-bypass authors and never re-evaluates on review, which is its designed behaviour once a maintainer has approved.
2026-08-23 11:15:01 +02:00

177 lines
6.3 KiB
JavaScript

/**
* Global guest-feedback defaults (#1044).
*
* Before this module the creation defaults lived in four places that had
* already drifted: the admin create route's destructuring defaults, the v1
* create route's hard-coded insert (which omitted `allow_reactions`
* entirely), getEventFeedbackSettings()'s no-row fallback, and the migration
* column defaults. Pinned here:
*
* - with no app_settings rows, the built-in fallbacks apply (so installs
* that never open Settings > Events keep behaving exactly as before)
* - a stored global overrides its fallback, whether it was written as a JSON
* boolean, a bare "true"/"false" string, or SQLite's 0/1
* - garbage in a settings row falls back rather than writing null into a
* boolean column
* - an explicitly-sent body value always beats the global
* - keybind_mode only accepts the known schemes
* - getEventFeedbackSettings()'s no-row fallback reads the same globals
*/
const path = require('path');
const fs = require('fs');
const os = require('os');
process.env.NODE_ENV = 'test';
process.env.TEST_DATABASE_PATH = path.join(
fs.mkdtempSync(path.join(os.tmpdir(), 'picpeak-feedback-defaults-')), 'db.sqlite',
);
process.env.JWT_SECRET = process.env.JWT_SECRET || 'feedback-defaults-test-secret';
const { bootCrmDb, seedMinimal } = require('../integration/helpers/crmDb');
const {
FEEDBACK_TOGGLES,
resolveEventFeedbackDefaults,
applyFeedbackDefaults,
} = require('../../src/services/feedbackDefaults');
const feedbackService = require('../../src/services/feedbackService');
let db;
let cleanup;
async function setGlobal(key, value) {
await db('app_settings').where('setting_key', key).delete();
await db('app_settings').insert({
setting_key: key,
setting_value: value,
setting_type: 'general',
updated_at: new Date().toISOString(),
});
}
async function clearGlobals() {
await db('app_settings').where('setting_key', 'like', 'event_default_%').delete();
}
beforeAll(async () => {
({ db, cleanup } = await bootCrmDb());
await seedMinimal(db);
}, 120000);
afterAll(async () => {
if (cleanup) await cleanup();
});
beforeEach(async () => {
await clearGlobals();
});
describe('resolveEventFeedbackDefaults (#1044)', () => {
it('falls back to the built-ins when nothing is configured', async () => {
const defaults = await resolveEventFeedbackDefaults();
expect(defaults).toEqual({
allow_ratings: true,
allow_likes: true,
allow_favorites: true,
allow_comments: true,
allow_reactions: true,
// Colour labels are opt-in so no existing gallery grows a colour bar
// on upgrade.
allow_color_labels: false,
keybind_mode: 'colors',
});
});
it('covers every toggle the UI shows — no type can be silently missing', async () => {
const defaults = await resolveEventFeedbackDefaults();
for (const toggle of FEEDBACK_TOGGLES) {
expect(defaults).toHaveProperty(toggle.column);
}
});
it('lets a stored global override its fallback', async () => {
await setGlobal('event_default_allow_comments', JSON.stringify(false));
await setGlobal('event_default_allow_color_labels', JSON.stringify(true));
const defaults = await resolveEventFeedbackDefaults();
expect(defaults.allow_comments).toBe(false);
expect(defaults.allow_color_labels).toBe(true);
expect(defaults.allow_likes).toBe(true); // untouched
});
it('accepts the value shapes older writers left behind', async () => {
await setGlobal('event_default_allow_likes', 'false'); // bare string, not JSON-quoted
await setGlobal('event_default_allow_ratings', '0'); // SQLite boolean
const defaults = await resolveEventFeedbackDefaults();
expect(defaults.allow_likes).toBe(false);
expect(defaults.allow_ratings).toBe(false);
});
it('falls back rather than writing garbage into a boolean column', async () => {
await setGlobal('event_default_allow_favorites', 'not-a-boolean');
const defaults = await resolveEventFeedbackDefaults();
expect(defaults.allow_favorites).toBe(true);
});
it('only accepts known keybind schemes', async () => {
await setGlobal('event_default_keybind_mode', JSON.stringify('lightroom'));
expect((await resolveEventFeedbackDefaults()).keybind_mode).toBe('lightroom');
await setGlobal('event_default_keybind_mode', JSON.stringify('vim'));
expect((await resolveEventFeedbackDefaults()).keybind_mode).toBe('colors');
});
});
describe('applyFeedbackDefaults (#1044)', () => {
const globals = {
allow_ratings: true,
allow_likes: true,
allow_favorites: true,
allow_comments: true,
allow_reactions: true,
allow_color_labels: false,
keybind_mode: 'colors',
};
it('inherits every value the caller omitted', () => {
expect(applyFeedbackDefaults({}, globals)).toEqual(globals);
expect(applyFeedbackDefaults(undefined, globals)).toEqual(globals);
});
it('lets an explicit body value win — including an explicit false', () => {
const resolved = applyFeedbackDefaults(
{ allow_likes: false, allow_color_labels: true, keybind_mode: 'lightroom' },
globals,
);
expect(resolved.allow_likes).toBe(false);
expect(resolved.allow_color_labels).toBe(true);
expect(resolved.keybind_mode).toBe('lightroom');
expect(resolved.allow_ratings).toBe(true); // still inherited
});
it('ignores an unknown keybind mode from the body', () => {
expect(applyFeedbackDefaults({ keybind_mode: 'emacs' }, globals).keybind_mode).toBe('colors');
});
it('does not mutate the globals object it was handed', () => {
const snapshot = { ...globals };
applyFeedbackDefaults({ allow_likes: false }, globals);
expect(globals).toEqual(snapshot);
});
});
describe('getEventFeedbackSettings no-row fallback (#1044)', () => {
it('reads the same globals instead of a fourth hard-coded copy', async () => {
await setGlobal('event_default_allow_comments', JSON.stringify(false));
await setGlobal('event_default_allow_color_labels', JSON.stringify(true));
// An event id with no event_feedback_settings row.
const settings = await feedbackService.getEventFeedbackSettings(999999);
expect(settings.feedback_enabled).toBe(false); // no row still means feedback off
expect(settings.allow_comments).toBe(false);
expect(settings.allow_color_labels).toBe(true);
expect(settings.keybind_mode).toBe('colors');
});
});