Files
TREK/server/tests/unit/nest/admin-default-user-settings.controller.test.ts
T
Maurice 8a09f55571 Give three more admin surfaces back to the domains that own them
default-user-settings, notification-preferences and oidc leave AdminController.
The first two were pure pass-throughs on AdminService that did nothing but
forward and cast; the third had its SQL there for a domain that already had a
module of its own. AdminService loses SettingsService and
NotificationPreferencesService entirely.

Two things came out of the move rather than going into it.

The OIDC lockout guard now has tests. It refuses to clear the SSO config while
password login is disabled — otherwise the instance has no SSO to log in through
and no password form either — and nothing covered that branch before, because it
sat among admin tests that only exercised the happy path.

The admin e2e app had to learn about the split. It assembles AdminModule alone,
so the moved routes 404'd there while working in production; it now imports the
three domains too. That is the same failure mode the module-provider guards
catch for RPC classes, and it is worth knowing that an e2e app which builds a
subset will find it first.

Also fixes the audit route note: audit-log stays on AdminController on purpose.
AuthModule imports AuditModule, so an admin-gated controller in AuditModule
closes a cycle — the same trap the addons toggles hit.
2026-08-09 20:54:39 +02:00

83 lines
3.3 KiB
TypeScript

/**
* The account defaults, after the two routes moved off AdminController next to
* SettingsService, which owns them. Same path, same audit action, same 400 envelope on
* a rejected write — these cases came over with the routes.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HttpException } from '@nestjs/common';
import { AdminDefaultUserSettingsController } from '../../../src/nest/settings/settings.controller';
import { SettingsModule } from '../../../src/nest/settings/settings.module';
import type { SettingsService } from '../../../src/nest/settings/settings.service';
import type { AuditService } from '../../../src/nest/audit/audit.service';
import type { User } from '../../../src/types';
import { expectRegisteredController } from '../../helpers/module-providers';
const user = { id: 1, role: 'admin' } as User;
const req = { headers: {}, socket: {} } as never;
const writeAudit = vi.fn();
function controller(over: Partial<SettingsService> = {}) {
const settings = {
getAdminUserDefaults: vi.fn(() => ({ theme: 'dark' })),
setAdminUserDefaults: vi.fn(),
...over,
} as unknown as SettingsService;
return { c: new AdminDefaultUserSettingsController(settings, { writeAudit } as unknown as AuditService), settings };
}
const thrown = (run: () => unknown) => {
try {
run();
return null;
} catch (e) {
return e instanceof HttpException ? { status: e.getStatus(), body: e.getResponse() } : e;
}
};
describe('AdminDefaultUserSettingsController', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('DEFAULTS-001 GET returns the stored defaults verbatim', () => {
expect(controller().c.get()).toEqual({ theme: 'dark' });
});
it('DEFAULTS-002 PUT writes, audits, and answers with the STORED defaults', () => {
const { c, settings } = controller();
// Not the request body: the service normalises and drops unknown keys, and the
// admin panel renders straight from this response.
expect(c.update(user, { theme: 'light' } as never, req)).toEqual({ theme: 'dark' });
expect(settings.setAdminUserDefaults).toHaveBeenCalledWith({ theme: 'light' });
expect(writeAudit).toHaveBeenCalledWith(
expect.objectContaining({ userId: 1, action: 'admin.default_user_settings_update', details: { theme: 'light' } }),
);
});
it('DEFAULTS-003 a rejected write is a 400 carrying the message, and is not audited', () => {
const { c } = controller({
setAdminUserDefaults: vi.fn(() => {
throw new Error('unknown setting: nope');
}),
} as Partial<SettingsService>);
expect(thrown(() => c.update(user, { nope: 1 } as never, req))).toEqual({
status: 400,
body: { error: 'unknown setting: nope' },
});
expect(writeAudit).not.toHaveBeenCalled();
});
it('DEFAULTS-004 a non-Error throw is stringified rather than swallowed', () => {
const { c } = controller({
setAdminUserDefaults: vi.fn(() => {
throw 'plain string';
}),
} as Partial<SettingsService>);
expect(thrown(() => c.update(user, {} as never, req))).toEqual({ status: 400, body: { error: 'plain string' } });
});
it('DEFAULTS-005 the class is listed in its module controllers', () => {
expectRegisteredController(SettingsModule, AdminDefaultUserSettingsController);
});
});