// A real server F for the editor's browser tests: bundled as the build // bundles it (dist/main.mjs and its room thread), started as a process with // a throwaway Postgres (packages/server/test/database.ts), on the host the // editor's page comes from, so that F's cookie goes with the page's // requests. A project is made in F's store straight from a FILIUS file; a // person signs in as the dashboard would let them, with a ticket at /__auth. // Given a built editor, F hands it out itself, as in the image: then the // editor's page comes from F's own origin. import { spawn, type ChildProcess } from 'node:child_process'; import { mkdtempSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { randomUUID } from 'node:crypto'; import type { BrowserContext } from '@playwright/test'; import { build } from 'esbuild'; import pg from 'pg'; import * as Y from 'yjs'; import { writeProject } from '@mdg-filius/doc'; import { openProjectArchive } from '@mdg-filius/fls'; import { testDatabase, type TestDatabase } from '../../server/test/database.js'; import { signTicket } from '../../server/test/tickets.js'; const TICKET_SECRET = 'browser-ticket-secret'; export const SERVICE_KEY = 'browser-service-key'; export interface ServerF { readonly base: string; /** A project made from a FILIUS file of the corpus; its devices' ids by name. */ project(file: string): Promise<{ readonly id: string; readonly node: (name: string) => string }>; /** Signs a person in, in this browser context. */ signIn(context: BrowserContext, project: string, person: { readonly name: string; readonly level?: 'read' | 'write'; readonly user?: string }): Promise; service(method: string, path: string, body?: unknown): Promise; /** The size of a blob in F's store, none without it. */ blobSize(sha: string): Promise; /** How many updates of a project F has stored since it last compacted it. */ updateCount(project: string): Promise; close(): Promise; } /** Server F on the editor page's host (`editorBase`), for pages from there, or from itself with `editorFolder`. */ export async function startServerF(editorBase: string, editorFolder?: string): Promise { const editor = new URL(editorBase); const database: TestDatabase = await testDatabase(); const out = mkdtempSync(join(tmpdir(), 'filius-server-f-')); const source = new URL('../../server/src/', import.meta.url).pathname; await build({ entryPoints: [join(source, 'main.ts'), join(source, 'simWorker.ts')], bundle: true, platform: 'node', format: 'esm', target: 'node20', outdir: out, outExtension: { '.js': '.mjs' }, external: ['pg-native'], banner: { js: "import { createRequire as createBundleRequire } from 'node:module'; const require = createBundleRequire(import.meta.url);" }, logLevel: 'error', }); const port = 20_000 + Math.floor(Math.random() * 20_000); const base = `${editor.protocol}//${editor.hostname}:${port}`; const child: ChildProcess = spawn(process.execPath, [join(out, 'main.mjs')], { env: { ...process.env, DATABASE_URL: database.url, FILIUS_TICKET_SECRET: TICKET_SECRET, FILIUS_SERVICE_KEY: SERVICE_KEY, FILIUS_ORIGINS: editorFolder === undefined ? editor.origin : base, ...(editorFolder === undefined ? {} : { FILIUS_EDITOR_DIR: editorFolder }), FILIUS_PORT: String(port), FILIUS_INSECURE_COOKIE: '1', }, stdio: ['ignore', 'pipe', 'inherit'], }); await new Promise((resolve, reject) => { child.stdout!.on('data', (data: Buffer) => { if (data.toString().includes('"server.listening"')) resolve(); }); child.once('exit', (code) => reject(new Error(`server F ended (${code})`))); }); const sql = new pg.Pool({ connectionString: database.url }); return { base, async project(file) { const id = randomUUID(); const imported = openProjectArchive(new Uint8Array(readFileSync(new URL(`../../../corpus/${file}`, import.meta.url))), () => randomUUID()); const doc = new Y.Doc(); writeProject(doc, imported); await sql.query('INSERT INTO filius_doc (project_uuid, state) VALUES ($1, $2)', [id, Buffer.from(Y.encodeStateAsUpdate(doc))]); return { id, node: (name) => imported.graph.nodes.find((each) => each.displayName === name)!.id }; }, async signIn(context, project, { name, level = 'write', user = randomUUID() }) { const ticket = signTicket({ u: user, n: name.toLowerCase(), d: name, p: project, lvl: level }, TICKET_SECRET); const page = await context.newPage(); const response = await page.goto(`${base}/__auth?ticket=${encodeURIComponent(ticket)}&next=/healthz`); if (response?.status() !== 200) throw new Error(`not signed in: ${response?.status()}`); await page.close(); }, async blobSize(sha) { const result = await sql.query<{ size: number }>('SELECT size FROM filius_blob WHERE sha = $1', [sha]); return result.rows[0]?.size; }, async updateCount(project) { const result = await sql.query<{ count: string }>('SELECT count(*) FROM filius_doc_update WHERE project_uuid = $1', [project]); return Number(result.rows[0]!.count); }, service: (method, path, body) => fetch(`${base}${path}`, { method, headers: { 'x-filius-service-key': SERVICE_KEY }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }), async close() { const ended = new Promise((resolve) => child.once('exit', resolve)); child.kill('SIGTERM'); await ended; await sql.end(); await database.stop(); }, }; }