Files
admin e6956aa001 Initial commit: TaskPool React panel
- React frontend with route-level code splitting
- Backend rebranded from Baihu to TaskPool
- DB brand migration script and local compatibility
2026-07-26 08:43:52 +08:00

201 lines
7.0 KiB
JavaScript

/**
* 本地后端端到端联调:通知 / SSE / 互联
* 用法: node scripts/e2e-check.mjs [baseUrl] [user] [pass]
*/
import fs from 'node:fs'
import path from 'node:path'
const base = process.argv[2] || 'http://127.0.0.1:8052'
const username = process.argv[3] || 'admin'
const password = process.argv[4] || 'admin123'
const cookieJar = path.join(process.cwd(), 'cookies-e2e.txt')
const results = []
function log(name, ok, detail = '') {
results.push({ name, ok, detail })
const mark = ok ? 'PASS' : 'FAIL'
console.log(`[${mark}] ${name}${detail ? ' - ' + detail : ''}`)
}
function parseSetCookie(res) {
// undici/node fetch: getSetCookie if available
const list =
typeof res.headers.getSetCookie === 'function'
? res.headers.getSetCookie()
: (() => {
const single = res.headers.get('set-cookie')
return single ? [single] : []
})()
const map = new Map()
if (fs.existsSync(cookieJar)) {
for (const line of fs.readFileSync(cookieJar, 'utf8').split(/\r?\n/)) {
if (!line || line.startsWith('#')) continue
const parts = line.split('\t')
if (parts.length >= 7) map.set(parts[5], parts[6])
}
}
for (const c of list) {
const [pair] = c.split(';')
const eq = pair.indexOf('=')
if (eq > 0) map.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim())
}
const lines = ['# Netscape HTTP Cookie File', '']
for (const [k, v] of map) {
lines.push(`127.0.0.1\tFALSE\t/\tFALSE\t0\t${k}\t${v}`)
}
fs.writeFileSync(cookieJar, lines.join('\n'))
return [...map.entries()].map(([k, v]) => `${k}=${v}`).join('; ')
}
function loadCookieHeader() {
if (!fs.existsSync(cookieJar)) return ''
const pairs = []
for (const line of fs.readFileSync(cookieJar, 'utf8').split(/\r?\n/)) {
if (!line || line.startsWith('#')) continue
const parts = line.split('\t')
if (parts.length >= 7) pairs.push(`${parts[5]}=${parts[6]}`)
}
return pairs.join('; ')
}
async function api(method, p, body) {
const headers = { Accept: 'application/json' }
const cookie = loadCookieHeader()
if (cookie) headers.Cookie = cookie
if (body !== undefined) headers['Content-Type'] = 'application/json'
const res = await fetch(`${base}${p}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
})
parseSetCookie(res)
const text = await res.text()
let json
try {
json = JSON.parse(text)
} catch {
json = { raw: text }
}
return { status: res.status, json, headers: res.headers }
}
async function readSSE(p, ms = 2000) {
const cookie = loadCookieHeader()
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(), ms)
try {
const res = await fetch(`${base}${p}`, {
headers: { Accept: 'text/event-stream', Cookie: cookie },
signal: ctrl.signal,
})
const reader = res.body?.getReader()
if (!reader) return { status: res.status, text: '' }
const dec = new TextDecoder()
let text = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
text += dec.decode(value, { stream: true })
if (text.includes('data:')) break
}
reader.cancel().catch(() => {})
return { status: res.status, text }
} catch (e) {
return { status: 0, text: String(e?.message || e) }
} finally {
clearTimeout(timer)
}
}
async function main() {
console.log(`E2E base=${base}`)
// login
const login = await api('POST', '/api/v1/auth/login', { username, password })
log('auth.login', login.status === 200 && login.json?.code === 200, JSON.stringify(login.json?.msg || login.json))
const me = await api('GET', '/api/v1/auth/me')
log('auth.me', me.status === 200 && me.json?.code === 200, JSON.stringify(me.json?.data || me.json?.msg))
// notify
const types = await api('GET', '/api/v1/notify/types')
const typeList = types.json?.data?.channel_types || []
log('notify.types', types.status === 200 && typeList.length > 0, `count=${typeList.length}`)
const channelBody = {
name: 'e2e-custom',
type: 'Custom',
enabled: true,
config: {
webhook: 'http://127.0.0.1:9/hook',
body: '{"text":"TEXT"}',
},
}
const saveCh = await api('POST', '/api/v1/notify/channels', channelBody)
log('notify.saveChannel', saveCh.status === 200 && saveCh.json?.code === 200, JSON.stringify(saveCh.json?.msg || saveCh.json))
const channels = await api('GET', '/api/v1/notify/channels')
const chList = Array.isArray(channels.json?.data) ? channels.json.data : []
log('notify.listChannels', channels.status === 200 && channels.json?.code === 200, `count=${chList.length}`)
const testCh = await api('POST', '/api/v1/notify/channels/test', channelBody)
// 预期连接失败但接口应返回 200 + result 结构
log(
'notify.testChannel',
testCh.status === 200 && testCh.json?.code === 200,
JSON.stringify(testCh.json?.data || testCh.json?.msg),
)
const settingsNotify = await api('GET', '/api/v1/settings/notify')
const token = settingsNotify.json?.data?.token || settingsNotify.json?.data?.notify_token || ''
log('settings.notify', settingsNotify.status === 200 && settingsNotify.json?.code === 200, token ? `token=${token.slice(0, 8)}...` : 'no token')
// interconnect
const nodeBody = {
name: `e2e-node-${Date.now()}`,
url: 'http://127.0.0.1:18052',
token: 'test-token',
remark: 'e2e',
}
const createNode = await api('POST', '/api/v1/interconnect/nodes', nodeBody)
const nodeId = createNode.json?.data?.id
log('interconnect.createNode', createNode.status === 200 && !!nodeId, nodeId || JSON.stringify(createNode.json))
const nodes = await api('GET', '/api/v1/interconnect/nodes')
const nodeList = Array.isArray(nodes.json?.data) ? nodes.json.data : []
log('interconnect.listNodes', nodes.status === 200 && nodeList.length > 0, `count=${nodeList.length}`)
const child = await api('GET', '/api/v1/interconnect/child/status')
log('interconnect.childStatus', child.status === 200 && child.json?.code === 200, JSON.stringify(child.json?.data))
if (nodeId) {
const st = await api('GET', `/api/v1/interconnect/nodes/${nodeId}/status`)
log('interconnect.nodeStatus', st.status === 200, JSON.stringify(st.json?.data || st.json?.msg))
}
// monitor SSE
const mon = await readSSE('/api/v1/monitor/sse', 2500)
log('monitor.sse', mon.text.includes('data:') || mon.text.includes('cpu') || mon.text.includes('host'), mon.text.slice(0, 120).replace(/\s+/g, ' '))
// logs SSE param validation
const bad = await api('GET', '/api/v1/logs/sse?id=test')
log('logs.sse.paramGuard', bad.status === 400 || bad.json?.error || bad.json?.msg, JSON.stringify(bad.json))
// stats
const stats = await api('GET', '/api/v1/stats')
log('dashboard.stats', stats.status === 200 && stats.json?.code === 200, JSON.stringify(stats.json?.data))
const failed = results.filter((r) => !r.ok)
console.log('\n==== summary ====')
console.log(`total=${results.length} pass=${results.length - failed.length} fail=${failed.length}`)
if (failed.length) {
process.exitCode = 1
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})