mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
A persisted account id with no subject yet read as decided: Everyone's noOffline wiped mail a group scope allowed, and a wider scope went out at every reload. A public, partial policy counted as the organisation's word, and turned on the desktop default in a forbidding organisation. - an organisation's cap on one file now binds a pin - Clear and the off switch read what would be lost when asked, messages, changes and drafts, and Clear waits for a connection - turning one account off survives the last one going off, and off then on no longer warns of an eviction - a tab that takes over the store sends no scope until it has heard the policy - a dropped account is judged by what applies to anyone - a choice reaches the store once it settles; progress announces phases only - restored Pro tabs ask for their titles
190 lines
8.8 KiB
TypeScript
190 lines
8.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { DEFAULT_ORGANISATION_POLICY } from '@/lib/admin/organisation-policy';
|
|
import { DEFAULT_SCOPE } from '@/lib/store/area';
|
|
import {
|
|
attachmentCapOptions,
|
|
effectiveScope,
|
|
LEGACY_CHOICE,
|
|
offlineDefaultOn,
|
|
olderThanCap,
|
|
platformDefault,
|
|
userChoiceFor,
|
|
windowOptions,
|
|
} from '../scope';
|
|
import { offlineScopeFor, useOfflineStore } from '../state';
|
|
|
|
const NO_LIMITS = DEFAULT_ORGANISATION_POLICY.devices;
|
|
const MB = 1024 * 1024;
|
|
const BROWSER = { on: false, scope: DEFAULT_SCOPE };
|
|
const DESKTOP = { on: true, scope: DEFAULT_SCOPE };
|
|
|
|
describe('effectiveScope', () => {
|
|
it('the default when nothing else says', () => {
|
|
expect(effectiveScope('a', null, NO_LIMITS, BROWSER)).toMatchObject({ account: 'a', enabled: false, scope: DEFAULT_SCOPE });
|
|
expect(effectiveScope('a', null, NO_LIMITS, DESKTOP).enabled).toBe(true);
|
|
});
|
|
|
|
it('the user over the default', () => {
|
|
const effective = effectiveScope('a', { on: true, days: 30, attachments: false, maxAttachmentBytes: 5 * MB }, NO_LIMITS, BROWSER);
|
|
expect(effective.enabled).toBe(true);
|
|
expect(effective.scope).toEqual({ ...DEFAULT_SCOPE, days: 30, attachments: false, maxAttachmentBytes: 5 * MB, orgMaxFileBytes: null });
|
|
expect(effectiveScope('a', { on: false }, NO_LIMITS, DESKTOP).enabled).toBe(false);
|
|
expect(effectiveScope('a', { days: null }, NO_LIMITS, BROWSER).scope.days).toBeNull();
|
|
});
|
|
|
|
it('the organisation over the user and over the desktop default', () => {
|
|
const policy = { ...NO_LIMITS, noOffline: true };
|
|
expect(effectiveScope('a', { on: true }, policy, BROWSER).enabled).toBe(false);
|
|
expect(effectiveScope('a', null, policy, DESKTOP).enabled).toBe(false);
|
|
expect(effectiveScope('a', { on: true }, policy, DESKTOP).locked.offline).toBe(true);
|
|
});
|
|
|
|
it('a cap lowers the choice and the default, also "everything", and never raises', () => {
|
|
const policy = { ...NO_LIMITS, offlineMaxDays: 60, offlineMaxAttachmentMb: 10, offlineMaxTotalMb: 100 };
|
|
const capped = effectiveScope('a', { days: null, maxAttachmentBytes: 500 * MB }, policy, DESKTOP);
|
|
expect(capped.scope).toMatchObject({ days: 60, attachments: true, maxAttachmentBytes: 10 * MB, maxTotalBytes: 100 * MB });
|
|
expect(capped.locked).toMatchObject({ days: true, maxAttachment: true, maxTotal: true, attachments: false });
|
|
// Sent on its own: a pin obeys this one, not the user's size.
|
|
expect(capped.scope.orgMaxFileBytes).toBe(10 * MB);
|
|
expect(effectiveScope('a', { maxAttachmentBytes: 5 * MB }, NO_LIMITS, BROWSER).scope.orgMaxFileBytes).toBeNull();
|
|
expect(effectiveScope('a', null, { ...policy, offlineMaxDays: 30 }, DESKTOP).scope.days).toBe(30);
|
|
const under = effectiveScope('a', { days: 30, maxAttachmentBytes: 5 * MB }, policy, BROWSER);
|
|
expect(under.scope.days).toBe(30);
|
|
expect(under.scope.maxAttachmentBytes).toBe(5 * MB);
|
|
});
|
|
|
|
it('forbidden attachments stay off whatever the user chose', () => {
|
|
const effective = effectiveScope('a', { attachments: true }, { ...NO_LIMITS, noOfflineAttachments: true }, DESKTOP);
|
|
expect(effective.scope.attachments).toBe(false);
|
|
expect(effective.locked.attachments).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('the platform default', () => {
|
|
afterEach(() => { delete (window as { bulwarkDesktop?: unknown }).bulwarkDesktop; });
|
|
|
|
it('is off in a browser and on with the desktop bridge', () => {
|
|
expect(offlineDefaultOn()).toBe(false);
|
|
expect(platformDefault().on).toBe(false);
|
|
(window as { bulwarkDesktop?: unknown }).bulwarkDesktop = { version: 1 };
|
|
expect(offlineDefaultOn()).toBe(true);
|
|
expect(platformDefault().on).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('the OfflineEnabled policy of the desktop app', () => {
|
|
const withPolicy = (OfflineEnabled: unknown) => {
|
|
(window as { bulwarkDesktop?: unknown }).bulwarkDesktop = { version: 1, policy: { get: () => ({ OfflineEnabled }) } };
|
|
};
|
|
afterEach(() => { delete (window as { bulwarkDesktop?: unknown }).bulwarkDesktop; });
|
|
|
|
it('recommended off changes the default, and a user can still turn it on', () => {
|
|
withPolicy({ value: false, locked: false, source: 'machine' });
|
|
expect(offlineDefaultOn()).toBe(false);
|
|
expect(effectiveScope('a', null, NO_LIMITS, platformDefault()).enabled).toBe(false);
|
|
expect(effectiveScope('a', { on: true }, NO_LIMITS, platformDefault()).enabled).toBe(true);
|
|
});
|
|
|
|
it('the shipped default is no recommendation', () => {
|
|
withPolicy({ value: false, locked: false, source: 'default' });
|
|
expect(offlineDefaultOn()).toBe(true);
|
|
});
|
|
|
|
it('forced beats what the user chose, either way', () => {
|
|
withPolicy({ value: true, locked: true, source: 'machine' });
|
|
expect(effectiveScope('a', { on: false }, NO_LIMITS, platformDefault())).toMatchObject({ enabled: true, locked: { forced: true } });
|
|
withPolicy({ value: false, locked: true, source: 'machine' });
|
|
expect(effectiveScope('a', { on: true }, NO_LIMITS, platformDefault()).enabled).toBe(false);
|
|
});
|
|
|
|
it('the organisation still beats it', () => {
|
|
withPolicy({ value: true, locked: true, source: 'machine' });
|
|
expect(effectiveScope('a', null, { ...NO_LIMITS, noOffline: true }, platformDefault()).enabled).toBe(false);
|
|
});
|
|
|
|
it('a shell without the member, or a member that throws, is the plain default', () => {
|
|
(window as { bulwarkDesktop?: unknown }).bulwarkDesktop = { version: 1 };
|
|
expect(offlineDefaultOn()).toBe(true);
|
|
(window as { bulwarkDesktop?: unknown }).bulwarkDesktop = { version: 1, policy: { get: () => { throw new Error('gone'); } } };
|
|
expect(platformDefault()).toMatchObject({ on: true, forced: null });
|
|
});
|
|
});
|
|
|
|
describe('the seam for what a user chose', () => {
|
|
it('reads bulwark-offline-scope over the defaults, without the switch', () => {
|
|
localStorage.setItem('bulwark-offline-scope', JSON.stringify({ acc: { on: true, days: 30 } }));
|
|
expect(offlineScopeFor('acc')).toEqual({ ...DEFAULT_SCOPE, days: 30 });
|
|
expect(offlineScopeFor('other')).toEqual(DEFAULT_SCOPE);
|
|
localStorage.removeItem('bulwark-offline-scope');
|
|
});
|
|
});
|
|
|
|
describe('what the user chose', () => {
|
|
beforeEach(() => useOfflineStore.setState({ enabled: [], choices: {} }));
|
|
|
|
it('nothing for an account that never had it on', () => {
|
|
expect(userChoiceFor('a')).toBeNull();
|
|
});
|
|
|
|
it('an account on from before the local store keeps the 30 days it had', () => {
|
|
useOfflineStore.setState({ enabled: ['a'] });
|
|
expect(userChoiceFor('a')).toEqual(LEGACY_CHOICE);
|
|
expect(LEGACY_CHOICE.days).toBe(30);
|
|
expect(effectiveScope('a', userChoiceFor('a'), NO_LIMITS, BROWSER).scope.days).toBe(30);
|
|
});
|
|
|
|
it('a stored choice wins', () => {
|
|
useOfflineStore.setState({ enabled: ['a'], choices: { a: { on: true, days: 365 } } });
|
|
expect(userChoiceFor('a')?.days).toBe(365);
|
|
});
|
|
});
|
|
|
|
describe('what the page offers', () => {
|
|
it('every window without a cap', () => {
|
|
expect(windowOptions(0)).toEqual([30, 90, 365, null]);
|
|
});
|
|
|
|
it('what fits under a cap, and the cap itself', () => {
|
|
expect(windowOptions(90)).toEqual([30, 90]);
|
|
expect(windowOptions(60)).toEqual([30, 60]);
|
|
expect(windowOptions(7)).toEqual([7]);
|
|
expect(attachmentCapOptions(0)).toEqual([5, 25, 100, 500]);
|
|
expect(attachmentCapOptions(10)).toEqual([5, 10]);
|
|
});
|
|
});
|
|
|
|
describe('Lite', () => {
|
|
it('reads the caps from the same policy.json', async () => {
|
|
const { applyLitePolicy } = await import('@/lib/lite-config');
|
|
const policy = applyLitePolicy({ organisation: { devices: { offlineMaxDays: 30, noOfflineAttachments: true, offlineMaxTotalMb: 'lots' } } } as never);
|
|
expect(policy.organisation?.devices).toMatchObject({ offlineMaxDays: 30, noOfflineAttachments: true, offlineMaxTotalMb: 0 });
|
|
// A cap is lower than anything, also where nothing else is known.
|
|
expect(effectiveScope('a', { days: null }, policy.organisation!.devices, BROWSER).scope).toMatchObject({ days: 30, attachments: false });
|
|
});
|
|
|
|
it('has no store to apply them to, whatever a caller asks', async () => {
|
|
vi.resetModules();
|
|
vi.doMock('@/lib/lite', async (original) => ({ ...(await original<typeof import('@/lib/lite')>()), IS_LITE: true }));
|
|
try {
|
|
const { enableOffline, offlineSupported } = await import('../offline-mode');
|
|
const { useOfflineStore: lite } = await import('../state');
|
|
expect(offlineSupported()).toBe(false);
|
|
await enableOffline('a');
|
|
expect(lite.getState().enabled).toEqual([]);
|
|
} finally {
|
|
vi.doUnmock('@/lib/lite');
|
|
vi.resetModules();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('olderThanCap', () => {
|
|
const now = Date.UTC(2026, 10, 3);
|
|
it('only with a cap, and only past it', () => {
|
|
expect(olderThanCap('2026-01-01T00:00:00Z', 0, now)).toBe(false);
|
|
expect(olderThanCap('2026-01-01T00:00:00Z', 30, now)).toBe(true);
|
|
expect(olderThanCap('2026-10-30T00:00:00Z', 30, now)).toBe(false);
|
|
expect(olderThanCap(undefined, 30, now)).toBe(false);
|
|
});
|
|
});
|