From a193bc8236314a0c4a1927e418f47780c6aa4164 Mon Sep 17 00:00:00 2001
From: Lucas Berger
Date: Tue, 16 Jun 2026 16:53:14 -0400
Subject: [PATCH] =?UTF-8?q?fix(12):=20satisfy=20CI=20fast-checks=20?=
=?UTF-8?q?=E2=80=94=20lint=20unused=20vars,=20typed=20contract-test=20bod?=
=?UTF-8?q?y,=20prettier?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Remove unused 'res'/'container' assignments (no-unused-vars)
- setupClient.contract.test.ts: typed parseSentBody helper + non-async json mock
(no-unsafe-*/require-await)
- Prettier format 7 setup files
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/api/src/auth/middleware.ts | 12 ++-
apps/api/src/routes/setup.ts | 61 +++++++------
apps/api/tests/routes/setup.test.ts | 14 ++-
apps/pwa/src/api/client.ts | 16 +++-
apps/pwa/src/api/setupClient.contract.test.ts | 67 ++++++++------
apps/pwa/src/routes/SetupPage.test.tsx | 47 +++++++---
apps/pwa/src/routes/SetupPage.tsx | 87 ++++++++-----------
7 files changed, 172 insertions(+), 132 deletions(-)
diff --git a/apps/api/src/auth/middleware.ts b/apps/api/src/auth/middleware.ts
index 494cf0d..1737635 100644
--- a/apps/api/src/auth/middleware.ts
+++ b/apps/api/src/auth/middleware.ts
@@ -89,15 +89,21 @@ export async function oidcConfigFallbackMiddleware(c: Context, next: Next): Prom
// container is already running, the in-process value is stale until restart.
// A container restart is required to pick up any changed OIDC config values.
if (key === 'oidc_issuer') {
- console.info('[oidcFallback] Writing OIDC_ISSUER from app_config — container restart required to update this value');
+ console.info(
+ '[oidcFallback] Writing OIDC_ISSUER from app_config — container restart required to update this value',
+ );
process.env.OIDC_ISSUER = row.value;
}
if (key === 'oidc_client_id') {
- console.info('[oidcFallback] Writing OIDC_CLIENT_ID from app_config — container restart required to update this value');
+ console.info(
+ '[oidcFallback] Writing OIDC_CLIENT_ID from app_config — container restart required to update this value',
+ );
process.env.OIDC_CLIENT_ID = row.value;
}
if (key === 'app_external_url') {
- console.info('[oidcFallback] Writing OIDC_AUTH_EXTERNAL_URL from app_config — container restart required to update this value');
+ console.info(
+ '[oidcFallback] Writing OIDC_AUTH_EXTERNAL_URL from app_config — container restart required to update this value',
+ );
process.env.OIDC_AUTH_EXTERNAL_URL = row.value;
}
}
diff --git a/apps/api/src/routes/setup.ts b/apps/api/src/routes/setup.ts
index a347e85..8526818 100644
--- a/apps/api/src/routes/setup.ts
+++ b/apps/api/src/routes/setup.ts
@@ -148,7 +148,10 @@ setupRouter.post('/validate/db', async (c) => {
await db.execute(sql`SELECT 1`);
return c.json({ ok: true }, 200);
} catch (err) {
- console.error('[setup/validate/db] DB round-trip failed:', err instanceof Error ? err.message : String(err));
+ console.error(
+ '[setup/validate/db] DB round-trip failed:',
+ err instanceof Error ? err.message : String(err),
+ );
return c.json({ ok: false, error: 'DB unavailable' }, 503);
}
});
@@ -217,7 +220,10 @@ setupRouter.post('/validate/vapid', async (c) => {
const publicKey = process.env.VAPID_PUBLIC_KEY ?? '';
if (!privateKey || !publicKey) {
- return c.json({ ok: false, error: 'VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY env vars must be set' }, 400);
+ return c.json(
+ { ok: false, error: 'VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY env vars must be set' },
+ 400,
+ );
}
// Gap 2: assert the operator-submitted public key matches the env public key BEFORE
@@ -230,11 +236,14 @@ setupRouter.post('/validate/vapid', async (c) => {
const submittedPublicKey = submittedRow?.value;
if (!submittedPublicKey || submittedPublicKey !== publicKey) {
- return c.json({
- ok: false,
- error:
- 'VAPID public key does not match the configured key pair. Paste the exact VAPID_PUBLIC_KEY printed by `npm run generate-secrets`.',
- }, 400);
+ return c.json(
+ {
+ ok: false,
+ error:
+ 'VAPID public key does not match the configured key pair. Paste the exact VAPID_PUBLIC_KEY printed by `npm run generate-secrets`.',
+ },
+ 400,
+ );
}
try {
@@ -248,10 +257,13 @@ setupRouter.post('/validate/vapid', async (c) => {
);
return c.json({ ok: true }, 200);
} catch (err) {
- return c.json({
- ok: false,
- error: err instanceof Error ? err.message : 'VAPID validation failed',
- }, 400);
+ return c.json(
+ {
+ ok: false,
+ error: err instanceof Error ? err.message : 'VAPID validation failed',
+ },
+ 400,
+ );
}
});
@@ -298,7 +310,9 @@ setupRouter.post('/credential', zValidator('json', credentialSchema, noEchoHook)
)) as unknown as [{ count: string | number }[], unknown];
const unclaimedCount = Number(countRows[0][0]?.count ?? 0);
if (unclaimedCount > 0) {
- throw Object.assign(new Error('An unclaimed user already exists'), { code: 'DUPLICATE_UNCLAIMED' });
+ throw Object.assign(new Error('An unclaimed user already exists'), {
+ code: 'DUPLICATE_UNCLAIMED',
+ });
}
// Step 1: Assign color (first unused from palette, or round-robin fallback)
@@ -322,20 +336,22 @@ setupRouter.post('/credential', zValidator('json', credentialSchema, noEchoHook)
})
.$returningId();
- const [row] = await tx
- .select()
- .from(users)
- .where(eq(users.id, inserted.id))
- .limit(1);
+ const [row] = await tx.select().from(users).where(eq(users.id, inserted.id)).limit(1);
return row;
});
} catch (err) {
- if (err instanceof Error && (err as NodeJS.ErrnoException & { code?: string }).code === 'DUPLICATE_UNCLAIMED') {
+ if (
+ err instanceof Error &&
+ (err as NodeJS.ErrnoException & { code?: string }).code === 'DUPLICATE_UNCLAIMED'
+ ) {
// A concurrent request already created the unclaimed admin row
return c.json({ error: 'Setup already in progress' }, 409);
}
- console.error('[setup/POST /credential] Transaction error:', err instanceof Error ? err.message : String(err));
+ console.error(
+ '[setup/POST /credential] Transaction error:',
+ err instanceof Error ? err.message : String(err),
+ );
return c.json({ error: 'Service unavailable' }, 503);
}
@@ -353,12 +369,7 @@ setupRouter.post('/credential', zValidator('json', credentialSchema, noEchoHook)
// Step 3: Validate + encrypt + store the credential via the shared helper (D-09)
// The user row MUST exist before this call (Pitfall 5 — FK constraint).
try {
- await validateEncryptAndStoreCredential(
- localUser.id,
- fastmailEmail,
- appPassword,
- 'caldav',
- );
+ await validateEncryptAndStoreCredential(localUser.id, fastmailEmail, appPassword, 'caldav');
} catch (err) {
// Roll back the local user insert on credential failure to avoid orphaned rows
await db.delete(users).where(eq(users.id, localUser.id));
diff --git a/apps/api/tests/routes/setup.test.ts b/apps/api/tests/routes/setup.test.ts
index e8ea60f..74900c9 100644
--- a/apps/api/tests/routes/setup.test.ts
+++ b/apps/api/tests/routes/setup.test.ts
@@ -203,7 +203,8 @@ async function seedCredential(userId: number): Promise {
// Env and DB cleanup between tests
// ---------------------------------------------------------------------------
-const VAPID_PUBLIC_KEY = 'BKO9RLPqxNQ7GOLHsQ5kFXqMkfJBfElkd5h9ECQ0gRu7R5SJP6Ct5GkRuPxqY1u0UVY84_z7JGJsLhO-wChk_sE';
+const VAPID_PUBLIC_KEY =
+ 'BKO9RLPqxNQ7GOLHsQ5kFXqMkfJBfElkd5h9ECQ0gRu7R5SJP6Ct5GkRuPxqY1u0UVY84_z7JGJsLhO-wChk_sE';
const VAPID_PRIVATE_KEY = 'QEkqsrxLqpv_ynpCECWFLYlOxCEGd-_O5u0AXzY_qEY';
beforeEach(async () => {
@@ -223,9 +224,7 @@ beforeEach(async () => {
afterEach(async () => {
// Clean up seeded rows between tests
await db.delete(memberCredentials);
- await db
- .delete(users)
- .where(eq(users.oidcIss, 'https://auth.test.setup'));
+ await db.delete(users).where(eq(users.oidcIss, 'https://auth.test.setup'));
// Delete local (wizard-created) users with null oidcIss
// We identify them by displayName prefix for safety
// Use raw delete of unclaimed users (the test-seeded ones)
@@ -554,7 +553,7 @@ describe('POST /api/setup/credential', () => {
displayName: 'OIDC User With claimed=false',
color: '#4A90D9',
isAdmin: false,
- claimed: false, // legacy/hypothetical — oidcIss is NOT NULL
+ claimed: false, // legacy/hypothetical — oidcIss is NOT NULL
});
mockValidateCredentialShouldThrow = false;
@@ -632,9 +631,8 @@ describe('/api/setup/* — 423 when effectively configured (D-10 effective-confi
process.env.VAPID_PUBLIC_KEY = VAPID_PUBLIC_KEY;
const app = await getApp();
- const res = await app.fetch(jsonRequest('GET', '/api/setup/status'));
- // status route always returns the status — but guard kicks in on mutation routes
- // Test a mutation route to confirm 423
+ // The /status route always returns the status; the guard kicks in on mutation
+ // routes. Test a mutation route to confirm 423.
const completeRes = await app.fetch(jsonRequest('POST', '/api/setup/complete'));
expect(completeRes.status).toBe(423);
});
diff --git a/apps/pwa/src/api/client.ts b/apps/pwa/src/api/client.ts
index 6b02998..6bfd1fd 100644
--- a/apps/pwa/src/api/client.ts
+++ b/apps/pwa/src/api/client.ts
@@ -636,7 +636,9 @@ export async function validateSetupDb(): Promise {
if (res.status === 423) throw new SetupAlreadyLockedError();
if (!res.ok) {
- throw new Error('Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again.');
+ throw new Error(
+ 'Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again.',
+ );
}
}
@@ -650,7 +652,9 @@ export async function validateSetupOidc(): Promise {
if (res.status === 423) throw new SetupAlreadyLockedError();
if (!res.ok) {
- throw new Error('OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server.');
+ throw new Error(
+ 'OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server.',
+ );
}
}
@@ -663,7 +667,9 @@ export async function validateSetupVapid(): Promise {
if (res.status === 423) throw new SetupAlreadyLockedError();
if (!res.ok) {
- throw new Error('VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.');
+ throw new Error(
+ 'VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.',
+ );
}
}
@@ -686,7 +692,9 @@ export async function postSetupCredential(payload: SetupCredentialPayload): Prom
if (res.status === 423) throw new SetupAlreadyLockedError();
if (!res.ok) {
- throw new Error("Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again.");
+ throw new Error(
+ "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again.",
+ );
}
}
diff --git a/apps/pwa/src/api/setupClient.contract.test.ts b/apps/pwa/src/api/setupClient.contract.test.ts
index e00942e..78294b5 100644
--- a/apps/pwa/src/api/setupClient.contract.test.ts
+++ b/apps/pwa/src/api/setupClient.contract.test.ts
@@ -20,7 +20,12 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { postSetupConfig, postSetupCredential, type SetupConfigPayload, type SetupCredentialPayload } from './client.js';
+import {
+ postSetupConfig,
+ postSetupCredential,
+ type SetupConfigPayload,
+ type SetupCredentialPayload,
+} from './client.js';
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -28,11 +33,21 @@ function mockFetchResponse(body: unknown, status = 200): Response {
return {
ok: status >= 200 && status < 300,
status,
- json: async () => body,
- clone: function () { return this as unknown as Response; },
+ json: () => Promise.resolve(body),
+ clone: function () {
+ return this as unknown as Response;
+ },
} as unknown as Response;
}
+// Extract the JSON body sent on the first fetch call as a typed record, so the
+// strict typed-lint rules (no-unsafe-*) are satisfied without scattering casts.
+function parseSentBody(spy: ReturnType): Record {
+ const calls = spy.mock.calls as unknown as Array<[unknown, RequestInit]>;
+ const init = calls[0][1];
+ return JSON.parse(init.body as string) as Record;
+}
+
// Build a canonical camelCase payload using the contract type.
// If SetupConfigPayload still has snake_case fields, TypeScript will error here
// (the interface and this test agree on the camelCase contract).
@@ -61,7 +76,7 @@ describe('postSetupConfig — payload contract (BUG 1 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupConfig(VALID_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).toHaveProperty('oidcIssuer', 'https://auth.example.com');
expect(body).not.toHaveProperty('oidc_issuer');
});
@@ -70,7 +85,7 @@ describe('postSetupConfig — payload contract (BUG 1 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupConfig(VALID_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).toHaveProperty('oidcClientId', 'familysync');
expect(body).not.toHaveProperty('oidc_client_id');
});
@@ -79,7 +94,7 @@ describe('postSetupConfig — payload contract (BUG 1 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupConfig(VALID_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).toHaveProperty('vapidPublicKey', 'BH_example_public_key');
expect(body).not.toHaveProperty('vapid_public_key');
});
@@ -88,7 +103,7 @@ describe('postSetupConfig — payload contract (BUG 1 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupConfig(VALID_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).toHaveProperty('appExternalUrl', 'https://familysync.example.com');
expect(body).not.toHaveProperty('app_url');
});
@@ -97,7 +112,7 @@ describe('postSetupConfig — payload contract (BUG 1 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupConfig(VALID_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(Object.keys(body).sort()).toEqual(
['appExternalUrl', 'oidcClientId', 'oidcIssuer', 'vapidPublicKey'].sort(),
);
@@ -125,15 +140,17 @@ describe('postSetupConfig — error rendering (BUG 2 regression)', () => {
error: {
name: 'ZodError',
issues: [
- { code: 'invalid_string', message: 'oidcIssuer must be an https URL', path: ['oidcIssuer'] },
+ {
+ code: 'invalid_string',
+ message: 'oidcIssuer must be an https URL',
+ path: ['oidcIssuer'],
+ },
],
},
};
fetchSpy.mockResolvedValueOnce(mockFetchResponse(zodErrorBody, 400));
- await expect(
- postSetupConfig(VALID_PAYLOAD),
- ).rejects.toSatisfy((err: Error) => {
+ await expect(postSetupConfig(VALID_PAYLOAD)).rejects.toSatisfy((err: Error) => {
// The error message must NOT be "[object Object]"
expect(err.message).not.toBe('[object Object]');
// The error message must be a non-empty string
@@ -149,31 +166,29 @@ describe('postSetupConfig — error rendering (BUG 2 regression)', () => {
error: {
name: 'ZodError',
issues: [
- { code: 'invalid_string', message: 'oidcIssuer must be an https URL', path: ['oidcIssuer'] },
+ {
+ code: 'invalid_string',
+ message: 'oidcIssuer must be an https URL',
+ path: ['oidcIssuer'],
+ },
],
},
};
fetchSpy.mockResolvedValueOnce(mockFetchResponse(zodErrorBody, 400));
- await expect(
- postSetupConfig(VALID_PAYLOAD),
- ).rejects.toThrow('oidcIssuer must be an https URL');
+ await expect(postSetupConfig(VALID_PAYLOAD)).rejects.toThrow('oidcIssuer must be an https URL');
});
it('throws a fallback status error when error body is empty', async () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse({}, 400));
- await expect(
- postSetupConfig(VALID_PAYLOAD),
- ).rejects.toThrow('400');
+ await expect(postSetupConfig(VALID_PAYLOAD)).rejects.toThrow('400');
});
it('returns void on 200 success', async () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
- await expect(
- postSetupConfig(VALID_PAYLOAD),
- ).resolves.toBeUndefined();
+ await expect(postSetupConfig(VALID_PAYLOAD)).resolves.toBeUndefined();
});
});
@@ -202,7 +217,7 @@ describe('postSetupCredential — payload contract (CR-01 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupCredential(VALID_CREDENTIAL_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(Object.keys(body).sort()).toEqual(['appPassword', 'fastmailEmail'].sort());
});
@@ -210,7 +225,7 @@ describe('postSetupCredential — payload contract (CR-01 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupCredential(VALID_CREDENTIAL_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).not.toHaveProperty('providerType');
});
@@ -218,7 +233,7 @@ describe('postSetupCredential — payload contract (CR-01 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupCredential(VALID_CREDENTIAL_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).toHaveProperty('fastmailEmail', 'user@fastmail.com');
});
@@ -226,7 +241,7 @@ describe('postSetupCredential — payload contract (CR-01 regression)', () => {
fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200));
await postSetupCredential(VALID_CREDENTIAL_PAYLOAD);
- const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string);
+ const body = parseSentBody(fetchSpy);
expect(body).toHaveProperty('appPassword', 'secret-app-password');
});
});
diff --git a/apps/pwa/src/routes/SetupPage.test.tsx b/apps/pwa/src/routes/SetupPage.test.tsx
index 9634506..ccff8ba 100644
--- a/apps/pwa/src/routes/SetupPage.test.tsx
+++ b/apps/pwa/src/routes/SetupPage.test.tsx
@@ -51,7 +51,10 @@ function makeQueryClient() {
});
}
-function renderSetupPage(queryClient: QueryClient, props: React.ComponentProps = {}) {
+function renderSetupPage(
+ queryClient: QueryClient,
+ props: React.ComponentProps = {},
+) {
return render(
@@ -149,7 +152,7 @@ describe('SetupPage — Welcome step', () => {
});
it('has aria-live region for validation status', async () => {
- const { container } = renderSetupPage(queryClient);
+ renderSetupPage(queryClient);
// The step 2 config form has aria-live; step 1 welcome step does not yet show validation
// but the structure has it via the general error block. We advance to step 2 to test.
// For now we verify that once rendered, the component tree has the correct aria-live
@@ -239,15 +242,20 @@ describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => {
const { postSetupConfig } = await import('../api/client.js');
(postSetupConfig as ReturnType).mockResolvedValue(undefined);
- fireEvent.change(screen.getByLabelText('App URL'), { target: { value: 'https://app.example.com' } });
- fireEvent.change(screen.getByLabelText('OIDC issuer URL'), { target: { value: 'https://auth.example.com' } });
+ fireEvent.change(screen.getByLabelText('App URL'), {
+ target: { value: 'https://app.example.com' },
+ });
+ fireEvent.change(screen.getByLabelText('OIDC issuer URL'), {
+ target: { value: 'https://auth.example.com' },
+ });
fireEvent.change(screen.getByLabelText('OIDC client ID'), { target: { value: 'familysync' } });
fireEvent.change(screen.getByLabelText('VAPID public key'), { target: { value: 'BHtest123' } });
fireEvent.click(screen.getByRole('button', { name: 'Save & Validate' }));
}
it('calls validateSetupVapid after DB and OIDC pass in step 2', async () => {
- const { validateSetupDb, validateSetupOidc, validateSetupVapid } = await import('../api/client.js');
+ const { validateSetupDb, validateSetupOidc, validateSetupVapid } =
+ await import('../api/client.js');
(validateSetupDb as ReturnType).mockResolvedValue(undefined);
(validateSetupOidc as ReturnType).mockResolvedValue(undefined);
(validateSetupVapid as ReturnType).mockResolvedValue(undefined);
@@ -261,7 +269,8 @@ describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => {
});
it('renders a VAPID validation row after step 2 validation completes', async () => {
- const { validateSetupDb, validateSetupOidc, validateSetupVapid } = await import('../api/client.js');
+ const { validateSetupDb, validateSetupOidc, validateSetupVapid } =
+ await import('../api/client.js');
(validateSetupDb as ReturnType).mockResolvedValue(undefined);
(validateSetupOidc as ReturnType).mockResolvedValue(undefined);
(validateSetupVapid as ReturnType).mockResolvedValue(undefined);
@@ -276,11 +285,14 @@ describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => {
});
it('does NOT show Continue when VAPID validation fails', async () => {
- const { validateSetupDb, validateSetupOidc, validateSetupVapid } = await import('../api/client.js');
+ const { validateSetupDb, validateSetupOidc, validateSetupVapid } =
+ await import('../api/client.js');
(validateSetupDb as ReturnType).mockResolvedValue(undefined);
(validateSetupOidc as ReturnType).mockResolvedValue(undefined);
(validateSetupVapid as ReturnType).mockRejectedValue(
- new Error('VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.')
+ new Error(
+ 'VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.',
+ ),
);
await advanceToStep2();
@@ -296,7 +308,8 @@ describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => {
});
it('shows Continue only when db, oidc, AND vapid all pass', async () => {
- const { validateSetupDb, validateSetupOidc, validateSetupVapid } = await import('../api/client.js');
+ const { validateSetupDb, validateSetupOidc, validateSetupVapid } =
+ await import('../api/client.js');
(validateSetupDb as ReturnType).mockResolvedValue(undefined);
(validateSetupOidc as ReturnType).mockResolvedValue(undefined);
(validateSetupVapid as ReturnType).mockResolvedValue(undefined);
@@ -360,8 +373,12 @@ describe('SetupPage — Step 2 Instance copy + DB-name field (gaps 1, 3)', () =>
(validateSetupVapid as ReturnType).mockResolvedValue(undefined);
await advanceToStep2();
- fireEvent.change(screen.getByLabelText('App URL'), { target: { value: 'https://app.example.com' } });
- fireEvent.change(screen.getByLabelText('OIDC issuer URL'), { target: { value: 'https://auth.example.com' } });
+ fireEvent.change(screen.getByLabelText('App URL'), {
+ target: { value: 'https://app.example.com' },
+ });
+ fireEvent.change(screen.getByLabelText('OIDC issuer URL'), {
+ target: { value: 'https://auth.example.com' },
+ });
fireEvent.change(screen.getByLabelText('OIDC client ID'), { target: { value: 'familysync' } });
fireEvent.change(screen.getByLabelText('VAPID public key'), { target: { value: 'BHtest123' } });
fireEvent.click(screen.getByRole('button', { name: 'Save & Validate' }));
@@ -403,8 +420,12 @@ describe('SetupPage — Back navigation preserves Instance fields (gap 4)', () =
fireEvent.click(await screen.findByRole('button', { name: 'Continue' }));
await screen.findByText('Instance Configuration');
- fireEvent.change(screen.getByLabelText('App URL'), { target: { value: 'https://app.example.com' } });
- fireEvent.change(screen.getByLabelText('OIDC issuer URL'), { target: { value: 'https://auth.example.com' } });
+ fireEvent.change(screen.getByLabelText('App URL'), {
+ target: { value: 'https://app.example.com' },
+ });
+ fireEvent.change(screen.getByLabelText('OIDC issuer URL'), {
+ target: { value: 'https://auth.example.com' },
+ });
fireEvent.change(screen.getByLabelText('OIDC client ID'), { target: { value: 'familysync' } });
fireEvent.change(screen.getByLabelText('VAPID public key'), { target: { value: 'BHtest123' } });
fireEvent.click(screen.getByRole('button', { name: 'Save & Validate' }));
diff --git a/apps/pwa/src/routes/SetupPage.tsx b/apps/pwa/src/routes/SetupPage.tsx
index b5652f3..2c200d8 100644
--- a/apps/pwa/src/routes/SetupPage.tsx
+++ b/apps/pwa/src/routes/SetupPage.tsx
@@ -175,14 +175,8 @@ function StepIndicator({ currentStep }: StepIndicatorProps) {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
- background:
- isCompleted || isActive
- ? 'var(--color-member-0, #4a90d9)'
- : 'transparent',
- border:
- isCompleted || isActive
- ? 'none'
- : '2px solid var(--color-border, #e2e4e9)',
+ background: isCompleted || isActive ? 'var(--color-member-0, #4a90d9)' : 'transparent',
+ border: isCompleted || isActive ? 'none' : '2px solid var(--color-border, #e2e4e9)',
color: isCompleted || isActive ? '#ffffff' : 'var(--color-text-muted, #9ca3af)',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
@@ -220,11 +214,7 @@ function StepIndicator({ currentStep }: StepIndicatorProps) {
)}
- {isCompleted ? (
-
- ) : (
- {stepNum}
- )}
+ {isCompleted ? : {stepNum}}
- This wizard will guide you through configuring your self-hosted instance. Before
- continuing, run{' '}
- npm run generate-secrets{' '}
- from the repo to generate your instance secrets and add them to your Docker environment.
- You'll also need: your OIDC client credentials (Authelia) and a Fastmail account
- with an app password. This takes about 5 minutes.
+ This wizard will guide you through configuring your self-hosted instance. Before continuing,
+ run npm run generate-secrets from the repo to generate your instance secrets
+ and add them to your Docker environment. You'll also need: your OIDC client credentials
+ (Authelia) and a Fastmail account with an app password. This takes about 5 minutes.
{/* Before you start note block */}
@@ -465,7 +453,9 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef, fields, setFields }: S
});
const dbName = setupStatus?.dbName ?? '';
- const [validationRows, setValidationRows] = useState>({
+ const [validationRows, setValidationRows] = useState<
+ Pick
+ >({
db: 'idle',
oidc: 'idle',
vapid: 'idle',
@@ -518,9 +508,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef, fields, setFields }: S
}
},
onError: (err) => {
- setFieldError(
- err instanceof Error ? err.message : 'Something went wrong. Please try again.',
- );
+ setFieldError(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
},
});
@@ -654,9 +642,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef, fields, setFields }: S
placeholder="familysync"
style={inputStyle(false)}
/>
-
- The client ID registered in Authelia for this application.
-
+ The client ID registered in Authelia for this application.
{/* VAPID Public Key */}
@@ -673,8 +659,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef, fields, setFields }: S
style={inputStyle(false)}
/>
- Paste the VAPID_PUBLIC_KEY value from{' '}
- npm run generate-secrets.
+ Paste the VAPID_PUBLIC_KEY value from npm run generate-secrets.
@@ -721,19 +706,22 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef, fields, setFields }: S
/>
{/* General field error (before validation rows show) */}
- {fieldError && validationRows.db === 'idle' && validationRows.oidc === 'idle' && validationRows.vapid === 'idle' && (
-
- {fieldError}
-
- )}
+ {fieldError &&
+ validationRows.db === 'idle' &&
+ validationRows.oidc === 'idle' &&
+ validationRows.vapid === 'idle' && (
+
+ {fieldError}
+
+ )}
{/* Continue + Back row — appears only when both validations pass */}
{bothPassed && configSaved && (
@@ -806,15 +794,12 @@ function Step3Credential({ onBack, onSuccess, onLocked, stepHeadingRef }: Step3P
onLocked();
return;
}
- setErrorText(
- err instanceof Error ? err.message : 'Something went wrong. Please try again.',
- );
+ setErrorText(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
},
});
const isPending = completeMutation.isPending || finalMutation.isPending;
- const saveDisabled =
- isPending || email.trim().length === 0 || password.trim().length === 0;
+ const saveDisabled = isPending || email.trim().length === 0 || password.trim().length === 0;
function handleValidate() {
if (saveDisabled) return;
@@ -853,9 +838,8 @@ function Step3Credential({ onBack, onSuccess, onLocked, stepHeadingRef }: Step3P
lineHeight: 1.5,
}}
>
- Add the Fastmail app password for the first household member. This credential is
- validated against Fastmail CalDAV before saving. The password is never stored in plain
- text.
+ Add the Fastmail app password for the first household member. This credential is validated
+ against Fastmail CalDAV before saving. The password is never stored in plain text.
{/* Fastmail email */}
@@ -1177,10 +1161,7 @@ export function SetupPage({ alreadyLocked = false }: SetupPageProps) {
{/* Step card (Surface 3) */}
{step === 1 && (
- setStep(2)}
- stepHeadingRef={stepHeadingRef}
- />
+ setStep(2)} stepHeadingRef={stepHeadingRef} />
)}
{step === 2 && (