168 lines
5.6 KiB
TypeScript
168 lines
5.6 KiB
TypeScript
process.env.DATABASE_PATH = ':memory:';
|
|
process.env.THEMES_DIR = './themes';
|
|
process.env.FRONTEND_DIST = './frontend/dist';
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { AdminGeneralService } from '../../backend/src/modules/admin/general.service';
|
|
import { GeneralSettingsSchema } from '../../backend/src/modules/admin/dto/general.dto';
|
|
import { THEME_IDS } from '../../backend/src/common/types/theme-ids';
|
|
|
|
function makeFakeThemeLoader() {
|
|
return THEME_IDS.map((id) => ({ id, name: id.charAt(0).toUpperCase() + id.slice(1), tokens: {} as any }));
|
|
}
|
|
|
|
describe('GeneralSettingsSchema - validation rules', () => {
|
|
it('rejects end <= start with a validation error', () => {
|
|
const r = GeneralSettingsSchema.safeParse({
|
|
pageTitle: 'T',
|
|
logo: '',
|
|
welcomeMarkdown: '',
|
|
themeKey: 'classic',
|
|
eventStartUtc: '2026-02-01T00:00:00Z',
|
|
eventEndUtc: '2026-01-01T00:00:00Z',
|
|
defaultChallengeIp: '127.0.0.1',
|
|
registrationsEnabled: false,
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it('rejects unknown themeKey', () => {
|
|
const r = GeneralSettingsSchema.safeParse({
|
|
pageTitle: 'T',
|
|
logo: '',
|
|
welcomeMarkdown: '',
|
|
themeKey: 'baroque',
|
|
eventStartUtc: '2026-01-01T00:00:00Z',
|
|
eventEndUtc: '2026-02-01T00:00:00Z',
|
|
defaultChallengeIp: '127.0.0.1',
|
|
registrationsEnabled: false,
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it('accepts a valid payload', () => {
|
|
const r = GeneralSettingsSchema.safeParse({
|
|
pageTitle: 'Hello',
|
|
logo: '',
|
|
welcomeMarkdown: '# x',
|
|
themeKey: 'classic',
|
|
eventStartUtc: '2026-01-01T00:00:00Z',
|
|
eventEndUtc: '2026-02-01T00:00:00Z',
|
|
defaultChallengeIp: '127.0.0.1',
|
|
registrationsEnabled: true,
|
|
});
|
|
expect(r.success).toBe(true);
|
|
});
|
|
|
|
it('rejects a whitespace-only pageTitle', () => {
|
|
const r = GeneralSettingsSchema.safeParse({
|
|
pageTitle: ' ',
|
|
logo: '',
|
|
welcomeMarkdown: '',
|
|
themeKey: 'classic',
|
|
eventStartUtc: '2026-01-01T00:00:00Z',
|
|
eventEndUtc: '2026-02-01T00:00:00Z',
|
|
defaultChallengeIp: '127.0.0.1',
|
|
registrationsEnabled: false,
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it('trims surrounding whitespace from a valid pageTitle', () => {
|
|
const r = GeneralSettingsSchema.safeParse({
|
|
pageTitle: ' OpenVelo ',
|
|
logo: '',
|
|
welcomeMarkdown: '',
|
|
themeKey: 'classic',
|
|
eventStartUtc: '2026-01-01T00:00:00Z',
|
|
eventEndUtc: '2026-02-01T00:00:00Z',
|
|
defaultChallengeIp: '127.0.0.1',
|
|
registrationsEnabled: false,
|
|
});
|
|
expect(r.success).toBe(true);
|
|
if (r.success) {
|
|
expect(r.data.pageTitle).toBe('OpenVelo');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('AdminGeneralService.updateSettings - happy path', () => {
|
|
it('persists all keys and emits a settings event', async () => {
|
|
const stored: Record<string, string> = {};
|
|
const fakeSettings = {
|
|
get: jest.fn().mockImplementation(async (k: string, d: string) => (k in stored ? stored[k] : d)),
|
|
set: jest.fn().mockImplementation(async (k: string, v: string) => { stored[k] = v; }),
|
|
} as any;
|
|
const fakeHub = { emitEvent: jest.fn() } as any;
|
|
const fakeThemes: any = { listThemes: () => makeFakeThemeLoader() };
|
|
const fakeConfig: any = { get: jest.fn().mockReturnValue('./themes') };
|
|
const svc = new AdminGeneralService(fakeSettings, fakeThemes, fakeHub, fakeConfig);
|
|
const updated = await svc.updateSettings({
|
|
pageTitle: 'Hello',
|
|
logo: '',
|
|
welcomeMarkdown: '# x',
|
|
themeKey: 'classic',
|
|
eventStartUtc: '2026-01-01T00:00:00Z',
|
|
eventEndUtc: '2026-02-01T00:00:00Z',
|
|
defaultChallengeIp: '127.0.0.1',
|
|
registrationsEnabled: true,
|
|
});
|
|
expect(updated.pageTitle).toBe('Hello');
|
|
expect(updated.registrationsEnabled).toBe(true);
|
|
expect(fakeHub.emitEvent).toHaveBeenCalledWith(expect.objectContaining({ topic: 'general' }));
|
|
});
|
|
});
|
|
|
|
describe('AdminGeneralService.listThemes - filter to on-disk themes', () => {
|
|
let themesDir: string;
|
|
|
|
beforeAll(() => {
|
|
themesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'admin-themes-'));
|
|
process.env.THEMES_DIR = themesDir;
|
|
for (const id of THEME_IDS) {
|
|
fs.writeFileSync(path.join(themesDir, `${id}.json`), JSON.stringify({
|
|
id,
|
|
name: id,
|
|
tokens: {
|
|
primary: '#000', secondary: '#111', accent: '#222', surface: '#fff', text: '#000',
|
|
success: '#0f0', warning: '#ff0', danger: '#f00',
|
|
fontFamily: 'sans', radii: { sm: '1px', md: '2px', lg: '3px' },
|
|
spacingScale: [4, 8, 12],
|
|
},
|
|
}));
|
|
}
|
|
});
|
|
|
|
it('returns the 10 theme entries that have on-disk files', () => {
|
|
const svc = new AdminGeneralService(
|
|
{ get: jest.fn(), set: jest.fn() } as any,
|
|
{ listThemes: () => makeFakeThemeLoader() } as any,
|
|
{ emitEvent: jest.fn() } as any,
|
|
{ get: jest.fn().mockReturnValue(themesDir) } as any,
|
|
);
|
|
const themes = svc.listThemes();
|
|
expect(themes.length).toBe(10);
|
|
for (const t of themes) {
|
|
expect(t.id).toBe(t.key);
|
|
expect(typeof t.name).toBe('string');
|
|
}
|
|
});
|
|
|
|
it('skips themes whose JSON file is missing on disk', () => {
|
|
fs.unlinkSync(path.join(themesDir, 'classic.json'));
|
|
const svc = new AdminGeneralService(
|
|
{ get: jest.fn(), set: jest.fn() } as any,
|
|
{ listThemes: () => makeFakeThemeLoader() } as any,
|
|
{ emitEvent: jest.fn() } as any,
|
|
{ get: jest.fn().mockReturnValue(themesDir) } as any,
|
|
);
|
|
const themes = svc.listThemes();
|
|
expect(themes.find((t) => t.id === 'classic')).toBeUndefined();
|
|
expect(themes.length).toBe(9);
|
|
});
|
|
});
|
|
|