0d986f60bd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
集成服务器托管模型、自然语言移动与有界抓放、内置 LeKiwi URL 导入和双摄像头;同步部署契约与指定域名 iframe 白名单,保留原有物理安全、会话及调用预算防护。 更新 npm 包及锁文件版本、CHANGELOG 与发布文档。提交前 typecheck、120 项定向前端测试和 44 项后端测试通过(3 项可选跳过);真实 v2 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
const site = 'https://cadworld-sim.robotquan.com';
|
|
const approvedParent = 'https://cadworld.robotquan.com';
|
|
|
|
test.skip(process.env.CADWORLD_PRODUCTION_TEST !== '1', '真实站点非付费检查须显式启用');
|
|
|
|
test('生产嵌入响应头:仅允许自身与指定同事域名,API 仍拒绝父页面来源', async ({ request }) => {
|
|
const response = await request.get(site);
|
|
expect(response.status()).toBe(200);
|
|
const headers = response.headers();
|
|
expect(headers['x-frame-options']).toBeUndefined();
|
|
expect(headers['content-security-policy']).toContain(`frame-ancestors 'self' ${approvedParent};`);
|
|
expect(headers['content-security-policy']).toContain("object-src 'none'");
|
|
expect(headers['x-content-type-options']).toBe('nosniff');
|
|
expect(headers['strict-transport-security']).toContain('max-age=31536000');
|
|
const forbidden = await request.post(`${site}/api/decision/v1/session`, {
|
|
headers: { Origin: approvedParent, 'Sec-Fetch-Site': 'same-site' },
|
|
data: {},
|
|
});
|
|
expect(forbidden.status()).toBe(403);
|
|
expect((await forbidden.json()).error).toBe('origin_forbidden');
|
|
expect(forbidden.headers()['access-control-allow-origin']).toBeUndefined();
|
|
});
|
|
|
|
test('生产嵌入:允许来源真实加载应用、Strict Cookie 会话和 CSRF 正常,无推理', async ({
|
|
page,
|
|
context,
|
|
}, info) => {
|
|
let inference = 0;
|
|
await page.route('**/api/decision/v1/**', async (route) => {
|
|
if (/\/(command|plan|decide|test)$/.test(new URL(route.request().url()).pathname)) {
|
|
inference++;
|
|
await route.abort();
|
|
} else await route.continue();
|
|
});
|
|
// Only the parent HTML is synthetic. The iframe, assets and session API are live.
|
|
const parentUrl = `${approvedParent}/__cadworld_embed_acceptance__`;
|
|
await page.route(parentUrl, (route) =>
|
|
route.fulfill({
|
|
contentType: 'text/html',
|
|
body: `<html><body style="margin:0"><iframe title="CADWorld Sim" src="${site}/" style="width:100%;height:95vh;border:0" allow="fullscreen"></iframe></body></html>`,
|
|
}),
|
|
);
|
|
await page.goto(parentUrl);
|
|
const frame = page.frameLocator('iframe');
|
|
await frame.getByRole('tab', { name: '控制台', exact: true }).click();
|
|
await frame.getByRole('button', { name: '机器人语言控制', exact: true }).click();
|
|
await expect(frame.getByRole('textbox', { name: '机器人指令' })).toBeEditable();
|
|
const embedded = page.frames().find((f) => f.url() === `${site}/`);
|
|
expect(embedded).toBeTruthy();
|
|
const sessionResult = await embedded!.evaluate(async () => {
|
|
const prefix = '/api/decision/v1';
|
|
const bootstrap = await fetch(prefix + '/session', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: '{}',
|
|
});
|
|
const session = await bootstrap.json();
|
|
const status = await fetch(prefix + '/status');
|
|
const csrfGuard = await fetch(prefix + '/cancel', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: '{}',
|
|
});
|
|
const destroyed = await fetch(prefix + '/session', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrfToken },
|
|
body: '{}',
|
|
});
|
|
const expired = await fetch(prefix + '/status');
|
|
return {
|
|
bootstrap: bootstrap.status,
|
|
ready: session.ready,
|
|
status: status.status,
|
|
csrfGuard: csrfGuard.status,
|
|
destroyed: destroyed.status,
|
|
expired: expired.status,
|
|
};
|
|
});
|
|
expect(sessionResult).toEqual({
|
|
bootstrap: 200,
|
|
ready: true,
|
|
status: 200,
|
|
csrfGuard: 403,
|
|
destroyed: 200,
|
|
expired: 401,
|
|
});
|
|
expect(
|
|
(await context.cookies(site)).filter((c) => c.name.includes('session')).map((c) => c.name),
|
|
).toHaveLength(0);
|
|
expect(inference).toBe(0);
|
|
await page.screenshot({ path: info.outputPath('approved-iframe.png') });
|
|
});
|
|
|
|
test('生产嵌入:未授权同站子域名仍被 CSP 拒绝', async ({ page }) => {
|
|
const violations: string[] = [];
|
|
page.on('console', (message) => {
|
|
if (message.type() === 'error') violations.push(message.text());
|
|
});
|
|
const parentUrl = 'https://unapproved.robotquan.com/__cadworld_embed_acceptance__';
|
|
await page.route(parentUrl, (route) =>
|
|
route.fulfill({
|
|
contentType: 'text/html',
|
|
body: `<iframe src="${site}/" title="forbidden"></iframe>`,
|
|
}),
|
|
);
|
|
await page.goto(parentUrl);
|
|
await expect.poll(() => violations.some((text) => text.includes('frame-ancestors'))).toBe(true);
|
|
await expect(
|
|
page.frameLocator('iframe').getByRole('tab', { name: '控制台', exact: true }),
|
|
).toHaveCount(0);
|
|
});
|