Initial hop. implementation and UGREEN NAS deployment guide
This commit is contained in:
149
apps/server/test/helpers.ts
Normal file
149
apps/server/test/helpers.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
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<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 };
|
||||
}
|
||||
Reference in New Issue
Block a user