mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
// Replicas that start in the setup state each print the setup token. With a
|
|
// shared store (#1066) they must print the same one, and the wizard must
|
|
// accept it on whichever replica the load balancer picks.
|
|
|
|
import { setStoreForTests } from '@/lib/shared-store';
|
|
import { resetLocalStoreForTests } from '@/lib/shared-store/local';
|
|
import { separateFakeStore } from '@/lib/admin/__tests__/shared-store-fake';
|
|
|
|
let dir: string;
|
|
|
|
let replicas = 0;
|
|
|
|
/**
|
|
* One replica's copy of lib/setup/token.ts, with a state dir of its own:
|
|
* replicas share the store, not a volume. Each call runs with that dir.
|
|
*/
|
|
async function replica() {
|
|
vi.resetModules();
|
|
const token = await import('@/lib/setup/token');
|
|
const own = path.join(dir, `state-${++replicas}`);
|
|
const inOwnDir = <A extends unknown[], R>(fn: (...args: A) => Promise<R>) => (...args: A) => {
|
|
vi.stubEnv('ADMIN_STATE_DIR', own);
|
|
return fn(...args);
|
|
};
|
|
return {
|
|
ensureSetupToken: inOwnDir(token.ensureSetupToken),
|
|
verifySetupToken: inOwnDir(token.verifySetupToken),
|
|
clearSetupToken: inOwnDir(token.clearSetupToken),
|
|
};
|
|
}
|
|
|
|
/** Whether any replica wrote a token file. */
|
|
const anyTokenFile = () => Array.from({ length: replicas }, (_, i) => path.join(dir, `state-${i + 1}`, '.setup-token')).some((f) => existsSync(f));
|
|
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(path.join(tmpdir(), 'bw-setup-token-'));
|
|
replicas = 0;
|
|
resetLocalStoreForTests();
|
|
setStoreForTests(separateFakeStore(path.join(dir, 'store')));
|
|
});
|
|
|
|
afterEach(() => {
|
|
setStoreForTests(null);
|
|
resetLocalStoreForTests();
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllEnvs();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('setup token with a shared store', () => {
|
|
it('gives replicas booting together one token that each of them accepts', async () => {
|
|
const replicas = [await replica(), await replica(), await replica()];
|
|
const tokens = await Promise.all(replicas.map((r) => r.ensureSetupToken()));
|
|
expect(new Set(tokens).size).toBe(1);
|
|
for (const r of replicas) expect(await r.verifySetupToken(tokens[0])).toBe(true);
|
|
expect(await replicas[0].verifySetupToken('0'.repeat(64))).toBe(false);
|
|
expect(anyTokenFile()).toBe(false);
|
|
});
|
|
|
|
it('keeps the token across restarts within the hour, then issues a new one', async () => {
|
|
const first = await (await replica()).ensureSetupToken();
|
|
expect(await (await replica()).ensureSetupToken()).toBe(first);
|
|
|
|
const now = Date.now();
|
|
vi.spyOn(Date, 'now').mockReturnValue(now + 61 * 60 * 1000);
|
|
const later = await replica();
|
|
expect(await later.verifySetupToken(first)).toBe(false);
|
|
const second = await later.ensureSetupToken();
|
|
expect(second).not.toBe(first);
|
|
expect(await (await replica()).verifySetupToken(second)).toBe(true);
|
|
});
|
|
|
|
it('stops the token working on every replica once the wizard finished on one', async () => {
|
|
const [a, b] = [await replica(), await replica()];
|
|
const token = await a.ensureSetupToken();
|
|
expect(await b.verifySetupToken(token)).toBe(true);
|
|
await a.clearSetupToken();
|
|
expect(await b.verifySetupToken(token)).toBe(false);
|
|
});
|
|
});
|