|
- import type { APIResponse, TestInfo } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
- import { adminAccessToken, envelopeData, loginAsAdmin } from './helpers'
-
- type JsonRecord = Record<string, unknown>
-
- interface IntegrationSnapshot {
- knowledgeProviderMode: 'local' | 'hybrid' | 'third_party'
- thirdPartyKnowledgeEnabled: boolean
- thirdPartyKnowledgeName: string
- thirdPartyKnowledgeBaseUrl: string
- thirdPartyKnowledgeApiPath: string
- thirdPartyKnowledgeAuthType: 'none' | 'bearer' | 'api_key'
- thirdPartyKnowledgeBaseId: string
- thirdPartyKnowledgeTimeoutSeconds: number
- thirdPartyKnowledgeKeyConfigured: boolean
- }
-
- interface Activity {
- module: string
- action: string
- result: 'PASS' | 'CLEANED' | 'CLEANUP_FAILED'
- httpStatus?: number
- detail?: string
- }
-
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object' && !Array.isArray(value)
- ? value as JsonRecord
- : {}
-
- function normalize(value: unknown): IntegrationSnapshot {
- const root = asRecord(envelopeData(value))
- const mode = String(root.knowledgeProviderMode || 'local')
- const authType = String(root.thirdPartyKnowledgeAuthType || 'none')
- return {
- knowledgeProviderMode: mode === 'hybrid' || mode === 'third_party' ? mode : 'local',
- thirdPartyKnowledgeEnabled: Boolean(root.thirdPartyKnowledgeEnabled),
- thirdPartyKnowledgeName: String(root.thirdPartyKnowledgeName || ''),
- thirdPartyKnowledgeBaseUrl: String(root.thirdPartyKnowledgeBaseUrl || ''),
- thirdPartyKnowledgeApiPath: String(root.thirdPartyKnowledgeApiPath || '/api/v1/knowledge/search'),
- thirdPartyKnowledgeAuthType: authType === 'bearer' || authType === 'api_key' ? authType : 'none',
- thirdPartyKnowledgeBaseId: String(root.thirdPartyKnowledgeBaseId || ''),
- thirdPartyKnowledgeTimeoutSeconds: Number(root.thirdPartyKnowledgeTimeoutSeconds || 15),
- thirdPartyKnowledgeKeyConfigured: Boolean(root.thirdPartyKnowledgeKeyConfigured),
- }
- }
-
- function savePayload(value: IntegrationSnapshot) {
- return {
- ...value,
- // The API intentionally never returns the credential. An empty value
- // means keep the existing encrypted secret during both test and restore.
- thirdPartyKnowledgeApiKey: '',
- }
- }
-
- async function responseBody(response: APIResponse) {
- return response.json().catch(() => ({}))
- }
-
- async function fillLabel(page: import('@playwright/test').Page, label: string, value: string) {
- const field = page.locator('label').filter({ hasText: label }).first()
- const input = field.locator('input:not([type="hidden"]), textarea').first()
- await expect(input, `应显示字段:${label}`).toBeVisible()
- await input.fill(value)
- }
-
- async function recordScreenshot(
- page: import('@playwright/test').Page,
- testInfo: TestInfo,
- label: string,
- ) {
- await captureScreenshot(page, testInfo, label, [page.locator('input[type="password"]')])
- }
-
- test('三方配置通过页面保存、格式检查、联调失败态与刷新持久化,并精确恢复原配置', async ({ page, request }, testInfo) => {
- const activities: Activity[] = []
- const token = await adminAccessToken(request)
- const headers = { Authorization: `Bearer ${token}` }
- const initialResponse = await request.get('/api/v1/settings/general', { headers })
- expect(initialResponse.status(), '读取三方配置快照').toBe(200)
- const snapshot = normalize(await responseBody(initialResponse))
- const testName = `${runPrefix}-装备知识中台`.slice(0, 80)
- const testSettings: IntegrationSnapshot = {
- knowledgeProviderMode: 'local',
- thirdPartyKnowledgeEnabled: false,
- thirdPartyKnowledgeName: testName,
- thirdPartyKnowledgeBaseUrl: 'https://demo.invalid',
- thirdPartyKnowledgeApiPath: '/api/v1/knowledge/search',
- thirdPartyKnowledgeAuthType: 'none',
- thirdPartyKnowledgeBaseId: `equipment-${runPrefix.toLowerCase()}`.slice(0, 160),
- thirdPartyKnowledgeTimeoutSeconds: 17,
- thirdPartyKnowledgeKeyConfigured: snapshot.thirdPartyKnowledgeKeyConfigured,
- }
-
- let restored = false
- try {
- await loginAsAdmin(page)
- await page.goto('/settings/integrations')
- await expect(page.getByRole('heading', { name: '三方配置' })).toBeVisible()
- await expect(page.locator('.management-loading')).toHaveCount(0, { timeout: 15_000 })
-
- await page.locator('input[type="radio"][value="local"]').check()
- const enabled = page.locator('.integration-enable-row input[type="checkbox"]')
- if (await enabled.isChecked()) await page.locator('.integration-enable-row .el-switch').click()
- await fillLabel(page, '服务名称', testSettings.thirdPartyKnowledgeName)
- await page.locator('.integration-advanced-settings > summary').click()
- await fillLabel(page, '知识库标识', testSettings.thirdPartyKnowledgeBaseId)
- await fillLabel(page, '服务地址', testSettings.thirdPartyKnowledgeBaseUrl)
- await fillLabel(page, '检索接口路径', testSettings.thirdPartyKnowledgeApiPath)
- await fillLabel(page, '请求超时', String(testSettings.thirdPartyKnowledgeTimeoutSeconds))
- await page.locator('label').filter({ hasText: '鉴权方式' }).locator('select').selectOption('none')
-
- await page.getByRole('button', { name: '检查格式' }).click()
- await expect(page.getByText('配置格式检查通过,可保存后进入接口联调', { exact: true }).last()).toBeVisible()
- await recordScreenshot(page, testInfo, '三方配置-01-页面填写与格式检查')
-
- const savedResponsePromise = page.waitForResponse((response) => (
- response.request().method() === 'PUT' && response.url().includes('/api/v1/settings/general')
- ))
- await page.getByRole('button', { name: '保存配置' }).click()
- const savedResponse = await savedResponsePromise
- expect(savedResponse.status(), await savedResponse.text()).toBe(200)
- await expect(page.getByText('第三方知识库配置已保存', { exact: true }).last()).toBeVisible()
- activities.push({ module: '三方配置', action: '页面保存禁用的本地策略与安全占位地址', result: 'PASS', httpStatus: 200 })
-
- await page.reload()
- await expect(page.locator('.management-loading')).toHaveCount(0, { timeout: 15_000 })
- await expect(page.locator('label').filter({ hasText: '服务名称' }).locator('input')).toHaveValue(testName)
- await expect(page.locator('.integration-status-grid')).toContainText('平台本地知识库')
- await page.locator('.integration-advanced-settings > summary').click()
- await expect(page.locator('label').filter({ hasText: '服务地址' }).locator('input')).toHaveValue('https://demo.invalid')
- await expect(page.locator('label').filter({ hasText: '知识库标识' }).locator('input')).toHaveValue(testSettings.thirdPartyKnowledgeBaseId)
- activities.push({ module: '三方配置', action: '刷新页面后配置仍持久化', result: 'PASS' })
- await recordScreenshot(page, testInfo, '三方配置-02-刷新后持久化')
-
- const testResponsePromise = page.waitForResponse((response) => (
- response.request().method() === 'POST' && response.url().includes('/api/v1/settings/knowledge-provider/test')
- ))
- await page.getByRole('button', { name: '保存并测试连接' }).click()
- const testResponse = await testResponsePromise
- expect(testResponse.status(), '安全占位域名联调不得造成服务端 5xx').toBeLessThan(500)
- await expect(page.locator('.integration-test-result')).toBeVisible()
- activities.push({
- module: '三方配置',
- action: '页面保存并执行真实服务端连接测试',
- result: 'PASS',
- httpStatus: testResponse.status(),
- detail: 'demo.invalid 是不可达安全占位地址;预期呈现可解释失败态,不伪造第三方成功。',
- })
- await recordScreenshot(page, testInfo, '三方配置-03-真实联调结果')
- } finally {
- const restoreResponse = await request.put('/api/v1/settings/general', {
- headers,
- data: savePayload(snapshot),
- })
- if (restoreResponse.status() === 200) {
- restored = true
- activities.push({ module: '三方配置', action: '恢复测试前完整非敏感配置且保留原加密凭证', result: 'CLEANED', httpStatus: 200 })
- } else {
- activities.push({ module: '三方配置', action: '恢复测试前配置', result: 'CLEANUP_FAILED', httpStatus: restoreResponse.status(), detail: await restoreResponse.text() })
- }
- await attachJson(testInfo, '三方配置页面提交活动', activities)
- }
-
- expect(restored, '全局三方配置必须恢复,避免污染后续用例').toBeTruthy()
- const finalResponse = await request.get('/api/v1/settings/general', { headers })
- expect(finalResponse.status()).toBe(200)
- expect(normalize(await responseBody(finalResponse))).toEqual(snapshot)
- })
|