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, vi, beforeEach } from "vitest";
2import { createAppContext, destroyAppContext } from "../app-context.js";
3import type { AppConfig } from "../config.js";
4
5// Mock dependencies
6vi.mock("@atbb/db", () => ({
7 createDb: vi.fn(() => ({})),
8}));
9
10vi.mock("../firehose.js", () => ({
11 FirehoseService: vi.fn(() => ({
12 start: vi.fn(),
13 stop: vi.fn(),
14 })),
15}));
16
17vi.mock("@atproto/oauth-client-node", () => ({
18 NodeOAuthClient: vi.fn(() => ({
19 clientMetadata: {},
20 })),
21}));
22
23vi.mock("../oauth-stores.js", () => ({
24 OAuthStateStore: vi.fn(() => ({ destroy: vi.fn() })),
25 OAuthSessionStore: vi.fn(() => ({ destroy: vi.fn() })),
26}));
27
28vi.mock("../cookie-session-store.js", () => ({
29 CookieSessionStore: vi.fn(() => ({ destroy: vi.fn() })),
30}));
31
32vi.mock("@atbb/atproto", () => ({
33 ForumAgent: vi.fn(() => ({
34 initialize: vi.fn(),
35 shutdown: vi.fn(),
36 isAuthenticated: vi.fn(() => true),
37 })),
38}));
39
40vi.mock("@atbb/logger", () => ({
41 createLogger: vi.fn(() => ({
42 debug: vi.fn(),
43 info: vi.fn(),
44 warn: vi.fn(),
45 error: vi.fn(),
46 fatal: vi.fn(),
47 child: vi.fn(() => ({
48 debug: vi.fn(),
49 info: vi.fn(),
50 warn: vi.fn(),
51 error: vi.fn(),
52 fatal: vi.fn(),
53 child: vi.fn(),
54 shutdown: vi.fn(),
55 })),
56 shutdown: vi.fn(),
57 })),
58}));
59
60describe("AppContext", () => {
61 let config: AppConfig;
62
63 beforeEach(() => {
64 config = {
65 port: 3000,
66 forumDid: "did:plc:test123",
67 databaseUrl: "postgres://localhost/test",
68 jetstreamUrl: "wss://jetstream.example.com",
69 pdsUrl: "https://pds.example.com",
70 logLevel: "info",
71 oauthPublicUrl: "http://localhost:3000",
72 sessionSecret: "test-secret-with-minimum-32-chars-for-validation",
73 sessionTtlDays: 7,
74 forumHandle: "forum.example.com",
75 forumPassword: "test-password",
76 backfillRateLimit: 10,
77 backfillConcurrency: 10,
78 backfillCursorMaxAgeHours: 48,
79 };
80 });
81
82 describe("createAppContext", () => {
83 it("creates ForumAgent when credentials are provided", async () => {
84 const ctx = await createAppContext(config);
85
86 expect(ctx.forumAgent).toBeDefined();
87 expect(ctx.forumAgent).not.toBeNull();
88 expect(ctx.forumAgent!.initialize).toHaveBeenCalled();
89 });
90
91 it("sets forumAgent to null when credentials are missing", async () => {
92 config.forumHandle = undefined;
93 config.forumPassword = undefined;
94
95 const ctx = await createAppContext(config);
96
97 expect(ctx.forumAgent).toBeNull();
98 expect(ctx.logger.warn).toHaveBeenCalledWith(
99 "ForumAgent credentials missing",
100 expect.objectContaining({ operation: "createAppContext" })
101 );
102 });
103 });
104
105 describe("destroyAppContext", () => {
106 it("shuts down ForumAgent if present", async () => {
107 const ctx = await createAppContext(config);
108 const shutdownSpy = vi.spyOn(ctx.forumAgent!, "shutdown");
109
110 await destroyAppContext(ctx);
111
112 expect(shutdownSpy).toHaveBeenCalled();
113 });
114
115 it("handles null ForumAgent gracefully", async () => {
116 config.forumHandle = undefined;
117 config.forumPassword = undefined;
118 const ctx = await createAppContext(config);
119
120 await expect(destroyAppContext(ctx)).resolves.not.toThrow();
121 });
122 });
123});