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}