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