Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@
"invalid": "Invalid",
"valid": "Valid",
"cas_settings": "CAS Settings",
"cas_connection_success": "CAS login endpoint is reachable. Sign in to verify the callback and account.",
"callback_domain_name": "Callback Domain",
"callback_domain_name_error": "Callback URL must match the current access domain",
"field_mapping": "User Attribute Mapping",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@
"invalid": "유효하지 않음",
"valid": "유효함",
"cas_settings": "CAS 설정",
"cas_connection_success": "CAS 로그인 엔드포인트에 연결할 수 있습니다. 실제 로그인으로 콜백과 계정을 확인하세요.",
"callback_domain_name": "콜백 도메인",
"callback_domain_name_error": "콜백 URL은 현재 접속 도메인과 일치해야 합니다",
"field_mapping": "사용자 속성 매핑",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/th-TH.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@
"invalid": "ไม่ถูกต้อง",
"valid": "ถูกต้อง",
"cas_settings": "การตั้งค่า CAS",
"cas_connection_success": "เข้าถึงปลายทางเข้าสู่ระบบ CAS ได้ โปรดเข้าสู่ระบบจริงเพื่อตรวจสอบ callback และบัญชี",
"callback_domain_name": "ที่อยู่เรียกกลับ",
"callback_domain_name_error": "ที่อยู่เรียกกลับต้องเป็นโดเมนที่ใช้เข้าถึงอยู่ในปัจจุบัน",
"field_mapping": "การจับคู่คุณสมบัติผู้ใช้",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@
"invalid": "无效",
"valid": "有效",
"cas_settings": "CAS 设置",
"cas_connection_success": "CAS 登录端点可达,请通过实际登录验证回调及账号。",
"callback_domain_name": "回调地址",
"callback_domain_name_error": "回调地址必须是当前访问域名",
"field_mapping": "用户属性映射",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@
"invalid": "無效",
"valid": "有效",
"cas_settings": "CAS 設定",
"cas_connection_success": "CAS 登入端點可達,請透過實際登入驗證回呼及帳號。",
"callback_domain_name": "回調位址",
"callback_domain_name_error": "回調位址必須是目前的存取網域名稱",
"field_mapping": "使用者屬性對應",
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/views/system/authentication/CasEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const validateCbUrl = (rule, value, callback) => {
const addr = getSQLBotAddr()
if (value === addr || `${value}/` === addr) {
callback()
return
}
callback(new Error(t('authentication.callback_domain_name_error')))
}
Expand Down Expand Up @@ -187,7 +188,7 @@ const validate = () => {
.patch(url, data)
.then((res) => {
if (res) {
ElMessage.success(t('ds.connection_success'))
ElMessage.success(t('authentication.cas_connection_success'))
} else {
ElMessage.error(t('ds.connection_failed'))
}
Expand Down
13 changes: 13 additions & 0 deletions tests/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,16 @@ npm --prefix tests/browser test -- --config=welcome-language.config.mjs
不访问业务数据库或 LLM,不替代完整 SDK 握手、真实鉴权和持久化验收。
后端 `tests/test_assistant_welcome_defaults.py` 提取真实 Schema 和保存函数,验证空字符串覆盖、
省略字段保留、会话提交和缓存失效;它同样隔离鉴权与数据库。

## CAS 地址与编辑组件回归

```sh
npm --prefix tests/browser test -- --config=cas.config.mjs
```

复用已安装的 frontend 依赖,独立 Vite 夹具仅监听 `127.0.0.1:4219`,加载真实
`CasEditor.vue`。HTTP 请求边界替换为内存记录,不访问或修改认证源配置。覆盖根回调
有无尾斜杠、历史 IdpUri 提交、错误回调拒绝、校验回调仅执行一次及连接检查提示。
组件库只采纳首次校验结果,因此用原始校验函数的调用计数补充 DOM 提交断言。
这不验证真实持久化、客户 CAS 服务或最终用户登录;后端地址、编码和票据回归在
独立 xpack 仓库的 `tests/test_cas_urls.py`。
8 changes: 8 additions & 0 deletions tests/browser/cas.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from '@playwright/test'
import base from './playwright.config.mjs'
export default defineConfig({
...base,
webServer: { command: 'node fixtures/cas/server.mjs', url: 'http://127.0.0.1:4219', reuseExistingServer: false, timeout: 30000 },
projects: [{ name: 'cas', testMatch: 'cas.spec.mjs', use: { baseURL: 'http://127.0.0.1:4219' } }],
outputDir: 'output/cas-results'
})
9 changes: 9 additions & 0 deletions tests/browser/fixtures/cas/bridge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Replace only the request boundary; no actual authentication configuration is changed.
export const getSQLBotAddr = () => location.origin + location.pathname
window.savedConfigs = []
export const request = {
get: async () => null,
patch: async () => true,
post: async (url, data) => { window.savedConfigs.push(data); return 1 },
put: async (url, data) => { window.savedConfigs.push(data); return 1 },
}
1 change: 1 addition & 0 deletions tests/browser/fixtures/cas/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="app"></div><script type="module" src="/main.mjs"></script>
11 changes: 11 additions & 0 deletions tests/browser/fixtures/cas/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createApp, h, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import ElementPlus from 'element-plus-secondary'
import 'element-plus-secondary/dist/index.css'
import Cas from 'cas-form'
import zh from '@/i18n/zh-CN.json'
import en from '@/i18n/en.json'
const form = ref()
createApp({ render: () => h('div', [h(Cas, { ref: form }),
h('button', { onClick: () => form.value.edit() }, 'Open CAS')
]) }).use(ElementPlus).use(createI18n({ legacy: false, locale: 'zh-CN', messages: { 'zh-CN': zh, en } })).mount('#app')
22 changes: 22 additions & 0 deletions tests/browser/fixtures/cas/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const root = fileURLToPath(new URL('.', import.meta.url))
const frontend = path.resolve(root, '../../../../frontend')
const require = createRequire(path.join(frontend, 'package.json'))
const { createServer } = await import(require.resolve('vite'))
const { default: vue } = await import(require.resolve('@vitejs/plugin-vue'))
const bridge = path.join(root, 'bridge.mjs')
const server = await createServer({
configFile: false, root, cacheDir: '../../output/cas-vite', plugins: [vue()],
resolve: { alias: [
...['@/stores/assistant', '@/utils/useCache', '@/utils/utils', '@/utils/request', 'vue-router'].map(find => ({ find, replacement: bridge })),
{ find: 'cas-form', replacement: path.join(frontend, 'src/views/system/authentication/CasEditor.vue') },
{ find: '@', replacement: path.join(frontend, 'src') },
...['vue', 'vue-i18n', 'element-plus-secondary', 'axios', 'json-bigint'].map(name => ({ find: name, replacement: path.join(frontend, 'node_modules', name) }))
] },
define: { 'import.meta.env.VITE_API_BASE_URL': JSON.stringify('/api/v1') },
server: { host: '127.0.0.1', port: 4219, strictPort: true, fs: { allow: [root, frontend] } }
})
await server.listen()
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, async () => { await server.close(); process.exit() })
53 changes: 53 additions & 0 deletions tests/browser/specs/cas.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { test, expect } from '@playwright/test'
import fs from 'node:fs'

test('valid callback invokes validation exactly once', async ({ page }) => {
// The async-validator library settles on the first callback; count all calls explicitly.
const source = fs.readFileSync(new URL('../../../frontend/src/views/system/authentication/CasEditor.vue', import.meta.url), 'utf8')
const body = source.match(/const validateCbUrl = \(rule, value, callback\) => \{([\s\S]*?)\n\}/)[1]
const results = await page.evaluate(body => {
const validate = new Function('rule', 'value', 'callback', 'getSQLBotAddr', 't', body)
return ['https://app.example.invalid', 'https://app.example.invalid/', 'https://wrong.example.invalid/'].map(value => {
const calls = []
validate(null, value, error => calls.push(error ? 'error' : 'success'), () => 'https://app.example.invalid/', key => key)
return calls
})
}, body)
expect(results).toEqual([['success'], ['success'], ['error']])
})

for (const suffix of ['', '/']) {
test(`saves historical root callback with suffix ${JSON.stringify(suffix)}`, async ({ page }) => {
await page.goto('/')
await page.getByRole('button', { name: 'Open CAS' }).click()
const fields = page.getByRole('textbox')
await fields.nth(0).fill('https://cas.example.invalid/cas/login/')
await fields.nth(1).fill(`http://127.0.0.1:4219${suffix}`)
await fields.nth(2).fill('{}')
await page.getByRole('button', { name: '保存', exact: true }).click()
await expect.poll(() => page.evaluate(() => window.savedConfigs.length)).toBe(1)
const config = await page.evaluate(() => JSON.parse(window.savedConfigs[0].config))
expect(config.casCallbackDomain).toBe(`http://127.0.0.1:4219${suffix}`)
expect(config.idpUri).toBe('https://cas.example.invalid/cas/login/')
})
}

test('rejects another callback origin', async ({ page }) => {
await page.goto('/')
await page.getByRole('button', { name: 'Open CAS' }).click()
const fields = page.getByRole('textbox')
await fields.nth(0).fill('https://cas.example.invalid/cas')
await fields.nth(1).fill('https://wrong.example.invalid/')
await fields.nth(2).fill('{}')
await page.getByRole('button', { name: '保存', exact: true }).click()
await expect(page.locator('.ed-form-item__error')).toBeVisible()
expect(await page.evaluate(() => window.savedConfigs)).toEqual([])
})

test('connection success explains its limited scope', async ({ page }) => {
await page.goto('/')
await page.getByRole('button', { name: 'Open CAS' }).click()
await page.getByRole('textbox').nth(0).fill('https://cas.example.invalid/cas')
await page.getByRole('button', { name: '测试连接', exact: true }).click()
await expect(page.getByText('CAS 登录端点可达,请通过实际登录验证回调及账号。')).toBeVisible()
})
Loading