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