fix: keep the stored heartbeat schedule when the setup wizard sets telemetry consent #1066

This commit is contained in:
Linus Rath
2026-11-01 00:01:14 +01:00
parent a87a9a5f6a
commit 48ac0d9196
3 changed files with 47 additions and 7 deletions
@@ -19,6 +19,7 @@ vi.mock('@/lib/telemetry', () => ({
effectiveConsent: async () => ({ source: 'env' }),
loadState: async () => ({}),
saveState: async () => {},
updateState: async () => ({}),
reschedule: async () => {},
}));
@@ -0,0 +1,39 @@
/**
* @vitest-environment node
*/
import { describe, it, expect, vi } from 'vitest';
import { NextRequest } from 'next/server';
// The wizard's telemetry choice used to load the whole state, change it and
// save it back, putting its copy over anything written meanwhile (the
// scheduler leader storing the next heartbeat, on this or another replica).
// It changes only the consent fields of what is stored now.
vi.mock('@/lib/setup/state', () => ({ detectSetupState: () => 'bootstrap' }));
vi.mock('@/lib/setup/session', () => ({ authenticateWizardRequest: async () => true }));
vi.mock('@/lib/admin/config-manager', () => ({
configManager: { ensureLoaded: async () => {}, setAdminConfig: async () => {} },
}));
const stored = vi.hoisted(() => ({ consent: 'off', consentedAt: null as string | null, nextScheduledAt: 'kept' }));
vi.mock('@/lib/telemetry', () => ({
effectiveConsent: async () => ({ source: 'file' }),
loadState: async () => { throw new Error('read-modify-write of the whole state'); },
saveState: async () => { throw new Error('read-modify-write of the whole state'); },
updateState: async (change: (s: typeof stored) => void) => { change(stored); return stored; },
reschedule: async () => {},
}));
import { POST } from '../step/route';
describe('POST /api/setup/step (security, telemetry)', () => {
it('turns consent on against the stored state', async () => {
const res = await POST(new NextRequest('http://localhost/api/setup/step', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ step: 'security', values: { telemetryConsent: 'on' } }),
}));
expect(res.status).toBe(200);
expect(stored).toMatchObject({ consent: 'on', nextScheduledAt: 'kept' });
expect(stored.consentedAt).not.toBeNull();
});
});
+7 -7
View File
@@ -4,7 +4,7 @@ import { authenticateWizardRequest } from '@/lib/setup/session';
import { configManager } from '@/lib/admin/config-manager';
import { CONFIG_ENV_MAP } from '@/lib/admin/types';
import { parseJmapServers } from '@/lib/admin/jmap-servers';
import { effectiveConsent, loadState, saveState, reschedule } from '@/lib/telemetry';
import { effectiveConsent, updateState, reschedule } from '@/lib/telemetry';
import { logger } from '@/lib/logger';
export const dynamic = 'force-dynamic';
@@ -93,12 +93,12 @@ export async function POST(request: NextRequest) {
// A BULWARK_TELEMETRY env var hard-locks the choice; don't fight it.
const { source } = await effectiveConsent();
if (source !== 'env') {
const tstate = await loadState();
tstate.consent = consent;
if (consent === 'on' && !tstate.consentedAt) {
tstate.consentedAt = new Date().toISOString();
}
await saveState(tstate);
const now = new Date().toISOString();
// Against the stored state, so a schedule written meanwhile stays.
await updateState((tstate) => {
tstate.consent = consent;
if (consent === 'on' && !tstate.consentedAt) tstate.consentedAt = now;
});
await reschedule();
}
}