import * as childProcess from "child_process"; import { SIGKILL } from "constants"; import * as fs from "node:fs"; import * as http from "node:http"; import * as path from "node:path"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import type { endpoints, Entities } from "firefish-js"; import FormData from "form-data"; import got from "got"; import fetch from "node-fetch"; import { DataSource } from "typeorm"; import WebSocket from "ws"; import loadConfig from "../src/config/load.js"; import { entities } from "../src/db/postgre.js"; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const config = loadConfig(); export const port = config.port; export const async = (fn: Function) => (done: Function) => { fn().then( () => { done(); }, (err: Error) => { done(err); }, ); }; export const api = async (endpoint: string, params: any, me?: any) => { endpoint = endpoint.replace(/^\//, ""); const auth = me ? { i: me.token, } : {}; const res = await got(`http://localhost:${port}/api/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(Object.assign(auth, params)), retry: { limit: 0, }, hooks: { beforeError: [ (error) => { const { response } = error; if (response && response.body) console.warn(response.body); return error; }, ], }, }); const status = res.statusCode; const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; return { status, body, }; }; export const request = async ( endpoint: string, params: any, me?: any, ): Promise<{ body: any; status: number }> => { const auth = me ? { i: me.token, } : {}; const res = await fetch(`http://localhost:${port}/api${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(Object.assign(auth, params)), }); const status = res.status; const body = res.status !== 204 ? await res.json().catch() : null; return { body, status, }; }; export const signup = async (params?: any): Promise => { const q = Object.assign( { username: "test", password: "test", }, params, ); const res = await api("signup", q); return res.body; }; export const post = async ( user: any, params?: Endpoints["notes/create"]["req"], ): Promise => { const q = Object.assign( { text: "test", }, params, ); const res = await api("notes/create", q, user); return res.body ? res.body.createdNote : null; }; export const react = async ( user: any, note: any, reaction: string, ): Promise => { await api( "notes/reactions/create", { noteId: note.id, reaction: reaction, }, user, ); }; /** * Upload file * @param user User * @param _path Optional, absolute path or relative from ./resources/ */ export const uploadFile = async (user: any, _path?: string): Promise => { const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`; const formData = new FormData() as any; formData.append("i", user.token); formData.append("file", fs.createReadStream(absPath)); formData.append("force", "true"); const res = await got( `http://localhost:${port}/api/drive/files/create`, { method: "POST", body: formData, retry: { limit: 0, }, }, ); const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; return body; }; export const uploadUrl = async (user: any, url: string) => { let file: any; const ws = await connectStream(user, "main", (msg) => { if (msg.type === "driveFileCreated") { file = msg.body; } }); await api( "drive/files/upload-from-url", { url, force: true, }, user, ); await sleep(5000); ws.close(); return file; }; export function connectStream( user: any, channel: string, listener: (message: Record) => any, params?: any, ): Promise { return new Promise((res, rej) => { const ws = new WebSocket( `ws://localhost:${port}/streaming?i=${user.token}`, ); ws.on("open", () => { ws.on("message", (data) => { const msg = JSON.parse(data.toString()); if (msg.type === "channel" && msg.body.id === "a") { listener(msg.body); } else if (msg.type === "connected" && msg.body.id === "a") { res(ws); } }); ws.send( JSON.stringify({ type: "connect", body: { channel: channel, id: "a", pong: true, params: params, }, }), ); }); }); } export const waitFire = async ( user: any, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any, ) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout; let ws: WebSocket; try { ws = await connectStream( user, channel, (msg) => { if (cond(msg)) { ws.close(); if (timer) clearTimeout(timer); res(true); } }, params, ); } catch (e) { rej(e); } if (!ws!) return; timer = setTimeout(() => { ws.close(); res(false); }, 3000); try { await trgr(); } catch (e) { ws.close(); if (timer) clearTimeout(timer); rej(e); } }); }; export const simpleGet = async ( path: string, accept = "*/*", ): Promise<{ status?: number; type?: string; location?: string }> => { // node-fetchだと3xxを取れない return await new Promise((resolve, reject) => { const req = http.request( `http://localhost:${port}${path}`, { headers: { Accept: accept, }, }, (res) => { if (res.statusCode! >= 400) { reject(res); } else { resolve({ status: res.statusCode, type: res.headers["content-type"], location: res.headers.location, }); } }, ); req.end(); }); }; export function launchServer( callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise = async () => {}, ) { return (done: (err?: Error) => any) => { const p = childProcess.spawn("node", [_dirname + "/../index.js"], { stdio: ["inherit", "inherit", "inherit", "ipc"], env: { NODE_ENV: "test", PATH: process.env.PATH }, }); callbackSpawnedProcess(p); p.on("message", (message) => { if (message === "ok") moreProcess() .then(() => done()) .catch((e) => done(e)); }); }; } export async function initTestDb(justBorrow = false, initEntities?: any[]) { if (process.env.NODE_ENV !== "test") throw "NODE_ENV is not a test"; const db = new DataSource({ type: "postgres", host: config.db.host, port: config.db.port, username: config.db.user, password: config.db.pass, database: config.db.db, synchronize: true && !justBorrow, dropSchema: true && !justBorrow, entities: initEntities || entities, }); await db.initialize(); return db; } export function startServer( timeout = 60 * 1000, ): Promise { return new Promise((res, rej) => { const t = setTimeout(() => { p.kill(SIGKILL); rej("timeout to start"); }, timeout); const p = childProcess.spawn("node", [_dirname + "/../built/index.js"], { stdio: ["inherit", "inherit", "inherit", "ipc"], env: { NODE_ENV: "test", PATH: process.env.PATH }, }); p.on("error", (e) => rej(e)); p.on("message", (message) => { if (message === "ok") { clearTimeout(t); res(p); } }); }); } export function shutdownServer( p: childProcess.ChildProcess, timeout = 20 * 1000, ) { return new Promise((res, rej) => { const t = setTimeout(() => { p.kill(SIGKILL); res("force exit"); }, timeout); p.once("exit", () => { clearTimeout(t); res("exited"); }); p.kill(); }); } export function sleep(msec: number) { return new Promise((res) => { setTimeout(() => { res(); }, msec); }); }