191 lines
7.3 KiB
JavaScript
Executable File
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();
|