From 100f6db30936a2a1c4408030b1bb42f9d8a0c6ff Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:00:13 +0100 Subject: [PATCH] fix: skip the setup wizard in demo mode without a mail server --- lib/__tests__/setup-state-demo.test.ts | 51 ++++++++++++++++++++++++++ lib/setup/state.ts | 9 ++++- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 lib/__tests__/setup-state-demo.test.ts diff --git a/lib/__tests__/setup-state-demo.test.ts b/lib/__tests__/setup-state-demo.test.ts new file mode 100644 index 000000000..bd5b4953d --- /dev/null +++ b/lib/__tests__/setup-state-demo.test.ts @@ -0,0 +1,51 @@ +// @vitest-environment node +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { configManager } from '@/lib/admin/config-manager'; +import { detectSetupState } from '@/lib/setup/state'; + +let dir: string; + +async function withConfig(config: Record) { + await writeFile(path.join(dir, 'config.json'), JSON.stringify(config), 'utf-8'); + await configManager.reload(); +} + +beforeEach(async () => { + dir = await mkdtemp(path.join(tmpdir(), 'bw-setup-state-')); + vi.stubEnv('ADMIN_CONFIG_DIR', dir); + vi.stubEnv('JMAP_SERVER_URL', ''); + vi.stubEnv('DEMO_MODE', ''); +}); + +afterEach(async () => { + vi.unstubAllEnvs(); + await rm(dir, { recursive: true, force: true }); +}); + +describe('detectSetupState', () => { + it('sends a fresh install without a server to the wizard', async () => { + await withConfig({}); + expect(detectSetupState()).toBe('bootstrap'); + }); + + it('needs no wizard with DEMO_MODE=true and no JMAP_SERVER_URL', async () => { + vi.stubEnv('DEMO_MODE', 'true'); + await withConfig({}); + expect(detectSetupState()).toBe('env-managed'); + }); + + it('lets an admin override of demoMode win over DEMO_MODE', async () => { + vi.stubEnv('DEMO_MODE', 'true'); + await withConfig({ demoMode: false }); + expect(detectSetupState()).toBe('bootstrap'); + }); + + it('still reports a finished wizard as configured', async () => { + vi.stubEnv('DEMO_MODE', 'true'); + await withConfig({ setupComplete: true }); + expect(detectSetupState()).toBe('configured'); + }); +}); diff --git a/lib/setup/state.ts b/lib/setup/state.ts index 2e6c3b824..7c653d607 100644 --- a/lib/setup/state.ts +++ b/lib/setup/state.ts @@ -10,8 +10,9 @@ import { isConfigReadOnly } from '@/lib/admin/paths'; * configured - setup wizard finished (admin override config.json carries * setupComplete=true). Normal app; /setup returns 404. * env-managed - JMAP_SERVER_URL is set in the environment, so the - * operator is configuring via .env (legacy / CI path). The - * wizard stays disabled. + * operator is configuring via .env (legacy / CI path), or + * demo mode is on (DEMO_MODE=true), which needs no mail + * server at all. The wizard stays disabled. */ export type SetupState = 'bootstrap' | 'configured' | 'env-managed'; @@ -25,6 +26,10 @@ export function detectSetupState(): SetupState { if (process.env.JMAP_SERVER_URL && process.env.JMAP_SERVER_URL.trim() !== '') { return 'env-managed'; } + // Demo mode signs everyone into built-in sample data; there is nothing for + // the wizard to set up. Read through the config manager so an admin + // override of demoMode wins over DEMO_MODE, as everywhere else. + if (configManager.get('demoMode', false) === true) return 'env-managed'; // Read-only config dir + no setupComplete flag means the volume was // mounted :ro before the wizard ran. Fall through to bootstrap so the // failure (write attempt during wizard) surfaces with a clear error