this repo has no description
1import { vi } from "vitest";
2import type { AppPassword, InviteCode, Session } from "../lib/api";
3import { _testSetState } from "../lib/auth.svelte";
4
5const originalPushState = globalThis.history.pushState.bind(globalThis.history);
6const originalReplaceState = globalThis.history.replaceState.bind(
7 globalThis.history,
8);
9
10globalThis.history.pushState = (
11 data: unknown,
12 unused: string,
13 url?: string | URL | null,
14) => {
15 originalPushState(data, unused, url);
16 if (url) {
17 const urlStr = typeof url === "string" ? url : url.toString();
18 Object.defineProperty(globalThis.location, "pathname", {
19 value: urlStr.split("?")[0],
20 writable: true,
21 configurable: true,
22 });
23 }
24};
25
26globalThis.history.replaceState = (
27 data: unknown,
28 unused: string,
29 url?: string | URL | null,
30) => {
31 originalReplaceState(data, unused, url);
32 if (url) {
33 const urlStr = typeof url === "string" ? url : url.toString();
34 Object.defineProperty(globalThis.location, "pathname", {
35 value: urlStr.split("?")[0],
36 writable: true,
37 configurable: true,
38 });
39 }
40};
41
42export interface MockResponse {
43 ok: boolean;
44 status: number;
45 json: () => Promise<unknown>;
46}
47export type MockHandler = (
48 url: string,
49 options?: RequestInit,
50) => MockResponse | Promise<MockResponse>;
51const mockHandlers: Map<string, MockHandler> = new Map();
52export function mockEndpoint(endpoint: string, handler: MockHandler): void {
53 mockHandlers.set(endpoint, handler);
54}
55export function mockEndpointOnce(endpoint: string, handler: MockHandler): void {
56 const originalHandler = mockHandlers.get(endpoint);
57 mockHandlers.set(endpoint, (url, options) => {
58 mockHandlers.set(endpoint, originalHandler!);
59 return handler(url, options);
60 });
61}
62export function clearMocks(): void {
63 mockHandlers.clear();
64}
65function extractEndpoint(url: string): string {
66 const match = url.match(/\/xrpc\/([^?]+)/);
67 return match ? match[1] : url;
68}
69export function setupFetchMock(): void {
70 globalThis.fetch = vi.fn(
71 async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
72 const url = typeof input === "string" ? input : input.toString();
73 const endpoint = extractEndpoint(url);
74 const handler = mockHandlers.get(endpoint);
75 if (handler) {
76 const result = await handler(url, init);
77 return {
78 ok: result.ok,
79 status: result.status,
80 json: result.json,
81 text: async () => JSON.stringify(await result.json()),
82 headers: new Headers(),
83 redirected: false,
84 statusText: result.ok ? "OK" : "Error",
85 type: "basic",
86 url,
87 clone: () => ({ ...result }) as Response,
88 body: null,
89 bodyUsed: false,
90 arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
91 blob: () => Promise.resolve(new Blob()),
92 formData: () => Promise.resolve(new FormData()),
93 } as Response;
94 }
95 return {
96 ok: false,
97 status: 404,
98 json: () =>
99 Promise.resolve({
100 error: "NotFound",
101 message: `No mock for ${endpoint}`,
102 }),
103 text: () =>
104 Promise.resolve(
105 JSON.stringify({
106 error: "NotFound",
107 message: `No mock for ${endpoint}`,
108 }),
109 ),
110 headers: new Headers(),
111 redirected: false,
112 statusText: "Not Found",
113 type: "basic",
114 url,
115 clone: function () {
116 return this;
117 },
118 body: null,
119 bodyUsed: false,
120 arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
121 blob: () => Promise.resolve(new Blob()),
122 formData: () => Promise.resolve(new FormData()),
123 } as Response;
124 },
125 );
126}
127export function jsonResponse<T>(data: T, status = 200): MockResponse {
128 return {
129 ok: status >= 200 && status < 300,
130 status,
131 json: () => Promise.resolve(data),
132 };
133}
134export function errorResponse(
135 error: string,
136 message: string,
137 status = 400,
138): MockResponse {
139 return {
140 ok: false,
141 status,
142 json: () => Promise.resolve({ error, message }),
143 };
144}
145export const mockData = {
146 session: (overrides?: Partial<Session>): Session => ({
147 did: "did:web:test.tranquil.dev:u:testuser",
148 handle: "testuser.test.tranquil.dev",
149 email: "test@example.com",
150 emailConfirmed: true,
151 accessJwt: "mock-access-jwt-token",
152 refreshJwt: "mock-refresh-jwt-token",
153 ...overrides,
154 }),
155 appPassword: (overrides?: Partial<AppPassword>): AppPassword => ({
156 name: "Test App",
157 createdAt: new Date().toISOString(),
158 ...overrides,
159 }),
160 inviteCode: (overrides?: Partial<InviteCode>): InviteCode => ({
161 code: "test-invite-123",
162 available: 1,
163 disabled: false,
164 forAccount: "did:web:test.tranquil.dev:u:testuser",
165 createdBy: "did:web:test.tranquil.dev:u:testuser",
166 createdAt: new Date().toISOString(),
167 uses: [],
168 ...overrides,
169 }),
170 notificationPrefs: (overrides?: Record<string, unknown>) => ({
171 preferredChannel: "email",
172 email: "test@example.com",
173 discordId: null,
174 discordVerified: false,
175 telegramUsername: null,
176 telegramVerified: false,
177 signalNumber: null,
178 signalVerified: false,
179 ...overrides,
180 }),
181 describeServer: (overrides?: Record<string, unknown>) => ({
182 availableUserDomains: ["test.tranquil.dev"],
183 inviteCodeRequired: false,
184 links: {
185 privacyPolicy: "https://example.com/privacy",
186 termsOfService: "https://example.com/tos",
187 },
188 selfHostedDidWebEnabled: true,
189 availableCommsChannels: ["email", "discord", "telegram", "signal"],
190 ...overrides,
191 }),
192 describeRepo: (did: string) => ({
193 handle: "testuser.test.tranquil.dev",
194 did,
195 didDoc: {},
196 collections: [
197 "app.bsky.feed.post",
198 "app.bsky.feed.like",
199 "app.bsky.graph.follow",
200 ],
201 handleIsCorrect: true,
202 }),
203};
204export function setupDefaultMocks(): void {
205 setupFetchMock();
206 mockEndpoint(
207 "com.atproto.server.getSession",
208 () => jsonResponse(mockData.session()),
209 );
210 mockEndpoint("com.atproto.server.createSession", (_url, options) => {
211 const body = JSON.parse((options?.body as string) || "{}");
212 if (body.identifier && body.password === "correctpassword") {
213 return jsonResponse(
214 mockData.session({ handle: body.identifier.replace("@", "") }),
215 );
216 }
217 return errorResponse(
218 "AuthenticationRequired",
219 "Invalid identifier or password",
220 401,
221 );
222 });
223 mockEndpoint(
224 "com.atproto.server.refreshSession",
225 () => jsonResponse(mockData.session()),
226 );
227 mockEndpoint("com.atproto.server.deleteSession", () => jsonResponse({}));
228 mockEndpoint(
229 "com.atproto.server.listAppPasswords",
230 () => jsonResponse({ passwords: [mockData.appPassword()] }),
231 );
232 mockEndpoint("com.atproto.server.createAppPassword", (_url, options) => {
233 const body = JSON.parse((options?.body as string) || "{}");
234 return jsonResponse({
235 name: body.name,
236 password: "xxxx-xxxx-xxxx-xxxx",
237 createdAt: new Date().toISOString(),
238 });
239 });
240 mockEndpoint("com.atproto.server.revokeAppPassword", () => jsonResponse({}));
241 mockEndpoint(
242 "com.atproto.server.getAccountInviteCodes",
243 () => jsonResponse({ codes: [mockData.inviteCode()] }),
244 );
245 mockEndpoint(
246 "com.atproto.server.createInviteCode",
247 () => jsonResponse({ code: "new-invite-" + Date.now() }),
248 );
249 mockEndpoint(
250 "_account.getNotificationPrefs",
251 () => jsonResponse(mockData.notificationPrefs()),
252 );
253 mockEndpoint(
254 "_account.updateNotificationPrefs",
255 () => jsonResponse({ success: true }),
256 );
257 mockEndpoint(
258 "_account.getNotificationHistory",
259 () => jsonResponse({ notifications: [] }),
260 );
261 mockEndpoint(
262 "com.atproto.server.requestEmailUpdate",
263 () => jsonResponse({ tokenRequired: true }),
264 );
265 mockEndpoint("com.atproto.server.updateEmail", () => jsonResponse({}));
266 mockEndpoint("com.atproto.identity.updateHandle", () => jsonResponse({}));
267 mockEndpoint(
268 "com.atproto.server.requestAccountDelete",
269 () => jsonResponse({}),
270 );
271 mockEndpoint("com.atproto.server.deleteAccount", () => jsonResponse({}));
272 mockEndpoint(
273 "com.atproto.server.describeServer",
274 () => jsonResponse(mockData.describeServer()),
275 );
276 mockEndpoint("com.atproto.repo.describeRepo", (url) => {
277 const params = new URLSearchParams(url.split("?")[1]);
278 const repo = params.get("repo") || "did:web:test";
279 return jsonResponse(mockData.describeRepo(repo));
280 });
281 mockEndpoint(
282 "com.atproto.repo.listRecords",
283 () => jsonResponse({ records: [] }),
284 );
285 mockEndpoint(
286 "_backup.listBackups",
287 () => jsonResponse({ backups: [] }),
288 );
289}
290export function setupAuthenticatedUser(
291 sessionOverrides?: Partial<Session>,
292): Session {
293 const session = mockData.session(sessionOverrides);
294 _testSetState({
295 session,
296 loading: false,
297 error: null,
298 });
299 return session;
300}
301export function setupUnauthenticatedUser(): void {
302 _testSetState({
303 session: null,
304 loading: false,
305 error: null,
306 });
307}