Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

172 linhas
8.6 KiB

  1. import type { APIResponse, TestInfo } from '@playwright/test'
  2. import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
  3. import { adminAccessToken, envelopeData, loginAsAdmin } from './helpers'
  4. type JsonRecord = Record<string, unknown>
  5. interface IntegrationSnapshot {
  6. knowledgeProviderMode: 'local' | 'hybrid' | 'third_party'
  7. thirdPartyKnowledgeEnabled: boolean
  8. thirdPartyKnowledgeName: string
  9. thirdPartyKnowledgeBaseUrl: string
  10. thirdPartyKnowledgeApiPath: string
  11. thirdPartyKnowledgeAuthType: 'none' | 'bearer' | 'api_key'
  12. thirdPartyKnowledgeBaseId: string
  13. thirdPartyKnowledgeTimeoutSeconds: number
  14. thirdPartyKnowledgeKeyConfigured: boolean
  15. }
  16. interface Activity {
  17. module: string
  18. action: string
  19. result: 'PASS' | 'CLEANED' | 'CLEANUP_FAILED'
  20. httpStatus?: number
  21. detail?: string
  22. }
  23. const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object' && !Array.isArray(value)
  24. ? value as JsonRecord
  25. : {}
  26. function normalize(value: unknown): IntegrationSnapshot {
  27. const root = asRecord(envelopeData(value))
  28. const mode = String(root.knowledgeProviderMode || 'local')
  29. const authType = String(root.thirdPartyKnowledgeAuthType || 'none')
  30. return {
  31. knowledgeProviderMode: mode === 'hybrid' || mode === 'third_party' ? mode : 'local',
  32. thirdPartyKnowledgeEnabled: Boolean(root.thirdPartyKnowledgeEnabled),
  33. thirdPartyKnowledgeName: String(root.thirdPartyKnowledgeName || ''),
  34. thirdPartyKnowledgeBaseUrl: String(root.thirdPartyKnowledgeBaseUrl || ''),
  35. thirdPartyKnowledgeApiPath: String(root.thirdPartyKnowledgeApiPath || '/api/v1/knowledge/search'),
  36. thirdPartyKnowledgeAuthType: authType === 'bearer' || authType === 'api_key' ? authType : 'none',
  37. thirdPartyKnowledgeBaseId: String(root.thirdPartyKnowledgeBaseId || ''),
  38. thirdPartyKnowledgeTimeoutSeconds: Number(root.thirdPartyKnowledgeTimeoutSeconds || 15),
  39. thirdPartyKnowledgeKeyConfigured: Boolean(root.thirdPartyKnowledgeKeyConfigured),
  40. }
  41. }
  42. function savePayload(value: IntegrationSnapshot) {
  43. return {
  44. ...value,
  45. // The API intentionally never returns the credential. An empty value
  46. // means keep the existing encrypted secret during both test and restore.
  47. thirdPartyKnowledgeApiKey: '',
  48. }
  49. }
  50. async function responseBody(response: APIResponse) {
  51. return response.json().catch(() => ({}))
  52. }
  53. async function fillLabel(page: import('@playwright/test').Page, label: string, value: string) {
  54. const field = page.locator('label').filter({ hasText: label }).first()
  55. const input = field.locator('input:not([type="hidden"]), textarea').first()
  56. await expect(input, `应显示字段:${label}`).toBeVisible()
  57. await input.fill(value)
  58. }
  59. async function recordScreenshot(
  60. page: import('@playwright/test').Page,
  61. testInfo: TestInfo,
  62. label: string,
  63. ) {
  64. await captureScreenshot(page, testInfo, label, [page.locator('input[type="password"]')])
  65. }
  66. test('三方配置通过页面保存、格式检查、联调失败态与刷新持久化,并精确恢复原配置', async ({ page, request }, testInfo) => {
  67. const activities: Activity[] = []
  68. const token = await adminAccessToken(request)
  69. const headers = { Authorization: `Bearer ${token}` }
  70. const initialResponse = await request.get('/api/v1/settings/general', { headers })
  71. expect(initialResponse.status(), '读取三方配置快照').toBe(200)
  72. const snapshot = normalize(await responseBody(initialResponse))
  73. const testName = `${runPrefix}-装备知识中台`.slice(0, 80)
  74. const testSettings: IntegrationSnapshot = {
  75. knowledgeProviderMode: 'local',
  76. thirdPartyKnowledgeEnabled: false,
  77. thirdPartyKnowledgeName: testName,
  78. thirdPartyKnowledgeBaseUrl: 'https://demo.invalid',
  79. thirdPartyKnowledgeApiPath: '/api/v1/knowledge/search',
  80. thirdPartyKnowledgeAuthType: 'none',
  81. thirdPartyKnowledgeBaseId: `equipment-${runPrefix.toLowerCase()}`.slice(0, 160),
  82. thirdPartyKnowledgeTimeoutSeconds: 17,
  83. thirdPartyKnowledgeKeyConfigured: snapshot.thirdPartyKnowledgeKeyConfigured,
  84. }
  85. let restored = false
  86. try {
  87. await loginAsAdmin(page)
  88. await page.goto('/settings/integrations')
  89. await expect(page.getByRole('heading', { name: '三方配置' })).toBeVisible()
  90. await expect(page.locator('.management-loading')).toHaveCount(0, { timeout: 15_000 })
  91. await page.locator('input[type="radio"][value="local"]').check()
  92. const enabled = page.locator('.integration-enable-row input[type="checkbox"]')
  93. if (await enabled.isChecked()) await page.locator('.integration-enable-row .el-switch').click()
  94. await fillLabel(page, '服务名称', testSettings.thirdPartyKnowledgeName)
  95. await page.locator('.integration-advanced-settings > summary').click()
  96. await fillLabel(page, '知识库标识', testSettings.thirdPartyKnowledgeBaseId)
  97. await fillLabel(page, '服务地址', testSettings.thirdPartyKnowledgeBaseUrl)
  98. await fillLabel(page, '检索接口路径', testSettings.thirdPartyKnowledgeApiPath)
  99. await fillLabel(page, '请求超时', String(testSettings.thirdPartyKnowledgeTimeoutSeconds))
  100. await page.locator('label').filter({ hasText: '鉴权方式' }).locator('select').selectOption('none')
  101. await page.getByRole('button', { name: '检查格式' }).click()
  102. await expect(page.getByText('配置格式检查通过,可保存后进入接口联调', { exact: true }).last()).toBeVisible()
  103. await recordScreenshot(page, testInfo, '三方配置-01-页面填写与格式检查')
  104. const savedResponsePromise = page.waitForResponse((response) => (
  105. response.request().method() === 'PUT' && response.url().includes('/api/v1/settings/general')
  106. ))
  107. await page.getByRole('button', { name: '保存配置' }).click()
  108. const savedResponse = await savedResponsePromise
  109. expect(savedResponse.status(), await savedResponse.text()).toBe(200)
  110. await expect(page.getByText('第三方知识库配置已保存', { exact: true }).last()).toBeVisible()
  111. activities.push({ module: '三方配置', action: '页面保存禁用的本地策略与安全占位地址', result: 'PASS', httpStatus: 200 })
  112. await page.reload()
  113. await expect(page.locator('.management-loading')).toHaveCount(0, { timeout: 15_000 })
  114. await expect(page.locator('label').filter({ hasText: '服务名称' }).locator('input')).toHaveValue(testName)
  115. await expect(page.locator('.integration-status-grid')).toContainText('平台本地知识库')
  116. await page.locator('.integration-advanced-settings > summary').click()
  117. await expect(page.locator('label').filter({ hasText: '服务地址' }).locator('input')).toHaveValue('https://demo.invalid')
  118. await expect(page.locator('label').filter({ hasText: '知识库标识' }).locator('input')).toHaveValue(testSettings.thirdPartyKnowledgeBaseId)
  119. activities.push({ module: '三方配置', action: '刷新页面后配置仍持久化', result: 'PASS' })
  120. await recordScreenshot(page, testInfo, '三方配置-02-刷新后持久化')
  121. const testResponsePromise = page.waitForResponse((response) => (
  122. response.request().method() === 'POST' && response.url().includes('/api/v1/settings/knowledge-provider/test')
  123. ))
  124. await page.getByRole('button', { name: '保存并测试连接' }).click()
  125. const testResponse = await testResponsePromise
  126. expect(testResponse.status(), '安全占位域名联调不得造成服务端 5xx').toBeLessThan(500)
  127. await expect(page.locator('.integration-test-result')).toBeVisible()
  128. activities.push({
  129. module: '三方配置',
  130. action: '页面保存并执行真实服务端连接测试',
  131. result: 'PASS',
  132. httpStatus: testResponse.status(),
  133. detail: 'demo.invalid 是不可达安全占位地址;预期呈现可解释失败态,不伪造第三方成功。',
  134. })
  135. await recordScreenshot(page, testInfo, '三方配置-03-真实联调结果')
  136. } finally {
  137. const restoreResponse = await request.put('/api/v1/settings/general', {
  138. headers,
  139. data: savePayload(snapshot),
  140. })
  141. if (restoreResponse.status() === 200) {
  142. restored = true
  143. activities.push({ module: '三方配置', action: '恢复测试前完整非敏感配置且保留原加密凭证', result: 'CLEANED', httpStatus: 200 })
  144. } else {
  145. activities.push({ module: '三方配置', action: '恢复测试前配置', result: 'CLEANUP_FAILED', httpStatus: restoreResponse.status(), detail: await restoreResponse.text() })
  146. }
  147. await attachJson(testInfo, '三方配置页面提交活动', activities)
  148. }
  149. expect(restored, '全局三方配置必须恢复,避免污染后续用例').toBeTruthy()
  150. const finalResponse = await request.get('/api/v1/settings/general', { headers })
  151. expect(finalResponse.status()).toBe(200)
  152. expect(normalize(await responseBody(finalResponse))).toEqual(snapshot)
  153. })