import { vi } from 'vitest' import type { Session, AppPassword, InviteCode } from '../lib/api' import { _testSetState } from '../lib/auth.svelte' export interface MockResponse { ok: boolean status: number json: () => Promise } export type MockHandler = (url: string, options?: RequestInit) => MockResponse | Promise const mockHandlers: Map = new Map() export function mockEndpoint(endpoint: string, handler: MockHandler): void { mockHandlers.set(endpoint, handler) } export function mockEndpointOnce(endpoint: string, handler: MockHandler): void { const originalHandler = mockHandlers.get(endpoint) mockHandlers.set(endpoint, (url, options) => { mockHandlers.set(endpoint, originalHandler!) return handler(url, options) }) } export function clearMocks(): void { mockHandlers.clear() } function extractEndpoint(url: string): string { const match = url.match(/\/xrpc\/([^?]+)/) return match ? match[1] : url } export function setupFetchMock(): void { global.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = typeof input === 'string' ? input : input.toString() const endpoint = extractEndpoint(url) const handler = mockHandlers.get(endpoint) if (handler) { const result = await handler(url, init) return { ok: result.ok, status: result.status, json: result.json, text: async () => JSON.stringify(await result.json()), headers: new Headers(), redirected: false, statusText: result.ok ? 'OK' : 'Error', type: 'basic', url, clone: () => ({ ...result }) as Response, body: null, bodyUsed: false, arrayBuffer: async () => new ArrayBuffer(0), blob: async () => new Blob(), formData: async () => new FormData(), } as Response } return { ok: false, status: 404, json: async () => ({ error: 'NotFound', message: `No mock for ${endpoint}` }), text: async () => JSON.stringify({ error: 'NotFound', message: `No mock for ${endpoint}` }), headers: new Headers(), redirected: false, statusText: 'Not Found', type: 'basic', url, clone: function() { return this }, body: null, bodyUsed: false, arrayBuffer: async () => new ArrayBuffer(0), blob: async () => new Blob(), formData: async () => new FormData(), } as Response }) } export function jsonResponse(data: T, status = 200): MockResponse { return { ok: status >= 200 && status < 300, status, json: async () => data, } } export function errorResponse(error: string, message: string, status = 400): MockResponse { return { ok: false, status, json: async () => ({ error, message }), } } export const mockData = { session: (overrides?: Partial): Session => ({ did: 'did:web:test.tranquil.dev:u:testuser', handle: 'testuser.test.tranquil.dev', email: 'test@example.com', emailConfirmed: true, accessJwt: 'mock-access-jwt-token', refreshJwt: 'mock-refresh-jwt-token', ...overrides, }), appPassword: (overrides?: Partial): AppPassword => ({ name: 'Test App', createdAt: new Date().toISOString(), ...overrides, }), inviteCode: (overrides?: Partial): InviteCode => ({ code: 'test-invite-123', available: 1, disabled: false, forAccount: 'did:web:test.tranquil.dev:u:testuser', createdBy: 'did:web:test.tranquil.dev:u:testuser', createdAt: new Date().toISOString(), uses: [], ...overrides, }), notificationPrefs: (overrides?: Record) => ({ preferredChannel: 'email', email: 'test@example.com', discordId: null, discordVerified: false, telegramUsername: null, telegramVerified: false, signalNumber: null, signalVerified: false, ...overrides, }), describeServer: () => ({ availableUserDomains: ['test.tranquil.dev'], inviteCodeRequired: false, links: { privacyPolicy: 'https://example.com/privacy', termsOfService: 'https://example.com/tos', }, }), describeRepo: (did: string) => ({ handle: 'testuser.test.tranquil.dev', did, didDoc: {}, collections: ['app.bsky.feed.post', 'app.bsky.feed.like', 'app.bsky.graph.follow'], handleIsCorrect: true, }), } export function setupDefaultMocks(): void { setupFetchMock() mockEndpoint('com.atproto.server.getSession', () => jsonResponse(mockData.session()) ) mockEndpoint('com.atproto.server.createSession', (_url, options) => { const body = JSON.parse((options?.body as string) || '{}') if (body.identifier && body.password === 'correctpassword') { return jsonResponse(mockData.session({ handle: body.identifier.replace('@', '') })) } return errorResponse('AuthenticationRequired', 'Invalid identifier or password', 401) }) mockEndpoint('com.atproto.server.refreshSession', () => jsonResponse(mockData.session()) ) mockEndpoint('com.atproto.server.deleteSession', () => jsonResponse({}) ) mockEndpoint('com.atproto.server.listAppPasswords', () => jsonResponse({ passwords: [mockData.appPassword()] }) ) mockEndpoint('com.atproto.server.createAppPassword', (_url, options) => { const body = JSON.parse((options?.body as string) || '{}') return jsonResponse({ name: body.name, password: 'xxxx-xxxx-xxxx-xxxx', createdAt: new Date().toISOString(), }) }) mockEndpoint('com.atproto.server.revokeAppPassword', () => jsonResponse({}) ) mockEndpoint('com.atproto.server.getAccountInviteCodes', () => jsonResponse({ codes: [mockData.inviteCode()] }) ) mockEndpoint('com.atproto.server.createInviteCode', () => jsonResponse({ code: 'new-invite-' + Date.now() }) ) mockEndpoint('com.tranquil.account.getNotificationPrefs', () => jsonResponse(mockData.notificationPrefs()) ) mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => jsonResponse({ success: true }) ) mockEndpoint('com.atproto.server.requestEmailUpdate', () => jsonResponse({ tokenRequired: true }) ) mockEndpoint('com.atproto.server.updateEmail', () => jsonResponse({}) ) mockEndpoint('com.atproto.identity.updateHandle', () => jsonResponse({}) ) mockEndpoint('com.atproto.server.requestAccountDelete', () => jsonResponse({}) ) mockEndpoint('com.atproto.server.deleteAccount', () => jsonResponse({}) ) mockEndpoint('com.atproto.server.describeServer', () => jsonResponse(mockData.describeServer()) ) mockEndpoint('com.atproto.repo.describeRepo', (url) => { const params = new URLSearchParams(url.split('?')[1]) const repo = params.get('repo') || 'did:web:test' return jsonResponse(mockData.describeRepo(repo)) }) mockEndpoint('com.atproto.repo.listRecords', () => jsonResponse({ records: [] }) ) } export function setupAuthenticatedUser(sessionOverrides?: Partial): Session { const session = mockData.session(sessionOverrides) _testSetState({ session, loading: false, error: null, }) return session } export function setupUnauthenticatedUser(): void { _testSetState({ session: null, loading: false, error: null, }) }