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
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(web): address code review issues — ATB-59

- Fix GET /admin/themes/:rkey to call public /api/themes/:rkey instead
of nonexistent /api/admin/themes/:rkey; remove unused cookie variable
- Validate name before AppView PUT in save handler; redirect with error
if empty (prevents wasteful round-trip and unclear AppView message)
- Replace c.json() with redirect-on-error in reset-to-preset handler so
browser form POSTs show friendly error pages instead of raw JSON
- Add network failure test for GET /admin/themes/:rkey (500 unavailable)
- Add empty-name validation test for save handler
- Move ATB-59 plan docs to docs/plans/complete/

Malpercio 1c846717 382d970e

+56 -13
+41 -7
apps/web/src/routes/__tests__/admin-themes.test.tsx
··· 93 93 expect(res.status).toBe(404); 94 94 }); 95 95 96 + // ── Network failure loading theme ───────────────────────────────────────── 97 + 98 + it("renders error page when AppView fetch throws (network failure)", async () => { 99 + setupAuthenticatedSession([MANAGE_THEMES]); 100 + mockFetch.mockRejectedValueOnce(new Error("fetch failed")); 101 + const routes = await loadThemeRoutes(); 102 + const res = await routes.request("/admin/themes/abc123", { 103 + headers: { cookie: "atbb_session=token" }, 104 + }); 105 + expect(res.status).toBe(500); 106 + const html = await res.text(); 107 + expect(html.toLowerCase()).toContain("unavailable"); 108 + }); 109 + 96 110 // ── Happy path: renders editor with theme tokens ────────────────────────── 97 111 98 112 it("renders editor with theme name and token inputs", async () => { ··· 463 477 464 478 // ── AppView 400 → redirect ?error=<msg> ──────────────────────────────────── 465 479 480 + it("redirects with ?error when name is empty (client-side validation)", async () => { 481 + setupAuthenticatedSession([MANAGE_THEMES]); 482 + 483 + const routes = await loadThemeRoutes(); 484 + const res = await routes.request("/admin/themes/abc123/save", { 485 + method: "POST", 486 + headers: { 487 + "content-type": "application/x-www-form-urlencoded", 488 + cookie: "atbb_session=token", 489 + }, 490 + body: makeFormBody({ name: "" }), 491 + }); 492 + 493 + expect(res.status).toBe(302); 494 + const location = res.headers.get("location") ?? ""; 495 + expect(location).toContain("error="); 496 + expect(decodeURIComponent(location).toLowerCase()).toContain("name"); 497 + }); 498 + 466 499 it("redirects with ?error=<msg> when AppView returns 400", async () => { 467 500 setupAuthenticatedSession([MANAGE_THEMES]); 468 501 mockFetch.mockResolvedValueOnce( 469 - mockResponse({ error: "Name is required" }, false, 400) 502 + mockResponse({ error: "Tokens are invalid" }, false, 400) 470 503 ); 471 504 472 505 const routes = await loadThemeRoutes(); ··· 476 509 "content-type": "application/x-www-form-urlencoded", 477 510 cookie: "atbb_session=token", 478 511 }, 479 - body: makeFormBody({ name: "" }), 512 + body: makeFormBody(), 480 513 }); 481 514 482 515 expect(res.status).toBe(302); 483 516 const location = res.headers.get("location") ?? ""; 484 517 expect(location).toContain("error="); 485 - expect(decodeURIComponent(location)).toContain("Name is required"); 518 + expect(decodeURIComponent(location)).toContain("Tokens are invalid"); 486 519 }); 487 520 488 521 // ── Network failure → redirect generic error ─────────────────────────────── ··· 661 694 662 695 // ── Invalid preset → 400 ─────────────────────────────────────────────────── 663 696 664 - it("returns 400 for unknown preset name", async () => { 697 + it("redirects with ?error for unknown preset name", async () => { 665 698 setupAuthenticatedSession([MANAGE_THEMES]); 666 699 667 700 const routes = await loadThemeRoutes(); ··· 674 707 body: new URLSearchParams({ preset: "hacked" }).toString(), 675 708 }); 676 709 677 - expect(res.status).toBe(400); 678 - const html = await res.text(); 679 - expect(html.toLowerCase()).toMatch(/invalid|unknown|preset/); 710 + expect(res.status).toBe(302); 711 + const location = res.headers.get("location") ?? ""; 712 + expect(location).toContain("error="); 713 + expect(decodeURIComponent(location).toLowerCase()).toMatch(/invalid|unknown|preset/); 680 714 }); 681 715 });
+15 -6
apps/web/src/routes/admin-themes.tsx
··· 534 534 } 535 535 536 536 const themeRkey = c.req.param("rkey"); 537 - const cookie = c.req.header("cookie") ?? ""; 538 537 const presetParam = c.req.query("preset") ?? null; 539 538 const successMsg = c.req.query("success") === "1" ? "Theme saved successfully." : null; 540 539 const errorMsg = c.req.query("error") ?? null; ··· 542 541 // Fetch theme from AppView 543 542 let theme: AdminThemeEntry | null = null; 544 543 try { 545 - const res = await fetch(`${appviewUrl}/api/admin/themes/${themeRkey}`, { 546 - headers: { Cookie: cookie }, 547 - }); 544 + const res = await fetch(`${appviewUrl}/api/themes/${themeRkey}`); 548 545 if (res.status === 404) { 549 546 return c.html( 550 547 <BaseLayout title="Theme Not Found — atBB Admin" auth={auth}> ··· 1032 1029 } 1033 1030 1034 1031 const name = typeof rawBody.name === "string" ? rawBody.name.trim() : ""; 1032 + if (!name) { 1033 + return c.redirect( 1034 + `/admin/themes/${themeRkey}?error=${encodeURIComponent("Theme name is required.")}`, 1035 + 302 1036 + ); 1037 + } 1035 1038 const colorScheme = typeof rawBody.colorScheme === "string" ? rawBody.colorScheme : "light"; 1036 1039 const fontUrlsRaw = typeof rawBody.fontUrls === "string" ? rawBody.fontUrls : ""; 1037 1040 const fontUrls = fontUrlsRaw ··· 1102 1105 body = await c.req.parseBody(); 1103 1106 } catch (error) { 1104 1107 if (isProgrammingError(error)) throw error; 1105 - return c.json({ error: "Invalid form submission." }, 400); 1108 + return c.redirect( 1109 + `/admin/themes/${themeRkey}?error=${encodeURIComponent("Invalid form submission.")}`, 1110 + 302 1111 + ); 1106 1112 } 1107 1113 1108 1114 const preset = typeof body.preset === "string" ? body.preset : ""; 1109 1115 if (!(preset in THEME_PRESETS)) { 1110 - return c.json({ error: `Unknown preset: ${preset}` }, 400); 1116 + return c.redirect( 1117 + `/admin/themes/${themeRkey}?error=${encodeURIComponent("Invalid preset name.")}`, 1118 + 302 1119 + ); 1111 1120 } 1112 1121 1113 1122 return c.redirect(`/admin/themes/${themeRkey}?preset=${encodeURIComponent(preset)}`, 302);