import test from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; const endpoint = process.env.CHAT_SYNC_RUNTIME_HTTP; const socketEndpoint = process.env.CHAT_SYNC_RUNTIME_SOCKET; const secondSocketEndpoint = process.env.CHAT_SYNC_RUNTIME_SOCKET_SECOND; test('local HTTP and WebSocket enforce disable, restore and credential revocation', { skip: !endpoint || !socketEndpoint, timeout: 30000, }, async () => { assert.match(endpoint, /^http:\/\/(127\.0\.0\.1|localhost):[0-9]+$/); assert.match(socketEndpoint, /^ws:\/\/(127\.0\.0\.1|localhost):[0-9]+$/); assert.equal(typeof WebSocket, 'function', 'Node 20 requires --experimental-websocket'); const source = await readFile(new URL('../../../../Friendship-and-chat-im/box-im-master/im-platform/src/main/java/com/bx/implatform/enums/third/ThirdKeyTypeEnum.java', import.meta.url), 'utf8'); const match = source.match(/\(\s*"10"\s*,\s*"[^"]*"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/); assert.ok(match, 'Fixture signing configuration must be available locally'); const [, key, secret] = match; async function third(path, data) { const body = JSON.stringify(data); const timestamp = String(Math.floor(Date.now() / 1000)); const sign = createHash('md5').update(key + secret + '10' + timestamp + body).digest('hex'); const response = await fetch(endpoint + path, { method: 'POST', body, headers: { 'Content-Type': 'application/json', 'APP-KEY': key, 'BIZ-TYPE': '10', TIMESTAMP: timestamp, SIGN: sign } }); return response.json(); } async function connect(token, target = socketEndpoint) { assert.match(target, /^ws:\/\/(127\.0\.0\.1|localhost):[0-9]+$/); const socket = new WebSocket(new URL('/im', target)); const packets = []; let notify; const first = new Promise(resolve => { notify = resolve; }); socket.addEventListener('message', message => { const packet = JSON.parse(message.data); packets.push(packet); notify(packet); }); const closed = new Promise(resolve => socket.addEventListener('close', resolve)); await new Promise((resolve, reject) => { socket.addEventListener('open', resolve, { once: true }); socket.addEventListener('error', reject, { once: true }); }); socket.send(JSON.stringify({ cmd: 0, data: { accessToken: token } })); return { socket, packets, first, closed }; } const current = await third('/third/user/getUserByUserId', { userId: 131 }); const version = Number(current.data?.syncVersion ?? 0) + 1; const profile = { userId: 131, userName: '老芋头', nickName: '老芋头', avatar: '', memberState: 0, syncVersion: version }; const synchronized = await third('/third/user/createOrUpdate', profile); assert.equal(synchronized.code, 200, synchronized.message); assert.equal(synchronized.data.appliedVersion, version); const initial = await third('/third/user/login', { userId: 131, terminal: 1 }); assert.equal(initial.code, 200); const old = await connect(initial.data.accessToken); const otherNode = secondSocketEndpoint ? await connect(initial.data.accessToken, secondSocketEndpoint) : null; try { assert.equal((await old.first).cmd, 0); if (otherNode) assert.equal((await otherNode.first).cmd, 0); assert.equal((await third('/third/user/createOrUpdate', { ...profile, memberState: 1, syncVersion: version + 1 })).code, 200); await old.closed; if (otherNode) await otherNode.closed; assert.ok(old.packets.some(packet => packet.cmd === 2 && packet.data.includes('禁用'))); const blocked = await fetch(endpoint + '/user/self', { headers: { accessToken: initial.data.accessToken } }).then(response => response.json()); assert.equal(blocked.code, 10005); const refresh = await fetch(endpoint + '/refreshToken', { method: 'PUT', headers: { refreshToken: initial.data.refreshToken } }).then(response => response.json()); assert.equal(refresh.code, 10005); assert.equal((await third('/third/user/createOrUpdate', { ...profile, syncVersion: version + 2 })).code, 200); const restored = await third('/third/user/login', { userId: 131, terminal: 1 }); assert.equal(restored.code, 200); assert.notEqual(restored.data.accessToken, initial.data.accessToken); const stale = await connect(initial.data.accessToken); assert.equal((await stale.first).cmd, 2); await stale.closed; const fresh = await connect(restored.data.accessToken); try { assert.equal((await fresh.first).cmd, 0); const revoked = await fetch(endpoint + '/user/self', { headers: { accessToken: initial.data.accessToken } }).then(response => response.json()); assert.equal(revoked.code, 401); const active = await fetch(endpoint + '/user/self', { headers: { accessToken: restored.data.accessToken } }).then(response => response.json()); assert.equal(active.code, 200); assert.equal(active.data.userName, 'u131'); const independent = await fetch(endpoint + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userName: 'testuser', password: 'Password123', terminal: 1 }) }).then(response => response.json()); assert.equal(independent.code, 10007); const missedTransition = await third('/third/user/createOrUpdate', { ...profile, syncVersion: version + 4, accessChangeVersion: version + 4, }); assert.equal(missedTransition.code, 200); const revokedAfterMissedDisable = await fetch(endpoint + '/user/self', { headers: { accessToken: restored.data.accessToken }, }).then(response => response.json()); assert.equal(revokedAfterMissedDisable.code, 401); fresh.socket.send(JSON.stringify({ cmd: 1, data: {} })); await fresh.closed; assert.ok(fresh.packets.some(packet => packet.cmd === 2)); } finally { fresh.socket.close(); } } finally { old.socket.close(); otherNode?.socket.close(); } });