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): add failing tests for GET /admin/themes/:rkey (ATB-59)

+174
+174
apps/web/src/routes/__tests__/admin-themes.test.tsx
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + const mockFetch = vi.fn(); 4 + 5 + describe("createAdminThemeRoutes — GET /admin/themes/:rkey", () => { 6 + beforeEach(() => { 7 + vi.stubGlobal("fetch", mockFetch); 8 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 9 + vi.resetModules(); 10 + }); 11 + 12 + afterEach(() => { 13 + vi.unstubAllGlobals(); 14 + vi.unstubAllEnvs(); 15 + mockFetch.mockReset(); 16 + }); 17 + 18 + function mockResponse(body: unknown, ok = true, status = 200) { 19 + return { 20 + ok, 21 + status, 22 + statusText: ok ? "OK" : "Error", 23 + json: () => Promise.resolve(body), 24 + }; 25 + } 26 + 27 + /** 28 + * Sets up the two-fetch mock sequence for an authenticated session. 29 + * Call 1: GET /api/auth/session 30 + * Call 2: GET /api/admin/members/me 31 + */ 32 + function setupAuth(permissions: string[]) { 33 + mockFetch.mockResolvedValueOnce( 34 + mockResponse({ authenticated: true, did: "did:plc:forum", handle: "admin.bsky.social" }) 35 + ); 36 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 37 + } 38 + 39 + async function loadThemeRoutes() { 40 + const { createAdminThemeRoutes } = await import("../admin-themes.js"); 41 + return createAdminThemeRoutes("http://localhost:3000"); 42 + } 43 + 44 + const sampleTheme = { 45 + id: "1", 46 + uri: "at://did:plc:forum/space.atbb.forum.theme/abc123", 47 + name: "My Theme", 48 + colorScheme: "light", 49 + tokens: { "color-bg": "#f5f0e8", "color-text": "#1a1a1a" }, 50 + cssOverrides: null, 51 + fontUrls: null, 52 + createdAt: "2026-01-01T00:00:00.000Z", 53 + indexedAt: "2026-01-01T00:00:00.000Z", 54 + }; 55 + 56 + // ── Unauthenticated ────────────────────────────────────────────────────── 57 + 58 + it("redirects unauthenticated users to /login", async () => { 59 + // No atbb_session cookie → session check returns unauthenticated 60 + mockFetch.mockResolvedValueOnce( 61 + mockResponse({ authenticated: false }) 62 + ); 63 + const routes = await loadThemeRoutes(); 64 + const res = await routes.request("/admin/themes/abc123"); 65 + expect(res.status).toBe(302); 66 + expect(res.headers.get("location")).toBe("/login"); 67 + }); 68 + 69 + // ── No manageThemes permission → 403 ──────────────────────────────────── 70 + 71 + it("returns 403 for users without manageThemes permission", async () => { 72 + setupAuth([]); 73 + const routes = await loadThemeRoutes(); 74 + const res = await routes.request("/admin/themes/abc123", { 75 + headers: { cookie: "atbb_session=token" }, 76 + }); 77 + expect(res.status).toBe(403); 78 + const html = await res.text(); 79 + expect(html.toLowerCase()).toMatch(/access denied|permission/); 80 + }); 81 + 82 + // ── Theme not found → 404 ──────────────────────────────────────────────── 83 + 84 + it("returns 404 when theme not found", async () => { 85 + setupAuth(["space.atbb.permission.manageThemes"]); 86 + // Third fetch: AppView returns 404 87 + mockFetch.mockResolvedValueOnce( 88 + mockResponse({ error: "Theme not found" }, false, 404) 89 + ); 90 + const routes = await loadThemeRoutes(); 91 + const res = await routes.request("/admin/themes/abc123", { 92 + headers: { cookie: "atbb_session=token" }, 93 + }); 94 + expect(res.status).toBe(404); 95 + }); 96 + 97 + // ── Happy path: renders editor with theme tokens ────────────────────────── 98 + 99 + it("renders editor with theme name and token inputs", async () => { 100 + setupAuth(["space.atbb.permission.manageThemes"]); 101 + mockFetch.mockResolvedValueOnce(mockResponse(sampleTheme)); 102 + const routes = await loadThemeRoutes(); 103 + const res = await routes.request("/admin/themes/abc123", { 104 + headers: { cookie: "atbb_session=token" }, 105 + }); 106 + expect(res.status).toBe(200); 107 + const html = await res.text(); 108 + expect(html).toContain("My Theme"); 109 + expect(html).toContain('name="color-bg"'); 110 + expect(html).toContain("#f5f0e8"); 111 + }); 112 + 113 + // ── Preset override ─────────────────────────────────────────────────────── 114 + 115 + it("uses preset tokens when ?preset=neobrutal-light is present", async () => { 116 + setupAuth(["space.atbb.permission.manageThemes"]); 117 + // Theme has empty tokens — preset should fill them in 118 + mockFetch.mockResolvedValueOnce( 119 + mockResponse({ ...sampleTheme, tokens: {} }) 120 + ); 121 + const routes = await loadThemeRoutes(); 122 + const res = await routes.request("/admin/themes/abc123?preset=neobrutal-light", { 123 + headers: { cookie: "atbb_session=token" }, 124 + }); 125 + expect(res.status).toBe(200); 126 + const html = await res.text(); 127 + // neobrutal-light color-bg is #f5f0e8 — preset fills empty token slots 128 + expect(html).toContain("#f5f0e8"); 129 + }); 130 + 131 + // ── Success banner ──────────────────────────────────────────────────────── 132 + 133 + it("shows success banner when ?success=1 is present", async () => { 134 + setupAuth(["space.atbb.permission.manageThemes"]); 135 + mockFetch.mockResolvedValueOnce(mockResponse(sampleTheme)); 136 + const routes = await loadThemeRoutes(); 137 + const res = await routes.request("/admin/themes/abc123?success=1", { 138 + headers: { cookie: "atbb_session=token" }, 139 + }); 140 + expect(res.status).toBe(200); 141 + const html = await res.text(); 142 + expect(html.toLowerCase()).toContain("saved"); 143 + }); 144 + 145 + // ── Error banner ────────────────────────────────────────────────────────── 146 + 147 + it("shows error banner when ?error= is present", async () => { 148 + setupAuth(["space.atbb.permission.manageThemes"]); 149 + mockFetch.mockResolvedValueOnce(mockResponse(sampleTheme)); 150 + const routes = await loadThemeRoutes(); 151 + const res = await routes.request( 152 + "/admin/themes/abc123?error=Something+went+wrong", 153 + { headers: { cookie: "atbb_session=token" } } 154 + ); 155 + expect(res.status).toBe(200); 156 + const html = await res.text(); 157 + expect(html).toContain("Something went wrong"); 158 + }); 159 + 160 + // ── CSS overrides field is disabled ───────────────────────────────────── 161 + 162 + it("renders CSS overrides field as disabled", async () => { 163 + setupAuth(["space.atbb.permission.manageThemes"]); 164 + mockFetch.mockResolvedValueOnce(mockResponse(sampleTheme)); 165 + const routes = await loadThemeRoutes(); 166 + const res = await routes.request("/admin/themes/abc123", { 167 + headers: { cookie: "atbb_session=token" }, 168 + }); 169 + expect(res.status).toBe(200); 170 + const html = await res.text(); 171 + expect(html).toContain("css-overrides"); 172 + expect(html).toContain("disabled"); 173 + }); 174 + });