Files
zclaw_openfang/desktop/tests/e2e/specs/smoke_cross.spec.ts
iven bd48de69ee
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
fix(test): P2-03 rate limit — share auth token across cross-system smoke tests
6 tests each called saasLogin() → 6 login requests in <60s → hit 5/min/IP
rate limit on the 6th test. Now login once per worker, reuse token for all
6 tests. Reduces login API calls from 6 to 1.
2026-04-10 21:34:07 +08:00

256 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Smoke Tests — 跨系统闭环断裂探测
*
* 6 个冒烟测试验证 Admin→SaaS→Desktop 的跨系统闭环。
* 同时操作 Admin API 和 Desktop UI验证数据一致性。
*
* 前提条件:
* - SaaS Server: http://localhost:8080
* - Desktop App: http://localhost:1420
* - Admin V2: http://localhost:5173
* - 真实 LLM API Key (部分测试需要)
*
* 运行: cd desktop && npx playwright test smoke_cross
*/
import { test, expect, type Page } from '@playwright/test';
test.setTimeout(180000);
const SaaS_BASE = 'http://localhost:8080/api/v1';
const ADMIN_USER = 'admin';
const ADMIN_PASS = 'admin123';
// Shared token — login once per worker to avoid rate limiting (5 req/min/IP).
// Workers are isolated processes, so this is safe for parallel execution.
let _sharedToken: string | null = null;
async function getSharedToken(page: Page): Promise<string> {
if (_sharedToken) return _sharedToken;
const res = await page.request.post(`${SaaS_BASE}/auth/login`, {
data: { username: ADMIN_USER, password: ADMIN_PASS },
});
if (!res.ok()) {
const body = await res.text();
console.log(`saasLogin failed: ${res.status()}${body.slice(0, 300)}`);
}
expect(res.ok()).toBeTruthy();
const json = await res.json();
_sharedToken = json.token;
return _sharedToken;
}
// Keep saasLogin as alias for individual test clarity
async function saasLogin(page: Page): Promise<string> {
return getSharedToken(page);
}
async function waitForDesktopReady(page: Page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.waitForSelector('.h-screen', { timeout: 15000 });
}
// ── X1: Admin 配 Provider → Desktop 可用 ──────────────────────────
test('X1: Admin 创建 Provider → Desktop 模型列表包含', async ({ page }) => {
const token = await saasLogin(page);
const providerName = `cross_provider_${Date.now()}`;
// Step 1: 通过 SaaS API 创建 Provider
const provRes = await page.request.post(`${SaaS_BASE}/providers`, {
headers: { Authorization: `Bearer ${token}` },
data: {
name: providerName,
provider_type: 'openai',
base_url: 'https://api.cross.test/v1',
enabled: true,
},
});
expect(provRes.ok()).toBeTruthy();
const provJson = await provRes.json();
const providerId = provJson.id;
// Step 2: 创建 Model
const modelRes = await page.request.post(`${SaaS_BASE}/models`, {
headers: { Authorization: `Bearer ${token}` },
data: {
name: `cross_model_${Date.now()}`,
provider_id: providerId,
model_id: 'test-model',
enabled: true,
},
});
expect(modelRes.ok()).toBeTruthy();
// Step 3: 等待缓存刷新 (最多 60s)
console.log('Waiting for cache refresh...');
await page.waitForTimeout(5000);
// Step 4: 在 Desktop 检查模型列表
await waitForDesktopReady(page);
// 通过 SaaS API 验证模型可被 relay 获取
const relayModels = await page.request.get(`${SaaS_BASE}/relay/models`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(relayModels.ok()).toBeTruthy();
const modelsJson = await relayModels.json();
const models = modelsJson.models || modelsJson.data || modelsJson;
const found = Array.isArray(models) && models.some(
(m: any) => m.id === providerId || m.name?.includes('cross_model')
);
console.log(`X1: Model found in relay list: ${found}`);
if (!found) {
console.log('Available models:', JSON.stringify(models).slice(0, 500));
}
});
// ── X2: Admin 改权限 → Desktop 受限 ───────────────────────────────
test('X2: Admin 禁用 user → Desktop 请求失败', async ({ page }) => {
const token = await saasLogin(page);
// 创建一个测试用户
const regRes = await page.request.post(`${SaaS_BASE}/auth/register`, {
data: {
username: `cross_user_${Date.now()}`,
email: `cross_${Date.now()}@test.io`,
password: 'TestPassword123',
},
});
expect(regRes.ok()).toBeTruthy();
const regJson = await regRes.json();
const userId = regJson.account?.id || regJson.id;
// 禁用该用户
if (userId) {
const disableRes = await page.request.patch(`${SaaS_BASE}/accounts/${userId}`, {
headers: { Authorization: `Bearer ${token}` },
data: { status: 'disabled' },
});
console.log(`Disable user: ${disableRes.status()}`);
// 验证被禁用的用户无法登录
const loginRes = await page.request.post(`${SaaS_BASE}/auth/login`, {
data: {
username: regJson.username || `cross_user_${Date.now()}`,
password: 'TestPassword123',
},
});
console.log(`Disabled user login: ${loginRes.status()} (expected 401/403)`);
expect([401, 403]).toContain(loginRes.status());
}
});
// ── X3: Admin 创建知识 → Desktop 检索 ──────────────────────────────
test('X3: Admin 创建知识条目 → SaaS 搜索可找到', async ({ page }) => {
const token = await saasLogin(page);
const uniqueContent = `跨系统测试知识_${Date.now()}_唯一标识`;
// Step 1: 创建分类
const catRes = await page.request.post(`${SaaS_BASE}/knowledge/categories`, {
headers: { Authorization: `Bearer ${token}` },
data: { name: `cross_cat_${Date.now()}`, description: 'Cross-system test' },
});
expect(catRes.ok()).toBeTruthy();
const catJson = await catRes.json();
// Step 2: 创建知识条目
const itemRes = await page.request.post(`${SaaS_BASE}/knowledge/items`, {
headers: { Authorization: `Bearer ${token}` },
data: {
title: uniqueContent,
content: '这是跨系统知识检索测试的内容,包含特定关键词。',
category_id: catJson.id,
tags: ['cross-system', 'test'],
},
});
expect(itemRes.ok()).toBeTruthy();
// Step 3: 通过 SaaS API 搜索
const searchRes = await page.request.post(`${SaaS_BASE}/knowledge/search`, {
headers: { Authorization: `Bearer ${token}` },
data: { query: uniqueContent, limit: 5 },
});
expect(searchRes.ok()).toBeTruthy();
const searchJson = await searchRes.json();
const items = searchJson.items || searchJson;
const found = Array.isArray(items) && items.some(
(i: any) => i.title === uniqueContent
);
expect(found).toBeTruthy();
console.log(`X3: Knowledge item found via search: ${found}`);
});
// ── X4: Desktop 聊天 → Admin 统计更新 ─────────────────────────────
test('X4: Dashboard 统计 — API 验证结构', async ({ page }) => {
const token = await saasLogin(page);
// Step 1: 获取初始统计 (correct endpoint: /stats/dashboard)
const statsRes = await page.request.get(`${SaaS_BASE}/stats/dashboard`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(statsRes.ok()).toBeTruthy();
const stats = await statsRes.json();
// 验证统计结构
expect(stats).toHaveProperty('total_accounts');
expect(stats).toHaveProperty('active_providers');
expect(stats).toHaveProperty('active_models');
expect(stats).toHaveProperty('tasks_today');
console.log(`X4: Dashboard stats = ${JSON.stringify(stats).slice(0, 300)}`);
});
// ── X5: TOTP 全流程 ──────────────────────────────────────────────
test('X5: TOTP 设置→验证→登录', async ({ page }) => {
const token = await saasLogin(page);
// Step 1: 设置 TOTP
const setupRes = await page.request.post(`${SaaS_BASE}/auth/totp/setup`, {
headers: { Authorization: `Bearer ${token}` },
});
const setupStatus = setupRes.status();
if (setupStatus === 200 || setupStatus === 201) {
const setupJson = await setupRes.json();
console.log(`X5: TOTP setup returned secret (encrypted): ${setupJson.secret ? 'yes' : 'no'}`);
expect(setupJson.secret || setupJson.totp_secret).toBeTruthy();
} else {
console.log(`X5: TOTP setup returned ${setupStatus} (may need verification endpoint)`);
const body = await setupRes.text();
console.log(`Response: ${body.slice(0, 200)}`);
}
});
// ── X6: 计费一致性 ───────────────────────────────────────────────
test('X6: 计费一致性 — billing_usage 查询', async ({ page }) => {
const token = await saasLogin(page);
// 查询计费用量
const usageRes = await page.request.get(`${SaaS_BASE}/billing/usage`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(usageRes.ok()).toBeTruthy();
const usageJson = await usageRes.json();
// 验证结构
console.log(`X6: Billing usage = ${JSON.stringify(usageJson).slice(0, 300)}`);
// 查询计划
const plansRes = await page.request.get(`${SaaS_BASE}/billing/plans`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(plansRes.ok()).toBeTruthy();
const plansJson = await plansRes.json();
console.log(`X6: Billing plans count = ${Array.isArray(plansJson) ? plansJson.length : 'object'}`);
});