-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathwebapp.ts
More file actions
146 lines (128 loc) · 4.55 KB
/
webapp.ts
File metadata and controls
146 lines (128 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { spawn } from "child_process";
import { createServer } from "net";
import { resolve } from "path";
import { Network } from "testcontainers";
import { PrismaClient } from "@trigger.dev/database";
import { createPostgresContainer } from "./utils";
const WEBAPP_ROOT = resolve(__dirname, "../../../apps/webapp");
// pnpm hoists transitive deps to node_modules/.pnpm/node_modules but does NOT symlink them
// to the root node_modules. We need NODE_PATH so the webapp process can find them at runtime.
const PNPM_HOISTED_MODULES = resolve(__dirname, "../../../node_modules/.pnpm/node_modules");
async function findFreePort(): Promise<number> {
return new Promise((res, rej) => {
const srv = createServer();
srv.listen(0, () => {
const port = (srv.address() as { port: number }).port;
srv.close((err) => (err ? rej(err) : res(port)));
});
});
}
async function waitForHealthcheck(url: string, timeoutMs = 60000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const res = await fetch(url);
if (res.ok) return;
} catch {}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error(`Webapp did not become healthy at ${url} within ${timeoutMs}ms`);
}
export interface WebappInstance {
baseUrl: string;
fetch(path: string, init?: RequestInit): Promise<Response>;
}
export async function startWebapp(databaseUrl: string): Promise<{
instance: WebappInstance;
stop: () => Promise<void>;
}> {
const port = await findFreePort();
// Merge NODE_PATH so transitive pnpm deps (hoisted to .pnpm/node_modules) are resolvable
const existingNodePath = process.env.NODE_PATH;
const nodePath = existingNodePath
? `${PNPM_HOISTED_MODULES}:${existingNodePath}`
: PNPM_HOISTED_MODULES;
const proc = spawn(process.execPath, ["build/server.js"], {
cwd: WEBAPP_ROOT,
env: {
...process.env,
NODE_ENV: "test",
DATABASE_URL: databaseUrl,
DIRECT_URL: databaseUrl,
PORT: String(port),
REMIX_APP_PORT: String(port), // override .env file value (vitest loads .env via Vite)
SESSION_SECRET: "test-session-secret-for-e2e-tests",
MAGIC_LINK_SECRET: "test-magic-link-secret-32chars!!",
ENCRYPTION_KEY: "test-encryption-key-for-e2e!!!!!", // exactly 32 bytes
CLICKHOUSE_URL: "http://localhost:19123", // dummy, auth paths never connect
DEPLOY_REGISTRY_HOST: "registry.example.com", // dummy, not needed for auth tests
ELECTRIC_ORIGIN: "http://localhost:3060",
NODE_PATH: nodePath,
},
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
proc.stderr?.on("data", (d: Buffer) => {
const line = d.toString();
stderr.push(line);
if (process.env.WEBAPP_TEST_VERBOSE) {
process.stderr.write(line);
}
});
const stdout: string[] = [];
proc.stdout?.on("data", (d: Buffer) => {
const line = d.toString();
stdout.push(line);
if (process.env.WEBAPP_TEST_VERBOSE) {
process.stdout.write(line);
}
});
proc.on("error", (err) => {
throw new Error(`Failed to start webapp: ${err.message}`);
});
const baseUrl = `http://localhost:${port}`;
try {
await waitForHealthcheck(`${baseUrl}/healthcheck`);
} catch (err) {
proc.kill("SIGTERM");
const output = [...stdout, ...stderr].join("\n");
throw new Error(`Webapp failed to start.\nOutput:\n${output}\n\nOriginal error: ${err}`);
}
return {
instance: {
baseUrl,
fetch: (path: string, init?: RequestInit) => fetch(`${baseUrl}${path}`, init),
},
stop: () =>
new Promise<void>((res) => {
const timer = setTimeout(() => {
proc.kill("SIGKILL");
res();
}, 10_000);
proc.once("exit", () => {
clearTimeout(timer);
res();
});
proc.kill("SIGTERM");
}),
};
}
export interface TestServer {
webapp: WebappInstance;
prisma: PrismaClient;
stop: () => Promise<void>;
}
/** Convenience helper: starts a postgres container + webapp and returns both for testing. */
export async function startTestServer(): Promise<TestServer> {
const network = await new Network().start();
const { url: databaseUrl, container } = await createPostgresContainer(network);
const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
const { instance: webapp, stop: stopWebapp } = await startWebapp(databaseUrl);
const stop = async () => {
await stopWebapp();
await prisma.$disconnect();
await container.stop();
await network.stop();
};
return { webapp, prisma, stop };
}