Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ac3b501adf | ||
|
da02e04edf | ||
|
1a81adaabd |
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartnetwork",
|
"name": "@push.rocks/smartnetwork",
|
||||||
"version": "4.1.1",
|
"version": "4.1.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
||||||
"main": "dist_ts/index.js",
|
"exports": {
|
||||||
"typings": "dist_ts/index.d.ts",
|
".": "./dist_ts/index.js"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
392
test/test.ports.ts
Normal file
392
test/test.ports.ts
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { SmartNetwork, NetworkError } from '../ts/index.js';
|
||||||
|
import * as net from 'net';
|
||||||
|
import type { AddressInfo } from 'net';
|
||||||
|
|
||||||
|
// Helper to create a server on a specific port
|
||||||
|
const createServerOnPort = async (port: number): Promise<net.Server> => {
|
||||||
|
const server = net.createServer();
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(port, () => {
|
||||||
|
server.removeListener('error', reject);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to clean up servers
|
||||||
|
const cleanupServers = async (servers: net.Server[]): Promise<void> => {
|
||||||
|
await Promise.all(servers.map(s => new Promise<void>((res) => s.close(() => res()))));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========= isLocalPortUnused Tests =========
|
||||||
|
|
||||||
|
tap.test('isLocalPortUnused - should detect free port correctly', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
// Port 0 lets the OS assign a free port, we'll use a high range instead
|
||||||
|
const result = await sn.isLocalPortUnused(54321);
|
||||||
|
expect(typeof result).toEqual('boolean');
|
||||||
|
// Most likely this high port is free, but we can't guarantee it
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isLocalPortUnused - should detect occupied port', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const server = net.createServer();
|
||||||
|
await new Promise<void>((res) => server.listen(0, res));
|
||||||
|
const addr = server.address() as AddressInfo;
|
||||||
|
|
||||||
|
const isUnused = await sn.isLocalPortUnused(addr.port);
|
||||||
|
expect(isUnused).toBeFalse();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isLocalPortUnused - should handle multiple simultaneous checks', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const ports = [55001, 55002, 55003, 55004, 55005];
|
||||||
|
|
||||||
|
// Check all ports simultaneously
|
||||||
|
const results = await Promise.all(
|
||||||
|
ports.map(port => sn.isLocalPortUnused(port))
|
||||||
|
);
|
||||||
|
|
||||||
|
// All should likely be free
|
||||||
|
results.forEach(result => {
|
||||||
|
expect(typeof result).toEqual('boolean');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isLocalPortUnused - should work with IPv6 loopback', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const server = net.createServer();
|
||||||
|
|
||||||
|
// Explicitly bind to IPv6
|
||||||
|
await new Promise<void>((res) => server.listen(55100, '::', res));
|
||||||
|
const addr = server.address() as AddressInfo;
|
||||||
|
|
||||||
|
const isUnused = await sn.isLocalPortUnused(addr.port);
|
||||||
|
expect(isUnused).toBeFalse();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isLocalPortUnused - boundary port numbers', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test port 1 (usually requires root)
|
||||||
|
const port1Result = await sn.isLocalPortUnused(1);
|
||||||
|
expect(typeof port1Result).toEqual('boolean');
|
||||||
|
|
||||||
|
// Test port 65535
|
||||||
|
const port65535Result = await sn.isLocalPortUnused(65535);
|
||||||
|
expect(typeof port65535Result).toEqual('boolean');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========= findFreePort Tests =========
|
||||||
|
|
||||||
|
tap.test('findFreePort - should find free port in small range', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const freePort = await sn.findFreePort(50000, 50010);
|
||||||
|
|
||||||
|
expect(freePort).not.toBeNull();
|
||||||
|
expect(freePort).toBeGreaterThanOrEqual(50000);
|
||||||
|
expect(freePort).toBeLessThanOrEqual(50010);
|
||||||
|
|
||||||
|
// Verify the port is actually free
|
||||||
|
if (freePort !== null) {
|
||||||
|
const isUnused = await sn.isLocalPortUnused(freePort);
|
||||||
|
expect(isUnused).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should find first available port', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const servers = [];
|
||||||
|
|
||||||
|
// Occupy ports 50100 and 50101
|
||||||
|
servers.push(await createServerOnPort(50100));
|
||||||
|
servers.push(await createServerOnPort(50101));
|
||||||
|
|
||||||
|
// Port 50102 should be free
|
||||||
|
const freePort = await sn.findFreePort(50100, 50105);
|
||||||
|
expect(freePort).toEqual(50102);
|
||||||
|
|
||||||
|
await cleanupServers(servers);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should handle fully occupied range', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const servers = [];
|
||||||
|
const startPort = 50200;
|
||||||
|
const endPort = 50202;
|
||||||
|
|
||||||
|
// Occupy all ports in range
|
||||||
|
for (let port = startPort; port <= endPort; port++) {
|
||||||
|
servers.push(await createServerOnPort(port));
|
||||||
|
}
|
||||||
|
|
||||||
|
const freePort = await sn.findFreePort(startPort, endPort);
|
||||||
|
expect(freePort).toBeNull();
|
||||||
|
|
||||||
|
await cleanupServers(servers);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should validate port boundaries', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test port < 1
|
||||||
|
try {
|
||||||
|
await sn.findFreePort(0, 100);
|
||||||
|
throw new Error('Should have thrown for port < 1');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('EINVAL');
|
||||||
|
expect(err.message).toContain('between 1 and 65535');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test port > 65535
|
||||||
|
try {
|
||||||
|
await sn.findFreePort(100, 70000);
|
||||||
|
throw new Error('Should have thrown for port > 65535');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('EINVAL');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test negative ports
|
||||||
|
try {
|
||||||
|
await sn.findFreePort(-100, 100);
|
||||||
|
throw new Error('Should have thrown for negative port');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('EINVAL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should validate range order', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.findFreePort(200, 100);
|
||||||
|
throw new Error('Should have thrown for startPort > endPort');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('EINVAL');
|
||||||
|
expect(err.message).toContain('less than or equal to end port');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should handle single port range', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test when start and end are the same
|
||||||
|
const freePort = await sn.findFreePort(50300, 50300);
|
||||||
|
// Should either be 50300 or null
|
||||||
|
expect(freePort === 50300 || freePort === null).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should work with large ranges', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test with a large range
|
||||||
|
const freePort = await sn.findFreePort(40000, 50000);
|
||||||
|
expect(freePort).not.toBeNull();
|
||||||
|
expect(freePort).toBeGreaterThanOrEqual(40000);
|
||||||
|
expect(freePort).toBeLessThanOrEqual(50000);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('findFreePort - should handle intermittent occupied ports', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const servers = [];
|
||||||
|
|
||||||
|
// Occupy every other port
|
||||||
|
servers.push(await createServerOnPort(50400));
|
||||||
|
servers.push(await createServerOnPort(50402));
|
||||||
|
servers.push(await createServerOnPort(50404));
|
||||||
|
|
||||||
|
// Should find 50401, 50403, or 50405
|
||||||
|
const freePort = await sn.findFreePort(50400, 50405);
|
||||||
|
expect([50401, 50403, 50405]).toContain(freePort);
|
||||||
|
|
||||||
|
await cleanupServers(servers);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========= isRemotePortAvailable Tests =========
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should detect open HTTP port', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test with string format
|
||||||
|
const open1 = await sn.isRemotePortAvailable('example.com:80');
|
||||||
|
expect(open1).toBeTrue();
|
||||||
|
|
||||||
|
// Test with separate parameters
|
||||||
|
const open2 = await sn.isRemotePortAvailable('example.com', 80);
|
||||||
|
expect(open2).toBeTrue();
|
||||||
|
|
||||||
|
// Test with options object
|
||||||
|
const open3 = await sn.isRemotePortAvailable('example.com', { port: 80 });
|
||||||
|
expect(open3).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should detect closed port', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Port 12345 is likely closed on example.com
|
||||||
|
const closed = await sn.isRemotePortAvailable('example.com', 12345);
|
||||||
|
expect(closed).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should handle retries', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test with retries
|
||||||
|
const result = await sn.isRemotePortAvailable('example.com', {
|
||||||
|
port: 80,
|
||||||
|
retries: 3,
|
||||||
|
timeout: 1000
|
||||||
|
});
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should reject UDP protocol', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.isRemotePortAvailable('example.com', {
|
||||||
|
port: 53,
|
||||||
|
protocol: 'udp'
|
||||||
|
});
|
||||||
|
throw new Error('Should have thrown for UDP protocol');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('ENOTSUP');
|
||||||
|
expect(err.message).toContain('UDP port check not supported');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should require port specification', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.isRemotePortAvailable('example.com');
|
||||||
|
throw new Error('Should have thrown for missing port');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('EINVAL');
|
||||||
|
expect(err.message).toContain('Port not specified');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should parse port from host:port string', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Valid formats
|
||||||
|
const result1 = await sn.isRemotePortAvailable('example.com:443');
|
||||||
|
expect(result1).toBeTrue();
|
||||||
|
|
||||||
|
// With options overriding the string port
|
||||||
|
const result2 = await sn.isRemotePortAvailable('example.com:8080', { port: 80 });
|
||||||
|
expect(result2).toBeTrue(); // Should use port 80 from options, not 8080
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should handle localhost', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const server = net.createServer();
|
||||||
|
|
||||||
|
// Start a local server
|
||||||
|
await new Promise<void>((res) => server.listen(51000, 'localhost', res));
|
||||||
|
|
||||||
|
// Should detect it as open
|
||||||
|
const isOpen = await sn.isRemotePortAvailable('localhost', 51000);
|
||||||
|
expect(isOpen).toBeTrue();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
|
||||||
|
// After closing, might still show as open due to TIME_WAIT, or closed
|
||||||
|
// We won't assert on this as it's OS-dependent
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - should handle invalid hosts gracefully', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Non-existent domain
|
||||||
|
const result = await sn.isRemotePortAvailable('this-domain-definitely-does-not-exist-12345.com', 80);
|
||||||
|
expect(result).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable - edge case ports', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Test HTTPS port
|
||||||
|
const https = await sn.isRemotePortAvailable('example.com', 443);
|
||||||
|
expect(https).toBeTrue();
|
||||||
|
|
||||||
|
// Test SSH port (likely closed on example.com)
|
||||||
|
const ssh = await sn.isRemotePortAvailable('example.com', 22);
|
||||||
|
expect(ssh).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========= Integration Tests =========
|
||||||
|
|
||||||
|
tap.test('Integration - findFreePort and isLocalPortUnused consistency', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
|
||||||
|
// Find a free port
|
||||||
|
const freePort = await sn.findFreePort(52000, 52100);
|
||||||
|
expect(freePort).not.toBeNull();
|
||||||
|
|
||||||
|
if (freePort !== null) {
|
||||||
|
// Verify it's actually free
|
||||||
|
const isUnused1 = await sn.isLocalPortUnused(freePort);
|
||||||
|
expect(isUnused1).toBeTrue();
|
||||||
|
|
||||||
|
// Start a server on it
|
||||||
|
const server = await createServerOnPort(freePort);
|
||||||
|
|
||||||
|
// Now it should be in use
|
||||||
|
const isUnused2 = await sn.isLocalPortUnused(freePort);
|
||||||
|
expect(isUnused2).toBeFalse();
|
||||||
|
|
||||||
|
// findFreePort should skip it
|
||||||
|
const nextFreePort = await sn.findFreePort(freePort, freePort + 10);
|
||||||
|
expect(nextFreePort).not.toEqual(freePort);
|
||||||
|
|
||||||
|
await cleanupServers([server]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Integration - stress test with many concurrent port checks', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const portRange = Array.from({ length: 20 }, (_, i) => 53000 + i);
|
||||||
|
|
||||||
|
// Check all ports concurrently
|
||||||
|
const results = await Promise.all(
|
||||||
|
portRange.map(async port => ({
|
||||||
|
port,
|
||||||
|
isUnused: await sn.isLocalPortUnused(port)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// All operations should complete without error
|
||||||
|
results.forEach(result => {
|
||||||
|
expect(typeof result.isUnused).toEqual('boolean');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Performance - findFreePort with large range', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// This should be fast even with a large range
|
||||||
|
const freePort = await sn.findFreePort(30000, 60000);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
expect(freePort).not.toBeNull();
|
||||||
|
// Should complete quickly (within 100ms) as it should find a port early
|
||||||
|
expect(duration).toBeLessThan(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
Reference in New Issue
Block a user