import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { io, type Socket } from 'socket.io-client'; import { eq } from 'drizzle-orm'; import { createApp } from '../src/app.js'; import { loadConfig } from '../src/config.js'; import * as schema from '../src/db/schema.js'; import { clientIp } from '../src/security.js'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; export function urlFor(value: string) { return /^https?:\/\//.test(value) ? value .split('\n') .map((url) => new URL(url).href) .join('\n') : `https://example.test/${encodeURIComponent(value)}`; } const origin = 'http://hop.test'; type Client = { cookie: string; csrf: string; deviceId: string; socket?: Socket; ip: string }; export async function fixture(discovery = false, mailer?: import('../src/auth.js').Mailer) { const folder = await mkdtemp(join(tmpdir(), 'hop-api-')); const cfg = { ...loadConfig({ APP_URL: origin, DATABASE_PATH: ':memory:', SESSION_SECRET: 's'.repeat(40), DISCOVERY_SECRET: 'd'.repeat(40), }), discoveryEnabled: discovery, uploadDir: join(folder, 'uploads'), minFreeBytes: 0, trustedProxies: ['127.0.0.1', '::1'], offlineGraceMs: 20, }; const runtime = await createApp(cfg, { mailer }); await runtime.app.listen({ port: 0, host: '127.0.0.1' }); const address = runtime.app.server.address(); assert(address && typeof address !== 'string'); const url = `http://127.0.0.1:${address.port}`; const sockets: Socket[] = []; const call = async ( c: Client | null, path: string, method = 'GET', payload?: unknown, key?: string, headers: Record = {}, ) => runtime.app.inject({ method: method as any, url: `/api${path}`, headers: { origin, ...(c ? { cookie: c.cookie, 'x-csrf-token': c.csrf, 'x-forwarded-for': c.ip } : {}), ...(key ? { 'idempotency-key': key } : {}), ...headers, }, payload, }); const create = async (ip = '203.0.113.1', online = true) => { const response = await call(null, '/guest/session', 'POST', {}); assert.equal(response.statusCode, 200); const result = response.json(); const c: Client = { cookie: String(response.headers['set-cookie']).split(';')[0]!, csrf: result.csrfToken, deviceId: result.device.id, ip, }; if (online) await connect(c); return c; }; const connect = async (c: Client) => { const socket = io(url, { transports: ['websocket'], reconnection: false, auth: { csrfToken: c.csrf }, extraHeaders: { Origin: origin, Cookie: c.cookie, 'X-Forwarded-For': c.ip }, }); sockets.push(socket); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(Error('socket connect timeout')), 3000); socket.once('connect', () => { clearTimeout(timer); resolve(); }); socket.once('connect_error', (err) => { clearTimeout(timer); reject(err); }); }); c.socket = socket; return socket; }; const invitations = new Map(); const space = async (a: Client, b: Client, approvalRequired = false) => { const current = (await call(a, '/spaces')).json().spaces; let result = invitations.get(a.deviceId); if (!result || !current.some((room: any) => room.id === result.space.id)) { const response = await call(a, '/spaces', 'POST', { approvalRequired }); assert.equal(response.statusCode, 200, response.body); result = response.json(); invitations.set(a.deviceId, result); } const join = await call(b, '/spaces/join', 'POST', { code: result.code }); assert.equal(join.statusCode, 200); return result; }; const request = async ( a: Client, b: Client, text = '안녕하세요', kind: 'url' = 'url', count = 1, key = randomUUID(), ) => { const r = await call( a, '/transfer-requests', 'POST', { targetDeviceId: b.deviceId, kind, byteSize: Buffer.byteLength(urlFor(text)), count }, key, ); return r; }; const complete = async (a: Client, b: Client, text = '안녕하세요') => { const r = await request(a, b, text); assert.equal(r.statusCode, 200); const id = r.json().requestId; assert.equal( (await call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode, 200, ); assert.equal( (await call(a, `/transfers/${id}/complete`, 'POST', { urls: [urlFor(text)] }, randomUUID())) .statusCode, 200, ); return id; }; const close = async () => { for (const socket of sockets) socket.disconnect(); await runtime.app.close(); await rm(folder, { recursive: true, force: true }); }; return { ...runtime, cfg, url, call, create, connect, space, request, complete, close }; }