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
1import { describe, it, expect, beforeEach, vi } from "vitest";
2import { BanEnforcer } from "../ban-enforcer.js";
3import { createMockLogger } from "./mock-logger.js";
4import type { Database } from "@atbb/db";
5
6const createMockDb = () => {
7 const mockSelect = vi.fn();
8 const mockUpdate = vi.fn();
9
10 return {
11 select: mockSelect,
12 update: mockUpdate,
13 } as unknown as Database;
14};
15
16describe("BanEnforcer", () => {
17 let mockDb: Database;
18 let enforcer: BanEnforcer;
19 let mockLogger: ReturnType<typeof createMockLogger>;
20
21 beforeEach(() => {
22 vi.clearAllMocks();
23 mockDb = createMockDb();
24 mockLogger = createMockLogger();
25 enforcer = new BanEnforcer(mockDb, mockLogger);
26 });
27
28 describe("isBanned", () => {
29 it("returns true when an active ban exists (no expiry)", async () => {
30 (mockDb.select as ReturnType<typeof vi.fn>).mockReturnValue({
31 from: vi.fn().mockReturnValue({
32 where: vi.fn().mockReturnValue({
33 limit: vi.fn().mockResolvedValue([{ id: 1n }]),
34 }),
35 }),
36 });
37
38 expect(await enforcer.isBanned("did:plc:banned123")).toBe(true);
39 });
40
41 it("returns false when no ban exists", async () => {
42 (mockDb.select as ReturnType<typeof vi.fn>).mockReturnValue({
43 from: vi.fn().mockReturnValue({
44 where: vi.fn().mockReturnValue({
45 limit: vi.fn().mockResolvedValue([]),
46 }),
47 }),
48 });
49
50 expect(await enforcer.isBanned("did:plc:user123")).toBe(false);
51 });
52
53 it("returns true (fail closed) when DB throws", async () => {
54 (mockDb.select as ReturnType<typeof vi.fn>).mockReturnValue({
55 from: vi.fn().mockReturnValue({
56 where: vi.fn().mockReturnValue({
57 limit: vi.fn().mockRejectedValue(new Error("DB connection lost")),
58 }),
59 }),
60 });
61
62 expect(await enforcer.isBanned("did:plc:user123")).toBe(true);
63 });
64
65 it("re-throws programming errors (TypeError) without fail-closed", async () => {
66 (mockDb.select as ReturnType<typeof vi.fn>).mockReturnValue({
67 from: vi.fn().mockReturnValue({
68 where: vi.fn().mockReturnValue({
69 limit: vi.fn().mockRejectedValue(new TypeError("Cannot read property")),
70 }),
71 }),
72 });
73
74 await expect(enforcer.isBanned("did:plc:user123")).rejects.toThrow(TypeError);
75 });
76 });
77
78 describe("applyBan", () => {
79 it("sets bannedByMod=true on all posts for the subject DID", async () => {
80 const mockWhere = vi.fn().mockResolvedValue(undefined);
81 const mockSet = vi.fn().mockReturnValue({ where: mockWhere });
82 (mockDb.update as ReturnType<typeof vi.fn>).mockReturnValue({ set: mockSet });
83
84 await enforcer.applyBan("did:plc:banned123");
85
86 expect(mockSet).toHaveBeenCalledWith({ bannedByMod: true });
87 expect(mockWhere).toHaveBeenCalled();
88 });
89
90 it("re-throws when DB throws during applyBan", async () => {
91 const dbError = new Error("DB connection lost");
92 (mockDb.update as ReturnType<typeof vi.fn>).mockReturnValue({
93 set: vi.fn().mockReturnValue({
94 where: vi.fn().mockRejectedValue(dbError),
95 }),
96 });
97
98 await expect(enforcer.applyBan("did:plc:banned123")).rejects.toThrow("DB connection lost");
99 });
100 });
101
102 describe("liftBan", () => {
103 it("sets bannedByMod=false on all posts for the subject DID without touching deleted", async () => {
104 const mockWhere = vi.fn().mockResolvedValue(undefined);
105 const mockSet = vi.fn().mockReturnValue({ where: mockWhere });
106 (mockDb.update as ReturnType<typeof vi.fn>).mockReturnValue({ set: mockSet });
107
108 await enforcer.liftBan("did:plc:unbanned123");
109
110 // Must only set bannedByMod — never touch deleted (user-initiated deletes must not be resurrected)
111 expect(mockSet).toHaveBeenCalledWith({ bannedByMod: false });
112 expect(mockSet).not.toHaveBeenCalledWith(expect.objectContaining({ deleted: expect.anything() }));
113 expect(mockWhere).toHaveBeenCalled();
114 });
115
116 it("re-throws when DB throws during liftBan", async () => {
117 const dbError = new Error("DB connection lost");
118 (mockDb.update as ReturnType<typeof vi.fn>).mockReturnValue({
119 set: vi.fn().mockReturnValue({
120 where: vi.fn().mockRejectedValue(dbError),
121 }),
122 });
123
124 await expect(enforcer.liftBan("did:plc:unbanned123")).rejects.toThrow("DB connection lost");
125 });
126 });
127});