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(appview): add failing tests for DELETE /api/admin/boards/:id (ATB-45)

+181 -1
+181 -1
apps/appview/src/routes/__tests__/admin.test.ts
··· 2 2 import { createTestContext, type TestContext } from "../../lib/__tests__/test-context.js"; 3 3 import { Hono } from "hono"; 4 4 import type { Variables } from "../../types.js"; 5 - import { memberships, roles, rolePermissions, users, forums, categories, boards } from "@atbb/db"; 5 + import { memberships, roles, rolePermissions, users, forums, categories, boards, posts } from "@atbb/db"; 6 + import { eq } from "drizzle-orm"; 6 7 7 8 // Mock middleware at module level 8 9 let mockUser: any; ··· 1889 1890 1890 1891 expect(res.status).toBe(403); 1891 1892 expect(mockPutRecord).not.toHaveBeenCalled(); 1893 + 1894 + mockRequirePermission.mockImplementation(() => async (_c: any, next: any) => { 1895 + await next(); 1896 + }); 1897 + }); 1898 + }); 1899 + 1900 + describe.sequential("DELETE /api/admin/boards/:id", () => { 1901 + let boardId: string; 1902 + let categoryUri: string; 1903 + 1904 + beforeEach(async () => { 1905 + await ctx.cleanDatabase(); 1906 + 1907 + mockUser = { did: "did:plc:test-admin" }; 1908 + mockPutRecord.mockClear(); 1909 + mockDeleteRecord.mockClear(); 1910 + mockDeleteRecord.mockResolvedValue({}); 1911 + 1912 + // Insert a category and a board 1913 + const [cat] = await ctx.db.insert(categories).values({ 1914 + did: ctx.config.forumDid, 1915 + rkey: "tid-test-cat", 1916 + cid: "bafycat", 1917 + name: "Test Category", 1918 + createdAt: new Date("2026-01-01T00:00:00.000Z"), 1919 + indexedAt: new Date(), 1920 + }).returning({ id: categories.id }); 1921 + 1922 + categoryUri = `at://${ctx.config.forumDid}/space.atbb.forum.category/tid-test-cat`; 1923 + 1924 + const [brd] = await ctx.db.insert(boards).values({ 1925 + did: ctx.config.forumDid, 1926 + rkey: "tid-test-board", 1927 + cid: "bafyboard", 1928 + name: "Test Board", 1929 + categoryId: cat.id, 1930 + categoryUri, 1931 + createdAt: new Date("2026-01-01T00:00:00.000Z"), 1932 + indexedAt: new Date(), 1933 + }).returning({ id: boards.id }); 1934 + 1935 + boardId = brd.id.toString(); 1936 + }); 1937 + 1938 + it("deletes empty board → 200 and deleteRecord called", async () => { 1939 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1940 + method: "DELETE", 1941 + }); 1942 + 1943 + expect(res.status).toBe(200); 1944 + const data = await res.json(); 1945 + expect(data.success).toBe(true); 1946 + expect(mockDeleteRecord).toHaveBeenCalledWith( 1947 + expect.objectContaining({ 1948 + repo: ctx.config.forumDid, 1949 + collection: "space.atbb.forum.board", 1950 + rkey: "tid-test-board", 1951 + }) 1952 + ); 1953 + }); 1954 + 1955 + it("returns 409 when board has posts → deleteRecord NOT called", async () => { 1956 + // Insert a user and a post referencing this board 1957 + await ctx.db.insert(users).values({ 1958 + did: "did:plc:test-user", 1959 + handle: "testuser.bsky.social", 1960 + indexedAt: new Date(), 1961 + }); 1962 + 1963 + const [brd] = await ctx.db.select().from(boards).where(eq(boards.rkey, "tid-test-board")).limit(1); 1964 + 1965 + await ctx.db.insert(posts).values({ 1966 + did: "did:plc:test-user", 1967 + rkey: "tid-test-post", 1968 + cid: "bafypost", 1969 + text: "Hello world", 1970 + boardId: brd.id, 1971 + boardUri: `at://${ctx.config.forumDid}/space.atbb.forum.board/tid-test-board`, 1972 + forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, 1973 + createdAt: new Date(), 1974 + indexedAt: new Date(), 1975 + }); 1976 + 1977 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1978 + method: "DELETE", 1979 + }); 1980 + 1981 + expect(res.status).toBe(409); 1982 + const data = await res.json(); 1983 + expect(data.error).toContain("posts"); 1984 + expect(mockDeleteRecord).not.toHaveBeenCalled(); 1985 + }); 1986 + 1987 + it("returns 400 for non-numeric ID", async () => { 1988 + const res = await app.request("/api/admin/boards/not-a-number", { 1989 + method: "DELETE", 1990 + }); 1991 + 1992 + expect(res.status).toBe(400); 1993 + expect(mockDeleteRecord).not.toHaveBeenCalled(); 1994 + }); 1995 + 1996 + it("returns 404 when board not found", async () => { 1997 + const res = await app.request("/api/admin/boards/99999", { 1998 + method: "DELETE", 1999 + }); 2000 + 2001 + expect(res.status).toBe(404); 2002 + const data = await res.json(); 2003 + expect(data.error).toContain("Board not found"); 2004 + expect(mockDeleteRecord).not.toHaveBeenCalled(); 2005 + }); 2006 + 2007 + it("returns 401 when unauthenticated", async () => { 2008 + mockUser = null; 2009 + 2010 + const res = await app.request(`/api/admin/boards/${boardId}`, { 2011 + method: "DELETE", 2012 + }); 2013 + 2014 + expect(res.status).toBe(401); 2015 + expect(mockDeleteRecord).not.toHaveBeenCalled(); 2016 + }); 2017 + 2018 + it("returns 503 when PDS network error", async () => { 2019 + mockDeleteRecord.mockRejectedValue(new Error("fetch failed")); 2020 + 2021 + const res = await app.request(`/api/admin/boards/${boardId}`, { 2022 + method: "DELETE", 2023 + }); 2024 + 2025 + expect(res.status).toBe(503); 2026 + const data = await res.json(); 2027 + expect(data.error).toContain("Unable to reach external service"); 2028 + }); 2029 + 2030 + it("returns 500 when ForumAgent unavailable", async () => { 2031 + ctx.forumAgent = null; 2032 + 2033 + const res = await app.request(`/api/admin/boards/${boardId}`, { 2034 + method: "DELETE", 2035 + }); 2036 + 2037 + expect(res.status).toBe(500); 2038 + const data = await res.json(); 2039 + expect(data.error).toContain("Forum agent not available"); 2040 + }); 2041 + 2042 + it("returns 503 when ForumAgent not authenticated", async () => { 2043 + const originalAgent = ctx.forumAgent; 2044 + ctx.forumAgent = { getAgent: () => null } as any; 2045 + 2046 + const res = await app.request(`/api/admin/boards/${boardId}`, { 2047 + method: "DELETE", 2048 + }); 2049 + 2050 + expect(res.status).toBe(503); 2051 + const data = await res.json(); 2052 + expect(data.error).toBe("Forum agent not authenticated. Please try again later."); 2053 + expect(mockDeleteRecord).not.toHaveBeenCalled(); 2054 + 2055 + ctx.forumAgent = originalAgent; 2056 + }); 2057 + 2058 + it("returns 403 when user lacks manageCategories permission", async () => { 2059 + const { requirePermission } = await import("../../middleware/permissions.js"); 2060 + const mockRequirePermission = requirePermission as any; 2061 + mockRequirePermission.mockImplementation(() => async (c: any) => { 2062 + return c.json({ error: "Forbidden" }, 403); 2063 + }); 2064 + 2065 + const testApp = new Hono<{ Variables: Variables }>().route("/api/admin", createAdminRoutes(ctx)); 2066 + const res = await testApp.request(`/api/admin/boards/${boardId}`, { 2067 + method: "DELETE", 2068 + }); 2069 + 2070 + expect(res.status).toBe(403); 2071 + expect(mockDeleteRecord).not.toHaveBeenCalled(); 1892 2072 1893 2073 mockRequirePermission.mockImplementation(() => async (_c: any, next: any) => { 1894 2074 await next();