Files
Friendship-and-chat-api/script/repair-im-users/test/runtime.test.mjs
T

110 lines
5.9 KiB
JavaScript

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();
}
});