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