mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-14 17:26:03 +00:00
134 lines
5.4 KiB
JavaScript
134 lines
5.4 KiB
JavaScript
import { chromium } from '@playwright/test';
|
|
import { existsSync } from 'node:fs';
|
|
import { readFile, mkdir, stat } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const STORAGE_STATE_PATH = path.join(__dirname, '.auth', 'state.json');
|
|
const ENV_FILE_PATH = path.join(__dirname, '.env');
|
|
const CACHE_TTL_MS = 1000 * 60 * 60 * 24;
|
|
|
|
// Tiny dotenv loader. Loads tests/e2e/.env into process.env if it exists.
|
|
// Existing process.env wins, so CLI overrides still work.
|
|
async function loadDotEnv (filePath) {
|
|
if ( ! existsSync(filePath) ) return;
|
|
const text = await readFile(filePath, 'utf8');
|
|
for ( const rawLine of text.split('\n') ) {
|
|
const line = rawLine.trim();
|
|
if ( !line || line.startsWith('#') ) continue;
|
|
const eq = line.indexOf('=');
|
|
if ( eq < 0 ) continue;
|
|
const key = line.slice(0, eq).trim();
|
|
let value = line.slice(eq + 1).trim();
|
|
if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
if ( ! (key in process.env) ) process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
async function isCachedStateFresh () {
|
|
if ( ! existsSync(STORAGE_STATE_PATH) ) return false;
|
|
if ( process.env.PUTER_TEST_RESET_AUTH === '1' ) return false;
|
|
try {
|
|
const s = await stat(STORAGE_STATE_PATH);
|
|
return (Date.now() - s.mtimeMs) < CACHE_TTL_MS;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function loginAsAdmin ({ origin, username, password }) {
|
|
// Puter rejects POSTs without an Origin header (403 Forbidden), so send
|
|
// one matching the target origin to satisfy its CSRF check.
|
|
const res = await fetch(`${origin}/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': origin,
|
|
},
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
if ( ! res.ok ) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(`POST /login → HTTP ${res.status}: ${text || '(empty body)'}`);
|
|
}
|
|
const data = await res.json();
|
|
if ( ! data.token ) {
|
|
throw new Error(`POST /login response had no token: ${JSON.stringify(data)}`);
|
|
}
|
|
return data.token;
|
|
}
|
|
|
|
export default async function globalSetup () {
|
|
await loadDotEnv(ENV_FILE_PATH);
|
|
|
|
const PUTER_ORIGIN = process.env.PUTER_TEST_ORIGIN || 'http://puter.localhost:4100';
|
|
const ADMIN_USERNAME = process.env.PUTER_ADMIN_USERNAME || 'admin';
|
|
const ADMIN_PASSWORD = process.env.PUTER_ADMIN_PASSWORD || '';
|
|
|
|
await mkdir(path.dirname(STORAGE_STATE_PATH), { recursive: true });
|
|
|
|
if ( await isCachedStateFresh() ) {
|
|
|
|
console.log(`[e2e] Reusing cached auth state at ${STORAGE_STATE_PATH}`);
|
|
return;
|
|
}
|
|
|
|
if ( ! ADMIN_PASSWORD ) {
|
|
throw new Error(
|
|
'PUTER_ADMIN_PASSWORD is not set.\n' +
|
|
'Your local Puter prints the admin password at startup (look for "Password: <hex>" in the monorepo\'s npm start output).\n' +
|
|
'Set it in tests/e2e/.env (gitignored):\n' +
|
|
' cp tests/e2e/.env.example tests/e2e/.env\n' +
|
|
' # then edit tests/e2e/.env and set PUTER_ADMIN_PASSWORD\n' +
|
|
'Or pass it inline: PUTER_ADMIN_PASSWORD=<value> npm run test:e2e',
|
|
);
|
|
}
|
|
|
|
// 1) Direct API login → token (no browser, no /signup, no rate-limit risk).
|
|
const token = await loginAsAdmin({ origin: PUTER_ORIGIN, username: ADMIN_USERNAME, password: ADMIN_PASSWORD });
|
|
|
|
// 2) Open Puter with ?auth_token=<token> so initgui's auth_token flow runs
|
|
// the full setup (puter.setAuthToken, /session/sync-cookie,
|
|
// update_auth_data). The GUI points the SDK at its own templated
|
|
// api_origin — that is not overridable from the URL.
|
|
const browser = await chromium.launch();
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
|
|
// Probe the GUI's templated api_origin so we can assert the SDK adopts it.
|
|
await page.goto(PUTER_ORIGIN);
|
|
const apiOrigin = await page.evaluate(() => window.api_origin || null);
|
|
if ( ! apiOrigin ) {
|
|
await browser.close();
|
|
throw new Error(`Could not determine api_origin from ${PUTER_ORIGIN} (window.api_origin was empty).`);
|
|
}
|
|
|
|
await page.goto(`${PUTER_ORIGIN}/?auth_token=${encodeURIComponent(token)}`);
|
|
|
|
await page.waitForFunction(() => !!window.puter, null, { timeout: 60_000 });
|
|
|
|
// A token from the URL is confirmed before it's adopted; nothing reaches
|
|
// localStorage until this is dismissed.
|
|
await page.locator('.alert-resp-button.button-primary')
|
|
.first()
|
|
.click({ timeout: 60_000 });
|
|
|
|
await page.waitForFunction(
|
|
expectedApiOrigin => {
|
|
// v2 key since the v1→v2 cutover; legacy key tolerated.
|
|
const ls = (typeof localStorage !== 'undefined') ? (localStorage.getItem('auth_token_v2') || localStorage.getItem('auth_token')) : null;
|
|
return !!(ls && window.auth_token && window.puter?.authToken && window.puter?.APIOrigin === expectedApiOrigin);
|
|
},
|
|
apiOrigin,
|
|
{ timeout: 60_000 },
|
|
);
|
|
|
|
await context.storageState({ path: STORAGE_STATE_PATH });
|
|
await browser.close();
|
|
|
|
console.log(`[e2e] Logged in as "${ADMIN_USERNAME}", saved auth state to ${STORAGE_STATE_PATH}`);
|
|
}
|