handoff-knowledge.spec(真后端 48080, MSW off): - 正路:市场资产→获取授权→bind-precheck 就绪→前往绑定(createHandoff token)→落地页(URL token 经 replaceState 清除)→确认→kbBindPrecheck(后端核验+核销 token)→createBinding→绑定完成。证红线兑现闭环真生效。 - 负路:伪造 handoffToken 直打 kbBindPrecheck→后端 verify 拒(code 非 0)。证"不信客户端 token"红线后端真拦截。 - 用 work4(creator=test1、未占 uk);work1+kb1 已被 installed binding 占,避 uk_muse_knowledge_binding_work_kb 冲突。 global-setup #12:每轮清理 handoff 兑现产生的 market_kb 绑定(work4+kb1,按 source_snapshot_id 精准删),供闭环幂等重跑。 验证:handoff-knowledge 2 passed + 二次幂等绿;全量 48 passed。 (knowledge-disable-restore 全量时序 flaky——单跑 2 passed,预存在共享 fixture 时序、与本改动无关:handoff 不碰 KB id=1。) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
4.3 KiB
TypeScript
95 lines
4.3 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
/**
|
|
* 跨空间 handoff → knowledge 兑现端到端 e2e(真实后端,MSW off,反假绿)。
|
|
*
|
|
* 前置:vite VITE_API_MOCK=false、真实 muse-server 48080(含 P1 verify/consume)、muse_slice_live asset 1(market_kb)+ work 1。
|
|
*
|
|
* 正路(UI 端到端):资产详情→获取授权→bind-precheck 就绪→前往绑定(createHandoff 一次性 token)→落地页(URL token
|
|
* 被 replaceState 清除)→确认→kbBindPrecheck(后端核验+核销 token)→createBinding→绑定完成。证红线兑现闭环真生效。
|
|
* 负路(API 直打):伪造 handoffToken 调 kbBindPrecheck→后端 verify 拒(code 非 0)。证"不信客户端 token"红线在后端真拦截。
|
|
*/
|
|
const TOKEN = 'test1';
|
|
const API = 'http://localhost:48080/app-api/muse';
|
|
const AUTH = { Authorization: 'Bearer test1', 'tenant-id': '1', 'X-API-Version': '1' };
|
|
|
|
async function seedToken(page: Page): Promise<void> {
|
|
await page.addInitScript((t) => {
|
|
window.localStorage.setItem('accessToken', t);
|
|
window.localStorage.setItem('tenantId', '1');
|
|
}, TOKEN);
|
|
}
|
|
|
|
test('正路:市场资产 handoff 兑现到 knowledge 闭环(真实后端)', async ({ page, request }) => {
|
|
await seedToken(page);
|
|
// 前提:asset 1 已授权(幂等 best-effort;bind-precheck 需授权存在)
|
|
await request
|
|
.post(`${API}/marketplace/assets/1/purchase`, { headers: AUTH, data: { commandId: `e2e-acq-${Date.now()}` } })
|
|
.catch(() => undefined);
|
|
|
|
await page.goto('/market/assets/1');
|
|
|
|
// 1. 来源侧授权预检 → 就绪(用 work4:creator=test1、未占 uk(work,kb);work1+kb1 已被 installed binding 占)
|
|
await page.getByLabel('目标作品 ID').fill('4');
|
|
await page.getByRole('button', { name: '授权预检' }).click();
|
|
await expect(page.getByText('可绑定(就绪)')).toBeVisible({ timeout: 15_000 });
|
|
|
|
// 2. 前往绑定:createHandoff 创建一次性 token + 跳落地页
|
|
const [handoffResp] = await Promise.all([
|
|
page.waitForResponse(
|
|
(r) => /\/marketplace\/handoffs$/.test(r.url()) && r.request().method() === 'POST',
|
|
{ timeout: 15_000 }
|
|
),
|
|
page.getByRole('button', { name: /前往绑定/ }).click(),
|
|
]);
|
|
expect(handoffResp.status()).toBe(200);
|
|
const handoffBody = await handoffResp.json();
|
|
expect(handoffBody.code).toBe(0);
|
|
expect(handoffBody.data?.handoffToken).toBeTruthy();
|
|
|
|
// 3. 落地页:URL token 已被 replaceState 清除(红线:不残留可重放凭据)
|
|
await expect(page).toHaveURL(/\/handoff\/land\/knowledge/, { timeout: 15_000 });
|
|
expect(page.url()).not.toContain('token=');
|
|
await expect(page.getByText('确认绑定市场知识来源')).toBeVisible({ timeout: 15_000 });
|
|
|
|
// 4. 确认绑定 → kbBindPrecheck(后端核验 + 核销 token)
|
|
await page.getByRole('button', { name: /确认绑定/ }).click();
|
|
const kbResp = await page.waitForResponse(
|
|
(r) => /\/knowledge-bindings\/prechecks$/.test(r.url()) && r.request().method() === 'POST',
|
|
{ timeout: 20_000 }
|
|
);
|
|
expect(kbResp.status()).toBe(200);
|
|
expect((await kbResp.json()).code, 'kbBindPrecheck code').toBe(0);
|
|
|
|
// 5. createBinding(只消费 precheckId,落 knowledge_binding)
|
|
const bindResp = await page.waitForResponse(
|
|
(r) => /\/knowledge-bindings$/.test(r.url()) && r.request().method() === 'POST',
|
|
{ timeout: 20_000 }
|
|
);
|
|
const bindBody = await bindResp.json();
|
|
expect(bindBody.code, `createBinding 返回: ${JSON.stringify(bindBody)}`).toBe(0);
|
|
|
|
// 6. 兑现完成
|
|
await expect(page.getByText('绑定完成')).toBeVisible({ timeout: 15_000 });
|
|
});
|
|
|
|
test('负路:伪造 handoffToken 调 kbBindPrecheck 被后端核验拒绝(真实后端 API)', async ({ request }) => {
|
|
// 直打后端:伪造 token(从未由 Market 发起),后端 verify 应拒,不落任何绑定。
|
|
const resp = await request.post(`${API}/works/1/knowledge-bindings/prechecks`, {
|
|
headers: AUTH,
|
|
data: {
|
|
commandId: `e2e-forge-${Date.now()}`,
|
|
sourceType: 'market_kb',
|
|
sourceId: '1',
|
|
sourceVersion: 1,
|
|
sourceStatus: 'available',
|
|
handoffToken: 'handoff_forged_invalid_token_000000000000',
|
|
authorizationSummaryId: '9001',
|
|
authorizationSnapshotId: 'snapshot-forged',
|
|
},
|
|
});
|
|
// 后端核验拒绝:HTTP 4xx 或 CommonResult.code 非 0(KNOWLEDGE_MARKET_HANDOFF_UNAVAILABLE)。
|
|
const body = await resp.json().catch(() => ({ code: -1 }));
|
|
expect(body.code).not.toBe(0);
|
|
});
|