Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
12 kB · 402 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403import { vi } from "vitest";import type { AppPassword, InviteCode, Session } from "../lib/api.ts";import { _testResetState, _testSetState } from "../lib/auth.svelte.ts";import { clearAllToasts, getToasts, toast } from "../lib/toast.svelte.ts";import { unsafeAsAccessToken, unsafeAsDid, unsafeAsEmail, unsafeAsHandle, unsafeAsInviteCode, unsafeAsISODateString, unsafeAsRefreshToken,} from "../lib/types/branded.ts";
function createMockIndexedDB() { const stores: Map<string, Map<string, unknown>> = new Map();
return { open: vi.fn((_name: string, _version?: number) => { const createTransaction = (_storeName: string, _mode?: string) => { const tx = { objectStore: (name: string) => { if (!stores.has(name)) { stores.set(name, new Map()); } const store = stores.get(name)!; return { put: (value: unknown, key: string) => { store.set(key, value); return { result: undefined }; }, get: (key: string) => ({ result: store.get(key), }), }; }, oncomplete: null as (() => void) | null, onerror: null as (() => void) | null, }; setTimeout(() => tx.oncomplete?.(), 0); return tx; };
const request = { result: { objectStoreNames: { contains: () => true }, createObjectStore: vi.fn(), transaction: createTransaction, close: vi.fn(), }, error: null, onsuccess: null as (() => void) | null, onerror: null as (() => void) | null, onupgradeneeded: null as (() => void) | null, };
setTimeout(() => { request.onupgradeneeded?.(); request.onsuccess?.(); }, 0);
return request; }), };}
export function setupIndexedDBMock(): void { (globalThis as unknown as { indexedDB: unknown }).indexedDB = createMockIndexedDB();}
const originalPushState = globalThis.history.pushState.bind(globalThis.history);const originalReplaceState = globalThis.history.replaceState.bind( globalThis.history,);
globalThis.history.pushState = ( data: unknown, unused: string, url?: string | URL | null,) => { originalPushState(data, unused, url); if (url) { const urlStr = typeof url === "string" ? url : url.toString(); Object.defineProperty(globalThis.location, "pathname", { value: urlStr.split("?")[0], writable: true, configurable: true, }); }};
globalThis.history.replaceState = ( data: unknown, unused: string, url?: string | URL | null,) => { originalReplaceState(data, unused, url); if (url) { const urlStr = typeof url === "string" ? url : url.toString(); Object.defineProperty(globalThis.location, "pathname", { value: urlStr.split("?")[0], writable: true, configurable: true, }); }};
export interface MockResponse { ok: boolean; status: number; json: () => Promise<unknown>;}export type MockHandler = ( url: string, options?: RequestInit,) => MockResponse | Promise<MockResponse>;const mockHandlers: Map<string, MockHandler> = 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(); _testResetState(); clearAllToasts();}
export function getErrorToasts(): string[] { return getToasts() .filter((t) => t.type === "error") .map((t) => t.message);}
export { getToasts, toast };function extractEndpoint(url: string): string { const match = url.match(/\/xrpc\/([^?]+)/); if (match) return match[1]; const pathOnly = url.split("?")[0]; return pathOnly;}export function setupFetchMock(): void { globalThis.fetch = vi.fn( async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => { 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: () => Promise.resolve(new ArrayBuffer(0)), blob: () => Promise.resolve(new Blob()), formData: () => Promise.resolve(new FormData()), } as Response; } return { ok: false, status: 404, json: () => Promise.resolve({ error: "NotFound", message: `No mock for ${endpoint}`, }), text: () => Promise.resolve( 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: () => Promise.resolve(new ArrayBuffer(0)), blob: () => Promise.resolve(new Blob()), formData: () => Promise.resolve(new FormData()), } as Response; }, );}export function jsonResponse<T>(data: T, status = 200): MockResponse { return { ok: status >= 200 && status < 300, status, json: () => Promise.resolve(data), };}export function errorResponse( error: string, message: string, status = 400,): MockResponse { return { ok: false, status, json: () => Promise.resolve({ error, message }), };}export const mockData = { session: (overrides?: Partial<Session>): Session => { const base = { did: unsafeAsDid("did:web:test.tranquil.dev:u:testuser"), handle: unsafeAsHandle("testuser.test.tranquil.dev"), accessJwt: unsafeAsAccessToken("mock-access-jwt-token"), refreshJwt: unsafeAsRefreshToken("mock-refresh-jwt-token"), contactKind: "email" as const, email: unsafeAsEmail("test@example.com"), emailConfirmed: true, accountKind: "active" as const, isAdmin: false, }; return { ...base, ...overrides } as Session; }, appPassword: (overrides?: Partial<AppPassword>): AppPassword => ({ name: "Test App", createdAt: unsafeAsISODateString(new Date().toISOString()), ...overrides, }), inviteCode: (overrides?: Partial<InviteCode>): InviteCode => ({ code: unsafeAsInviteCode("test-invite-123"), available: 1, disabled: false, forAccount: unsafeAsDid("did:web:test.tranquil.dev:u:testuser"), createdBy: unsafeAsDid("did:web:test.tranquil.dev:u:testuser"), createdAt: unsafeAsISODateString(new Date().toISOString()), uses: [], ...overrides, }), notificationPrefs: (overrides?: Record<string, unknown>) => ({ preferredChannel: "email", email: "test@example.com", discordUsername: null, discordVerified: false, telegramUsername: null, telegramVerified: false, signalUsername: null, signalVerified: false, legacyLoginAlerts: true, ...overrides, }), describeServer: (overrides?: Record<string, unknown>) => ({ availableUserDomains: ["test.tranquil.dev"], inviteCodeRequired: false, did: "did:web:test.tranquil.dev", contact: { email: "admin@test.tranquil.dev", }, links: { privacyPolicy: "https://example.com/privacy", termsOfService: "https://example.com/tos", }, selfHostedDidWebEnabled: true, availableCommsChannels: ["email", "discord", "telegram", "signal"], discordBotUsername: "test-bot", discordAppId: "123456789", telegramBotUsername: "test_tg_bot", ...overrides, }), serverStats: () => ({ userCount: 42, repoCount: 42, recordCount: 1234, blobStorageBytes: 5678, }), 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(); setupIndexedDBMock(); 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( "_account.getNotificationPrefs", () => jsonResponse(mockData.notificationPrefs()), ); mockEndpoint( "_account.updateNotificationPrefs", () => jsonResponse({ success: true }), ); mockEndpoint( "_account.getNotificationHistory", () => jsonResponse({ notifications: [] }), ); 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>,): 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, });}