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

test(web): write failing tests for POST /admin/themes/:rkey/save (ATB-59)

+143
+143
apps/web/src/routes/__tests__/admin-themes.test.tsx
··· 364 364 expect(html).toContain(".preview-pane-inner"); 365 365 }); 366 366 }); 367 + 368 + describe("createAdminThemeRoutes — POST /admin/themes/:rkey/save", () => { 369 + const MANAGE_THEMES = "space.atbb.permission.manageThemes"; 370 + 371 + beforeEach(() => { 372 + vi.stubGlobal("fetch", mockFetch); 373 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 374 + vi.resetModules(); 375 + }); 376 + 377 + afterEach(() => { 378 + vi.unstubAllGlobals(); 379 + vi.unstubAllEnvs(); 380 + mockFetch.mockReset(); 381 + }); 382 + 383 + function mockResponse(body: unknown, ok = true, status = 200) { 384 + return { 385 + ok, 386 + status, 387 + statusText: ok ? "OK" : "Error", 388 + json: () => Promise.resolve(body), 389 + }; 390 + } 391 + 392 + function setupAuthenticatedSession(permissions: string[]) { 393 + mockFetch.mockResolvedValueOnce( 394 + mockResponse({ authenticated: true, did: "did:plc:forum", handle: "admin.bsky.social" }) 395 + ); 396 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 397 + } 398 + 399 + async function loadThemeRoutes() { 400 + const { createAdminThemeRoutes } = await import("../admin-themes.js"); 401 + return createAdminThemeRoutes("http://localhost:3000"); 402 + } 403 + 404 + function makeFormBody(overrides: Record<string, string> = {}): string { 405 + return new URLSearchParams({ 406 + name: "My Theme", 407 + colorScheme: "light", 408 + fontUrls: "", 409 + "color-bg": "#f5f0e8", 410 + ...overrides, 411 + }).toString(); 412 + } 413 + 414 + // ── Unauthenticated ──────────────────────────────────────────────────────── 415 + 416 + it("redirects unauthenticated users to /login", async () => { 417 + const routes = await loadThemeRoutes(); 418 + const res = await routes.request("/admin/themes/abc123/save", { 419 + method: "POST", 420 + headers: { "content-type": "application/x-www-form-urlencoded" }, 421 + body: makeFormBody(), 422 + }); 423 + expect(res.status).toBe(302); 424 + expect(res.headers.get("location")).toBe("/login"); 425 + }); 426 + 427 + // ── No manageThemes permission → 403 ─────────────────────────────────────── 428 + 429 + it("returns 403 for users without manageThemes permission", async () => { 430 + setupAuthenticatedSession([]); 431 + const routes = await loadThemeRoutes(); 432 + const res = await routes.request("/admin/themes/abc123/save", { 433 + method: "POST", 434 + headers: { 435 + "content-type": "application/x-www-form-urlencoded", 436 + cookie: "atbb_session=token", 437 + }, 438 + body: makeFormBody(), 439 + }); 440 + expect(res.status).toBe(403); 441 + }); 442 + 443 + // ── Happy path: AppView 200 → redirect ?success=1 ───────────────────────── 444 + 445 + it("redirects to ?success=1 on AppView 200", async () => { 446 + setupAuthenticatedSession([MANAGE_THEMES]); 447 + mockFetch.mockResolvedValueOnce(mockResponse({ id: "1", name: "My Theme" })); 448 + 449 + const routes = await loadThemeRoutes(); 450 + const res = await routes.request("/admin/themes/abc123/save", { 451 + method: "POST", 452 + headers: { 453 + "content-type": "application/x-www-form-urlencoded", 454 + cookie: "atbb_session=token", 455 + }, 456 + body: makeFormBody(), 457 + }); 458 + 459 + expect(res.status).toBe(302); 460 + expect(res.headers.get("location")).toContain("/admin/themes/abc123"); 461 + expect(res.headers.get("location")).toContain("success=1"); 462 + }); 463 + 464 + // ── AppView 400 → redirect ?error=<msg> ──────────────────────────────────── 465 + 466 + it("redirects with ?error=<msg> when AppView returns 400", async () => { 467 + setupAuthenticatedSession([MANAGE_THEMES]); 468 + mockFetch.mockResolvedValueOnce( 469 + mockResponse({ error: "Name is required" }, false, 400) 470 + ); 471 + 472 + const routes = await loadThemeRoutes(); 473 + const res = await routes.request("/admin/themes/abc123/save", { 474 + method: "POST", 475 + headers: { 476 + "content-type": "application/x-www-form-urlencoded", 477 + cookie: "atbb_session=token", 478 + }, 479 + body: makeFormBody({ name: "" }), 480 + }); 481 + 482 + expect(res.status).toBe(302); 483 + const location = res.headers.get("location") ?? ""; 484 + expect(location).toContain("error="); 485 + expect(decodeURIComponent(location)).toContain("Name is required"); 486 + }); 487 + 488 + // ── Network failure → redirect generic error ─────────────────────────────── 489 + 490 + it("redirects with generic error on network failure", async () => { 491 + setupAuthenticatedSession([MANAGE_THEMES]); 492 + mockFetch.mockRejectedValueOnce(new Error("fetch failed")); 493 + 494 + const routes = await loadThemeRoutes(); 495 + const res = await routes.request("/admin/themes/abc123/save", { 496 + method: "POST", 497 + headers: { 498 + "content-type": "application/x-www-form-urlencoded", 499 + cookie: "atbb_session=token", 500 + }, 501 + body: makeFormBody(), 502 + }); 503 + 504 + expect(res.status).toBe(302); 505 + const location = res.headers.get("location") ?? ""; 506 + expect(location).toContain("error="); 507 + expect(decodeURIComponent(location).toLowerCase()).toContain("unavailable"); 508 + }); 509 + });