WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

fix: address code review feedback on Phase 2 settings routes

- [Critical] Wrap decodeURIComponent(errorParam) in try/catch to prevent URIError on malformed URLs
- [Minor] Change array type syntax from T[] to Array<T> for consistency
- [Minor] Remove duplicate POST happy-path test

All settings tests pass; build and lint succeed.

+16 -20
-14
apps/web/src/routes/__tests__/settings.test.tsx
··· 381 381 expect(lightCookie).toContain("SameSite=Lax"); 382 382 }); 383 383 384 - it("POST /settings/appearance accepts valid URIs both in availableThemes", async () => { 385 - setupAuthenticatedSessionPost(); 386 - const routes = await loadSettingsRoutes(); 387 - const res = await routes.request("/settings/appearance", { 388 - method: "POST", 389 - headers: { cookie: "atbb_session=token", "content-type": "application/x-www-form-urlencoded" }, 390 - body: new URLSearchParams({ 391 - lightThemeUri: "at://did:plc:forum/space.atbb.forum.theme/3lbllight", 392 - darkThemeUri: "at://did:plc:forum/space.atbb.forum.theme/3lbldark", 393 - }).toString(), 394 - }); 395 - expect(res.status).toBe(302); 396 - }); 397 - 398 384 it("POST /settings/appearance does not set cookies when policy fetch fails", async () => { 399 385 // Same as the unavailable test - just verifying no cookies are set 400 386 mockFetch.mockResolvedValueOnce(
+16 -6
apps/web/src/routes/settings.tsx
··· 8 8 9 9 type ThemeSummary = { uri: string; name: string; colorScheme: string }; 10 10 type Policy = { 11 - availableThemes: { uri: string }[]; 11 + availableThemes: Array<{ uri: string }>; 12 12 allowUserChoice: boolean; 13 13 defaultLightThemeUri: string | null; 14 14 defaultDarkThemeUri: string | null; ··· 25 25 if (!auth.authenticated) return c.redirect("/login"); 26 26 27 27 const saved = c.req.query("saved") === "1"; 28 - const errorParam = c.req.query("error"); 28 + const rawError = c.req.query("error"); 29 + // decodeURIComponent throws URIError on malformed percent-encoding (e.g. %ZZ) 30 + const errorMessage = rawError 31 + ? (() => { 32 + try { 33 + return decodeURIComponent(rawError); 34 + } catch { 35 + return rawError; 36 + } 37 + })() 38 + : undefined; 29 39 30 40 // Fetch theme policy 31 41 let policy: Policy | null = null; ··· 56 66 } 57 67 58 68 // Fetch available themes list (already filtered by policy server-side in AppView) 59 - let allThemes: ThemeSummary[] = []; 69 + let allThemes: Array<ThemeSummary> = []; 60 70 try { 61 71 const themesRes = await fetch(`${appviewUrl}/api/themes`); 62 72 if (themesRes.ok) { 63 - const data = (await themesRes.json()) as { themes: ThemeSummary[] }; 73 + const data = (await themesRes.json()) as { themes: Array<ThemeSummary> }; 64 74 allThemes = data.themes ?? []; 65 75 } 66 76 } catch (err) { ··· 89 99 {saved && ( 90 100 <p class="settings-banner settings-banner--success">Preferences saved.</p> 91 101 )} 92 - {errorParam && ( 102 + {errorMessage && ( 93 103 <p class="settings-banner settings-banner--error"> 94 - {decodeURIComponent(errorParam)} 104 + {errorMessage} 95 105 </p> 96 106 )} 97 107 <section>