mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
Eviction now goes by the base's signal: `storeLost` and the `store-reset` notice, with `bulwark-store-opened` as the marker and `prune-files` for files with no key. The storage page and the lost count hang off that. What a user picks is stored under `bulwark-offline-scope`, the key `offlineScopeFor` reads, and `email-snapshot` follows the base's rule. `OfflineEnabled` is read from the desktop bridge when it's there.
170 lines
7.8 KiB
TypeScript
170 lines
7.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } 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 });
|
|
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).toEqual({ days: 60, attachments: true, maxAttachmentBytes: 10 * MB, maxTotalBytes: 100 * MB });
|
|
expect(capped.locked).toMatchObject({ days: true, maxAttachment: true, maxTotal: true, attachments: false });
|
|
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, and has no store to apply them to', 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 });
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|