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

Malpercio d904ad04 50932332

+232
+232
apps/appview/src/routes/__tests__/admin.test.ts
··· 1662 1662 }); 1663 1663 }); 1664 1664 1665 + describe.sequential("PUT /api/admin/boards/:id", () => { 1666 + let boardId: string; 1667 + let categoryUri: string; 1668 + 1669 + beforeEach(async () => { 1670 + await ctx.cleanDatabase(); 1671 + 1672 + mockUser = { did: "did:plc:test-admin" }; 1673 + mockPutRecord.mockClear(); 1674 + mockDeleteRecord.mockClear(); 1675 + mockPutRecord.mockResolvedValue({ 1676 + data: { 1677 + uri: `at://${ctx.config.forumDid}/space.atbb.forum.board/tid-test-board`, 1678 + cid: "bafyboardupdated", 1679 + }, 1680 + }); 1681 + 1682 + // Insert a category and a board 1683 + const [cat] = await ctx.db.insert(categories).values({ 1684 + did: ctx.config.forumDid, 1685 + rkey: "tid-test-cat", 1686 + cid: "bafycat", 1687 + name: "Test Category", 1688 + createdAt: new Date("2026-01-01T00:00:00.000Z"), 1689 + indexedAt: new Date(), 1690 + }).returning({ id: categories.id }); 1691 + 1692 + categoryUri = `at://${ctx.config.forumDid}/space.atbb.forum.category/tid-test-cat`; 1693 + 1694 + const [brd] = await ctx.db.insert(boards).values({ 1695 + did: ctx.config.forumDid, 1696 + rkey: "tid-test-board", 1697 + cid: "bafyboard", 1698 + name: "Original Name", 1699 + description: "Original description", 1700 + sortOrder: 1, 1701 + categoryId: cat.id, 1702 + categoryUri, 1703 + createdAt: new Date("2026-01-01T00:00:00.000Z"), 1704 + indexedAt: new Date(), 1705 + }).returning({ id: boards.id }); 1706 + 1707 + boardId = brd.id.toString(); 1708 + }); 1709 + 1710 + it("updates board with all fields → 200 and putRecord called with same rkey", async () => { 1711 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1712 + method: "PUT", 1713 + headers: { "Content-Type": "application/json" }, 1714 + body: JSON.stringify({ name: "Renamed Board", description: "New description", sortOrder: 2 }), 1715 + }); 1716 + 1717 + expect(res.status).toBe(200); 1718 + const data = await res.json(); 1719 + expect(data.uri).toContain("/space.atbb.forum.board/"); 1720 + expect(data.cid).toBe("bafyboardupdated"); 1721 + expect(mockPutRecord).toHaveBeenCalledWith( 1722 + expect.objectContaining({ 1723 + repo: ctx.config.forumDid, 1724 + collection: "space.atbb.forum.board", 1725 + rkey: "tid-test-board", 1726 + record: expect.objectContaining({ 1727 + $type: "space.atbb.forum.board", 1728 + name: "Renamed Board", 1729 + description: "New description", 1730 + sortOrder: 2, 1731 + category: { category: { uri: categoryUri, cid: "bafycat" } }, 1732 + }), 1733 + }) 1734 + ); 1735 + }); 1736 + 1737 + it("updates board without optional fields → falls back to existing values", async () => { 1738 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1739 + method: "PUT", 1740 + headers: { "Content-Type": "application/json" }, 1741 + body: JSON.stringify({ name: "Renamed Only" }), 1742 + }); 1743 + 1744 + expect(res.status).toBe(200); 1745 + expect(mockPutRecord).toHaveBeenCalledWith( 1746 + expect.objectContaining({ 1747 + record: expect.objectContaining({ 1748 + name: "Renamed Only", 1749 + description: "Original description", 1750 + sortOrder: 1, 1751 + }), 1752 + }) 1753 + ); 1754 + }); 1755 + 1756 + it("returns 400 when name is missing", async () => { 1757 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1758 + method: "PUT", 1759 + headers: { "Content-Type": "application/json" }, 1760 + body: JSON.stringify({ description: "No name" }), 1761 + }); 1762 + 1763 + expect(res.status).toBe(400); 1764 + const data = await res.json(); 1765 + expect(data.error).toContain("name"); 1766 + expect(mockPutRecord).not.toHaveBeenCalled(); 1767 + }); 1768 + 1769 + it("returns 400 when name is empty string", async () => { 1770 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1771 + method: "PUT", 1772 + headers: { "Content-Type": "application/json" }, 1773 + body: JSON.stringify({ name: " " }), 1774 + }); 1775 + 1776 + expect(res.status).toBe(400); 1777 + expect(mockPutRecord).not.toHaveBeenCalled(); 1778 + }); 1779 + 1780 + it("returns 400 for non-numeric ID", async () => { 1781 + const res = await app.request("/api/admin/boards/not-a-number", { 1782 + method: "PUT", 1783 + headers: { "Content-Type": "application/json" }, 1784 + body: JSON.stringify({ name: "Test" }), 1785 + }); 1786 + 1787 + expect(res.status).toBe(400); 1788 + expect(mockPutRecord).not.toHaveBeenCalled(); 1789 + }); 1790 + 1791 + it("returns 404 when board not found", async () => { 1792 + const res = await app.request("/api/admin/boards/99999", { 1793 + method: "PUT", 1794 + headers: { "Content-Type": "application/json" }, 1795 + body: JSON.stringify({ name: "Test" }), 1796 + }); 1797 + 1798 + expect(res.status).toBe(404); 1799 + const data = await res.json(); 1800 + expect(data.error).toContain("Board not found"); 1801 + expect(mockPutRecord).not.toHaveBeenCalled(); 1802 + }); 1803 + 1804 + it("returns 400 for malformed JSON", async () => { 1805 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1806 + method: "PUT", 1807 + headers: { "Content-Type": "application/json" }, 1808 + body: "{ bad json }", 1809 + }); 1810 + 1811 + expect(res.status).toBe(400); 1812 + expect(mockPutRecord).not.toHaveBeenCalled(); 1813 + }); 1814 + 1815 + it("returns 401 when unauthenticated", async () => { 1816 + mockUser = null; 1817 + 1818 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1819 + method: "PUT", 1820 + headers: { "Content-Type": "application/json" }, 1821 + body: JSON.stringify({ name: "Test" }), 1822 + }); 1823 + 1824 + expect(res.status).toBe(401); 1825 + expect(mockPutRecord).not.toHaveBeenCalled(); 1826 + }); 1827 + 1828 + it("returns 503 when PDS network error", async () => { 1829 + mockPutRecord.mockRejectedValue(new Error("fetch failed")); 1830 + 1831 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1832 + method: "PUT", 1833 + headers: { "Content-Type": "application/json" }, 1834 + body: JSON.stringify({ name: "Test" }), 1835 + }); 1836 + 1837 + expect(res.status).toBe(503); 1838 + const data = await res.json(); 1839 + expect(data.error).toContain("Unable to reach external service"); 1840 + }); 1841 + 1842 + it("returns 500 when ForumAgent unavailable", async () => { 1843 + ctx.forumAgent = null; 1844 + 1845 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1846 + method: "PUT", 1847 + headers: { "Content-Type": "application/json" }, 1848 + body: JSON.stringify({ name: "Test" }), 1849 + }); 1850 + 1851 + expect(res.status).toBe(500); 1852 + const data = await res.json(); 1853 + expect(data.error).toContain("Forum agent not available"); 1854 + }); 1855 + 1856 + it("returns 503 when ForumAgent not authenticated", async () => { 1857 + const originalAgent = ctx.forumAgent; 1858 + ctx.forumAgent = { getAgent: () => null } as any; 1859 + 1860 + const res = await app.request(`/api/admin/boards/${boardId}`, { 1861 + method: "PUT", 1862 + headers: { "Content-Type": "application/json" }, 1863 + body: JSON.stringify({ name: "Test" }), 1864 + }); 1865 + 1866 + expect(res.status).toBe(503); 1867 + const data = await res.json(); 1868 + expect(data.error).toBe("Forum agent not authenticated. Please try again later."); 1869 + expect(mockPutRecord).not.toHaveBeenCalled(); 1870 + 1871 + ctx.forumAgent = originalAgent; 1872 + }); 1873 + 1874 + it("returns 403 when user lacks manageCategories permission", async () => { 1875 + const { requirePermission } = await import("../../middleware/permissions.js"); 1876 + const mockRequirePermission = requirePermission as any; 1877 + mockRequirePermission.mockImplementation(() => async (c: any) => { 1878 + return c.json({ error: "Forbidden" }, 403); 1879 + }); 1880 + 1881 + const testApp = new Hono<{ Variables: Variables }>().route("/api/admin", createAdminRoutes(ctx)); 1882 + const res = await testApp.request(`/api/admin/boards/${boardId}`, { 1883 + method: "PUT", 1884 + headers: { "Content-Type": "application/json" }, 1885 + body: JSON.stringify({ name: "Test" }), 1886 + }); 1887 + 1888 + expect(res.status).toBe(403); 1889 + expect(mockPutRecord).not.toHaveBeenCalled(); 1890 + 1891 + mockRequirePermission.mockImplementation(() => async (_c: any, next: any) => { 1892 + await next(); 1893 + }); 1894 + }); 1895 + }); 1896 + 1665 1897 }); 1666 1898