import { spawn } from "node:child_process";
import { mkdtemp, open } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import assert from "node:assert/strict";
const directory = await mkdtemp(join(tmpdir(), "relay-integration-"));
const database = `relay_integration_${Date.now()}`;
const processes = [];
const logs = [];
const ports = [29187, 29188];
try {
for (const port of ports) {
const log = await open(join(directory, `${port}.jsonl`), "w"); logs.push(log);
const child = spawn(resolve(process.env.RELAY_BINARY || "target/release/relay"), ["run", "examples/twitter_clone/app.ry"], {
env: { ...process.env, RELAY_ENV: "development", RELAY_BIND: `127.0.0.1:${port}`,
RELAY_TRACE: process.argv.includes("--trace") ? "1" : "0",
RELAY_MONGO_URI: process.env.RELAY_TEST_MONGO_URI || "mongodb://127.0.0.1:27187", RELAY_MONGO_DATABASE: database },
stdio: ["ignore", "ignore", log.fd],
});
const stopped = new Promise(resolve => child.on("exit", (code, signal) => resolve({ code, signal })));
processes.push({ child, stopped });
let ready = false;
for (let attempt = 0; attempt < 150 && child.exitCode === null; attempt++) {
try { ready = (await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(1000) })).ok; } catch {}
if (ready) break;
await delay(200);
}
assert(ready, `Server ${port} failed to start; see ${directory}`);
}
const client = () => ({ cookie: "", csrf: "" });
async function request(user, port, path, body, expected = 200, token = true) {
const headers = { cookie: user.cookie };
if (token && user.csrf) headers["x-csrf-token"] = user.csrf;
if (body) headers["content-type"] = "application/json";
const response = await fetch(`http://127.0.0.1:${port}${path}`, { method: body ? "POST" : "GET", headers,
body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(10000) });
const raw = await response.text();
assert.equal(response.status, expected, `${path}: ${raw}`);
const cookie = response.headers.get("set-cookie");
if (cookie) user.cookie = cookie.split(";")[0];
const data = JSON.parse(raw);
if (data.csrf_token) user.csrf = data.csrf_token;
return data;
}
const ada = client(), bob = client();
assert.equal((await request(ada, ports[0], "/api/me")).authenticated, false);
const preLoginCookie = ada.cookie;
await request(ada, ports[0], "/api/auth/signup", { username: "ada", password: "test-password", bio: "Hello" });
assert.notEqual(ada.cookie, preLoginCookie, "Login must rotate the session");
assert.equal((await request(ada, ports[1], "/api/me")).user.username, "ada", "Session must work on another process");
await request(ada, ports[1], "/api/posts", { body: "CSRF must fail" }, 403, false);
await request(ada, ports[1], "/api/posts", { body: "Hello from Relay" });
await request(bob, ports[0], "/api/me");
await request(bob, ports[0], "/api/auth/signup", { username: "bob", password: "test-password", bio: "Hi" });
await request(bob, ports[1], "/api/me");
await request(bob, ports[1], "/api/users/ada/follow", {});
const feed = await request(bob, ports[0], "/api/feed/home");
assert(feed.items.some(post => post.body === "Hello from Relay"));
await Promise.all(Array.from({ length: 32 }, (_, index) => request(index % 2 ? ada : bob, ports[index % 2], "/api/me")
.then(data => assert.equal(data.user.username, index % 2 ? "ada" : "bob"))));
await request(bob, ports[0], "/api/users/ada/unfollow", {});
const oldCookie = ada.cookie;
await request(ada, ports[1], "/api/auth/logout", {});
assert.equal((await request({ cookie: oldCookie, csrf: ada.csrf }, ports[0], "/api/me")).authenticated, false);
await request(ada, ports[0], "/api/me");
await request(ada, ports[0], "/api/auth/login", { username: "ada", password: "test-password" });
assert.equal((await request(ada, ports[1], "/api/me")).user.username, "ada");
console.log(`Integration passed: two processes, persistent sessions, CSRF, signup/login/logout, posts, follows and feeds. Logs: ${directory}; isolated database: ${database}`);
} finally {
for (const { child } of processes) if (child.exitCode === null) child.kill("SIGTERM");
for (const { child, stopped } of processes) {
const result = await Promise.race([stopped, delay(35000, null, { ref: false })]);
if (!result) { child.kill("SIGKILL"); throw new Error("Graceful shutdown failed"); }
assert.equal(result.code, 0, `Server shutdown: ${JSON.stringify(result)}`);
}
for (const log of logs) await log.close();
}