Files
mc-bot/test/registry.test.js
T
2026-08-22 11:09:05 -05:00

158 lines
5.4 KiB
JavaScript

'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]);
});