/** * 本地后端端到端联调:通知 / 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) })