initial
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { AuthStateMachine } = require('../src/bot/auth');
|
||||
|
||||
test('AuthStateMachine: register success via translate', () => {
|
||||
const sm = new AuthStateMachine({});
|
||||
sm.markRegisterSent();
|
||||
sm.markAwaitRegister();
|
||||
const r = sm.consume({ text: '', translate: 'commands.register.success' });
|
||||
assert.strictEqual(r, 'auth_done');
|
||||
assert.strictEqual(sm.isDone(), true);
|
||||
});
|
||||
|
||||
test('AuthStateMachine: register success via phrase', () => {
|
||||
const sm = new AuthStateMachine({ register_success: ['successfully registered'] });
|
||||
sm.markRegisterSent();
|
||||
sm.markAwaitRegister();
|
||||
const r = sm.consume({ text: 'You have been successfully registered' });
|
||||
assert.strictEqual(r, 'auth_done');
|
||||
});
|
||||
|
||||
test('AuthStateMachine: already registered → login', () => {
|
||||
const sm = new AuthStateMachine({});
|
||||
const r = sm.consume({ text: 'You are already registered' });
|
||||
assert.strictEqual(r, 'login_sent');
|
||||
});
|
||||
|
||||
test('AuthStateMachine: login success', () => {
|
||||
const sm = new AuthStateMachine({});
|
||||
sm.markLoginSent();
|
||||
sm.markAwaitLogin();
|
||||
const r = sm.consume({ text: 'You have been logged in' });
|
||||
assert.strictEqual(r, 'auth_done');
|
||||
});
|
||||
|
||||
test('AuthStateMachine: out-of-order consume is no-op', () => {
|
||||
const sm = new AuthStateMachine({});
|
||||
sm.markRegisterSent();
|
||||
sm.markAwaitRegister();
|
||||
sm.consume({ text: 'You have been logged in' });
|
||||
assert.strictEqual(sm.state, 'AWAIT_REGISTER');
|
||||
});
|
||||
|
||||
test('AuthStateMachine: done consumes nothing', () => {
|
||||
const sm = new AuthStateMachine({});
|
||||
sm.markRegisterSent();
|
||||
sm.markAwaitRegister();
|
||||
sm.consume({ translate: 'commands.register.success' });
|
||||
const r = sm.consume({ translate: 'commands.register.success' });
|
||||
assert.strictEqual(r, null);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { parsePositionAndLook, parseChat, parseJoinGame } = require('../src/bot/codec');
|
||||
|
||||
test('parsePositionAndLook: full payload', () => {
|
||||
const data = Buffer.alloc(33);
|
||||
data.writeDoubleLE(1.5, 0);
|
||||
data.writeDoubleLE(64.0, 8);
|
||||
data.writeDoubleLE(-3.25, 16);
|
||||
data.writeFloatLE(45.0, 24);
|
||||
data.writeFloatLE(15.0, 28);
|
||||
data[32] = 0;
|
||||
const r = parsePositionAndLook(data);
|
||||
assert.ok(r);
|
||||
assert.strictEqual(r.x, 1.5);
|
||||
assert.strictEqual(r.y, 64.0);
|
||||
assert.strictEqual(r.z, -3.25);
|
||||
assert.strictEqual(r.yaw, 45.0);
|
||||
assert.strictEqual(r.pitch, 15.0);
|
||||
});
|
||||
|
||||
test('parsePositionAndLook: too short returns null', () => {
|
||||
const data = Buffer.alloc(10);
|
||||
assert.strictEqual(parsePositionAndLook(data), null);
|
||||
});
|
||||
|
||||
test('parseChat: simple text', () => {
|
||||
const json = JSON.stringify({ text: 'Hello world' });
|
||||
const len = Buffer.byteLength(json);
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([len]),
|
||||
Buffer.from(json, 'utf8'),
|
||||
]);
|
||||
const r = parseChat(data);
|
||||
assert.strictEqual(r.text, 'Hello world');
|
||||
});
|
||||
|
||||
test('parseChat: with translate', () => {
|
||||
const json = JSON.stringify({ translate: 'commands.register.success', with: ['alice'] });
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([Buffer.byteLength(json)]),
|
||||
Buffer.from(json, 'utf8'),
|
||||
]);
|
||||
const r = parseChat(data);
|
||||
assert.strictEqual(r.translate, 'commands.register.success');
|
||||
assert.ok(r.text.includes('commands.register.success'));
|
||||
});
|
||||
|
||||
test('parseChat: strips color codes', () => {
|
||||
const json = JSON.stringify({ text: '§aGreen§r text' });
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([Buffer.byteLength(json)]),
|
||||
Buffer.from(json, 'utf8'),
|
||||
]);
|
||||
const r = parseChat(data);
|
||||
assert.strictEqual(r.text, 'Green text');
|
||||
});
|
||||
|
||||
test('parseJoinGame: minimal', () => {
|
||||
const data = Buffer.alloc(10);
|
||||
data[0] = 1;
|
||||
data[1] = 0;
|
||||
data.writeInt32BE(0, 2);
|
||||
data[6] = 0;
|
||||
data[7] = 20;
|
||||
data[8] = 0;
|
||||
const r = parseJoinGame(data);
|
||||
assert.ok(r);
|
||||
assert.strictEqual(r.entityId, 1);
|
||||
assert.strictEqual(r.gamemode, 0);
|
||||
assert.strictEqual(r.dimension, 0);
|
||||
assert.strictEqual(r.maxPlayers, 20);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { MCStream } = require('../src/bot/compression');
|
||||
|
||||
test('MCStream: deflate below threshold is passthrough', () => {
|
||||
const s = new MCStream();
|
||||
s.setThreshold(256);
|
||||
const data = Buffer.from('hello');
|
||||
assert.strictEqual(s.deflate(data), data);
|
||||
});
|
||||
|
||||
test('MCStream: inflate of uncompressed data is passthrough', () => {
|
||||
const s = new MCStream();
|
||||
s.setThreshold(256);
|
||||
const data = Buffer.from('hello');
|
||||
assert.strictEqual(s.inflate(data), data);
|
||||
});
|
||||
|
||||
test('MCStream: deflate then inflate round-trip', () => {
|
||||
const s = new MCStream();
|
||||
s.setThreshold(8);
|
||||
const data = Buffer.from('a'.repeat(64));
|
||||
const compressed = s.deflate(data);
|
||||
assert.ok(compressed.length < data.length, 'compressed should be smaller');
|
||||
const back = s.inflate(compressed);
|
||||
assert.deepStrictEqual([...back], [...data]);
|
||||
});
|
||||
|
||||
test('MCStream: corrupt zlib stream returns original', () => {
|
||||
const s = new MCStream();
|
||||
s.setThreshold(8);
|
||||
const bad = Buffer.from('not a zlib stream at all - just bytes');
|
||||
const result = s.inflate(bad);
|
||||
assert.strictEqual(result, bad);
|
||||
});
|
||||
|
||||
test('MCStream: disabled returns input as-is', () => {
|
||||
const s = new MCStream();
|
||||
assert.strictEqual(s.isEnabled(), false);
|
||||
const data = Buffer.from('x'.repeat(1024));
|
||||
assert.strictEqual(s.deflate(data), data);
|
||||
assert.strictEqual(s.inflate(data), data);
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const {
|
||||
encodeVarInt,
|
||||
parseVarInt,
|
||||
encodeString,
|
||||
readString,
|
||||
buildPacket,
|
||||
buildMCFrame,
|
||||
parseMCFrame,
|
||||
drainMCFrames,
|
||||
} = require('../src/bot/packets');
|
||||
|
||||
test('encodeVarInt: 0', () => {
|
||||
assert.deepStrictEqual([...encodeVarInt(0)], [0x00]);
|
||||
});
|
||||
|
||||
test('encodeVarInt: 127', () => {
|
||||
assert.deepStrictEqual([...encodeVarInt(127)], [0x7F]);
|
||||
});
|
||||
|
||||
test('encodeVarInt: 128', () => {
|
||||
assert.deepStrictEqual([...encodeVarInt(128)], [0x80, 0x01]);
|
||||
});
|
||||
|
||||
test('encodeVarInt: 16383', () => {
|
||||
assert.deepStrictEqual([...encodeVarInt(16383)], [0xFF, 0x7F]);
|
||||
});
|
||||
|
||||
test('encodeVarInt: 16384', () => {
|
||||
assert.deepStrictEqual([...encodeVarInt(16384)], [0x80, 0x80, 0x01]);
|
||||
});
|
||||
|
||||
test('encodeVarInt: 2^31-1', () => {
|
||||
assert.deepStrictEqual([...encodeVarInt(0x7FFFFFFF)], [0xFF, 0xFF, 0xFF, 0xFF, 0x07]);
|
||||
});
|
||||
|
||||
test('parseVarInt: round-trip', () => {
|
||||
for (const v of [0, 1, 127, 128, 255, 16383, 16384, 2 ** 31 - 1]) {
|
||||
const buf = encodeVarInt(v);
|
||||
const r = parseVarInt(buf, 0);
|
||||
assert.strictEqual(r.value, v, `round-trip ${v}`);
|
||||
assert.strictEqual(r.bytesRead, buf.length, `bytes ${v}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('parseVarInt: incomplete returns 0 bytes', () => {
|
||||
const buf = Buffer.from([0x80]);
|
||||
const r = parseVarInt(buf, 0);
|
||||
assert.strictEqual(r.bytesRead, 0);
|
||||
});
|
||||
|
||||
test('encodeString/readString: ASCII', () => {
|
||||
const s = 'Hello';
|
||||
const buf = encodeString(s);
|
||||
const r = readString(buf, 0);
|
||||
assert.strictEqual(r.value, s);
|
||||
assert.strictEqual(r.bytesRead, buf.length);
|
||||
});
|
||||
|
||||
test('encodeString/readString: UTF-8 multibyte', () => {
|
||||
const s = 'héllo★world';
|
||||
const buf = encodeString(s);
|
||||
const r = readString(buf, 0);
|
||||
assert.strictEqual(r.value, s);
|
||||
assert.strictEqual(r.bytesRead, buf.length);
|
||||
});
|
||||
|
||||
test('buildPacket: id + data', () => {
|
||||
const buf = buildPacket(0x02, encodeString('hi'));
|
||||
assert.strictEqual(buf[0], 0x02);
|
||||
assert.strictEqual(buf[1], 2);
|
||||
assert.strictEqual(buf[2], 0x68);
|
||||
assert.strictEqual(buf[3], 0x69);
|
||||
});
|
||||
|
||||
test('buildMCFrame + parseMCFrame round-trip', () => {
|
||||
const frame = buildMCFrame(0x1F, Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]));
|
||||
const r = parseMCFrame(frame, 0);
|
||||
assert.strictEqual(r.complete, true);
|
||||
assert.strictEqual(r.packetId, 0x1F);
|
||||
assert.deepStrictEqual([...r.data], [1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
assert.strictEqual(r.bytesRead, frame.length);
|
||||
});
|
||||
|
||||
test('parseMCFrame: incomplete returns complete=false', () => {
|
||||
const frame = buildMCFrame(0x1F, Buffer.from([1, 2, 3]));
|
||||
const partial = frame.slice(0, frame.length - 2);
|
||||
const r = parseMCFrame(partial, 0);
|
||||
assert.strictEqual(r.complete, false);
|
||||
assert.strictEqual(r.bytesRead, 0);
|
||||
});
|
||||
|
||||
test('drainMCFrames: consumes multiple', () => {
|
||||
const f1 = buildMCFrame(0x1F, Buffer.from([0xAA, 0xBB]));
|
||||
const f2 = buildMCFrame(0x23, Buffer.from([0x01]));
|
||||
const f3 = buildMCFrame(0x2F, Buffer.alloc(33));
|
||||
const combined = Buffer.concat([f1, f2, f3]);
|
||||
const seen = [];
|
||||
const { consumed, frames } = drainMCFrames(combined, (id, data) => seen.push({ id, data }));
|
||||
assert.strictEqual(consumed, combined.length);
|
||||
assert.strictEqual(frames, 3);
|
||||
assert.strictEqual(seen[0].id, 0x1F);
|
||||
assert.strictEqual(seen[1].id, 0x23);
|
||||
assert.strictEqual(seen[2].id, 0x2F);
|
||||
});
|
||||
|
||||
test('drainMCFrames: partial at end', () => {
|
||||
const f1 = buildMCFrame(0x1F, Buffer.from([0xAA]));
|
||||
const f2 = buildMCFrame(0x23, Buffer.from([0x01]));
|
||||
const partial = Buffer.concat([f1, f2]).slice(0, f1.length + 2);
|
||||
const seen = [];
|
||||
const { consumed, frames } = drainMCFrames(partial, (id, data) => seen.push({ id }));
|
||||
assert.strictEqual(frames, 1);
|
||||
assert.strictEqual(seen[0].id, 0x1F);
|
||||
assert.strictEqual(consumed, f1.length);
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { ProxyRegistry, proxyKey } = require('../src/proxy/registry');
|
||||
|
||||
function newReg() {
|
||||
return new ProxyRegistry({
|
||||
validationTimeoutMs: 100,
|
||||
validationConcurrency: 2,
|
||||
scorePenaltyPerFail: 10,
|
||||
aliveLatencyMaxMs: 5000,
|
||||
persistPath: null,
|
||||
txtPath: null,
|
||||
log: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
test('proxyKey: format', () => {
|
||||
assert.strictEqual(proxyKey('socks5', '1.2.3.4', 1080), 'socks5://1.2.3.4:1080');
|
||||
});
|
||||
|
||||
test('parseProxyString: socks5', () => {
|
||||
const r = newReg();
|
||||
const p = r.parseProxyString('socks5://1.2.3.4:1080');
|
||||
assert.deepStrictEqual(p, { host: '1.2.3.4', port: 1080, type: 'socks5', auth: null });
|
||||
});
|
||||
|
||||
test('parseProxyString: socks4', () => {
|
||||
const r = newReg();
|
||||
const p = r.parseProxyString('socks4://5.6.7.8:1080');
|
||||
assert.deepStrictEqual(p, { host: '5.6.7.8', port: 1080, type: 'socks4', auth: null });
|
||||
});
|
||||
|
||||
test('parseProxyString: with auth', () => {
|
||||
const r = newReg();
|
||||
const p = r.parseProxyString('socks5://user:pass@9.10.11.12:1080');
|
||||
assert.deepStrictEqual(p, { host: '9.10.11.12', port: 1080, type: 'socks5', auth: { user: 'user', password: 'pass' } });
|
||||
});
|
||||
|
||||
test('parseProxyString: invalid', () => {
|
||||
const r = newReg();
|
||||
assert.strictEqual(r.parseProxyString('not a proxy'), null);
|
||||
assert.strictEqual(r.parseProxyString(''), null);
|
||||
assert.strictEqual(r.parseProxyString('socks5://host:notaport'), null);
|
||||
assert.strictEqual(r.parseProxyString('socks5://host:0'), null);
|
||||
assert.strictEqual(r.parseProxyString('socks5://host:99999'), null);
|
||||
assert.strictEqual(r.parseProxyString('# comment'), null);
|
||||
});
|
||||
|
||||
test('add: dedupes by key', () => {
|
||||
const r = newReg();
|
||||
const e1 = r.add({ host: '1.2.3.4', port: 1080, type: 'socks5' });
|
||||
const e2 = r.add({ host: '1.2.3.4', port: 1080, type: 'socks5' });
|
||||
assert.strictEqual(e1, e2);
|
||||
assert.strictEqual(r.count(), 1);
|
||||
});
|
||||
|
||||
test('remove and find', () => {
|
||||
const r = newReg();
|
||||
r.add({ host: '1.2.3.4', port: 1080, type: 'socks5' });
|
||||
assert.ok(r.find('socks5://1.2.3.4:1080'));
|
||||
assert.ok(r.remove('socks5://1.2.3.4:1080'));
|
||||
assert.strictEqual(r.find('socks5://1.2.3.4:1080'), null);
|
||||
assert.strictEqual(r.remove('socks5://nope:0'), false);
|
||||
});
|
||||
|
||||
test('markSuccess: increases score, sets alive', () => {
|
||||
const r = newReg();
|
||||
const e = r.add({ host: '1.2.3.4', port: 1080, type: 'socks5' });
|
||||
e.status = 'unknown';
|
||||
e.score = 50;
|
||||
r.markSuccess('socks5://1.2.3.4:1080', 100);
|
||||
assert.strictEqual(e.status, 'alive');
|
||||
assert.ok(e.score >= 50);
|
||||
assert.strictEqual(e.avgLatencyMs, 100);
|
||||
assert.strictEqual(e.p50LatencyMs, 100);
|
||||
});
|
||||
|
||||
test('markFailure: decreases score, dead at 0', () => {
|
||||
const r = newReg();
|
||||
r.add({ host: '1.2.3.4', port: 1080, type: 'socks5' });
|
||||
for (let i = 0; i < 10; i++) r.markFailure('socks5://1.2.3.4:1080', 'timeout');
|
||||
const e = r.find('socks5://1.2.3.4:1080');
|
||||
assert.strictEqual(e.score, 0);
|
||||
assert.strictEqual(e.status, 'dead');
|
||||
assert.ok(e.retestAfterAt);
|
||||
assert.strictEqual(e.failureReasons.timeout, 10);
|
||||
});
|
||||
|
||||
test('markFailure: degraded below 30', () => {
|
||||
const r = newReg();
|
||||
r.add({ host: '1.2.3.4', port: 1080, type: 'socks5' });
|
||||
r.markSuccess('socks5://1.2.3.4:1080');
|
||||
r.markFailure('socks5://1.2.3.4:1080');
|
||||
const e = r.find('socks5://1.2.3.4:1080');
|
||||
assert.ok(['degraded', 'alive'].includes(e.status));
|
||||
});
|
||||
|
||||
test('pick: respects excludes', () => {
|
||||
const r = newReg();
|
||||
const a = r.add({ host: '1.1.1.1', port: 1080, type: 'socks5' });
|
||||
const b = r.add({ host: '2.2.2.2', port: 1080, type: 'socks5' });
|
||||
a.status = 'alive'; b.status = 'alive';
|
||||
const picked = r.pick({ strategy: 'random', excludeKeys: ['socks5://1.1.1.1:1080'] });
|
||||
assert.strictEqual(picked.key, 'socks5://2.2.2.2:1080');
|
||||
});
|
||||
|
||||
test('pick: inflight cap blocks', () => {
|
||||
const r = newReg();
|
||||
r.add({ host: '1.1.1.1', port: 1080, type: 'socks5' });
|
||||
const e = r.find('socks5://1.1.1.1:1080');
|
||||
e.status = 'alive';
|
||||
for (let i = 0; i < 5; i++) r.pick();
|
||||
const p = r.pick();
|
||||
assert.strictEqual(p, null);
|
||||
});
|
||||
|
||||
test('release: decrements inflight', () => {
|
||||
const r = newReg();
|
||||
r.add({ host: '1.1.1.1', port: 1080, type: 'socks5' });
|
||||
r.find('socks5://1.1.1.1:1080').status = 'alive';
|
||||
r.pick();
|
||||
r.release('socks5://1.1.1.1:1080');
|
||||
assert.strictEqual(r.find('socks5://1.1.1.1:1080').inFlight, 0);
|
||||
});
|
||||
|
||||
test('pruneDead: removes dead', () => {
|
||||
const r = newReg();
|
||||
r.add({ host: '1.1.1.1', port: 1080, type: 'socks5' });
|
||||
r.add({ host: '2.2.2.2', port: 1080, type: 'socks5' });
|
||||
r.markFailure('socks5://1.1.1.1:1080');
|
||||
for (let i = 0; i < 10; i++) r.markFailure('socks5://1.1.1.1:1080');
|
||||
r.markFailure('socks5://2.2.2.2:1080');
|
||||
for (let i = 0; i < 10; i++) r.markFailure('socks5://2.2.2.2:1080');
|
||||
const removed = r.pruneDead();
|
||||
assert.strictEqual(removed, 2);
|
||||
assert.strictEqual(r.count(), 0);
|
||||
});
|
||||
|
||||
test('stats: aggregates', () => {
|
||||
const r = newReg();
|
||||
const a = r.add({ host: '1.1.1.1', port: 1080, type: 'socks5' });
|
||||
a.status = 'alive'; a.score = 80; a.p50LatencyMs = 50;
|
||||
const b = r.add({ host: '2.2.2.2', port: 1080, type: 'socks5' });
|
||||
b.status = 'dead';
|
||||
const c = r.add({ host: '3.3.3.3', port: 1080, type: 'socks5' });
|
||||
c.status = 'unknown';
|
||||
const s = r.stats();
|
||||
assert.strictEqual(s.total, 3);
|
||||
assert.strictEqual(s.alive, 1);
|
||||
assert.strictEqual(s.dead, 1);
|
||||
assert.strictEqual(s.unknown, 1);
|
||||
assert.strictEqual(s.avgScore, 80);
|
||||
assert.strictEqual(s.p50Latency, 50);
|
||||
assert.deepStrictEqual(s.scoreHistogram, [0, 0, 0, 0, 1]);
|
||||
});
|
||||
Reference in New Issue
Block a user