// Point the DB at a closed port before the pool is built; getPublic() is fully // monkeypatched below so no query runs, and db.close() releases the pool at the // end so the process exits cleanly. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') // Lock the /public/settings brand contract the mobile app themes itself from // (§8.6 of the Android plan). Exercises settings.getPublic() against an in-memory // fake by monkeypatching settings.db — no DB. The brand block is sourced from the // BRAND_* config defaults, with admin site_title / contact_email overriding. const settingsDb = require('../src/model/settings/settings.db') const settings = require('../src/model/settings/settings.model') const brand = require('../src/config/brand') const db = require('../src/utils/db') after(() => db.close()) let savedGetAll beforeEach(() => { savedGetAll = settingsDb.getAll settingsDb.getAll = async () => [] // no stored settings → pure BRAND_* defaults }) afterEach(() => { settingsDb.getAll = savedGetAll }) const THEMING_FIELDS = ['name', 'shortName', 'tagline', 'description', 'contactEmail', 'url', 'accent', 'logo', 'hero', 'favicon'] test('getPublic exposes the full brand theming contract the app depends on', async () => { const pub = await settings.getPublic() assert.ok(pub.brand, 'brand block present') for (const key of THEMING_FIELDS) { assert.ok(key in pub.brand, `brand.${key} present`) } // Defaults flow from BRAND_* config when nothing is stored. assert.equal(pub.brand.name, brand.name) assert.equal(pub.brand.accent, brand.accent) assert.equal(pub.brand.logo, brand.logo) assert.equal(pub.brand.hero, brand.hero) assert.equal(pub.brand.favicon, brand.favicon) // Never leak the Discord-only integer accent form to a public client. assert.equal(pub.brand.accentInt, undefined) }) test('admin site_title / contact_email override the brand defaults', async () => { settingsDb.getAll = async () => [ { key: 'site_title', value: 'My Shard' }, { key: 'contact_email', value: 'hi@shard.tld' }, ] const pub = await settings.getPublic() assert.equal(pub.brand.name, 'My Shard') // site_title overrides brand.name assert.equal(pub.brand.contactEmail, 'hi@shard.tld') // contact_email overrides assert.equal(pub.brand.accent, brand.accent) // colors still from config }) // ── Effective theming (THEMING_AND_NAV.md §4.5) ─────────────────────── // // brand.accent and the asset fields are a cross-repo contract: the Android app // themes its whole Material palette from brand.accent and the Discord bot // colors its embeds from it. Resolving the EFFECTIVE value here is what lets // both track admin theming with no client change — so these tests are really // about the app and the bot, not about the website. test('no theme row leaves the brand block exactly as env defines it', async () => { // Explicitly re-asserted next to the theming cases: this is the acceptance // criterion the whole feature rests on, and it is the assertion a future // change to the resolver would break first. const pub = await settings.getPublic() assert.equal(pub.theme, undefined, 'no theme block at all when untouched') assert.equal(pub.brand.accent, brand.accent) assert.equal(pub.brand.logo, brand.logo) assert.equal(pub.brand.hero, brand.hero) assert.equal(pub.brand.favicon, brand.favicon) }) test('a theme preset overrides brand.accent and ships the token block', async () => { settingsDb.getAll = async () => [{ key: 'theme_visual', value: JSON.stringify({ preset: 'fantasy' }) }] const pub = await settings.getPublic() assert.equal(pub.brand.accent, '#c9973f', 'the app sees the themed accent, not BRAND_ACCENT_COLOR') assert.equal(pub.theme['--accent'], '#c9973f') assert.equal(pub.theme['--bg'], '#1a120b') // Assets are a different key and must not move with the theme. assert.equal(pub.brand.logo, brand.logo) assert.equal(pub.brand.hero, brand.hero) }) test('a custom accent beats the preset accent in brand.accent', async () => { settingsDb.getAll = async () => [ { key: 'theme_visual', value: JSON.stringify({ preset: 'fantasy', custom: { colors: { accent: '#123456' } } }) }, ] const pub = await settings.getPublic() assert.equal(pub.brand.accent, '#123456') assert.equal(pub.theme['--accent'], '#123456') }) test('a malformed theme row reads as absent, not as an error', async () => { for (const value of ['{oops', '"x"', '{"preset":"parchment"}']) { settingsDb.getAll = async () => [{ key: 'theme_visual', value }] const pub = await settings.getPublic() assert.equal(pub.theme, undefined, value) assert.equal(pub.brand.accent, brand.accent, value) } }) test('brand_assets overrides one asset without disturbing the others', async () => { settingsDb.getAll = async () => [ { key: 'brand_assets', value: JSON.stringify({ favicon: '/uploads/1234-abcd.png' }) }, ] const pub = await settings.getPublic() assert.equal(pub.brand.favicon, '/uploads/1234-abcd.png') assert.equal(pub.brand.logo, brand.logo, 'logo still from env') assert.equal(pub.brand.hero, brand.hero, 'hero still from env') }) test('a malformed brand_assets row falls back to env for every asset', async () => { settingsDb.getAll = async () => [{ key: 'brand_assets', value: 'not json' }] const pub = await settings.getPublic() assert.equal(pub.brand.logo, brand.logo) assert.equal(pub.brand.hero, brand.hero) assert.equal(pub.brand.favicon, brand.favicon) }) test('the Discord-only integer accent is still never exposed, themed or not', async () => { settingsDb.getAll = async () => [{ key: 'theme_visual', value: JSON.stringify({ preset: 'modern' }) }] const pub = await settings.getPublic() assert.equal(pub.brand.accentInt, undefined) }) // The push relay block the app's embedded distributor discovers its ntfy base // URL from (M7 Part 2). Null when nothing is configured; NTFY_PUBLIC_URL wins, // else the first NTFY_ALLOWED_ORIGINS entry; NTFY_BASE_URL is never surfaced. test('push.ntfyUrl is null when no ntfy env is configured', async () => { const saved = { pub: process.env.NTFY_PUBLIC_URL, allow: process.env.NTFY_ALLOWED_ORIGINS, base: process.env.NTFY_BASE_URL } delete process.env.NTFY_PUBLIC_URL delete process.env.NTFY_ALLOWED_ORIGINS process.env.NTFY_BASE_URL = 'http://ntfy:80' // internal-only, must NOT leak try { const pub = await settings.getPublic() assert.ok(pub.push, 'push block present') assert.equal(pub.push.ntfyUrl, null) } finally { restoreNtfyEnv(saved) } }) test('push.ntfyUrl prefers NTFY_PUBLIC_URL and trims a trailing slash', async () => { const saved = { pub: process.env.NTFY_PUBLIC_URL, allow: process.env.NTFY_ALLOWED_ORIGINS, base: process.env.NTFY_BASE_URL } process.env.NTFY_PUBLIC_URL = 'https://ntfy.shard.tld/' process.env.NTFY_ALLOWED_ORIGINS = 'https://other.tld' try { const pub = await settings.getPublic() assert.equal(pub.push.ntfyUrl, 'https://ntfy.shard.tld') } finally { restoreNtfyEnv(saved) } }) test('push.ntfyUrl falls back to the first NTFY_ALLOWED_ORIGINS entry', async () => { const saved = { pub: process.env.NTFY_PUBLIC_URL, allow: process.env.NTFY_ALLOWED_ORIGINS, base: process.env.NTFY_BASE_URL } delete process.env.NTFY_PUBLIC_URL process.env.NTFY_ALLOWED_ORIGINS = 'https://ntfy.shard.tld, https://second.tld' try { const pub = await settings.getPublic() assert.equal(pub.push.ntfyUrl, 'https://ntfy.shard.tld') } finally { restoreNtfyEnv(saved) } }) function restoreNtfyEnv(saved) { for (const [name, val] of [['NTFY_PUBLIC_URL', saved.pub], ['NTFY_ALLOWED_ORIGINS', saved.allow], ['NTFY_BASE_URL', saved.base]]) { if (val === undefined) delete process.env[name] else process.env[name] = val } }