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): write failing tests for POST /admin/themes/:rkey/preview (ATB-59 TDD red)

Malpercio 54a5fb94 0ae2987f

+146
+146
apps/web/src/routes/__tests__/admin-themes.test.tsx
··· 173 173 expect(html).toMatch(/name="css-overrides"[^>]*disabled|disabled[^>]*name="css-overrides"/); 174 174 }); 175 175 }); 176 + 177 + describe("createAdminThemeRoutes — POST /admin/themes/:rkey/preview", () => { 178 + const MANAGE_THEMES = "space.atbb.permission.manageThemes"; 179 + 180 + beforeEach(() => { 181 + vi.stubGlobal("fetch", mockFetch); 182 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 183 + vi.resetModules(); 184 + }); 185 + 186 + afterEach(() => { 187 + vi.unstubAllGlobals(); 188 + vi.unstubAllEnvs(); 189 + mockFetch.mockReset(); 190 + }); 191 + 192 + function mockResponse(body: unknown, ok = true, status = 200) { 193 + return { 194 + ok, 195 + status, 196 + statusText: ok ? "OK" : "Error", 197 + json: () => Promise.resolve(body), 198 + }; 199 + } 200 + 201 + function setupAuthenticatedSession(permissions: string[]) { 202 + mockFetch.mockResolvedValueOnce( 203 + mockResponse({ authenticated: true, did: "did:plc:admin", handle: "admin.test" }) 204 + ); 205 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 206 + } 207 + 208 + async function loadThemeRoutes() { 209 + const { createAdminThemeRoutes } = await import("../admin-themes.js"); 210 + return createAdminThemeRoutes("http://localhost:3000"); 211 + } 212 + 213 + it("redirects unauthenticated users to /login", async () => { 214 + // No atbb_session cookie → getSession returns early without fetch 215 + const routes = await loadThemeRoutes(); 216 + const body = new URLSearchParams({ "color-bg": "#ff0000" }); 217 + const res = await routes.request("/admin/themes/abc123/preview", { 218 + method: "POST", 219 + headers: { "content-type": "application/x-www-form-urlencoded" }, 220 + body: body.toString(), 221 + }); 222 + expect(res.status).toBe(302); 223 + expect(res.headers.get("location")).toBe("/login"); 224 + }); 225 + 226 + it("returns an HTML fragment with a scoped style block containing submitted token values", async () => { 227 + setupAuthenticatedSession([MANAGE_THEMES]); 228 + 229 + const routes = await loadThemeRoutes(); 230 + const body = new URLSearchParams({ 231 + "color-bg": "#ff0000", 232 + "color-text": "#0000ff", 233 + }); 234 + const res = await routes.request("/admin/themes/abc123/preview", { 235 + method: "POST", 236 + headers: { 237 + "content-type": "application/x-www-form-urlencoded", 238 + cookie: "atbb_session=token", 239 + }, 240 + body: body.toString(), 241 + }); 242 + 243 + expect(res.status).toBe(200); 244 + const html = await res.text(); 245 + expect(html).toContain("--color-bg"); 246 + expect(html).toContain("#ff0000"); 247 + expect(html).toContain("--color-text"); 248 + expect(html).toContain("#0000ff"); 249 + expect(html).toContain(".preview-pane-inner"); 250 + // Fragment only — no full page wrapper 251 + expect(html).not.toContain("<html"); 252 + }); 253 + 254 + it("drops token values containing '<' (HTML injection prevention)", async () => { 255 + setupAuthenticatedSession([MANAGE_THEMES]); 256 + 257 + const routes = await loadThemeRoutes(); 258 + const body = new URLSearchParams({ 259 + "color-bg": "<script>alert(1)</script>", 260 + "color-text": "#1a1a1a", 261 + }); 262 + const res = await routes.request("/admin/themes/abc123/preview", { 263 + method: "POST", 264 + headers: { 265 + "content-type": "application/x-www-form-urlencoded", 266 + cookie: "atbb_session=token", 267 + }, 268 + body: body.toString(), 269 + }); 270 + 271 + expect(res.status).toBe(200); 272 + const html = await res.text(); 273 + expect(html).not.toContain("<script>"); 274 + expect(html).not.toContain("alert(1)"); 275 + // Clean token still renders 276 + expect(html).toContain("#1a1a1a"); 277 + }); 278 + 279 + it("drops token values containing ';' (CSS declaration injection prevention)", async () => { 280 + setupAuthenticatedSession([MANAGE_THEMES]); 281 + 282 + const routes = await loadThemeRoutes(); 283 + const body = new URLSearchParams({ 284 + "color-bg": "red; --injected: 1", 285 + }); 286 + const res = await routes.request("/admin/themes/abc123/preview", { 287 + method: "POST", 288 + headers: { 289 + "content-type": "application/x-www-form-urlencoded", 290 + cookie: "atbb_session=token", 291 + }, 292 + body: body.toString(), 293 + }); 294 + 295 + expect(res.status).toBe(200); 296 + const html = await res.text(); 297 + expect(html).not.toContain("--injected"); 298 + }); 299 + 300 + it("drops token values containing '}' (CSS block-escape injection prevention)", async () => { 301 + setupAuthenticatedSession([MANAGE_THEMES]); 302 + 303 + const routes = await loadThemeRoutes(); 304 + const body = new URLSearchParams({ 305 + "color-bg": "red} body{background:red", 306 + }); 307 + const res = await routes.request("/admin/themes/abc123/preview", { 308 + method: "POST", 309 + headers: { 310 + "content-type": "application/x-www-form-urlencoded", 311 + cookie: "atbb_session=token", 312 + }, 313 + body: body.toString(), 314 + }); 315 + 316 + expect(res.status).toBe(200); 317 + const html = await res.text(); 318 + // The injected block-escape value must not appear 319 + expect(html).not.toContain("red} body"); 320 + }); 321 + });