Files
hop.noname.company/apps/server/test/helpers.ts

155 lines
5.0 KiB
TypeScript

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,
overrides: Partial<import('../src/config.js').Config> = {},
) {
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,
...overrides,
};
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<string, string> = {},
) =>
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<void>((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<string, any>();
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 };
}