an app to share curated trails
sidetrail.app
1/**
2 * Test setup for action tests with eventual consistency verification
3 *
4 * This setup allows actions to run (unlike the main setup which mocks them out)
5 * and automatically verifies that optimistic writes match ingester behavior.
6 *
7 * Usage: Create test files in data/__tests__/actions/ that import this setup.
8 */
9
10import { beforeAll, beforeEach, afterAll, afterEach, vi } from "vitest";
11import {
12 initTestDb,
13 clearTestDb,
14 closeTestDb,
15 resetCounters,
16 resetSeqCounter,
17 resetCurrentUser,
18 resetTidCounter,
19 getCurrentTestDid,
20 resolveHandle,
21 resolveDid,
22 getAvatar,
23} from "./helpers";
24import {
25 createAfterMock,
26 createCapturingLexClient,
27 verifyEventualConsistency,
28 reset as resetEventualConsistency,
29} from "./helpers/eventual-consistency";
30
31// ============================================================================
32// Global Setup
33// ============================================================================
34
35beforeAll(async () => {
36 await initTestDb();
37});
38
39afterAll(async () => {
40 await closeTestDb();
41});
42
43beforeEach(async () => {
44 await clearTestDb();
45 resetCounters();
46 resetSeqCounter();
47 resetTidCounter();
48 resetCurrentUser();
49 resetEventualConsistency();
50 vi.clearAllMocks();
51});
52
53afterEach(async () => {
54 // Verify eventual consistency after each test
55 await verifyEventualConsistency();
56});
57
58// ============================================================================
59// Module Mocks
60// ============================================================================
61
62// Mock server-only
63vi.mock("server-only", () => ({}));
64
65// Mock next/cache
66vi.mock("next/cache", () => ({
67 revalidatePath: vi.fn(),
68 revalidateTag: vi.fn(),
69 updateTag: vi.fn(),
70 cacheTag: vi.fn(),
71 cacheLife: vi.fn(),
72 refresh: vi.fn(),
73}));
74
75// Mock next/headers
76vi.mock("next/headers", () => ({
77 cookies: vi.fn(async () => ({
78 get: vi.fn(() => null),
79 set: vi.fn(),
80 delete: vi.fn(),
81 })),
82}));
83
84// Mock next/navigation
85class NotFoundError extends Error {
86 constructor() {
87 super("NEXT_NOT_FOUND");
88 this.name = "NotFoundError";
89 }
90}
91
92vi.mock("next/navigation", () => ({
93 notFound: () => {
94 throw new NotFoundError();
95 },
96}));
97
98// Mock next/server - capture after() callbacks
99vi.mock("next/server", () => ({
100 after: createAfterMock(),
101}));
102
103// Mock @/auth
104vi.mock("@/auth", () => ({
105 getCurrentDid: vi.fn(() => getCurrentTestDid()),
106 getCurrentUser: vi.fn(async () => {
107 const did = getCurrentTestDid();
108 if (!did) return null;
109 const handle = resolveDid(did);
110 const avatar = getAvatar(did);
111 return handle ? { did, handle, avatar } : null;
112 }),
113 getSessionAgent: vi.fn(async () => null),
114}));
115
116// Mock @/data/lex-client - use capturing client that tracks PDS operations
117const capturingClient = createCapturingLexClient(() => getCurrentTestDid() || "did:plc:test");
118
119vi.mock("@/data/lex-client", () => ({
120 getLexClient: vi.fn(async () => capturingClient),
121}));
122
123// Mock @/data/db to use test database
124vi.mock("@/data/db", async () => {
125 const { getTestDb, trails, walks, completions, drafts, accounts } =
126 await import("./helpers/test-db");
127
128 return {
129 getDb: () => getTestDb(),
130 trails,
131 walks,
132 completions,
133 drafts,
134 accounts,
135 type: {} as any,
136 };
137});
138
139vi.mock("@sidetrail/db", async (importOriginal) => {
140 const original = await importOriginal<typeof import("@sidetrail/db")>();
141 return { ...original };
142});
143
144// Mock @atproto/identity
145vi.mock("@atproto/identity", () => {
146 class IdResolver {
147 handle = {
148 resolve: async (handle: string) => resolveHandle(handle),
149 };
150 did = {
151 resolve: async (did: string) => {
152 const handle = resolveDid(did);
153 if (!handle) return null;
154 return {
155 id: did,
156 alsoKnownAs: [`at://${handle}`],
157 };
158 },
159 };
160 }
161
162 return { IdResolver };
163});
164
165// Mock fetch
166const mockFetch = vi.fn(async (url: string) => {
167 if (url.includes("app.bsky.actor.getProfile")) {
168 const match = url.match(/actor=([^&]+)/);
169 if (match) {
170 const did = decodeURIComponent(match[1]);
171 const avatar = getAvatar(did);
172 const handle = resolveDid(did);
173 return {
174 ok: true,
175 json: async () => ({ did, handle: handle || did, avatar }),
176 };
177 }
178 }
179 return { ok: false, status: 404, json: async () => ({}) };
180});
181
182global.fetch = mockFetch as any;