|
- import { request } from '@playwright/test';
- import { spawn } from 'node:child_process';
- import { access, open, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
- import { setTimeout as delay } from 'node:timers/promises';
- import {
- adminPassword,
- adminStorageStatePath,
- adminUsername,
- authDir,
- clusterRunDir,
- cnBaseURL,
- metadataPath,
- ovBaseURL,
- playwrightArtifactRoot,
- readyMarkerPath,
- repoRoot,
- rootPassword,
- rootStorageStatePath,
- rootUsername,
- serverBinaryDir,
- serverBinaryPath,
- serverLogPath,
- webRoot,
- } from './fixtures/cluster';
-
- type LoginResponse = {
- success: boolean;
- message?: string;
- data?: {
- id: number;
- username: string;
- display_name: string;
- role: number;
- status: number;
- group: string;
- };
- };
-
- type SimpleResponse = {
- success: boolean;
- message?: string;
- error?: string;
- };
-
- async function runCommand(command: string, args: string[], cwd: string) {
- await new Promise<void>((resolve, reject) => {
- const child = spawn(command, args, {
- cwd,
- stdio: ['ignore', 'pipe', 'pipe'],
- shell: false,
- });
-
- let stdout = '';
- let stderr = '';
- child.stdout.on('data', (chunk) => {
- stdout += chunk.toString();
- });
- child.stderr.on('data', (chunk) => {
- stderr += chunk.toString();
- });
- child.on('error', reject);
- child.on('close', (code) => {
- if (code === 0) {
- resolve();
- return;
- }
- reject(
- new Error(
- `${command} ${args.join(' ')} failed with code ${code}\n${stdout}${stderr}`,
- ),
- );
- });
- });
- }
-
- async function killPreviousServer() {
- try {
- const raw = await readFile(metadataPath, 'utf8');
- const metadata = JSON.parse(raw) as { serverPid?: number };
- if (!metadata.serverPid) {
- return;
- }
- await killProcessTree(metadata.serverPid);
- } catch {
- // ignore stale or missing metadata
- }
- }
-
- async function killProcessTree(pid: number) {
- if (process.platform === 'win32') {
- await runCommand('taskkill', ['/PID', String(pid), '/T', '/F'], repoRoot);
- return;
- }
- process.kill(pid, 'SIGTERM');
- }
-
- async function cleanupListenerPorts() {
- if (process.platform !== 'win32') {
- return;
- }
-
- await runCommand(
- 'powershell',
- [
- '-NoProfile',
- '-Command',
- "$ports = 3100,3101; " +
- "Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | " +
- "Where-Object { $ports -contains $_.LocalPort } | " +
- 'Select-Object -ExpandProperty OwningProcess -Unique | ' +
- 'ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }',
- ],
- repoRoot,
- );
- }
-
- async function waitForServerReady() {
- const deadline = Date.now() + 180_000;
-
- while (Date.now() < deadline) {
- try {
- await access(readyMarkerPath);
- const response = await fetch(`${cnBaseURL}/api/status`, {
- headers: { 'Cache-Control': 'no-store' },
- });
- if (response.ok) {
- return;
- }
- } catch {
- // retry
- }
- await delay(500);
- }
-
- throw new Error(
- `user migration UI server did not become ready in time, see ${serverLogPath}`,
- );
- }
-
- async function login(username: string, password: string) {
- const api = await request.newContext({
- baseURL: cnBaseURL,
- extraHTTPHeaders: {
- 'Cache-Control': 'no-store',
- },
- });
-
- const response = await api.post('/api/user/login', {
- data: {
- username,
- password,
- },
- });
- if (!response.ok()) {
- throw new Error(`login request for ${username} failed: ${response.status()}`);
- }
-
- const payload = (await response.json()) as LoginResponse;
- if (!payload.success || !payload.data) {
- throw new Error(`login failed for ${username}: ${payload.message ?? 'unknown error'}`);
- }
-
- return { api, user: payload.data };
- }
-
- async function loginWithRetry(
- username: string,
- password: string,
- timeoutMs = 60_000,
- ) {
- const deadline = Date.now() + timeoutMs;
- let lastError = `login failed for ${username}`;
-
- while (Date.now() < deadline) {
- try {
- return await login(username, password);
- } catch (error) {
- lastError = error instanceof Error ? error.message : String(error);
- await delay(500);
- }
- }
-
- throw new Error(lastError);
- }
-
- async function writeStorageState(
- api: Awaited<ReturnType<typeof request.newContext>>,
- user: LoginResponse['data'],
- outputPath: string,
- ) {
- const state = await api.storageState();
- await writeFile(
- outputPath,
- JSON.stringify(
- {
- cookies: state.cookies,
- origins: [
- {
- origin: cnBaseURL,
- localStorage: [
- {
- name: 'user',
- value: JSON.stringify(user),
- },
- ],
- },
- ],
- },
- null,
- 2,
- ),
- 'utf8',
- );
- }
-
- export default async function globalSetup() {
- await killPreviousServer();
- await cleanupListenerPorts();
- await rm(playwrightArtifactRoot, { recursive: true, force: true });
- await mkdir(authDir, { recursive: true });
- await mkdir(serverBinaryDir, { recursive: true });
-
- await runCommand('bun', ['run', 'build'], webRoot);
- await runCommand(
- 'go',
- ['build', '-o', serverBinaryPath, './test/e2e/cmd/user_migration_ui_server'],
- repoRoot,
- );
-
- const logFile = await open(serverLogPath, 'a');
- const child = spawn(serverBinaryPath, [], {
- cwd: repoRoot,
- stdio: ['ignore', logFile.fd, logFile.fd],
- });
- await logFile.close();
-
- await writeFile(
- metadataPath,
- JSON.stringify(
- {
- serverPid: child.pid,
- serverBinaryPath,
- serverLogPath,
- runDir: clusterRunDir,
- baseURL: cnBaseURL,
- ovBaseURL,
- },
- null,
- 2,
- ),
- 'utf8',
- );
-
- await waitForServerReady();
-
- const rootSession = await loginWithRetry(rootUsername, rootPassword);
- try {
- const createAdminResponse = await rootSession.api.post('/api/user/', {
- headers: {
- 'New-API-User': String(rootSession.user.id),
- },
- data: {
- username: adminUsername,
- password: adminPassword,
- display_name: 'Playwright Admin',
- role: 10,
- },
- });
- if (!createAdminResponse.ok()) {
- throw new Error(
- `create admin request failed: ${createAdminResponse.status()} ${createAdminResponse.statusText()}`,
- );
- }
-
- const createAdminPayload = (await createAdminResponse.json()) as SimpleResponse;
- const createAdminMessage =
- createAdminPayload.message ?? createAdminPayload.error ?? 'unknown error';
- const adminAlreadyExists =
- createAdminMessage.includes('UNIQUE constraint failed: users.username') ||
- createAdminMessage.includes('Duplicate entry');
- if (!createAdminPayload.success && !adminAlreadyExists) {
- throw new Error(
- `create admin failed: ${createAdminMessage}`,
- );
- }
-
- await writeStorageState(
- rootSession.api,
- rootSession.user,
- rootStorageStatePath,
- );
- } finally {
- await rootSession.api.dispose();
- }
-
- const adminSession = await loginWithRetry(adminUsername, adminPassword, 20_000);
- try {
- await writeStorageState(
- adminSession.api,
- adminSession.user,
- adminStorageStatePath,
- );
- } finally {
- await adminSession.api.dispose();
- }
- }
|