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

feat(web): POST /admin/themes/:rkey/delete — proxy delete to AppView with 409 handling (ATB-58)

+82
+82
apps/web/src/routes/__tests__/admin.test.tsx
··· 2427 expect(res.headers.get("location")).toContain("/admin/themes?error="); 2428 }); 2429 });
··· 2427 expect(res.headers.get("location")).toContain("/admin/themes?error="); 2428 }); 2429 }); 2430 + 2431 + describe("createAdminRoutes — POST /admin/themes/:rkey/delete", () => { 2432 + beforeEach(() => { 2433 + vi.stubGlobal("fetch", mockFetch); 2434 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 2435 + vi.resetModules(); 2436 + }); 2437 + 2438 + afterEach(() => { 2439 + vi.unstubAllGlobals(); 2440 + vi.unstubAllEnvs(); 2441 + mockFetch.mockReset(); 2442 + }); 2443 + 2444 + function mockResponse(body: unknown, ok = true, status = 200) { 2445 + return { ok, status, json: () => Promise.resolve(body) }; 2446 + } 2447 + 2448 + function setupAuthenticatedSession(permissions: string[]) { 2449 + mockFetch.mockResolvedValueOnce( 2450 + mockResponse({ authenticated: true, did: "did:plc:user", handle: "alice.bsky.social" }) 2451 + ); 2452 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 2453 + } 2454 + 2455 + async function loadAdminRoutes() { 2456 + const { createAdminRoutes } = await import("../admin.js"); 2457 + return createAdminRoutes("http://localhost:3000"); 2458 + } 2459 + 2460 + it("deletes theme and redirects to /admin/themes on success", async () => { 2461 + setupAuthenticatedSession(["space.atbb.permission.manageThemes"]); 2462 + mockFetch.mockResolvedValueOnce(mockResponse({ deleted: true }, true, 200)); 2463 + 2464 + const routes = await loadAdminRoutes(); 2465 + const res = await routes.request("/admin/themes/3lbltheme1aa/delete", { 2466 + method: "POST", 2467 + headers: { cookie: "atbb_session=token" }, 2468 + }); 2469 + 2470 + expect(res.status).toBe(302); 2471 + expect(res.headers.get("location")).toBe("/admin/themes"); 2472 + }); 2473 + 2474 + it("redirects with human-friendly error message on 409 conflict (theme is a default)", async () => { 2475 + setupAuthenticatedSession(["space.atbb.permission.manageThemes"]); 2476 + mockFetch.mockResolvedValueOnce( 2477 + mockResponse( 2478 + { error: "Cannot delete a theme that is currently set as a default" }, 2479 + false, 2480 + 409 2481 + ) 2482 + ); 2483 + 2484 + const routes = await loadAdminRoutes(); 2485 + const res = await routes.request("/admin/themes/3lbltheme1aa/delete", { 2486 + method: "POST", 2487 + headers: { cookie: "atbb_session=token" }, 2488 + }); 2489 + 2490 + expect(res.status).toBe(302); 2491 + const location = res.headers.get("location") ?? ""; 2492 + expect(location).toContain("/admin/themes?error="); 2493 + expect(decodeURIComponent(location)).toContain("Cannot delete"); 2494 + }); 2495 + 2496 + it("redirects with error on generic AppView failure", async () => { 2497 + setupAuthenticatedSession(["space.atbb.permission.manageThemes"]); 2498 + mockFetch.mockResolvedValueOnce( 2499 + mockResponse({ error: "Internal server error" }, false, 500) 2500 + ); 2501 + 2502 + const routes = await loadAdminRoutes(); 2503 + const res = await routes.request("/admin/themes/3lbltheme1aa/delete", { 2504 + method: "POST", 2505 + headers: { cookie: "atbb_session=token" }, 2506 + }); 2507 + 2508 + expect(res.status).toBe(302); 2509 + expect(res.headers.get("location")).toContain("/admin/themes?error="); 2510 + }); 2511 + });