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 POST /api/admin/boards (ATB-45)

Malpercio b1e5e0d6 96062ce0

+199
+199
apps/appview/src/routes/__tests__/admin.test.ts
··· 1445 1445 }); 1446 1446 }); 1447 1447 1448 + describe.sequential("POST /api/admin/boards", () => { 1449 + let categoryUri: string; 1450 + 1451 + beforeEach(async () => { 1452 + await ctx.cleanDatabase(); 1453 + 1454 + mockUser = { did: "did:plc:test-admin" }; 1455 + mockPutRecord.mockClear(); 1456 + mockDeleteRecord.mockClear(); 1457 + mockPutRecord.mockResolvedValue({ 1458 + data: { 1459 + uri: `at://${ctx.config.forumDid}/space.atbb.forum.board/tid123`, 1460 + cid: "bafyboard", 1461 + }, 1462 + }); 1463 + 1464 + // Insert a category the tests can reference 1465 + await ctx.db.insert(categories).values({ 1466 + did: ctx.config.forumDid, 1467 + rkey: "tid-test-cat", 1468 + cid: "bafycat", 1469 + name: "Test Category", 1470 + createdAt: new Date("2026-01-01T00:00:00.000Z"), 1471 + indexedAt: new Date(), 1472 + }); 1473 + categoryUri = `at://${ctx.config.forumDid}/space.atbb.forum.category/tid-test-cat`; 1474 + }); 1475 + 1476 + it("creates board with valid body → 201 and putRecord called with categoryRef", async () => { 1477 + const res = await app.request("/api/admin/boards", { 1478 + method: "POST", 1479 + headers: { "Content-Type": "application/json" }, 1480 + body: JSON.stringify({ name: "General Chat", description: "Talk here.", sortOrder: 1, categoryUri }), 1481 + }); 1482 + 1483 + expect(res.status).toBe(201); 1484 + const data = await res.json(); 1485 + expect(data.uri).toContain("/space.atbb.forum.board/"); 1486 + expect(data.cid).toBe("bafyboard"); 1487 + expect(mockPutRecord).toHaveBeenCalledWith( 1488 + expect.objectContaining({ 1489 + repo: ctx.config.forumDid, 1490 + collection: "space.atbb.forum.board", 1491 + rkey: expect.any(String), 1492 + record: expect.objectContaining({ 1493 + $type: "space.atbb.forum.board", 1494 + name: "General Chat", 1495 + description: "Talk here.", 1496 + sortOrder: 1, 1497 + category: { category: { uri: categoryUri, cid: "bafycat" } }, 1498 + createdAt: expect.any(String), 1499 + }), 1500 + }) 1501 + ); 1502 + }); 1503 + 1504 + it("creates board without optional fields → 201", async () => { 1505 + const res = await app.request("/api/admin/boards", { 1506 + method: "POST", 1507 + headers: { "Content-Type": "application/json" }, 1508 + body: JSON.stringify({ name: "Minimal", categoryUri }), 1509 + }); 1510 + 1511 + expect(res.status).toBe(201); 1512 + expect(mockPutRecord).toHaveBeenCalledWith( 1513 + expect.objectContaining({ 1514 + record: expect.objectContaining({ name: "Minimal" }), 1515 + }) 1516 + ); 1517 + }); 1518 + 1519 + it("returns 400 when name is missing → no PDS write", async () => { 1520 + const res = await app.request("/api/admin/boards", { 1521 + method: "POST", 1522 + headers: { "Content-Type": "application/json" }, 1523 + body: JSON.stringify({ categoryUri }), 1524 + }); 1525 + 1526 + expect(res.status).toBe(400); 1527 + const data = await res.json(); 1528 + expect(data.error).toContain("name"); 1529 + expect(mockPutRecord).not.toHaveBeenCalled(); 1530 + }); 1531 + 1532 + it("returns 400 when name is empty string → no PDS write", async () => { 1533 + const res = await app.request("/api/admin/boards", { 1534 + method: "POST", 1535 + headers: { "Content-Type": "application/json" }, 1536 + body: JSON.stringify({ name: " ", categoryUri }), 1537 + }); 1538 + 1539 + expect(res.status).toBe(400); 1540 + expect(mockPutRecord).not.toHaveBeenCalled(); 1541 + }); 1542 + 1543 + it("returns 400 when categoryUri is missing → no PDS write", async () => { 1544 + const res = await app.request("/api/admin/boards", { 1545 + method: "POST", 1546 + headers: { "Content-Type": "application/json" }, 1547 + body: JSON.stringify({ name: "Test Board" }), 1548 + }); 1549 + 1550 + expect(res.status).toBe(400); 1551 + const data = await res.json(); 1552 + expect(data.error).toContain("categoryUri"); 1553 + expect(mockPutRecord).not.toHaveBeenCalled(); 1554 + }); 1555 + 1556 + it("returns 404 when categoryUri references unknown category → no PDS write", async () => { 1557 + const res = await app.request("/api/admin/boards", { 1558 + method: "POST", 1559 + headers: { "Content-Type": "application/json" }, 1560 + body: JSON.stringify({ name: "Test Board", categoryUri: `at://${ctx.config.forumDid}/space.atbb.forum.category/unknown999` }), 1561 + }); 1562 + 1563 + expect(res.status).toBe(404); 1564 + const data = await res.json(); 1565 + expect(data.error).toContain("Category not found"); 1566 + expect(mockPutRecord).not.toHaveBeenCalled(); 1567 + }); 1568 + 1569 + it("returns 400 for malformed JSON", async () => { 1570 + const res = await app.request("/api/admin/boards", { 1571 + method: "POST", 1572 + headers: { "Content-Type": "application/json" }, 1573 + body: "{ bad json }", 1574 + }); 1575 + 1576 + expect(res.status).toBe(400); 1577 + const data = await res.json(); 1578 + expect(data.error).toContain("Invalid JSON"); 1579 + expect(mockPutRecord).not.toHaveBeenCalled(); 1580 + }); 1581 + 1582 + it("returns 401 when unauthenticated → no PDS write", async () => { 1583 + mockUser = null; 1584 + 1585 + const res = await app.request("/api/admin/boards", { 1586 + method: "POST", 1587 + headers: { "Content-Type": "application/json" }, 1588 + body: JSON.stringify({ name: "Test", categoryUri }), 1589 + }); 1590 + 1591 + expect(res.status).toBe(401); 1592 + expect(mockPutRecord).not.toHaveBeenCalled(); 1593 + }); 1594 + 1595 + it("returns 503 when PDS network error", async () => { 1596 + mockPutRecord.mockRejectedValue(new Error("fetch failed")); 1597 + 1598 + const res = await app.request("/api/admin/boards", { 1599 + method: "POST", 1600 + headers: { "Content-Type": "application/json" }, 1601 + body: JSON.stringify({ name: "Test", categoryUri }), 1602 + }); 1603 + 1604 + expect(res.status).toBe(503); 1605 + const data = await res.json(); 1606 + expect(data.error).toContain("Unable to reach external service"); 1607 + expect(mockPutRecord).toHaveBeenCalled(); 1608 + }); 1609 + 1610 + it("returns 500 when ForumAgent unavailable", async () => { 1611 + ctx.forumAgent = null; 1612 + 1613 + const res = await app.request("/api/admin/boards", { 1614 + method: "POST", 1615 + headers: { "Content-Type": "application/json" }, 1616 + body: JSON.stringify({ name: "Test", categoryUri }), 1617 + }); 1618 + 1619 + expect(res.status).toBe(500); 1620 + const data = await res.json(); 1621 + expect(data.error).toContain("Forum agent not available"); 1622 + }); 1623 + 1624 + it("returns 403 when user lacks manageCategories permission", async () => { 1625 + const { requirePermission } = await import("../../middleware/permissions.js"); 1626 + const mockRequirePermission = requirePermission as any; 1627 + mockRequirePermission.mockImplementation(() => async (c: any) => { 1628 + return c.json({ error: "Forbidden" }, 403); 1629 + }); 1630 + 1631 + const testApp = new Hono<{ Variables: Variables }>().route("/api/admin", createAdminRoutes(ctx)); 1632 + const res = await testApp.request("/api/admin/boards", { 1633 + method: "POST", 1634 + headers: { "Content-Type": "application/json" }, 1635 + body: JSON.stringify({ name: "Test", categoryUri }), 1636 + }); 1637 + 1638 + expect(res.status).toBe(403); 1639 + expect(mockPutRecord).not.toHaveBeenCalled(); 1640 + 1641 + mockRequirePermission.mockImplementation(() => async (_c: any, next: any) => { 1642 + await next(); 1643 + }); 1644 + }); 1645 + }); 1646 + 1448 1647 }); 1449 1648