web/e2e/setup/global.ts
Rama master · réplica de lectura del repositorio original.
import { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
import { setTimeout as sleep } from "node:timers/promises";
import {
AUTH_SECRET,
BASE_URL,
E2E_LOGIN,
E2E_LOGIN_2,
E2E_MACHINE,
E2E_OWNER_ROLE_ID,
E2E_PROJECT_ID,
E2E_PROJECT_NAME,
E2E_UID,
E2E_UID_2,
E2E_WORKSPACE_ID,
mintToken,
openDb,
} from "../helpers";
// Mirrors what auth.Service.CompleteOwnerWizard / tenancy.BindDefaultWorkspaceOwner set up for a real owner, since this seed bypasses that flow and writes the DB directly; workspace membership is what grants every permission via access.Service.HasPermission's Owner-role bypass.
const DEFAULT_WORKSPACE_ID = E2E_WORKSPACE_ID;
const DEFAULT_OWNER_ROLE_ID = E2E_OWNER_ROLE_ID;
// Seeds a deterministic e2e owner (first_login_done so OnboardingGate lets the app load), bound to the default workspace since every permission flows through workspace membership; default statuses ship in migration 0022 so the board works out of the box.
export async function setup(): Promise<void> {
// The server migrates the DB before it listens, so a live gateway means the schema is complete.
await waitForServer();
const db = openDb();
const now = Math.floor(Date.now() / 1000);
db.prepare(
`INSERT INTO users (id, login, name, first_login_done, created_at, updated_at)
VALUES (?, ?, 'E2E User', 1, ?, ?)
ON CONFLICT(id)
DO UPDATE SET first_login_done = 1, login = excluded.login`,
).run(E2E_UID, E2E_LOGIN, now, now);
db.prepare(
`INSERT INTO user_identities (user_id, provider, provider_user_id, login, name, created_at)
VALUES (?, 'github', '0', ?, 'E2E User', ?)
ON CONFLICT(provider, provider_user_id)
DO UPDATE SET user_id = excluded.user_id, login = excluded.login`,
).run(E2E_UID, E2E_LOGIN, now);
// second user for real-time-collab presence (distinct editor identity)
db.prepare(
`INSERT INTO users (id, login, name, first_login_done, created_at, updated_at)
VALUES (?, ?, 'Collab Partner', 1, ?, ?)
ON CONFLICT(id)
DO UPDATE SET first_login_done = 1, login = excluded.login`,
).run(E2E_UID_2, E2E_LOGIN_2, now, now);
db.prepare(
`INSERT INTO user_identities (user_id, provider, provider_user_id, login, name, created_at)
VALUES (?, 'github', '1', ?, 'Collab Partner', ?)
ON CONFLICT(provider, provider_user_id)
DO UPDATE SET user_id = excluded.user_id, login = excluded.login`,
).run(E2E_UID_2, E2E_LOGIN_2, now);
db.prepare(
`INSERT INTO projects (id, name, position, workspace_id, created_at, updated_at)
VALUES (?, ?, 0, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name, workspace_id = excluded.workspace_id`,
).run(E2E_PROJECT_ID, E2E_PROJECT_NAME, DEFAULT_WORKSPACE_ID, now, now);
// What creating a project through the product seeds, which this raw insert bypasses: the board's statuses and ticket types, and the default folder a new doc lands in.
if (!db.prepare(`SELECT 1 FROM statuses WHERE project_id = ?`).get(E2E_PROJECT_ID)) {
const columns: [string, string][] = [["Backlog", "backlog"], ["In progress", "progress"], ["In review", "review"], ["Testing", "testing"], ["Done", "done"]];
for (const [i, [name, kind]] of columns.entries()) {
db.prepare(
`INSERT INTO statuses (id, project_id, name, position, kind, icon, created_at, updated_at) VALUES (?, ?, ?, ?, ?, '', ?, ?)`,
).run(randomUUID(), E2E_PROJECT_ID, name, i, kind, now, now);
}
}
if (!db.prepare(`SELECT 1 FROM ticket_types WHERE project_id = ?`).get(E2E_PROJECT_ID)) {
for (const [i, name] of ["task", "bug", "feature"].entries()) {
db.prepare(
`INSERT INTO ticket_types (id, project_id, name, position, color, body_template, created_at, updated_at) VALUES (?, ?, ?, ?, '', '', ?, ?)`,
).run(randomUUID(), E2E_PROJECT_ID, name, i, now, now);
}
}
db.prepare(
`INSERT INTO doc_folders (id, project_id, name, is_default, created_at, updated_at) VALUES (?, ?, 'Main', 1, ?, ?)
ON CONFLICT DO NOTHING`,
).run(randomUUID(), E2E_PROJECT_ID, now, now);
// Bind E2E_UID as the default workspace's Owner; idempotent because the singleton Owner-role unique index means only insert if this workspace has none yet.
db.prepare(
`INSERT INTO roles (id, workspace_id, name, is_owner_role, created_at, updated_at)
SELECT ?, ?, 'Owner', 1, ?, ?
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE workspace_id = ? AND is_owner_role = 1)`,
).run(DEFAULT_OWNER_ROLE_ID, DEFAULT_WORKSPACE_ID, now, now, DEFAULT_WORKSPACE_ID);
db.prepare(
`INSERT INTO workspace_members (user_id, workspace_id, created_at) VALUES (?, ?, ?)
ON CONFLICT(user_id, workspace_id) DO NOTHING`,
).run(E2E_UID, DEFAULT_WORKSPACE_ID, now);
// The collab partner is a plain member with no role, so a doc is theirs only once shared.
db.prepare(
`INSERT INTO workspace_members (user_id, workspace_id, created_at) VALUES (?, ?, ?)
ON CONFLICT(user_id, workspace_id) DO NOTHING`,
).run(E2E_UID_2, DEFAULT_WORKSPACE_ID, now);
db.prepare(`DELETE FROM member_roles WHERE user_id = ? AND workspace_id = ?`).run(E2E_UID, DEFAULT_WORKSPACE_ID);
db.prepare(
`INSERT INTO member_roles (user_id, workspace_id, role_id, created_at)
SELECT ?, ?, id, ? FROM roles WHERE workspace_id = ? AND is_owner_role = 1`,
).run(E2E_UID, DEFAULT_WORKSPACE_ID, now, DEFAULT_WORKSPACE_ID);
// POST /api/auth/bootstrap needs a setup pass, which exists only while no user does, and verifies the GitHub App against github.com, so the instance is configured here instead so the first-run gate never blocks the specs.
db.prepare(
`UPDATE instance_settings SET instance_url = ?, github_oauth_client_id = ?, github_oauth_client_secret = ? WHERE id = 1`,
).run(BASE_URL, "e2e-github-client-id", sealSecret("e2e-github-client-secret"));
db.close();
console.log("e2e seed: user + project + workspace membership + instance settings ready");
await waitForMachine();
}
// The server has no readiness gate in docker-compose.e2e.yml, so retry past a cold-start connection refusal.
async function waitForServer(): Promise<void> {
for (let i = 0; i < 120; i++) {
try {
await fetch(`${BASE_URL}/api/auth/bootstrap-status`);
return;
} catch {
await sleep(500);
}
}
throw new Error("timed out waiting for the server to accept connections");
}
// The runner enrolls after the server boots, and a stack names the machine it deploys to, so specs wait for it to be listed.
async function waitForMachine(): Promise<void> {
const headers = { Authorization: `Bearer ${mintToken()}` };
for (let i = 0; i < 120; i++) {
const res = await fetch(`${BASE_URL}/api/machines/deployable`, { headers });
const machines = (await res.json()) as { name: string }[];
if (machines.some((m) => m.name === E2E_MACHINE)) return;
await sleep(500);
}
throw new Error(`timed out waiting for machine ${E2E_MACHINE}`);
}
// Mirrors crypto.Encrypt: base64(nonce || AES-256-GCM(sha256(secret)) ciphertext || tag), unpadded.
function sealSecret(plain: string): string {
const nonce = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", createHash("sha256").update(AUTH_SECRET).digest(), nonce);
const sealed = Buffer.concat([nonce, cipher.update(plain, "utf8"), cipher.final(), cipher.getAuthTag()]);
return sealed.toString("base64").replace(/=+$/, "");
}
export function teardown(): void {
// nothing to do; the compose volume is discarded with `down -v`
}