191 lines
7.2 KiB
TypeScript
191 lines
7.2 KiB
TypeScript
|
import { tap, expect, expectAsync } from '@push.rocks/tapbundle';
|
||
|
import { SmartNetwork, NetworkError } from '../ts/index.js';
|
||
|
import * as net from 'net';
|
||
|
import type { AddressInfo } from 'net';
|
||
|
|
||
|
// DNS resolution
|
||
|
tap.test('resolveDns should return A records for localhost', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const res = await sn.resolveDns('localhost');
|
||
|
expect(res.A.length).toBeGreaterThan(0);
|
||
|
expect(Array.isArray(res.A)).toBeTrue();
|
||
|
expect(Array.isArray(res.AAAA)).toBeTrue();
|
||
|
expect(Array.isArray(res.MX)).toBeTrue();
|
||
|
});
|
||
|
|
||
|
// DNS resolution edge cases and MX records
|
||
|
tap.test('resolveDns should handle non-existent domains', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const res = await sn.resolveDns('no.such.domain.invalid');
|
||
|
expect(Array.isArray(res.A)).toBeTrue();
|
||
|
expect(Array.isArray(res.AAAA)).toBeTrue();
|
||
|
expect(Array.isArray(res.MX)).toBeTrue();
|
||
|
expect(res.A.length).toEqual(0);
|
||
|
expect(res.AAAA.length).toEqual(0);
|
||
|
expect(res.MX.length).toEqual(0);
|
||
|
});
|
||
|
|
||
|
tap.test('resolveDns MX records for google.com', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const res = await sn.resolveDns('google.com');
|
||
|
expect(Array.isArray(res.MX)).toBeTrue();
|
||
|
if (res.MX.length > 0) {
|
||
|
expect(typeof res.MX[0].exchange).toEqual('string');
|
||
|
expect(typeof res.MX[0].priority).toEqual('number');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// HTTP endpoint health-check
|
||
|
tap.test('checkEndpoint should return status and headers', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const result = await sn.checkEndpoint('https://example.com');
|
||
|
expect(result.status).toEqual(200);
|
||
|
expect(typeof result.rtt).toEqual('number');
|
||
|
expect(typeof result.headers).toEqual('object');
|
||
|
expect(result.headers).toHaveProperty('content-type');
|
||
|
});
|
||
|
|
||
|
// Traceroute stub
|
||
|
tap.test('traceroute should return at least one hop', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const hops = await sn.traceroute('127.0.0.1');
|
||
|
expect(Array.isArray(hops)).toBeTrue();
|
||
|
expect(hops.length).toBeGreaterThanOrEqual(1);
|
||
|
const hop = hops[0];
|
||
|
expect(typeof hop.ttl).toEqual('number');
|
||
|
expect(typeof hop.ip).toEqual('string');
|
||
|
expect(hop.rtt === null || typeof hop.rtt === 'number').toBeTrue();
|
||
|
});
|
||
|
// Traceroute fallback stub ensures consistent output when binary missing
|
||
|
tap.test('traceroute fallback stub returns a single-hop stub', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const hops = await sn.traceroute('example.com', { maxHops: 5 });
|
||
|
expect(Array.isArray(hops)).toBeTrue();
|
||
|
expect(hops).toHaveLength(1);
|
||
|
expect(hops[0]).toEqual({ ttl: 1, ip: 'example.com', rtt: null });
|
||
|
});
|
||
|
|
||
|
// getSpeed options
|
||
|
tap.test('getSpeed should accept options and return speeds', async () => {
|
||
|
const opts = { parallelStreams: 2, duration: 1 };
|
||
|
const sn = new SmartNetwork();
|
||
|
const result = await sn.getSpeed(opts);
|
||
|
expect(typeof result.downloadSpeed).toEqual('string');
|
||
|
expect(typeof result.uploadSpeed).toEqual('string');
|
||
|
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||
|
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||
|
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||
|
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||
|
});
|
||
|
|
||
|
// Ping multiple count
|
||
|
tap.test('ping with count > 1 should return stats', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const stats = await sn.ping('127.0.0.1', { count: 3 });
|
||
|
expect(stats.count).toEqual(3);
|
||
|
expect(Array.isArray(stats.times)).toBeTrue();
|
||
|
expect(stats.times.length).toEqual(3);
|
||
|
expect(typeof stats.min).toEqual('number');
|
||
|
expect(typeof stats.max).toEqual('number');
|
||
|
expect(typeof stats.avg).toEqual('number');
|
||
|
expect(typeof stats.stddev).toEqual('number');
|
||
|
expect(typeof stats.packetLoss).toEqual('number');
|
||
|
expect(typeof stats.alive).toEqual('boolean');
|
||
|
});
|
||
|
|
||
|
// Remote port UDP not supported
|
||
|
// Remote port UDP not supported
|
||
|
tap.test('isRemotePortAvailable should throw on UDP', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
// should throw NetworkError with code ENOTSUP when protocol is UDP
|
||
|
try {
|
||
|
await sn.isRemotePortAvailable('example.com', { protocol: 'udp' });
|
||
|
// If no error is thrown, the test should fail
|
||
|
throw new Error('Expected isRemotePortAvailable to throw for UDP');
|
||
|
} catch (err: any) {
|
||
|
expect(err).toBeInstanceOf(NetworkError);
|
||
|
expect(err.code).toEqual('ENOTSUP');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Plugin registry
|
||
|
tap.test('should register and unregister plugin', async () => {
|
||
|
class Dummy {}
|
||
|
SmartNetwork.registerPlugin('dummy', Dummy);
|
||
|
expect(SmartNetwork.pluginsRegistry.has('dummy')).toBeTrue();
|
||
|
SmartNetwork.unregisterPlugin('dummy');
|
||
|
expect(SmartNetwork.pluginsRegistry.has('dummy')).toBeFalse();
|
||
|
});
|
||
|
|
||
|
tap.test('getGateways should respect cacheTtl', async () => {
|
||
|
const sn = new SmartNetwork({ cacheTtl: 1000 });
|
||
|
const first = await sn.getGateways();
|
||
|
const second = await sn.getGateways();
|
||
|
expect(first).toEqual(second);
|
||
|
});
|
||
|
|
||
|
// Remote port checks: missing port should error
|
||
|
tap.test('isRemotePortAvailable should require a port', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
try {
|
||
|
await sn.isRemotePortAvailable('example.com');
|
||
|
throw new Error('Expected error when port is not specified');
|
||
|
} catch (err: any) {
|
||
|
expect(err).toBeInstanceOf(NetworkError);
|
||
|
expect(err.code).toEqual('EINVAL');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Remote port checks: detect open TCP port on example.com
|
||
|
tap.test('isRemotePortAvailable should detect open TCP port via string target', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const open = await sn.isRemotePortAvailable('example.com:80');
|
||
|
expect(open).toBeTrue();
|
||
|
});
|
||
|
|
||
|
tap.test('isRemotePortAvailable should detect open TCP port via numeric arg', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
const open = await sn.isRemotePortAvailable('example.com', 80);
|
||
|
expect(open).toBeTrue();
|
||
|
});
|
||
|
// Caching public IPs
|
||
|
tap.test('getPublicIps should respect cacheTtl', async () => {
|
||
|
const sn = new SmartNetwork({ cacheTtl: 1000 });
|
||
|
const first = await sn.getPublicIps();
|
||
|
const second = await sn.getPublicIps();
|
||
|
expect(first).toEqual(second);
|
||
|
});
|
||
|
|
||
|
// Local port usage detection
|
||
|
tap.test('isLocalPortUnused should detect used local port', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
// start a server on a random port
|
||
|
const server = net.createServer();
|
||
|
await new Promise<void>((res) => server.listen(0, res));
|
||
|
const addr = server.address() as AddressInfo;
|
||
|
// port is now in use
|
||
|
const inUse = await sn.isLocalPortUnused(addr.port);
|
||
|
expect(inUse).toBeFalse();
|
||
|
await new Promise<void>((res) => server.close(res));
|
||
|
});
|
||
|
// Real traceroute integration test (skipped if `traceroute` binary is unavailable)
|
||
|
tap.test('traceroute real integration against google.com', async () => {
|
||
|
const sn = new SmartNetwork();
|
||
|
// detect traceroute binary
|
||
|
const { spawnSync } = await import('child_process');
|
||
|
const probe = spawnSync('traceroute', ['-h']);
|
||
|
if (probe.error || probe.status !== 0) {
|
||
|
// Skip real integration when traceroute is not installed
|
||
|
return;
|
||
|
}
|
||
|
const hops = await sn.traceroute('google.com', { maxHops: 5, timeout: 5000 });
|
||
|
expect(Array.isArray(hops)).toBeTrue();
|
||
|
expect(hops.length).toBeGreaterThan(1);
|
||
|
for (const hop of hops) {
|
||
|
expect(typeof hop.ttl).toEqual('number');
|
||
|
expect(typeof hop.ip).toEqual('string');
|
||
|
expect(hop.rtt === null || typeof hop.rtt === 'number').toBeTrue();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.start();
|