mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
test: catch a wipe heard twice, a row put back and a mark left by a recovered refusal
This commit is contained in:
@@ -199,9 +199,23 @@ describe('the rules', () => {
|
||||
expect(await registry.getDevice(waiting.id)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('prunes the user\'s own rows also in the hour after the registry-wide look', async () => {
|
||||
const registry = await import('../registry');
|
||||
const start = Date.now() + 2 * 365 * 24 * 60 * 60 * 1000;
|
||||
vi.useFakeTimers({ now: start, toFake: ['Date'] });
|
||||
const old = await registry.registerDevice('alice@example.org', 'https://mail.example.org', info);
|
||||
// Someone else's registration has the look just before the row goes stale.
|
||||
vi.setSystemTime(start + registry.DEVICE_PRUNE_MS - 30 * 60 * 1000);
|
||||
await registry.registerDevice('bob@example.org', 'https://mail.example.org', info);
|
||||
vi.setSystemTime(start + registry.DEVICE_PRUNE_MS + 60_000);
|
||||
await registry.registerDevice('alice@example.org', 'https://mail.example.org', info);
|
||||
vi.useRealTimers();
|
||||
expect(await registry.getDevice(old.id)).toBeNull();
|
||||
});
|
||||
|
||||
it('abandoned rows expire with no admin looking, others\' too, through any registration', async () => {
|
||||
const registry = await import('../registry');
|
||||
const start = Date.now() + 365 * 24 * 60 * 60 * 1000;
|
||||
const start = Date.now() + 4 * 365 * 24 * 60 * 60 * 1000;
|
||||
vi.useFakeTimers({ now: start, toFake: ['Date'] });
|
||||
const abandoned = await registry.registerDevice('bob@example.org', 'https://mail.example.org', info);
|
||||
const waiting = await registry.registerDevice('bob@example.org', 'https://mail.example.org', info);
|
||||
|
||||
@@ -505,6 +505,8 @@ describe('a refused login (lib/offline/credentials-rejected.ts)', () => {
|
||||
expect(state.isEvicted(ALICE)).toBe(false);
|
||||
expect(state.visibleAccounts()).toEqual([ALICE]);
|
||||
expect((await queue.readQueue(ALICE)).outbox.map((row) => row.summary.subject)).toEqual(['Unsent']);
|
||||
// Signed in: the page says nothing from here on, even before the next look.
|
||||
expect(wipeState.refusedWaits()).toEqual([]);
|
||||
|
||||
await wipe.sweepRefusedLogins();
|
||||
expect(wipeState.readRefusedLogins()).toEqual({});
|
||||
@@ -717,8 +719,11 @@ describe('other windows', () => {
|
||||
vi.stubGlobal('location', { ...window.location, reload });
|
||||
useAccountStore.setState({ accounts: [entry(ALICE, 0), entry(BOB, 1)] } as never);
|
||||
state.writeEnabled([ALICE, BOB]);
|
||||
await keys.putDeviceRecord({ account: BOB, id: BOB_ROW, secret: SECRET, createdAt: 1 });
|
||||
const stop = wipe.startDeviceWipe();
|
||||
const other = new BroadcastChannel('bulwark-device-wipe');
|
||||
await vi.waitFor(() => expect(sent.length).toBeGreaterThan(0));
|
||||
sent.length = 0;
|
||||
|
||||
other.postMessage({ account: BOB, phase });
|
||||
await vi.waitFor(() => expect(useAccountStore.getState().getAccountById(BOB)).toBeUndefined());
|
||||
@@ -729,6 +734,9 @@ describe('other windows', () => {
|
||||
expect(state.isEvicted(BOB)).toBe(false);
|
||||
expect(store.removed).toEqual([]);
|
||||
expect(purge.files).toBe(0);
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
expect(sent.filter((call) => call.url.endsWith('/wiped'))).toEqual([]);
|
||||
expect((await keys.getDeviceRecord(BOB))?.id).toBe(BOB_ROW);
|
||||
expect(JSON.parse(localStorage.getItem('account-registry') ?? '{"state":{"accounts":[]}}').state.accounts.map((a: { id: string }) => a.id)).not.toContain(BOB);
|
||||
|
||||
other.close();
|
||||
@@ -928,10 +936,14 @@ describe('two wipes at once', () => {
|
||||
});
|
||||
|
||||
it('a wipe heard twice is done once: the second finds the row gone', async () => {
|
||||
let cleaned = 0;
|
||||
registry.onDeviceWipe(() => { cleaned += 1; });
|
||||
await wipe.wipeAccountFromDevice(ALICE, 'admin', { ifRow: ROW });
|
||||
expect(cleaned).toBe(1);
|
||||
const reports = sent.filter((call) => call.url.endsWith('/wiped')).length;
|
||||
expect(wipeState.consumeWipeNote()).toEqual({ unsent: 0 });
|
||||
await wipe.wipeAccountFromDevice(ALICE, 'admin', { ifRow: ROW });
|
||||
expect(cleaned).toBe(1);
|
||||
expect(sent.filter((call) => call.url.endsWith('/wiped'))).toHaveLength(reports);
|
||||
expect(wipeState.consumeWipeNote()).toBeNull();
|
||||
expect(wipeState.readPendingWipes()).toEqual({});
|
||||
@@ -960,6 +972,16 @@ describe('a sign-out', () => {
|
||||
expect(wipeState.readWipeConfirmations()).toEqual([]);
|
||||
});
|
||||
|
||||
it('has its row on the way before the key database goes with the rest', async () => {
|
||||
respond = () => json(503, {});
|
||||
let queuedAtPurge: string[] | null = null;
|
||||
purge.during = () => { queuedAtPurge ??= wipeState.readWipeConfirmations().map((entry) => entry.id); };
|
||||
wipe.registerDeviceWipe();
|
||||
await useAuthStore.getState().logout({ endProviderSession: false });
|
||||
await vi.waitFor(() => expect(purge.files).toBeGreaterThan(0));
|
||||
expect(queuedAtPurge).toEqual([ROW]);
|
||||
});
|
||||
|
||||
it('removing another account drops its row too, and keeps this one\'s', async () => {
|
||||
useAccountStore.setState({ accounts: [entry(ALICE, 0), entry(BOB, 1)] } as never);
|
||||
await keys.putDeviceRecord({ account: BOB, id: BOB_ROW, secret: SECRET, createdAt: 1 });
|
||||
@@ -1098,6 +1120,12 @@ describe('what else goes with a wipe', () => {
|
||||
await wipe.wipeAccountFromDevice(ALICE, 'admin');
|
||||
expect(closed).toEqual(['From Eve']);
|
||||
expect(unsubscribed).toBe(0);
|
||||
// No full sign-out comes along to clean up behind it. A store may write itself back, empty.
|
||||
for (const key of ['pro-tabs', 'search-history-storage', 'files-favorites', 'files-recent-files', 'files-last-parent-id', 'files-path-stack',
|
||||
'bulwark-delegate-names', 'calendar-notification-storage', 'webdav-favorites', 'webdav-recent-files', 'webdav-last-path', 'template-storage']) {
|
||||
expect([key, localStorage.getItem(key)]).not.toEqual([key, 'x']);
|
||||
}
|
||||
expect(deletedCaches).toContain('bulwark-push-state-v1');
|
||||
});
|
||||
|
||||
it('a wipe from settings takes them too', async () => {
|
||||
|
||||
@@ -74,6 +74,14 @@ function account(id: string, slot: number, authMode: 'basic' | 'oauth') {
|
||||
};
|
||||
}
|
||||
|
||||
/** How accounts leave: 'explicit' is what takes their data with it. */
|
||||
async function removalReasons(): Promise<string[]> {
|
||||
const { onAccountRemoved } = await import('@/stores/account-store');
|
||||
const reasons: string[] = [];
|
||||
onAccountRemoved((_id, reason) => { reasons.push(reason); });
|
||||
return reasons;
|
||||
}
|
||||
|
||||
type Answer = (url: string, method: string) => Response | null;
|
||||
let answer: Answer;
|
||||
const json = (status: number, body: unknown) => new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||||
@@ -165,11 +173,13 @@ describe('a login the server ended, at a start', () => {
|
||||
});
|
||||
|
||||
it('invalid_grant fires the hook, for certain, and only hides the data', async () => {
|
||||
const reasons = await removalReasons();
|
||||
useOfflineStore.setState({ enabled: [ALICE] });
|
||||
answer = (url, method) => (url.includes('/api/auth/token') && method === 'PUT' ? json(401, { error: 'invalid_grant' }) : null);
|
||||
await useAuthStore.getState().checkAuth();
|
||||
// With mail kept here the real connect goes on behind the local copy.
|
||||
await vi.waitFor(() => expect(heard).toEqual([ALICE]));
|
||||
await vi.waitFor(() => expect(reasons).toEqual(['evicted']));
|
||||
expect(certain).toEqual([true]);
|
||||
expect(isEvicted(ALICE)).toBe(true);
|
||||
expect(useOfflineStore.getState().enabled).toEqual([ALICE]);
|
||||
@@ -218,10 +228,12 @@ describe('a login the server ended, at a switch', () => {
|
||||
});
|
||||
|
||||
it('invalid_grant fires the hook and hides the account, keeping its data', async () => {
|
||||
const reasons = await removalReasons();
|
||||
useOfflineStore.setState({ enabled: [ALICE, BOB] });
|
||||
answer = (url, method) => (url.includes('/api/auth/token') && method === 'PUT' ? json(401, { error: 'invalid_grant' }) : null);
|
||||
await useAuthStore.getState().switchAccount(BOB);
|
||||
expect(heard).toEqual([BOB]);
|
||||
expect(reasons).toEqual(['evicted']);
|
||||
expect(useAccountStore.getState().getAccountById(BOB)).toBeUndefined();
|
||||
expect(isEvicted(BOB)).toBe(true);
|
||||
expect(useOfflineStore.getState().enabled).toContain(BOB);
|
||||
@@ -282,7 +294,7 @@ describe('a password the server keeps refusing', () => {
|
||||
|
||||
it('during the connect: a later failure isn\'t read as a refusal', async () => {
|
||||
clients.connect = async (client) => {
|
||||
if ((client as { username?: string }).username === 'bob') (client as { refused?: (ms: number) => void }).refused?.(0);
|
||||
(client as { refused?: (ms: number) => void }).refused?.(0);
|
||||
};
|
||||
await useAuthStore.getState().checkAuth();
|
||||
await vi.waitFor(() => expect(clients.made.length).toBeGreaterThanOrEqual(2));
|
||||
|
||||
Reference in New Issue
Block a user