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}