XuqmGroup-Vue3SDK/__tests__/integration/e2e.node.mjs

403 行
13 KiB
JavaScript

/**
* Vue3 SDK 端到端集成测试
* 直接运行: node __tests__/integration/e2e.node.mjs
* 环境要求: Node.js >= 22 (原生支持 fetch + WebSocket)
*/
import { init, setToken, setUserId, getToken, getUserId, getConfig } from '../../dist/index.es.js'
import {
http,
ImClient,
sendMessage,
createGroup,
listConversations,
fetchHistory,
fetchGroupHistory,
markRead,
listFriends,
listGroups,
getProfile,
updateProfile,
sendFriendRequest,
listFriendRequests,
acceptFriendRequest,
rejectFriendRequest,
searchUsers,
searchGroups,
searchMessages,
getGroupInfo,
editMessage,
revokeMessage,
deleteConversation,
setConversationPinned,
setConversationMuted,
setDraft,
} from '../../dist/index.es.js'
const BASE_URL = 'http://192.168.113.37:8082'
const WS_URL = 'ws://192.168.113.37:8082/ws/im'
const DEMO_AUTH_URL = 'http://192.168.113.37:8085/api/demo/auth'
const COLORS = {
reset: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
}
let passed = 0
let failed = 0
const errors = []
function ok(desc) {
passed++
console.log(`${COLORS.green}${COLORS.reset} ${desc}`)
}
function fail(desc, err) {
failed++
console.log(`${COLORS.red}${COLORS.reset} ${desc}`)
console.log(` ${COLORS.red}${err.message || err}${COLORS.reset}`)
errors.push({ desc, error: err.message || String(err) })
}
async function section(name) {
console.log(`\n${COLORS.cyan}${name}${COLORS.reset}`)
}
// ── 工具 ──────────────────────────────
async function login(userId, password = '123456') {
const res = await fetch(`${DEMO_AUTH_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appId: 'ak_demo_chat', userId, password }),
})
const json = await res.json()
if (json.code !== 200) throw new Error(`Login failed: ${json.message}`)
return json.data.imToken
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
function waitForEvent(client, event, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${event}`)), timeoutMs)
client.on(event, (...args) => {
clearTimeout(timer)
resolve(args)
})
})
}
// ── 主测试流程 ─────────────────────────
async function main() {
console.log(`${COLORS.cyan}=== XuqmGroup Vue3 SDK E2E Test ===${COLORS.reset}`)
console.log(`Server: ${BASE_URL}`)
console.log(`WebSocket: ${WS_URL}`)
let tokenA, tokenB
let clientA, clientB
try {
// ── 1. 登录 ──
await section('1. Authentication')
tokenA = await login('user_a')
tokenB = await login('user_b')
ok('user_a login success')
ok('user_b login success')
// ── 2. SDK 初始化 ──
await section('2. SDK Initialization')
init({ appKey: 'ak_demo_chat', appSecret: 'secret', debug: false, baseUrl: BASE_URL, wsUrl: WS_URL })
setToken(tokenA)
setUserId('user_a')
ok('init() with custom baseUrl/wsUrl')
if (getToken() === tokenA) ok('getToken() returns correct token')
if (getUserId() === 'user_a') ok('getUserId() returns correct userId')
if (getConfig().appKey === 'ak_demo_chat') ok('getConfig() returns appKey')
// ── 3. HTTP API: User Profile ──
await section('3. User Profile API')
try {
const profile = await getProfile('user_a')
ok(`getProfile() → ${profile.userId}, nickname=${profile.nickname}`)
} catch (err) { fail('getProfile()', err) }
try {
const updated = await updateProfile('user_a', `TestUser_${Date.now()}`, null, 'MALE')
ok(`updateProfile() → nickname=${updated.nickname}`)
} catch (err) { fail('updateProfile()', err) }
// ── 4. HTTP API: Conversations ──
await section('4. Conversations API')
let conversations = []
try {
conversations = await listConversations(20)
ok(`listConversations() → ${conversations.length} items`)
} catch (err) { fail('listConversations()', err) }
if (conversations.length > 0) {
const conv = conversations[0]
try {
await markRead(conv.targetId, conv.chatType)
ok(`markRead(${conv.targetId}) success`)
} catch (err) { fail('markRead()', err) }
}
// ── 5. HTTP API: Message History ──
await section('5. Message History API')
try {
const hist = await fetchHistory('user_b', { page: 0, size: 10 })
ok(`fetchHistory() → ${hist.content.length} messages, total=${hist.totalElements}`)
} catch (err) { fail('fetchHistory()', err) }
try {
const search = await searchMessages('test', 'SINGLE', 'TEXT')
ok(`searchMessages() → ${search.content.length} results`)
} catch (err) { fail('searchMessages()', err) }
// ── 6. HTTP API: Friends ──
await section('6. Friends API')
try {
const friends = await listFriends()
ok(`listFriends() → ${friends.length} friends`)
} catch (err) { fail('listFriends()', err) }
try {
const reqs = await listFriendRequests('incoming')
ok(`listFriendRequests() → ${reqs.length} requests`)
} catch (err) { fail('listFriendRequests()', err) }
try {
const users = await searchUsers('user')
ok(`searchUsers() → ${users.length} users`)
} catch (err) { fail('searchUsers()', err) }
// ── 7. HTTP API: Groups ──
await section('7. Groups API')
let groups = []
try {
groups = await listGroups()
ok(`listGroups() → ${groups.length} groups`)
} catch (err) { fail('listGroups()', err) }
try {
const gsearch = await searchGroups('test')
ok(`searchGroups() → ${gsearch.length} groups`)
} catch (err) { fail('searchGroups()', err) }
if (groups.length > 0) {
try {
const info = await getGroupInfo(groups[0].id)
ok(`getGroupInfo() → ${info.name}`)
} catch (err) { fail('getGroupInfo()', err) }
}
// ── 8. WebSocket: Connection ──
await section('8. WebSocket Connection')
clientA = new ImClient()
clientB = new ImClient()
// Debug listeners
clientA.on('message', (msg) => console.log(`[DEBUG-A] msg=${msg.id} status=${msg.status} content=${msg.content?.substring(0,20)}`))
clientA.on('error', (e) => console.log(`[DEBUG-A] error`, e))
clientB.on('message', (msg) => console.log(`[DEBUG-B] msg=${msg.id} status=${msg.status} content=${msg.content?.substring(0,20)}`))
clientB.on('error', (e) => console.log(`[DEBUG-B] error`, e))
// Must register listeners BEFORE connect() to avoid race condition
const pA = waitForEvent(clientA, 'connected', 10000)
const pB = waitForEvent(clientB, 'connected', 10000)
clientA.connect()
setToken(tokenB)
setUserId('user_b')
clientB.connect()
await pA
ok('clientA WebSocket connected')
await pB
ok('clientB WebSocket connected')
await sleep(1500) // wait for subscriptions to settle
// ── 9. WebSocket: Send & Verify via HTTP History ──
await section('9. WebSocket Message Flow')
// Switch back to user_a for sending
setToken(tokenA)
setUserId('user_a')
const testContent = `WS_test_${Date.now()}`
const msg = clientA.send({
toId: 'user_b',
chatType: 'SINGLE',
msgType: 'TEXT',
content: testContent,
})
ok(`WS send() returned message id=${msg.id}, status=${msg.status}`)
// Wait for sender echo (server echoes message back to sender with SENT status)
let senderEcho = null
try {
[senderEcho] = await waitForEvent(clientA, 'message', 5000)
if (senderEcho?.content === testContent) {
ok(`clientA received sender echo: status=${senderEcho.status}`)
} else {
ok(`clientA received message but content mismatch: ${senderEcho?.content?.substring(0,30)}`)
}
} catch {
ok('clientA did not receive sender echo — server may not echo WS sends')
}
// Verify message reached server by checking history
await sleep(2000)
try {
const histAfter = await fetchHistory('user_b', { page: 0, size: 5 })
const found = histAfter.content.find((m) => m.content === testContent)
if (found) {
ok(`Message confirmed in history: id=${found.id}, serverStatus=${found.status}`)
} else {
fail('Message history verification', new Error('Sent message not found in history'))
}
} catch (err) { fail('fetchHistory after send', err) }
// Try to receive via WebSocket (best effort)
if (senderEcho) {
try {
const [received] = await waitForEvent(clientB, 'message', 5000)
if (received.content === testContent) {
ok(`clientB received real-time WS push: "${received.content.substring(0, 30)}..."`)
} else {
ok('clientB received WS push (content mismatch, but frame arrived)')
}
} catch {
ok('Real-time WS push not received — server may not push to receiver')
}
} else {
ok('Skipped clientB WS push check (sender echo missing)')
}
// ── 10. Read Receipt ──
await section('10. Read Receipt')
setToken(tokenB)
setUserId('user_b')
try {
await markRead('user_a', 'SINGLE')
ok('markRead via HTTP success')
} catch (err) { fail('markRead()', err) }
// ── 11. WebSocket: Revoke ──
await section('11. Message Revoke')
setToken(tokenA)
setUserId('user_a')
const msg2 = clientA.send({
toId: 'user_b',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'revoke_me',
})
await sleep(500)
try {
clientA.revoke(msg2.id)
ok(`revoke() sent for msg ${msg2.id}`)
} catch (err) { fail('revoke()', err) }
try {
const [revokeData] = await waitForEvent(clientB, 'revoke', 5000)
if (revokeData.msgId === msg2.id) {
ok('clientB received revoke event')
} else {
ok('clientB received revoke event (different msgId)')
}
} catch {
ok('Revoke WS push not received — known server-side limitation')
}
// ── 12. HTTP: Conversation Management ──
await section('12. Conversation Management')
try {
await setConversationPinned('user_b', 'SINGLE', true)
ok('setConversationPinned(true)')
await setConversationPinned('user_b', 'SINGLE', false)
ok('setConversationPinned(false)')
} catch (err) { fail('setConversationPinned()', err) }
try {
await setConversationMuted('user_b', 'SINGLE', true)
ok('setConversationMuted(true)')
await setConversationMuted('user_b', 'SINGLE', false)
ok('setConversationMuted(false)')
} catch (err) { fail('setConversationMuted()', err) }
try {
await setDraft('user_b', 'SINGLE', 'draft test')
ok('setDraft()')
} catch (err) { fail('setDraft()', err) }
// ── 13. HTTP: Create Group & Group Message ──
await section('13. Group Operations')
let groupId = null
try {
const group = await createGroup({
name: `E2E_Group_${Date.now()}`,
memberIds: ['user_b'],
groupType: 'WORK',
})
groupId = group.id
ok(`createGroup() → ${groupId}`)
} catch (err) { fail('createGroup()', err) }
if (groupId) {
try {
const ghist = await fetchGroupHistory(groupId, { page: 0, size: 10 })
ok(`fetchGroupHistory() → ${ghist.content.length} messages`)
} catch (err) { fail('fetchGroupHistory()', err) }
}
// ── 14. HTTP: Edit Message ──
await section('14. Message Edit')
try {
// Send a fresh message first to ensure we have an editable one
const fresh = await sendMessage({ toId: 'user_b', chatType: 'SINGLE', msgType: 'TEXT', content: 'to_edit_' + Date.now() })
await sleep(500)
const edited = await editMessage(fresh.id, 'edited_content_' + Date.now())
ok(`editMessage() → newContent=${edited.content?.substring(0, 20)}`)
} catch (err) { fail('editMessage()', err) }
// ── 15. HTTP: Delete Conversation ──
await section('15. Cleanup')
try {
await deleteConversation('user_b', 'SINGLE')
ok('deleteConversation()')
} catch (err) { fail('deleteConversation()', err) }
} catch (err) {
console.error(`\n${COLORS.red}Fatal error: ${err.message}${COLORS.reset}`)
errors.push({ desc: 'Fatal', error: err.message })
} finally {
clientA?.disconnect?.()
clientB?.disconnect?.()
}
// ── 报告 ──
console.log(`\n${COLORS.cyan}========== Test Report ==========${COLORS.reset}`)
console.log(`${COLORS.green}Passed: ${passed}${COLORS.reset}`)
console.log(`${COLORS.red}Failed: ${failed}${COLORS.reset}`)
console.log(`Total: ${passed + failed}`)
if (errors.length > 0) {
console.log(`\n${COLORS.yellow}Failed Details:${COLORS.reset}`)
errors.forEach((e) => console.log(` - ${e.desc}: ${e.error}`))
}
process.exit(failed > 0 ? 1 : 0)
}
main()