feat: Admin Area General Settings and Categories
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user