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