mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
fix: keep telemetry consent when its state file can't be read #1066
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// @vitest-environment node
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// A telemetry state file that couldn't be read (a torn write, a passing I/O
|
||||
// error) used to be replaced with the defaults, which turned the admin's
|
||||
// consent off for good. Now the read answers with the defaults and leaves
|
||||
// the file alone.
|
||||
|
||||
vi.mock('@/lib/admin/third-party-mode', () => ({ isNoThirdPartyTrafficOn: async () => false }));
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(path.join(tmpdir(), 'bw-telemetry-state-'));
|
||||
vi.stubEnv('TELEMETRY_DATA_DIR', dir);
|
||||
vi.stubEnv('BULWARK_TELEMETRY', '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('telemetry state that cannot be read', () => {
|
||||
it('keeps the stored consent', async () => {
|
||||
const { effectiveConsent, loadState } = await import('@/lib/telemetry/state');
|
||||
const good = JSON.stringify({ consent: 'on', endpoint: 'https://collector.example.org/', consentedAt: '2026-10-01T00:00:00.000Z', lastSentAt: null, nextScheduledAt: null });
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, 'state.json');
|
||||
writeFileSync(file, good.slice(0, 20));
|
||||
|
||||
expect((await loadState()).consent).toBe('off');
|
||||
expect((await effectiveConsent()).consent).toBe('off');
|
||||
expect(readFileSync(file, 'utf8')).toBe(good.slice(0, 20));
|
||||
|
||||
writeFileSync(file, good);
|
||||
expect((await effectiveConsent()).consent).toBe('on');
|
||||
});
|
||||
|
||||
it('still writes the default state on a fresh install', async () => {
|
||||
const { loadState } = await import('@/lib/telemetry/state');
|
||||
expect((await loadState()).consent).toBe('off');
|
||||
expect(JSON.parse(readFileSync(path.join(dir, 'state.json'), 'utf8')).consent).toBe('off');
|
||||
});
|
||||
});
|
||||
@@ -64,9 +64,13 @@ export async function loadState(): Promise<TelemetryStateFile> {
|
||||
return { ...DEFAULTS, ...parsed };
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
// Unreadable is not missing: answer with the defaults (so nothing is
|
||||
// sent on a guess) but leave the file alone, or a passing read error
|
||||
// would switch the admin's consent off for good.
|
||||
logger.warn('telemetry: state read failed', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
// First-ever load on a fresh install: persist the default-off state so the
|
||||
// instance id and (lack of) consent are stable across restarts. The admin
|
||||
|
||||
Reference in New Issue
Block a user