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.

test(web): add admin landing page route tests (ATB-42)

Malpercio 30b64ff7 b2758b77

+195
+195
apps/web/src/routes/__tests__/admin.test.tsx
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + const mockFetch = vi.fn(); 4 + 5 + describe("createAdminRoutes — GET /admin", () => { 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 setupAuthenticatedSession(permissions: string[]) { 33 + mockFetch.mockResolvedValueOnce( 34 + mockResponse({ authenticated: true, did: "did:plc:user", handle: "alice.bsky.social" }) 35 + ); 36 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 37 + } 38 + 39 + async function loadAdminRoutes() { 40 + const { createAdminRoutes } = await import("../admin.js"); 41 + return createAdminRoutes("http://localhost:3000"); 42 + } 43 + 44 + // ── Unauthenticated ───────────────────────────────────────────────────── 45 + 46 + it("redirects unauthenticated users to /login", async () => { 47 + // No atbb_session cookie → zero fetch calls 48 + const routes = await loadAdminRoutes(); 49 + const res = await routes.request("/admin"); 50 + expect(res.status).toBe(302); 51 + expect(res.headers.get("location")).toBe("/login"); 52 + }); 53 + 54 + // ── No admin permissions → 403 ────────────────────────────────────────── 55 + 56 + it("returns 403 for authenticated user with no permissions", async () => { 57 + setupAuthenticatedSession([]); 58 + const routes = await loadAdminRoutes(); 59 + const res = await routes.request("/admin", { 60 + headers: { cookie: "atbb_session=token" }, 61 + }); 62 + expect(res.status).toBe(403); 63 + const html = await res.text(); 64 + expect(html).toContain("Access Denied"); 65 + }); 66 + 67 + it("returns 403 for authenticated user with only an unrelated permission", async () => { 68 + setupAuthenticatedSession(["space.atbb.permission.someOtherThing"]); 69 + const routes = await loadAdminRoutes(); 70 + const res = await routes.request("/admin", { 71 + headers: { cookie: "atbb_session=token" }, 72 + }); 73 + expect(res.status).toBe(403); 74 + }); 75 + 76 + // ── Wildcard → all cards ───────────────────────────────────────────────── 77 + 78 + it("grants access and shows all cards for wildcard (*) permission", async () => { 79 + setupAuthenticatedSession(["*"]); 80 + const routes = await loadAdminRoutes(); 81 + const res = await routes.request("/admin", { 82 + headers: { cookie: "atbb_session=token" }, 83 + }); 84 + expect(res.status).toBe(200); 85 + const html = await res.text(); 86 + expect(html).toContain('href="/admin/members"'); 87 + expect(html).toContain('href="/admin/structure"'); 88 + expect(html).toContain('href="/admin/modlog"'); 89 + }); 90 + 91 + // ── Single permission → only that card ────────────────────────────────── 92 + 93 + it("shows only Members card for user with only manageMembers", async () => { 94 + setupAuthenticatedSession(["space.atbb.permission.manageMembers"]); 95 + const routes = await loadAdminRoutes(); 96 + const res = await routes.request("/admin", { 97 + headers: { cookie: "atbb_session=token" }, 98 + }); 99 + expect(res.status).toBe(200); 100 + const html = await res.text(); 101 + expect(html).toContain('href="/admin/members"'); 102 + expect(html).not.toContain('href="/admin/structure"'); 103 + expect(html).not.toContain('href="/admin/modlog"'); 104 + }); 105 + 106 + it("shows only Structure card for user with only manageCategories", async () => { 107 + setupAuthenticatedSession(["space.atbb.permission.manageCategories"]); 108 + const routes = await loadAdminRoutes(); 109 + const res = await routes.request("/admin", { 110 + headers: { cookie: "atbb_session=token" }, 111 + }); 112 + expect(res.status).toBe(200); 113 + const html = await res.text(); 114 + expect(html).not.toContain('href="/admin/members"'); 115 + expect(html).toContain('href="/admin/structure"'); 116 + expect(html).not.toContain('href="/admin/modlog"'); 117 + }); 118 + 119 + it("shows only Mod Log card for user with only moderatePosts", async () => { 120 + setupAuthenticatedSession(["space.atbb.permission.moderatePosts"]); 121 + const routes = await loadAdminRoutes(); 122 + const res = await routes.request("/admin", { 123 + headers: { cookie: "atbb_session=token" }, 124 + }); 125 + expect(res.status).toBe(200); 126 + const html = await res.text(); 127 + expect(html).not.toContain('href="/admin/members"'); 128 + expect(html).not.toContain('href="/admin/structure"'); 129 + expect(html).toContain('href="/admin/modlog"'); 130 + }); 131 + 132 + it("shows only Mod Log card for user with only banUsers", async () => { 133 + setupAuthenticatedSession(["space.atbb.permission.banUsers"]); 134 + const routes = await loadAdminRoutes(); 135 + const res = await routes.request("/admin", { 136 + headers: { cookie: "atbb_session=token" }, 137 + }); 138 + expect(res.status).toBe(200); 139 + const html = await res.text(); 140 + expect(html).not.toContain('href="/admin/members"'); 141 + expect(html).toContain('href="/admin/modlog"'); 142 + }); 143 + 144 + it("shows only Mod Log card for user with only lockTopics", async () => { 145 + setupAuthenticatedSession(["space.atbb.permission.lockTopics"]); 146 + const routes = await loadAdminRoutes(); 147 + const res = await routes.request("/admin", { 148 + headers: { cookie: "atbb_session=token" }, 149 + }); 150 + expect(res.status).toBe(200); 151 + const html = await res.text(); 152 + expect(html).not.toContain('href="/admin/members"'); 153 + expect(html).toContain('href="/admin/modlog"'); 154 + }); 155 + 156 + // ── Multi-permission combos ────────────────────────────────────────────── 157 + 158 + it("shows Members and Mod Log cards for manageMembers + moderatePosts", async () => { 159 + setupAuthenticatedSession([ 160 + "space.atbb.permission.manageMembers", 161 + "space.atbb.permission.moderatePosts", 162 + ]); 163 + const routes = await loadAdminRoutes(); 164 + const res = await routes.request("/admin", { 165 + headers: { cookie: "atbb_session=token" }, 166 + }); 167 + expect(res.status).toBe(200); 168 + const html = await res.text(); 169 + expect(html).toContain('href="/admin/members"'); 170 + expect(html).not.toContain('href="/admin/structure"'); 171 + expect(html).toContain('href="/admin/modlog"'); 172 + }); 173 + 174 + // ── Page structure ─────────────────────────────────────────────────────── 175 + 176 + it("renders 'Admin Panel' page title", async () => { 177 + setupAuthenticatedSession(["space.atbb.permission.manageMembers"]); 178 + const routes = await loadAdminRoutes(); 179 + const res = await routes.request("/admin", { 180 + headers: { cookie: "atbb_session=token" }, 181 + }); 182 + const html = await res.text(); 183 + expect(html).toContain("Admin Panel"); 184 + }); 185 + 186 + it("renders admin-nav-grid container", async () => { 187 + setupAuthenticatedSession(["space.atbb.permission.manageMembers"]); 188 + const routes = await loadAdminRoutes(); 189 + const res = await routes.request("/admin", { 190 + headers: { cookie: "atbb_session=token" }, 191 + }); 192 + const html = await res.text(); 193 + expect(html).toContain("admin-nav-grid"); 194 + }); 195 + });