Files
beaver_project/scripts/smoke-auth-files.mjs
2026-06-03 12:06:34 +08:00

191 lines
7.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
const require = createRequire(import.meta.url);
const { chromium } = require('playwright');
const now = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
const username = process.env.BEAVER_SMOKE_USERNAME || `smoke${now}`;
const password = process.env.BEAVER_SMOKE_PASSWORD || 'TestPass123!';
const email = process.env.BEAVER_SMOKE_EMAIL || `${username}@example.test`;
const portalUrl = (process.env.BEAVER_SMOKE_PORTAL_URL || 'http://127.0.0.1:3081').replace(/\/$/, '');
const nextPath = process.env.BEAVER_SMOKE_NEXT_PATH || '/files';
const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || undefined;
const hostResolverRules = process.env.BEAVER_SMOKE_HOST_RESOLVER_RULES || '';
const headless = process.env.BEAVER_SMOKE_HEADLESS !== '0';
const verifyMinio = process.env.BEAVER_SMOKE_VERIFY_MINIO === '1';
const launchArgs = [];
if (hostResolverRules) {
launchArgs.push(`--host-resolver-rules=${hostResolverRules}`);
}
const failedResponses = [];
const failedRequests = [];
function remember(list, value) {
list.push(value);
if (list.length > 30) {
list.shift();
}
}
async function clickFirstVisible(locators) {
for (const locator of locators) {
if (await locator.first().isVisible().catch(() => false)) {
await locator.first().click();
return true;
}
}
return false;
}
async function main() {
const browser = await chromium.launch({
headless,
executablePath,
args: launchArgs,
});
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
page.on('response', (response) => {
if (response.status() >= 400) {
remember(failedResponses, `${response.status()} ${response.url()}`);
}
});
page.on('requestfailed', (request) => {
remember(failedRequests, `${request.url()} ${request.failure()?.errorText || ''}`);
});
try {
const registerUrl = new URL('/register', portalUrl);
registerUrl.searchParams.set('next', nextPath);
await page.goto(registerUrl.toString(), { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.locator('#username').fill(username);
await page.locator('#email').fill(email);
await page.locator('#password').fill(password);
await page.locator('#confirmPassword').fill(password);
const submitButton = page.locator('form').first().locator('button[type="submit"]');
await submitButton.waitFor({ state: 'visible', timeout: 30000 });
await submitButton.click();
await page.waitForResponse(
(response) => response.url().includes('/api/runtime/register'),
{ timeout: 180000 },
);
await page.getByText(/配置模型|Model Setup/).waitFor({ timeout: 180000 });
const skipped = await clickFirstVisible([
page.getByRole('button', { name: /跳过|Skip/i }),
page.locator('button.secondary-button'),
]);
if (!skipped) {
throw new Error('Model setup skip button was not found');
}
await page.waitForURL(new RegExp(`${nextPath.replace('/', '\\/')}$`), { timeout: 60000 });
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.getByText(/文件管理|Files/).waitFor({ timeout: 30000 });
for (const root of ['uploads', 'outputs', 'shared', 'tasks']) {
await page.getByText(root, { exact: true }).waitFor({ timeout: 30000 });
}
const token = await page.evaluate(() => window.localStorage.getItem('beaver_access_token') || '');
if (!token) {
throw new Error('No Beaver access token found after handoff');
}
const uploadName = `smoke-${Date.now()}.txt`;
const uploadBody = `hello from Beaver smoke ${username}`;
const uploadResult = await page.evaluate(async ({ token, uploadName, uploadBody }) => {
const form = new FormData();
form.set('path', 'uploads');
form.set('file', new File([uploadBody], uploadName, { type: 'text/plain' }));
const response = await fetch('/api/user-files/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: form,
});
return { status: response.status, body: await response.json().catch(() => ({})) };
}, { token, uploadName, uploadBody });
if (uploadResult.status !== 200 || uploadResult.body.path !== `uploads/${uploadName}`) {
throw new Error(`User file upload failed: ${JSON.stringify(uploadResult)}`);
}
const apiVisibility = await page.evaluate(async ({ token }) => {
const headers = { Authorization: `Bearer ${token}` };
const [status, root] = await Promise.all([
fetch('/api/user-files/status', { headers }).then((response) => response.json()),
fetch('/api/user-files/browse', { headers }).then((response) => response.json()),
]);
return JSON.stringify({ status, root });
}, { token });
for (const forbidden of ['access_key', 'secret_key', 'bucket', 'namespace', 'object_prefix']) {
if (apiVisibility.includes(forbidden)) {
throw new Error(`User file API leaked storage field: ${forbidden}`);
}
}
const minioVerified = verifyMinio ? verifyUploadedObjectInMinio(username, uploadName) : false;
const finalUrl = page.url();
await page.screenshot({ path: process.env.BEAVER_SMOKE_SCREENSHOT || '/tmp/beaver-auth-files-smoke.png', fullPage: true });
console.log(JSON.stringify({
ok: true,
username,
email,
finalUrl,
roots: ['uploads', 'outputs', 'shared', 'tasks'],
uploadedPath: `uploads/${uploadName}`,
minioVerified,
failedResponses,
failedRequests,
}, null, 2));
} catch (error) {
await page.screenshot({ path: process.env.BEAVER_SMOKE_ERROR_SCREENSHOT || '/tmp/beaver-auth-files-smoke-error.png', fullPage: true }).catch(() => {});
console.error(JSON.stringify({
ok: false,
username,
email,
currentUrl: page.url(),
error: String(error),
failedResponses,
failedRequests,
}, null, 2));
process.exitCode = 1;
} finally {
await browser.close();
}
}
function verifyUploadedObjectInMinio(username, uploadName) {
const endpoint = process.env.BEAVER_SMOKE_MINIO_ENDPOINT || 'http://127.0.0.1:9000';
const accessKey = process.env.BEAVER_SMOKE_MINIO_ACCESS_KEY || process.env.BEAVER_MINIO_ROOT_USER || '';
const secretKey = process.env.BEAVER_SMOKE_MINIO_SECRET_KEY || process.env.BEAVER_MINIO_ROOT_PASSWORD || '';
const bucket = process.env.BEAVER_SMOKE_MINIO_BUCKET || process.env.BEAVER_USER_FILES_BUCKET || 'beaver-user-files';
const network = process.env.BEAVER_SMOKE_MINIO_NETWORK || '';
if (!accessKey || !secretKey) {
throw new Error('MinIO verification requested but access key or secret key is missing');
}
const containerArgs = ['run', '--rm', '--entrypoint', '/bin/sh'];
if (network) {
containerArgs.push('--network', network);
}
containerArgs.push(
'minio/mc:latest',
'-lc',
[
`mc alias set beaver ${shellQuote(endpoint)} ${shellQuote(accessKey)} ${shellQuote(secretKey)} >/dev/null`,
`mc stat ${shellQuote(`beaver/${bucket}/users/${username}/uploads/${uploadName}`)} >/dev/null`,
].join(' && '),
);
execFileSync('docker', containerArgs, { stdio: 'pipe' });
return true;
}
function shellQuote(value) {
return `'${String(value).replace(/'/g, "'\\''")}'`;
}
await main();