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 refresh: vi.fn(),
66}));
67
68// Mock next/headers (no cookies in tests)
69vi.mock("next/headers", () => ({
70 cookies: vi.fn(async () => ({
71 get: vi.fn(() => null),
72 set: vi.fn(),
73 delete: vi.fn(),
74 })),
75}));
76
77// Mock next/navigation
78class NotFoundError extends Error {
79 constructor() {
80 super("NEXT_NOT_FOUND");
81 this.name = "NotFoundError";
82 }
83}
84
85vi.mock("next/navigation", () => ({
86 notFound: () => {
87 throw new NotFoundError();
88 },
89}));
90
91// Mock @/auth to use test users
92vi.mock("@/auth", () => ({
93 getCurrentDid: vi.fn(() => getCurrentTestDid()),
94 getCurrentUser: vi.fn(async () => {
95 const did = getCurrentTestDid();
96 if (!did) return null;
97 const handle = resolveDid(did);
98 const avatar = getAvatar(did);
99 return handle ? { did, handle, avatar } : null;
100 }),
101 getSessionAgent: vi.fn(async () => null),
102}));
103
104// Mock @/data/lex-client (PDS operations are not used in integration tests)
105// Actions that would write to PDS are tested via event emission instead
106vi.mock("@/data/lex-client", () => ({
107 getLexClient: vi.fn(async () => {
108 throw new Error(
109 "getLexClient should not be called in integration tests. " +
110 "Use event emission (emit.trail.create, etc.) to simulate PDS writes.",
111 );
112 }),
113}));
114
115// Mock @/data/db to use the test database
116vi.mock("@/data/db", async () => {
117 const { getTestDb, trails, walks, completions, drafts, accounts } =
118 await import("./helpers/test-db");
119
120 return {
121 getDb: () => getTestDb(),
122 trails,
123 walks,
124 completions,
125 drafts,
126 accounts,
127 // Re-export types from schema
128 type: {} as any,
129 };
130});
131
132// Re-export types for the db mock
133vi.mock("@sidetrail/db", async (importOriginal) => {
134 const original = await importOriginal<typeof import("@sidetrail/db")>();
135 return {
136 ...original,
137 // These are already in original, just re-exporting
138 };
139});
140
141// Mock @atproto/identity to use test users
142vi.mock("@atproto/identity", () => {
143 class IdResolver {
144 handle = {
145 resolve: async (handle: string) => resolveHandle(handle),
146 };
147 did = {
148 resolve: async (did: string) => {
149 const handle = resolveDid(did);
150 if (!handle) return null;
151 return {
152 id: did,
153 alsoKnownAs: [`at://${handle}`],
154 };
155 },
156 };
157 }
158
159 return { IdResolver };
160});
161
162// Mock fetch for avatar lookups
163const mockFetch = vi.fn(async (url: string) => {
164 // Handle bsky avatar requests
165 if (url.includes("app.bsky.actor.getProfile")) {
166 const match = url.match(/actor=([^&]+)/);
167 if (match) {
168 const did = decodeURIComponent(match[1]);
169 const avatar = getAvatar(did);
170 const handle = resolveDid(did);
171 return {
172 ok: true,
173 json: async () => ({
174 did,
175 handle: handle || did,
176 avatar,
177 }),
178 };
179 }
180 }
181
182 // Default: return empty response
183 return {
184 ok: false,
185 status: 404,
186 json: async () => ({}),
187 };
188});
189
190global.fetch = mockFetch as any;