155 lines
5.0 KiB
TypeScript
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 };
|
|
}
|