Files
webmail/lib/__tests__/lite-config.test.ts
T
lucamzanon 9560a8b74f feat: optional encrypted backup of the account list
Users with several accounts can save the list of accounts in their browser
(address, server, label, colour) on the webmail server and bring it back on
another device. The list is encrypted in the browser with a password the user
chooses (PBKDF2-SHA-256, 600,000 iterations, AES-256-GCM, owner bound as
additional data); the server stores only the envelope, one file per owner
under SETTINGS_DATA_DIR/account-archives. No mailbox passwords are included.

Off by default behind the admin feature gate accountArchiveEnabled, pinned
off in Lite. Every method of /api/account-archive needs a session cookie of
the owner, matched without regard to case, whose credential is checked
against the mail server on each call; listing included.

Restored accounts this browser lacks are listed as awaiting sign-in: they
survive a reload, and opening one leads to the sign-in form with the address
filled in. Accounts already present get their archived label and colour.
2026-10-09 10:54:38 +02:00

144 lines
6.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { applyLiteConfig, applyLitePolicy, liteStalwartDefaults, LITE_FORCED_FLAGS } from '@/lib/lite-config';
import { DEFAULT_POLICY } from '@/lib/admin/types';
describe('applyLiteConfig (config.json for the static build)', () => {
it('fills every field with the defaults useConfig assumes when the file is empty', () => {
const config = applyLiteConfig({});
expect(config.appName).toBe('Webmail');
expect(config.jmapServerUrl).toBe('');
expect(config.rememberMeEnabled).toBe(true);
expect(config.loginShowTotp).toBe(true);
expect(config.faviconUrl).toBe('/branding/Bulwark_Favicon.svg');
expect(config.loginLogoLightUrl).toBe('/branding/Bulwark_Logo_Color.svg');
expect(config.jmapServers).toEqual([]);
expect(config.demoMode).toBe(false);
});
it('allows a custom endpoint by default only when no server is configured', () => {
expect(applyLiteConfig({}).allowCustomJmapEndpoint).toBe(true);
expect(applyLiteConfig({ jmapServerUrl: 'https://mail.example.com' }).allowCustomJmapEndpoint).toBe(false);
expect(applyLiteConfig({ jmapServers: [{ id: 'eu', url: 'https://eu.example.com' }] }).allowCustomJmapEndpoint).toBe(false);
// An explicit value always wins.
expect(applyLiteConfig({ jmapServerUrl: 'https://mail.example.com', allowCustomJmapEndpoint: true }).allowCustomJmapEndpoint).toBe(true);
});
it('trims trailing slashes from server URLs', () => {
const config = applyLiteConfig({
jmapServerUrl: 'https://mail.example.com///',
jmapServers: [{ id: 'eu', label: 'EU', url: 'https://eu.example.com/', domains: ['example.com', 42] }],
});
expect(config.jmapServerUrl).toBe('https://mail.example.com');
expect(config.jmapServers).toEqual([{ id: 'eu', label: 'EU', url: 'https://eu.example.com', domains: ['example.com'] }]);
});
it('drops malformed server entries and labels unnamed ones by id', () => {
const config = applyLiteConfig({ jmapServers: [{ id: 'a', url: 'https://a.example' }, { url: 'nope' }, 'junk', null] });
expect(config.jmapServers).toEqual([{ id: 'a', label: 'a', url: 'https://a.example', domains: [] }]);
});
it('pins every server-backed flag off no matter what the file says', () => {
const config = applyLiteConfig({
oauthEnabled: true,
oauthOnly: true,
autoSsoEnabled: true,
settingsSyncEnabled: true,
stalwartFeaturesEnabled: true,
stalwartJmapPassthroughEnabled: true,
devMode: true,
});
for (const [key, value] of Object.entries(LITE_FORCED_FLAGS)) {
expect(config[key as keyof typeof LITE_FORCED_FLAGS]).toBe(value);
}
expect(config.oauthClientId).toBe('');
});
it('ignores values of the wrong type instead of crashing', () => {
const config = applyLiteConfig({ appName: 12, rememberMeEnabled: 'yes', loginShowVersion: 0, jmapServers: 'x' });
expect(config.appName).toBe('Webmail');
expect(config.rememberMeEnabled).toBe(true);
expect(config.loginShowVersion).toBe(true);
expect(config.jmapServers).toEqual([]);
expect(applyLiteConfig(null).appName).toBe('Webmail');
expect(applyLiteConfig('garbage').appName).toBe('Webmail');
});
it('keeps an http(s) source code URL and drops any other scheme', () => {
expect(applyLiteConfig({}).sourceCodeUrl).toBe('');
expect(applyLiteConfig({ sourceCodeUrl: 'https://git.example.com/acme/webmail' }).sourceCodeUrl)
.toBe('https://git.example.com/acme/webmail');
expect(applyLiteConfig({ sourceCodeUrl: 'javascript:alert(1)' }).sourceCodeUrl).toBe('');
});
it('keeps deployer-facing branding and login toggles', () => {
const config = applyLiteConfig({
appName: 'Acme Mail',
loginCompanyName: 'Acme',
loginImprintUrl: 'https://acme.example/imprint',
loginShowHeading: false,
demoMode: true,
embeddedMode: true,
parentOrigin: 'https://intranet.acme.example',
});
expect(config.appName).toBe('Acme Mail');
expect(config.loginCompanyName).toBe('Acme');
expect(config.loginImprintUrl).toBe('https://acme.example/imprint');
expect(config.loginShowHeading).toBe(false);
expect(config.demoMode).toBe(true);
expect(config.embeddedMode).toBe(true);
expect(config.parentOrigin).toBe('https://intranet.acme.example');
});
});
describe('applyLitePolicy (policy.json for the static build)', () => {
it('returns the defaults with plugins and sidebar apps pinned off for an empty file', () => {
const policy = applyLitePolicy({});
expect(policy.features.pluginsEnabled).toBe(false);
expect(policy.features.sidebarAppsEnabled).toBe(false);
expect(policy.features.pluginsUploadEnabled).toBe(false);
expect(policy.features.calendarEnabled).toBe(DEFAULT_POLICY.features.calendarEnabled);
expect(policy.restrictions).toEqual({});
expect(policy.pushRelays).toEqual([]);
});
it('honours deployer gates and restrictions but never re-enables plugins', () => {
const policy = applyLitePolicy({
features: { pluginsEnabled: true, sidebarAppsEnabled: true, calendarEnabled: false, accountArchiveEnabled: true },
restrictions: { theme: { locked: true, value: 'dark' } },
forceEnabledPlugins: ['evil'],
defaultSidebarApps: [{ id: 'x' }],
});
expect(policy.features.pluginsEnabled).toBe(false);
expect(policy.features.sidebarAppsEnabled).toBe(false);
expect(policy.features.accountArchiveEnabled).toBe(false);
expect(policy.features.calendarEnabled).toBe(false);
expect(policy.restrictions.theme).toEqual({ locked: true, value: 'dark' });
expect(policy.forceEnabledPlugins).toEqual([]);
expect(policy.defaultSidebarApps).toEqual([]);
});
it('tolerates junk input', () => {
expect(applyLitePolicy(null).features.themesEnabled).toBe(true);
expect(applyLitePolicy('nope').features.pluginsEnabled).toBe(false);
});
});
describe('applyLiteConfig for the Stalwart bundle (same-origin defaults)', () => {
it('uses the page origin when config.json names no server, and hides the server field', () => {
const defaults = liteStalwartDefaults();
expect(defaults.jmapServerUrl).toBe(window.location.origin);
const config = applyLiteConfig({}, defaults);
expect(config.jmapServerUrl).toBe(window.location.origin);
expect(config.allowCustomJmapEndpoint).toBe(false);
expect(config.rememberMeEnabled).toBe(true);
expect(config.demoMode).toBe(false);
expect(applyLiteConfig({ jmapServerUrl: '' }, { jmapServerUrl: 'https://mail.example.com/' }).jmapServerUrl).toBe('https://mail.example.com');
});
it('still lets a custom build point elsewhere or show the field explicitly', () => {
const defaults = { jmapServerUrl: 'https://mail.example.com' };
expect(applyLiteConfig({ jmapServerUrl: 'https://other.example.com' }, defaults).jmapServerUrl).toBe('https://other.example.com');
expect(applyLiteConfig({ allowCustomJmapEndpoint: true }, defaults).allowCustomJmapEndpoint).toBe(true);
});
});