an app to share curated trails
sidetrail.app
1/**
2 * Test setup for integration tests
3 *
4 * This setup uses a real PostgreSQL database and simulates Jetstream events
5 * by directly inserting into the database. The queries run against real data.
6 */
7
8import { beforeAll, beforeEach, afterAll, vi } from "vitest";
9import {
10 initTestDb,
11 clearTestDb,
12 closeTestDb,
13 resetCounters,
14 resetSeqCounter,
15 resetCurrentUser,
16 resetTidCounter,
17 getCurrentTestDid,
18 resolveHandle,
19 resolveDid,
20 getAvatar,
21} from "./helpers";
22
23// ============================================================================
24// Global Setup
25// ============================================================================
26
27beforeAll(async () => {
28 // Initialize test database with fresh schema
29 await initTestDb();
30});
31
32afterAll(async () => {
33 // Close database connection
34 await closeTestDb();
35});
36
37beforeEach(async () => {
38 // Clear all data between tests
39 await clearTestDb();
40
41 // Reset counters
42 resetCounters();
43 resetSeqCounter();
44 resetTidCounter();
45 resetCurrentUser();
46
47 // Clear mocks
48 vi.clearAllMocks();
49});
50
51// ============================================================================
52// Module Mocks
53// ============================================================================
54
55// Mock server-only (allow server code to run in tests)
56vi.mock("server-only", () => ({}));
57
58// Mock next/cache (cache operations are no-ops in tests)
59vi.mock("next/cache", () => ({
60 revalidatePath: vi.fn(),
61 revalidateTag: vi.fn(),
62 updateTag: vi.fn(),
63 cacheTag: vi.fn(),
64 cacheLife: vi.fn(),
65}));
66
67// Mock next/headers (no cookies in tests)
68vi.mock("next/headers", () => ({
69 cookies: vi.fn(async () => ({
70 get: vi.fn(() => null),
71 set: vi.fn(),
72 delete: vi.fn(),
73 })),
74}));
75
76// Mock next/navigation
77class NotFoundError extends Error {
78 constructor() {
79 super("NEXT_NOT_FOUND");
80 this.name = "NotFoundError";
81 }
82}
83
84vi.mock("next/navigation", () => ({
85 notFound: () => {
86 throw new NotFoundError();
87 },
88}));
89
90// Mock @/auth to use test users
91vi.mock("@/auth", () => ({
92 getCurrentDid: vi.fn(() => getCurrentTestDid()),
93 getCurrentUser: vi.fn(async () => {
94 const did = getCurrentTestDid();
95 if (!did) return null;
96 const handle = resolveDid(did);
97 const avatar = getAvatar(did);
98 return handle ? { did, handle, avatar } : null;
99 }),
100 getSessionAgent: vi.fn(async () => null),
101}));
102
103// Mock @/data/lex-client (PDS operations are not used in integration tests)
104// Actions that would write to PDS are tested via event emission instead
105vi.mock("@/data/lex-client", () => ({
106 getLexClient: vi.fn(async () => {
107 throw new Error(
108 "getLexClient should not be called in integration tests. " +
109 "Use event emission (emit.trail.create, etc.) to simulate PDS writes.",
110 );
111 }),
112}));
113
114// Mock @/data/db to use the test database
115vi.mock("@/data/db", async () => {
116 const { getTestDb, trails, walks, completions, drafts, accounts } =
117 await import("./helpers/test-db");
118
119 return {
120 getDb: () => getTestDb(),
121 trails,
122 walks,
123 completions,
124 drafts,
125 accounts,
126 // Re-export types from schema
127 type: {} as any,
128 };
129});
130
131// Re-export types for the db mock
132vi.mock("@sidetrail/db", async (importOriginal) => {
133 const original = await importOriginal<typeof import("@sidetrail/db")>();
134 return {
135 ...original,
136 // These are already in original, just re-exporting
137 };
138});
139
140// Mock @atproto/identity to use test users
141vi.mock("@atproto/identity", () => {
142 class IdResolver {
143 handle = {
144 resolve: async (handle: string) => resolveHandle(handle),
145 };
146 did = {
147 resolve: async (did: string) => {
148 const handle = resolveDid(did);
149 if (!handle) return null;
150 return {
151 id: did,
152 alsoKnownAs: [`at://${handle}`],
153 };
154 },
155 };
156 }
157
158 return { IdResolver };
159});
160
161// Mock fetch for avatar lookups
162const mockFetch = vi.fn(async (url: string) => {
163 // Handle bsky avatar requests
164 if (url.includes("app.bsky.actor.getProfile")) {
165 const match = url.match(/actor=([^&]+)/);
166 if (match) {
167 const did = decodeURIComponent(match[1]);
168 const avatar = getAvatar(did);
169 const handle = resolveDid(did);
170 return {
171 ok: true,
172 json: async () => ({
173 did,
174 handle: handle || did,
175 avatar,
176 }),
177 };
178 }
179 }
180
181 // Default: return empty response
182 return {
183 ok: false,
184 status: 404,
185 json: async () => ({}),
186 };
187});
188
189global.fetch = mockFetch as any;