diff --git a/backend/__tests__/routes/adminEvents.smoke.test.js b/backend/__tests__/routes/adminEvents.smoke.test.js index fdee970b..728ceb9e 100644 --- a/backend/__tests__/routes/adminEvents.smoke.test.js +++ b/backend/__tests__/routes/adminEvents.smoke.test.js @@ -180,6 +180,27 @@ describe('admin events CRUD endpoints (smoke)', () => { }); expect(res.status).toBe(404); }); + + // #822 — hero_logo_visible/position are nullable (null = "inherit the global + // branding toggle"), but the validator used .optional() without + // { nullable: true }, so an explicit null was rejected with 400. + it('accepts hero_logo_visible: null and stores NULL (inherit)', async () => { + const id = await insertEvent(db, adminId, { hero_logo_visible: 1 }); + const res = await auth(request(app).put(`/api/admin/events/${id}`)).send({ + hero_logo_visible: null, + }); + expect(res.status).toBe(200); + const row = await db('events').where({ id }).first(); + expect(row.hero_logo_visible).toBeNull(); + }); + + it('still rejects a non-boolean hero_logo_visible', async () => { + const id = await insertEvent(db, adminId); + const res = await auth(request(app).put(`/api/admin/events/${id}`)).send({ + hero_logo_visible: 'maybe', + }); + expect(res.status).toBe(400); + }); }); describe('DELETE /:id', () => { diff --git a/backend/src/routes/adminEvents/crud.js b/backend/src/routes/adminEvents/crud.js index d8dfbf13..56b8252f 100644 --- a/backend/src/routes/adminEvents/crud.js +++ b/backend/src/routes/adminEvents/crud.js @@ -94,7 +94,7 @@ module.exports = (router) => { body('allow_presigned_download').optional().isBoolean(), body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(), // Hero logo settings - body('hero_logo_visible').optional().isBoolean(), + body('hero_logo_visible').optional({ nullable: true }).isBoolean(), body('hero_logo_size').optional({ nullable: true }).isIn(['small', 'medium', 'large', 'xlarge']), body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']), // Header style settings (decoupled from layout) @@ -342,8 +342,10 @@ module.exports = (router) => { // hero_logo_visible: store NULL ("inherit") unless the admin explicitly // set it, so the global branding_logo_display_hero toggle keeps // controlling this gallery afterwards (#756). Only an explicit per-event - // choice overrides the global. - const effectiveHeroLogoVisible = req.body.hero_logo_visible !== undefined + // choice overrides the global. `!= null` treats an explicit null the same + // as omitted (both → inherit); otherwise formatBoolean(null) would coerce + // to 0/false on SQLite instead of NULL (the PUT handler already does this). + const effectiveHeroLogoVisible = req.body.hero_logo_visible != null ? formatBoolean(hero_logo_visible) : null; // NULL = inherit the global branding_logo_size (#756), resolved at read @@ -1224,7 +1226,7 @@ module.exports = (router) => { }), body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(), // Hero logo settings - body('hero_logo_visible').optional().isBoolean(), + body('hero_logo_visible').optional({ nullable: true }).isBoolean(), body('hero_logo_size').optional({ nullable: true }).isIn(['small', 'medium', 'large', 'xlarge']), body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']), // Header style settings (decoupled from layout)