BREAKING CHANGE(ts-api,rustproxy): remove deprecated TypeScript protocol and utility exports while hardening QUIC, HTTP/3, WebSocket, and rate limiter cleanup paths
This commit is contained in:
@@ -1,200 +0,0 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
delay,
|
||||
retryWithBackoff,
|
||||
withTimeout,
|
||||
parallelLimit,
|
||||
debounceAsync,
|
||||
AsyncMutex,
|
||||
CircuitBreaker
|
||||
} from '../../../ts/core/utils/async-utils.js';
|
||||
|
||||
tap.test('delay should pause execution for specified milliseconds', async () => {
|
||||
const startTime = Date.now();
|
||||
await delay(100);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
// Allow some tolerance for timing
|
||||
expect(elapsed).toBeGreaterThan(90);
|
||||
expect(elapsed).toBeLessThan(150);
|
||||
});
|
||||
|
||||
tap.test('retryWithBackoff should retry failed operations', async () => {
|
||||
let attempts = 0;
|
||||
const operation = async () => {
|
||||
attempts++;
|
||||
if (attempts < 3) {
|
||||
throw new Error('Test error');
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
|
||||
const result = await retryWithBackoff(operation, {
|
||||
maxAttempts: 3,
|
||||
initialDelay: 10
|
||||
});
|
||||
|
||||
expect(result).toEqual('success');
|
||||
expect(attempts).toEqual(3);
|
||||
});
|
||||
|
||||
tap.test('retryWithBackoff should throw after max attempts', async () => {
|
||||
let attempts = 0;
|
||||
const operation = async () => {
|
||||
attempts++;
|
||||
throw new Error('Always fails');
|
||||
};
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await retryWithBackoff(operation, {
|
||||
maxAttempts: 2,
|
||||
initialDelay: 10
|
||||
});
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toEqual('Always fails');
|
||||
expect(attempts).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('withTimeout should complete operations within timeout', async () => {
|
||||
const operation = async () => {
|
||||
await delay(50);
|
||||
return 'completed';
|
||||
};
|
||||
|
||||
const result = await withTimeout(operation, 100);
|
||||
expect(result).toEqual('completed');
|
||||
});
|
||||
|
||||
tap.test('withTimeout should throw on timeout', async () => {
|
||||
const operation = async () => {
|
||||
await delay(200);
|
||||
return 'never happens';
|
||||
};
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await withTimeout(operation, 50);
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toContain('timed out');
|
||||
});
|
||||
|
||||
tap.test('parallelLimit should respect concurrency limit', async () => {
|
||||
let concurrent = 0;
|
||||
let maxConcurrent = 0;
|
||||
|
||||
const items = [1, 2, 3, 4, 5, 6];
|
||||
const operation = async (item: number) => {
|
||||
concurrent++;
|
||||
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
||||
await delay(50);
|
||||
concurrent--;
|
||||
return item * 2;
|
||||
};
|
||||
|
||||
const results = await parallelLimit(items, operation, 2);
|
||||
|
||||
expect(results).toEqual([2, 4, 6, 8, 10, 12]);
|
||||
expect(maxConcurrent).toBeLessThan(3);
|
||||
expect(maxConcurrent).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('debounceAsync should debounce function calls', async () => {
|
||||
let callCount = 0;
|
||||
const fn = async (value: string) => {
|
||||
callCount++;
|
||||
return value;
|
||||
};
|
||||
|
||||
const debounced = debounceAsync(fn, 50);
|
||||
|
||||
// Make multiple calls quickly
|
||||
debounced('a');
|
||||
debounced('b');
|
||||
debounced('c');
|
||||
const result = await debounced('d');
|
||||
|
||||
// Wait a bit to ensure no more calls
|
||||
await delay(100);
|
||||
|
||||
expect(result).toEqual('d');
|
||||
expect(callCount).toEqual(1); // Only the last call should execute
|
||||
});
|
||||
|
||||
tap.test('AsyncMutex should ensure exclusive access', async () => {
|
||||
const mutex = new AsyncMutex();
|
||||
const results: number[] = [];
|
||||
|
||||
const operation = async (value: number) => {
|
||||
await mutex.runExclusive(async () => {
|
||||
results.push(value);
|
||||
await delay(10);
|
||||
results.push(value * 10);
|
||||
});
|
||||
};
|
||||
|
||||
// Run operations concurrently
|
||||
await Promise.all([
|
||||
operation(1),
|
||||
operation(2),
|
||||
operation(3)
|
||||
]);
|
||||
|
||||
// Results should show sequential execution
|
||||
expect(results).toEqual([1, 10, 2, 20, 3, 30]);
|
||||
});
|
||||
|
||||
tap.test('CircuitBreaker should open after failures', async () => {
|
||||
const breaker = new CircuitBreaker({
|
||||
failureThreshold: 2,
|
||||
resetTimeout: 100
|
||||
});
|
||||
|
||||
let attempt = 0;
|
||||
const failingOperation = async () => {
|
||||
attempt++;
|
||||
throw new Error('Test failure');
|
||||
};
|
||||
|
||||
// First two failures
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(failingOperation);
|
||||
} catch (e) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.isOpen()).toBeTrue();
|
||||
|
||||
// Next attempt should fail immediately
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await breaker.execute(failingOperation);
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error?.message).toEqual('Circuit breaker is open');
|
||||
expect(attempt).toEqual(2); // Operation not called when circuit is open
|
||||
|
||||
// Wait for reset timeout
|
||||
await delay(150);
|
||||
|
||||
// Circuit should be half-open now, allowing one attempt
|
||||
const successOperation = async () => 'success';
|
||||
const result = await breaker.execute(successOperation);
|
||||
|
||||
expect(result).toEqual('success');
|
||||
expect(breaker.getState()).toEqual('closed');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
@@ -1,206 +0,0 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { BinaryHeap } from '../../../ts/core/utils/binary-heap.js';
|
||||
|
||||
interface TestItem {
|
||||
id: string;
|
||||
priority: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
tap.test('should create empty heap', async () => {
|
||||
const heap = new BinaryHeap<number>((a, b) => a - b);
|
||||
|
||||
expect(heap.size).toEqual(0);
|
||||
expect(heap.isEmpty()).toBeTrue();
|
||||
expect(heap.peek()).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should insert and extract in correct order', async () => {
|
||||
const heap = new BinaryHeap<number>((a, b) => a - b);
|
||||
|
||||
heap.insert(5);
|
||||
heap.insert(3);
|
||||
heap.insert(7);
|
||||
heap.insert(1);
|
||||
heap.insert(9);
|
||||
heap.insert(4);
|
||||
|
||||
expect(heap.size).toEqual(6);
|
||||
|
||||
// Extract in ascending order
|
||||
expect(heap.extract()).toEqual(1);
|
||||
expect(heap.extract()).toEqual(3);
|
||||
expect(heap.extract()).toEqual(4);
|
||||
expect(heap.extract()).toEqual(5);
|
||||
expect(heap.extract()).toEqual(7);
|
||||
expect(heap.extract()).toEqual(9);
|
||||
expect(heap.extract()).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should work with custom objects and comparator', async () => {
|
||||
const heap = new BinaryHeap<TestItem>(
|
||||
(a, b) => a.priority - b.priority,
|
||||
(item) => item.id
|
||||
);
|
||||
|
||||
heap.insert({ id: 'a', priority: 5, value: 'five' });
|
||||
heap.insert({ id: 'b', priority: 2, value: 'two' });
|
||||
heap.insert({ id: 'c', priority: 8, value: 'eight' });
|
||||
heap.insert({ id: 'd', priority: 1, value: 'one' });
|
||||
|
||||
const first = heap.extract();
|
||||
expect(first?.priority).toEqual(1);
|
||||
expect(first?.value).toEqual('one');
|
||||
|
||||
const second = heap.extract();
|
||||
expect(second?.priority).toEqual(2);
|
||||
expect(second?.value).toEqual('two');
|
||||
});
|
||||
|
||||
tap.test('should support reverse order (max heap)', async () => {
|
||||
const heap = new BinaryHeap<number>((a, b) => b - a);
|
||||
|
||||
heap.insert(5);
|
||||
heap.insert(3);
|
||||
heap.insert(7);
|
||||
heap.insert(1);
|
||||
heap.insert(9);
|
||||
|
||||
// Extract in descending order
|
||||
expect(heap.extract()).toEqual(9);
|
||||
expect(heap.extract()).toEqual(7);
|
||||
expect(heap.extract()).toEqual(5);
|
||||
});
|
||||
|
||||
tap.test('should extract by predicate', async () => {
|
||||
const heap = new BinaryHeap<TestItem>((a, b) => a.priority - b.priority);
|
||||
|
||||
heap.insert({ id: 'a', priority: 5, value: 'five' });
|
||||
heap.insert({ id: 'b', priority: 2, value: 'two' });
|
||||
heap.insert({ id: 'c', priority: 8, value: 'eight' });
|
||||
|
||||
const extracted = heap.extractIf(item => item.id === 'b');
|
||||
expect(extracted?.id).toEqual('b');
|
||||
expect(heap.size).toEqual(2);
|
||||
|
||||
// Should not find it again
|
||||
const notFound = heap.extractIf(item => item.id === 'b');
|
||||
expect(notFound).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should extract by key', async () => {
|
||||
const heap = new BinaryHeap<TestItem>(
|
||||
(a, b) => a.priority - b.priority,
|
||||
(item) => item.id
|
||||
);
|
||||
|
||||
heap.insert({ id: 'a', priority: 5, value: 'five' });
|
||||
heap.insert({ id: 'b', priority: 2, value: 'two' });
|
||||
heap.insert({ id: 'c', priority: 8, value: 'eight' });
|
||||
|
||||
expect(heap.hasKey('b')).toBeTrue();
|
||||
|
||||
const extracted = heap.extractByKey('b');
|
||||
expect(extracted?.id).toEqual('b');
|
||||
expect(heap.size).toEqual(2);
|
||||
expect(heap.hasKey('b')).toBeFalse();
|
||||
|
||||
// Should not find it again
|
||||
const notFound = heap.extractByKey('b');
|
||||
expect(notFound).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should throw when using key operations without extractKey', async () => {
|
||||
const heap = new BinaryHeap<TestItem>((a, b) => a.priority - b.priority);
|
||||
|
||||
heap.insert({ id: 'a', priority: 5, value: 'five' });
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
heap.extractByKey('a');
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toContain('extractKey function must be provided');
|
||||
});
|
||||
|
||||
tap.test('should handle duplicates correctly', async () => {
|
||||
const heap = new BinaryHeap<number>((a, b) => a - b);
|
||||
|
||||
heap.insert(5);
|
||||
heap.insert(5);
|
||||
heap.insert(5);
|
||||
heap.insert(3);
|
||||
heap.insert(7);
|
||||
|
||||
expect(heap.size).toEqual(5);
|
||||
expect(heap.extract()).toEqual(3);
|
||||
expect(heap.extract()).toEqual(5);
|
||||
expect(heap.extract()).toEqual(5);
|
||||
expect(heap.extract()).toEqual(5);
|
||||
expect(heap.extract()).toEqual(7);
|
||||
});
|
||||
|
||||
tap.test('should convert to array without modifying heap', async () => {
|
||||
const heap = new BinaryHeap<number>((a, b) => a - b);
|
||||
|
||||
heap.insert(5);
|
||||
heap.insert(3);
|
||||
heap.insert(7);
|
||||
|
||||
const array = heap.toArray();
|
||||
expect(array).toContain(3);
|
||||
expect(array).toContain(5);
|
||||
expect(array).toContain(7);
|
||||
expect(array.length).toEqual(3);
|
||||
|
||||
// Heap should still be intact
|
||||
expect(heap.size).toEqual(3);
|
||||
expect(heap.extract()).toEqual(3);
|
||||
});
|
||||
|
||||
tap.test('should clear the heap', async () => {
|
||||
const heap = new BinaryHeap<TestItem>(
|
||||
(a, b) => a.priority - b.priority,
|
||||
(item) => item.id
|
||||
);
|
||||
|
||||
heap.insert({ id: 'a', priority: 5, value: 'five' });
|
||||
heap.insert({ id: 'b', priority: 2, value: 'two' });
|
||||
|
||||
expect(heap.size).toEqual(2);
|
||||
expect(heap.hasKey('a')).toBeTrue();
|
||||
|
||||
heap.clear();
|
||||
|
||||
expect(heap.size).toEqual(0);
|
||||
expect(heap.isEmpty()).toBeTrue();
|
||||
expect(heap.hasKey('a')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should handle complex extraction patterns', async () => {
|
||||
const heap = new BinaryHeap<number>((a, b) => a - b);
|
||||
|
||||
// Insert numbers 1-10 in random order
|
||||
[8, 3, 5, 9, 1, 7, 4, 10, 2, 6].forEach(n => heap.insert(n));
|
||||
|
||||
// Extract some in order
|
||||
expect(heap.extract()).toEqual(1);
|
||||
expect(heap.extract()).toEqual(2);
|
||||
|
||||
// Insert more
|
||||
heap.insert(0);
|
||||
heap.insert(1.5);
|
||||
|
||||
// Continue extracting
|
||||
expect(heap.extract()).toEqual(0);
|
||||
expect(heap.extract()).toEqual(1.5);
|
||||
expect(heap.extract()).toEqual(3);
|
||||
|
||||
// Verify remaining size (10 - 2 extracted + 2 inserted - 3 extracted = 7)
|
||||
expect(heap.size).toEqual(7);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
@@ -1,185 +0,0 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as path from 'path';
|
||||
import { AsyncFileSystem } from '../../../ts/core/utils/fs-utils.js';
|
||||
|
||||
// Use a temporary directory for tests
|
||||
const testDir = path.join(process.cwd(), '.nogit', 'test-fs-utils');
|
||||
const testFile = path.join(testDir, 'test.txt');
|
||||
const testJsonFile = path.join(testDir, 'test.json');
|
||||
|
||||
tap.test('should create and check directory existence', async () => {
|
||||
// Ensure directory
|
||||
await AsyncFileSystem.ensureDir(testDir);
|
||||
|
||||
// Check it exists
|
||||
const exists = await AsyncFileSystem.exists(testDir);
|
||||
expect(exists).toBeTrue();
|
||||
|
||||
// Check it's a directory
|
||||
const isDir = await AsyncFileSystem.isDirectory(testDir);
|
||||
expect(isDir).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should write and read text files', async () => {
|
||||
const testContent = 'Hello, async filesystem!';
|
||||
|
||||
// Write file
|
||||
await AsyncFileSystem.writeFile(testFile, testContent);
|
||||
|
||||
// Check file exists
|
||||
const exists = await AsyncFileSystem.exists(testFile);
|
||||
expect(exists).toBeTrue();
|
||||
|
||||
// Read file
|
||||
const content = await AsyncFileSystem.readFile(testFile);
|
||||
expect(content).toEqual(testContent);
|
||||
|
||||
// Check it's a file
|
||||
const isFile = await AsyncFileSystem.isFile(testFile);
|
||||
expect(isFile).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should write and read JSON files', async () => {
|
||||
const testData = {
|
||||
name: 'Test',
|
||||
value: 42,
|
||||
nested: {
|
||||
array: [1, 2, 3]
|
||||
}
|
||||
};
|
||||
|
||||
// Write JSON
|
||||
await AsyncFileSystem.writeJSON(testJsonFile, testData);
|
||||
|
||||
// Read JSON
|
||||
const readData = await AsyncFileSystem.readJSON(testJsonFile);
|
||||
expect(readData).toEqual(testData);
|
||||
});
|
||||
|
||||
tap.test('should copy files', async () => {
|
||||
const copyFile = path.join(testDir, 'copy.txt');
|
||||
|
||||
// Copy file
|
||||
await AsyncFileSystem.copyFile(testFile, copyFile);
|
||||
|
||||
// Check copy exists
|
||||
const exists = await AsyncFileSystem.exists(copyFile);
|
||||
expect(exists).toBeTrue();
|
||||
|
||||
// Check content matches
|
||||
const content = await AsyncFileSystem.readFile(copyFile);
|
||||
const originalContent = await AsyncFileSystem.readFile(testFile);
|
||||
expect(content).toEqual(originalContent);
|
||||
});
|
||||
|
||||
tap.test('should move files', async () => {
|
||||
const moveFile = path.join(testDir, 'moved.txt');
|
||||
const copyFile = path.join(testDir, 'copy.txt');
|
||||
|
||||
// Move file
|
||||
await AsyncFileSystem.moveFile(copyFile, moveFile);
|
||||
|
||||
// Check moved file exists
|
||||
const movedExists = await AsyncFileSystem.exists(moveFile);
|
||||
expect(movedExists).toBeTrue();
|
||||
|
||||
// Check original doesn't exist
|
||||
const originalExists = await AsyncFileSystem.exists(copyFile);
|
||||
expect(originalExists).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should list files in directory', async () => {
|
||||
const files = await AsyncFileSystem.listFiles(testDir);
|
||||
|
||||
expect(files).toContain('test.txt');
|
||||
expect(files).toContain('test.json');
|
||||
expect(files).toContain('moved.txt');
|
||||
});
|
||||
|
||||
tap.test('should list files with full paths', async () => {
|
||||
const files = await AsyncFileSystem.listFilesFullPath(testDir);
|
||||
|
||||
const fileNames = files.map(f => path.basename(f));
|
||||
expect(fileNames).toContain('test.txt');
|
||||
expect(fileNames).toContain('test.json');
|
||||
|
||||
// All paths should be absolute
|
||||
files.forEach(file => {
|
||||
expect(path.isAbsolute(file)).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should get file stats', async () => {
|
||||
const stats = await AsyncFileSystem.getStats(testFile);
|
||||
|
||||
expect(stats).not.toBeNull();
|
||||
expect(stats?.isFile()).toBeTrue();
|
||||
expect(stats?.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('should handle non-existent files gracefully', async () => {
|
||||
const nonExistent = path.join(testDir, 'does-not-exist.txt');
|
||||
|
||||
// Check existence
|
||||
const exists = await AsyncFileSystem.exists(nonExistent);
|
||||
expect(exists).toBeFalse();
|
||||
|
||||
// Get stats should return null
|
||||
const stats = await AsyncFileSystem.getStats(nonExistent);
|
||||
expect(stats).toBeNull();
|
||||
|
||||
// Remove should not throw
|
||||
await AsyncFileSystem.remove(nonExistent);
|
||||
});
|
||||
|
||||
tap.test('should remove files', async () => {
|
||||
// Remove a file
|
||||
await AsyncFileSystem.remove(testFile);
|
||||
|
||||
// Check it's gone
|
||||
const exists = await AsyncFileSystem.exists(testFile);
|
||||
expect(exists).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should ensure file exists', async () => {
|
||||
const ensureFile = path.join(testDir, 'ensure.txt');
|
||||
|
||||
// Ensure file
|
||||
await AsyncFileSystem.ensureFile(ensureFile);
|
||||
|
||||
// Check it exists
|
||||
const exists = await AsyncFileSystem.exists(ensureFile);
|
||||
expect(exists).toBeTrue();
|
||||
|
||||
// Check it's empty
|
||||
const content = await AsyncFileSystem.readFile(ensureFile);
|
||||
expect(content).toEqual('');
|
||||
});
|
||||
|
||||
tap.test('should recursively list files', async () => {
|
||||
// Create subdirectory with file
|
||||
const subDir = path.join(testDir, 'subdir');
|
||||
const subFile = path.join(subDir, 'nested.txt');
|
||||
|
||||
await AsyncFileSystem.ensureDir(subDir);
|
||||
await AsyncFileSystem.writeFile(subFile, 'nested content');
|
||||
|
||||
// List recursively
|
||||
const files = await AsyncFileSystem.listFilesRecursive(testDir);
|
||||
|
||||
// Should include files from subdirectory
|
||||
const fileNames = files.map(f => path.relative(testDir, f));
|
||||
expect(fileNames).toContain('test.json');
|
||||
expect(fileNames).toContain(path.join('subdir', 'nested.txt'));
|
||||
});
|
||||
|
||||
tap.test('should clean up test directory', async () => {
|
||||
// Remove entire test directory
|
||||
await AsyncFileSystem.removeDir(testDir);
|
||||
|
||||
// Check it's gone
|
||||
const exists = await AsyncFileSystem.exists(testDir);
|
||||
expect(exists).toBeFalse();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
@@ -1,156 +0,0 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { IpUtils } from '../../../ts/core/utils/ip-utils.js';
|
||||
|
||||
tap.test('ip-utils - normalizeIP', async () => {
|
||||
// IPv4 normalization
|
||||
const ipv4Variants = IpUtils.normalizeIP('127.0.0.1');
|
||||
expect(ipv4Variants).toEqual(['127.0.0.1', '::ffff:127.0.0.1']);
|
||||
|
||||
// IPv6-mapped IPv4 normalization
|
||||
const ipv6MappedVariants = IpUtils.normalizeIP('::ffff:127.0.0.1');
|
||||
expect(ipv6MappedVariants).toEqual(['::ffff:127.0.0.1', '127.0.0.1']);
|
||||
|
||||
// IPv6 normalization
|
||||
const ipv6Variants = IpUtils.normalizeIP('::1');
|
||||
expect(ipv6Variants).toEqual(['::1']);
|
||||
|
||||
// Invalid/empty input handling
|
||||
expect(IpUtils.normalizeIP('')).toEqual([]);
|
||||
expect(IpUtils.normalizeIP(null as any)).toEqual([]);
|
||||
expect(IpUtils.normalizeIP(undefined as any)).toEqual([]);
|
||||
});
|
||||
|
||||
tap.test('ip-utils - isGlobIPMatch', async () => {
|
||||
// Direct matches
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['127.0.0.1'])).toEqual(true);
|
||||
expect(IpUtils.isGlobIPMatch('::1', ['::1'])).toEqual(true);
|
||||
|
||||
// Wildcard matches
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['127.0.0.*'])).toEqual(true);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['127.0.*.*'])).toEqual(true);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['127.*.*.*'])).toEqual(true);
|
||||
|
||||
// IPv4-mapped IPv6 handling
|
||||
expect(IpUtils.isGlobIPMatch('::ffff:127.0.0.1', ['127.0.0.1'])).toEqual(true);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['::ffff:127.0.0.1'])).toEqual(true);
|
||||
|
||||
// Match multiple patterns
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['10.0.0.1', '127.0.0.1', '192.168.1.1'])).toEqual(true);
|
||||
|
||||
// Non-matching patterns
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['10.0.0.1'])).toEqual(false);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['128.0.0.1'])).toEqual(false);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', ['127.0.0.2'])).toEqual(false);
|
||||
|
||||
// Edge cases
|
||||
expect(IpUtils.isGlobIPMatch('', ['127.0.0.1'])).toEqual(false);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', [])).toEqual(false);
|
||||
expect(IpUtils.isGlobIPMatch('127.0.0.1', null as any)).toEqual(false);
|
||||
expect(IpUtils.isGlobIPMatch(null as any, ['127.0.0.1'])).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('ip-utils - isIPAuthorized', async () => {
|
||||
// Basic tests to check the core functionality works
|
||||
// No restrictions - all IPs allowed
|
||||
expect(IpUtils.isIPAuthorized('127.0.0.1')).toEqual(true);
|
||||
|
||||
// Basic blocked IP test
|
||||
const blockedIP = '8.8.8.8';
|
||||
const blockedIPs = [blockedIP];
|
||||
expect(IpUtils.isIPAuthorized(blockedIP, [], blockedIPs)).toEqual(false);
|
||||
|
||||
// Basic allowed IP test
|
||||
const allowedIP = '10.0.0.1';
|
||||
const allowedIPs = [allowedIP];
|
||||
expect(IpUtils.isIPAuthorized(allowedIP, allowedIPs)).toEqual(true);
|
||||
expect(IpUtils.isIPAuthorized('192.168.1.1', allowedIPs)).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('ip-utils - isPrivateIP', async () => {
|
||||
// Private IPv4 ranges
|
||||
expect(IpUtils.isPrivateIP('10.0.0.1')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('172.16.0.1')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('172.31.255.255')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('192.168.0.1')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('127.0.0.1')).toEqual(true);
|
||||
|
||||
// Public IPv4 addresses
|
||||
expect(IpUtils.isPrivateIP('8.8.8.8')).toEqual(false);
|
||||
expect(IpUtils.isPrivateIP('203.0.113.1')).toEqual(false);
|
||||
|
||||
// IPv4-mapped IPv6 handling
|
||||
expect(IpUtils.isPrivateIP('::ffff:10.0.0.1')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('::ffff:8.8.8.8')).toEqual(false);
|
||||
|
||||
// Private IPv6 addresses
|
||||
expect(IpUtils.isPrivateIP('::1')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('fd00::')).toEqual(true);
|
||||
expect(IpUtils.isPrivateIP('fe80::1')).toEqual(true);
|
||||
|
||||
// Public IPv6 addresses
|
||||
expect(IpUtils.isPrivateIP('2001:db8::1')).toEqual(false);
|
||||
|
||||
// Edge cases
|
||||
expect(IpUtils.isPrivateIP('')).toEqual(false);
|
||||
expect(IpUtils.isPrivateIP(null as any)).toEqual(false);
|
||||
expect(IpUtils.isPrivateIP(undefined as any)).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('ip-utils - isPublicIP', async () => {
|
||||
// Public IPv4 addresses
|
||||
expect(IpUtils.isPublicIP('8.8.8.8')).toEqual(true);
|
||||
expect(IpUtils.isPublicIP('203.0.113.1')).toEqual(true);
|
||||
|
||||
// Private IPv4 ranges
|
||||
expect(IpUtils.isPublicIP('10.0.0.1')).toEqual(false);
|
||||
expect(IpUtils.isPublicIP('172.16.0.1')).toEqual(false);
|
||||
expect(IpUtils.isPublicIP('192.168.0.1')).toEqual(false);
|
||||
expect(IpUtils.isPublicIP('127.0.0.1')).toEqual(false);
|
||||
|
||||
// Public IPv6 addresses
|
||||
expect(IpUtils.isPublicIP('2001:db8::1')).toEqual(true);
|
||||
|
||||
// Private IPv6 addresses
|
||||
expect(IpUtils.isPublicIP('::1')).toEqual(false);
|
||||
expect(IpUtils.isPublicIP('fd00::')).toEqual(false);
|
||||
expect(IpUtils.isPublicIP('fe80::1')).toEqual(false);
|
||||
|
||||
// Edge cases - the implementation treats these as non-private, which is technically correct but might not be what users expect
|
||||
const emptyResult = IpUtils.isPublicIP('');
|
||||
expect(emptyResult).toEqual(true);
|
||||
|
||||
const nullResult = IpUtils.isPublicIP(null as any);
|
||||
expect(nullResult).toEqual(true);
|
||||
|
||||
const undefinedResult = IpUtils.isPublicIP(undefined as any);
|
||||
expect(undefinedResult).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('ip-utils - cidrToGlobPatterns', async () => {
|
||||
// Class C network
|
||||
const classC = IpUtils.cidrToGlobPatterns('192.168.1.0/24');
|
||||
expect(classC).toEqual(['192.168.1.*']);
|
||||
|
||||
// Class B network
|
||||
const classB = IpUtils.cidrToGlobPatterns('172.16.0.0/16');
|
||||
expect(classB).toEqual(['172.16.*.*']);
|
||||
|
||||
// Class A network
|
||||
const classA = IpUtils.cidrToGlobPatterns('10.0.0.0/8');
|
||||
expect(classA).toEqual(['10.*.*.*']);
|
||||
|
||||
// Small subnet (/28 = 16 addresses)
|
||||
const smallSubnet = IpUtils.cidrToGlobPatterns('192.168.1.0/28');
|
||||
expect(smallSubnet.length).toEqual(16);
|
||||
expect(smallSubnet).toContain('192.168.1.0');
|
||||
expect(smallSubnet).toContain('192.168.1.15');
|
||||
|
||||
// Invalid inputs
|
||||
expect(IpUtils.cidrToGlobPatterns('')).toEqual([]);
|
||||
expect(IpUtils.cidrToGlobPatterns('192.168.1.0')).toEqual([]);
|
||||
expect(IpUtils.cidrToGlobPatterns('192.168.1.0/')).toEqual([]);
|
||||
expect(IpUtils.cidrToGlobPatterns('192.168.1.0/33')).toEqual([]);
|
||||
expect(IpUtils.cidrToGlobPatterns('invalid/24')).toEqual([]);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -1,252 +0,0 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { LifecycleComponent } from '../../../ts/core/utils/lifecycle-component.js';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
// Test implementation of LifecycleComponent
|
||||
class TestComponent extends LifecycleComponent {
|
||||
public timerCallCount = 0;
|
||||
public intervalCallCount = 0;
|
||||
public cleanupCalled = false;
|
||||
public testEmitter = new EventEmitter();
|
||||
public listenerCallCount = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setupTimers();
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupTimers() {
|
||||
// Set up a timeout
|
||||
this.setTimeout(() => {
|
||||
this.timerCallCount++;
|
||||
}, 100);
|
||||
|
||||
// Set up an interval
|
||||
this.setInterval(() => {
|
||||
this.intervalCallCount++;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.addEventListener(this.testEmitter, 'test-event', () => {
|
||||
this.listenerCallCount++;
|
||||
});
|
||||
}
|
||||
|
||||
protected async onCleanup(): Promise<void> {
|
||||
this.cleanupCalled = true;
|
||||
}
|
||||
|
||||
// Expose protected methods for testing
|
||||
public testSetTimeout(handler: Function, timeout: number): NodeJS.Timeout {
|
||||
return this.setTimeout(handler, timeout);
|
||||
}
|
||||
|
||||
public testSetInterval(handler: Function, interval: number): NodeJS.Timeout {
|
||||
return this.setInterval(handler, interval);
|
||||
}
|
||||
|
||||
public testClearTimeout(timer: NodeJS.Timeout): void {
|
||||
return this.clearTimeout(timer);
|
||||
}
|
||||
|
||||
public testClearInterval(timer: NodeJS.Timeout): void {
|
||||
return this.clearInterval(timer);
|
||||
}
|
||||
|
||||
public testAddEventListener(target: any, event: string, handler: Function, options?: { once?: boolean }): void {
|
||||
return this.addEventListener(target, event, handler, options);
|
||||
}
|
||||
|
||||
public testIsShuttingDown(): boolean {
|
||||
return this.isShuttingDownState();
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('should manage timers properly', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
// Wait for timers to fire
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(component.timerCallCount).toEqual(1);
|
||||
expect(component.intervalCallCount).toBeGreaterThan(2);
|
||||
|
||||
await component.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should manage event listeners properly', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
// Emit events
|
||||
component.testEmitter.emit('test-event');
|
||||
component.testEmitter.emit('test-event');
|
||||
|
||||
expect(component.listenerCallCount).toEqual(2);
|
||||
|
||||
// Cleanup and verify listeners are removed
|
||||
await component.cleanup();
|
||||
|
||||
component.testEmitter.emit('test-event');
|
||||
expect(component.listenerCallCount).toEqual(2); // Should not increase
|
||||
});
|
||||
|
||||
tap.test('should prevent timer execution after cleanup', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
let laterCallCount = 0;
|
||||
component.testSetTimeout(() => {
|
||||
laterCallCount++;
|
||||
}, 100);
|
||||
|
||||
// Cleanup immediately
|
||||
await component.cleanup();
|
||||
|
||||
// Wait for timer that would have fired
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
expect(laterCallCount).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should handle child components', async () => {
|
||||
class ParentComponent extends LifecycleComponent {
|
||||
public child: TestComponent;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.child = new TestComponent();
|
||||
this.registerChildComponent(this.child);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = new ParentComponent();
|
||||
|
||||
// Wait for child timers
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(parent.child.timerCallCount).toEqual(1);
|
||||
|
||||
// Cleanup parent should cleanup child
|
||||
await parent.cleanup();
|
||||
|
||||
expect(parent.child.cleanupCalled).toBeTrue();
|
||||
expect(parent.child.testIsShuttingDown()).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should handle multiple cleanup calls gracefully', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
// Call cleanup multiple times
|
||||
const promises = [
|
||||
component.cleanup(),
|
||||
component.cleanup(),
|
||||
component.cleanup()
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Should only clean up once
|
||||
expect(component.cleanupCalled).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should clear specific timers', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
let callCount = 0;
|
||||
const timer = component.testSetTimeout(() => {
|
||||
callCount++;
|
||||
}, 100);
|
||||
|
||||
// Clear the timer
|
||||
component.testClearTimeout(timer);
|
||||
|
||||
// Wait and verify it didn't fire
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
expect(callCount).toEqual(0);
|
||||
|
||||
await component.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should clear specific intervals', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
let callCount = 0;
|
||||
const interval = component.testSetInterval(() => {
|
||||
callCount++;
|
||||
}, 50);
|
||||
|
||||
// Let it run a bit
|
||||
await new Promise(resolve => setTimeout(resolve, 120));
|
||||
|
||||
const countBeforeClear = callCount;
|
||||
expect(countBeforeClear).toBeGreaterThan(1);
|
||||
|
||||
// Clear the interval
|
||||
component.testClearInterval(interval);
|
||||
|
||||
// Wait and verify it stopped
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(callCount).toEqual(countBeforeClear);
|
||||
|
||||
await component.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should handle once event listeners', async () => {
|
||||
const component = new TestComponent();
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
let callCount = 0;
|
||||
const handler = () => {
|
||||
callCount++;
|
||||
};
|
||||
|
||||
component.testAddEventListener(emitter, 'once-event', handler, { once: true });
|
||||
|
||||
// Check listener count before emit
|
||||
const beforeCount = emitter.listenerCount('once-event');
|
||||
expect(beforeCount).toEqual(1);
|
||||
|
||||
// Emit once - the listener should fire and auto-remove
|
||||
emitter.emit('once-event');
|
||||
expect(callCount).toEqual(1);
|
||||
|
||||
// Check listener was auto-removed
|
||||
const afterCount = emitter.listenerCount('once-event');
|
||||
expect(afterCount).toEqual(0);
|
||||
|
||||
// Emit again - should not increase count
|
||||
emitter.emit('once-event');
|
||||
expect(callCount).toEqual(1);
|
||||
|
||||
await component.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should not create timers when shutting down', async () => {
|
||||
const component = new TestComponent();
|
||||
|
||||
// Start cleanup
|
||||
const cleanupPromise = component.cleanup();
|
||||
|
||||
// Try to create timers during shutdown
|
||||
let timerFired = false;
|
||||
let intervalFired = false;
|
||||
|
||||
component.testSetTimeout(() => {
|
||||
timerFired = true;
|
||||
}, 10);
|
||||
|
||||
component.testSetInterval(() => {
|
||||
intervalFired = true;
|
||||
}, 10);
|
||||
|
||||
await cleanupPromise;
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(timerFired).toBeFalse();
|
||||
expect(intervalFired).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -1,158 +0,0 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { SharedSecurityManager } from '../../../ts/core/utils/shared-security-manager.js';
|
||||
import type { IRouteConfig, IRouteContext } from '../../../ts/proxies/smart-proxy/models/route-types.js';
|
||||
|
||||
// Test security manager
|
||||
tap.test('Shared Security Manager', async () => {
|
||||
let securityManager: SharedSecurityManager;
|
||||
|
||||
// Set up a new security manager for each test
|
||||
securityManager = new SharedSecurityManager({
|
||||
maxConnectionsPerIP: 5,
|
||||
connectionRateLimitPerMinute: 10
|
||||
});
|
||||
|
||||
tap.test('should validate IPs correctly', async () => {
|
||||
// Should allow IPs under connection limit
|
||||
expect(securityManager.validateIP('192.168.1.1').allowed).toBeTrue();
|
||||
|
||||
// Track multiple connections
|
||||
for (let i = 0; i < 4; i++) {
|
||||
securityManager.trackConnectionByIP('192.168.1.1', `conn_${i}`);
|
||||
}
|
||||
|
||||
// Should still allow IPs under connection limit
|
||||
expect(securityManager.validateIP('192.168.1.1').allowed).toBeTrue();
|
||||
|
||||
// Add one more to reach the limit
|
||||
securityManager.trackConnectionByIP('192.168.1.1', 'conn_4');
|
||||
|
||||
// Should now block IPs over connection limit
|
||||
expect(securityManager.validateIP('192.168.1.1').allowed).toBeFalse();
|
||||
|
||||
// Remove a connection
|
||||
securityManager.removeConnectionByIP('192.168.1.1', 'conn_0');
|
||||
|
||||
// Should allow again after connection is removed
|
||||
expect(securityManager.validateIP('192.168.1.1').allowed).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should authorize IPs based on allow/block lists', async () => {
|
||||
// Test with allow list only
|
||||
expect(securityManager.isIPAuthorized('192.168.1.1', ['192.168.1.*'])).toBeTrue();
|
||||
expect(securityManager.isIPAuthorized('192.168.2.1', ['192.168.1.*'])).toBeFalse();
|
||||
|
||||
// Test with block list
|
||||
expect(securityManager.isIPAuthorized('192.168.1.5', ['*'], ['192.168.1.5'])).toBeFalse();
|
||||
expect(securityManager.isIPAuthorized('192.168.1.1', ['*'], ['192.168.1.5'])).toBeTrue();
|
||||
|
||||
// Test with both allow and block lists
|
||||
expect(securityManager.isIPAuthorized('192.168.1.1', ['192.168.1.*'], ['192.168.1.5'])).toBeTrue();
|
||||
expect(securityManager.isIPAuthorized('192.168.1.5', ['192.168.1.*'], ['192.168.1.5'])).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should validate route access', async () => {
|
||||
const route: IRouteConfig = {
|
||||
match: {
|
||||
ports: [8080]
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'target.com', port: 443 }]
|
||||
},
|
||||
security: {
|
||||
ipAllowList: ['10.0.0.*', '192.168.1.*'],
|
||||
ipBlockList: ['192.168.1.100'],
|
||||
maxConnections: 3
|
||||
}
|
||||
};
|
||||
|
||||
const allowedContext: IRouteContext = {
|
||||
clientIp: '192.168.1.1',
|
||||
port: 8080,
|
||||
serverIp: '127.0.0.1',
|
||||
isTls: false,
|
||||
timestamp: Date.now(),
|
||||
connectionId: 'test_conn_1'
|
||||
};
|
||||
|
||||
const blockedByIPContext: IRouteContext = {
|
||||
...allowedContext,
|
||||
clientIp: '192.168.1.100'
|
||||
};
|
||||
|
||||
const blockedByRangeContext: IRouteContext = {
|
||||
...allowedContext,
|
||||
clientIp: '172.16.0.1'
|
||||
};
|
||||
|
||||
const blockedByMaxConnectionsContext: IRouteContext = {
|
||||
...allowedContext,
|
||||
connectionId: 'test_conn_4'
|
||||
};
|
||||
|
||||
expect(securityManager.isAllowed(route, allowedContext)).toBeTrue();
|
||||
expect(securityManager.isAllowed(route, blockedByIPContext)).toBeFalse();
|
||||
expect(securityManager.isAllowed(route, blockedByRangeContext)).toBeFalse();
|
||||
|
||||
// Test max connections for route - assuming implementation has been updated
|
||||
if ((securityManager as any).trackConnectionByRoute) {
|
||||
(securityManager as any).trackConnectionByRoute(route, 'conn_1');
|
||||
(securityManager as any).trackConnectionByRoute(route, 'conn_2');
|
||||
(securityManager as any).trackConnectionByRoute(route, 'conn_3');
|
||||
|
||||
// Should now block due to max connections
|
||||
expect(securityManager.isAllowed(route, blockedByMaxConnectionsContext)).toBeFalse();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should clean up expired entries', async () => {
|
||||
const route: IRouteConfig = {
|
||||
match: {
|
||||
ports: [8080]
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'target.com', port: 443 }]
|
||||
},
|
||||
security: {
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
maxRequests: 5,
|
||||
window: 60 // 60 seconds
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const context: IRouteContext = {
|
||||
clientIp: '192.168.1.1',
|
||||
port: 8080,
|
||||
serverIp: '127.0.0.1',
|
||||
isTls: false,
|
||||
timestamp: Date.now(),
|
||||
connectionId: 'test_conn_1'
|
||||
};
|
||||
|
||||
// Test rate limiting if method exists
|
||||
if ((securityManager as any).checkRateLimit) {
|
||||
// Add 5 attempts (max allowed)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expect((securityManager as any).checkRateLimit(route, context)).toBeTrue();
|
||||
}
|
||||
|
||||
// Should now be blocked
|
||||
expect((securityManager as any).checkRateLimit(route, context)).toBeFalse();
|
||||
|
||||
// Force cleanup (normally runs periodically)
|
||||
if ((securityManager as any).cleanup) {
|
||||
(securityManager as any).cleanup();
|
||||
}
|
||||
|
||||
// Should still be blocked since entries are not expired yet
|
||||
expect((securityManager as any).checkRateLimit(route, context)).toBeFalse();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export test runner
|
||||
export default tap.start();
|
||||
@@ -1,302 +0,0 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { ValidationUtils } from '../../../ts/core/utils/validation-utils.js';
|
||||
import type { IDomainOptions, IAcmeOptions } from '../../../ts/core/models/common-types.js';
|
||||
|
||||
tap.test('validation-utils - isValidPort', async () => {
|
||||
// Valid port values
|
||||
expect(ValidationUtils.isValidPort(1)).toEqual(true);
|
||||
expect(ValidationUtils.isValidPort(80)).toEqual(true);
|
||||
expect(ValidationUtils.isValidPort(443)).toEqual(true);
|
||||
expect(ValidationUtils.isValidPort(8080)).toEqual(true);
|
||||
expect(ValidationUtils.isValidPort(65535)).toEqual(true);
|
||||
|
||||
// Invalid port values
|
||||
expect(ValidationUtils.isValidPort(0)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPort(-1)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPort(65536)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPort(80.5)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPort(NaN)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPort(null as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPort(undefined as any)).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('validation-utils - isValidDomainName', async () => {
|
||||
// Valid domain names
|
||||
expect(ValidationUtils.isValidDomainName('example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidDomainName('sub.example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidDomainName('*.example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidDomainName('a-hyphenated-domain.example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidDomainName('example123.com')).toEqual(true);
|
||||
|
||||
// Invalid domain names
|
||||
expect(ValidationUtils.isValidDomainName('')).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName(null as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName(undefined as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName('-invalid.com')).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName('invalid-.com')).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName('inv@lid.com')).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName('example')).toEqual(false);
|
||||
expect(ValidationUtils.isValidDomainName('example.')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('validation-utils - isValidEmail', async () => {
|
||||
// Valid email addresses
|
||||
expect(ValidationUtils.isValidEmail('user@example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidEmail('admin@sub.example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidEmail('first.last@example.com')).toEqual(true);
|
||||
expect(ValidationUtils.isValidEmail('user+tag@example.com')).toEqual(true);
|
||||
|
||||
// Invalid email addresses
|
||||
expect(ValidationUtils.isValidEmail('')).toEqual(false);
|
||||
expect(ValidationUtils.isValidEmail(null as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidEmail(undefined as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidEmail('user')).toEqual(false);
|
||||
expect(ValidationUtils.isValidEmail('user@')).toEqual(false);
|
||||
expect(ValidationUtils.isValidEmail('@example.com')).toEqual(false);
|
||||
expect(ValidationUtils.isValidEmail('user example.com')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('validation-utils - isValidCertificate', async () => {
|
||||
// Valid certificate format
|
||||
const validCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIUJlq+zz9CO2E91rlD4vhx0CX1Z/kwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzAxMDEwMDAwMDBaFw0yNDAx
|
||||
MDEwMDAwMDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQC0aQeHIV9vQpZ4UVwW/xhx9zl01UbppLXdoqe3NP9x
|
||||
KfXTCB1YbtJ4GgKIlQqHGLGsLI5ZOE7KxmJeGEwK7ueP4f3WkUlM5C5yTbZ5hSUo
|
||||
R+OFnszFRJJiBXJlw57YAW9+zqKQHYxwve64O64dlgw6pekDYJhXtrUUZ78Lz0GX
|
||||
veJvCrci1M4Xk6/7/p1Ii9PNmbPKqHafdmkFLf6TXiWPuRDhPuHW7cXyE8xD5ahr
|
||||
NsDuwJyRUk+GS4/oJg0TqLSiD0IPxDH50V5MSfUIB82i+lc1t+OAGwLhjUDuQmJi
|
||||
Pv1+9Zvv+HA5PXBCsGXnSADrOOUO6t9q5R9PXbSvAgMBAAGjUzBRMB0GA1UdDgQW
|
||||
BBQEtdtBhH/z1XyIf+y+5O9ErDGCVjAfBgNVHSMEGDAWgBQEtdtBhH/z1XyIf+y+
|
||||
5O9ErDGCVjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBmJyQ0
|
||||
r0pBJkYJJVDJ6i3WMoEEFTD8MEUkWxASHRnuMzm7XlZ8WS1HvbEWF0+WfJPCYHnk
|
||||
tGbvUFGaZ4qUxZ4Ip2mvKXoeYTJCZRxxhHeSVWnZZu0KS3X7xVAFwQYQNhdLOqP8
|
||||
XOHyLhHV/1/kcFd3GvKKjXxE79jUUZ/RXHZ/IY50KvxGzWc/5ZOFYrPEW1/rNlRo
|
||||
7ixXo1hNnBQsG1YoFAxTBGegdTFJeTYHYjZZ5XlRvY2aBq6QveRbJGJLcPm1UQMd
|
||||
HQYxacbWSVAQf3ltYwSH+y3a97C5OsJJiQXpRRJlQKL3txklzcpg3E5swhr63bM2
|
||||
jUoNXr5G5Q5h3GD5
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
expect(ValidationUtils.isValidCertificate(validCert)).toEqual(true);
|
||||
|
||||
// Invalid certificate format
|
||||
expect(ValidationUtils.isValidCertificate('')).toEqual(false);
|
||||
expect(ValidationUtils.isValidCertificate(null as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidCertificate(undefined as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidCertificate('invalid certificate')).toEqual(false);
|
||||
expect(ValidationUtils.isValidCertificate('-----BEGIN CERTIFICATE-----')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('validation-utils - isValidPrivateKey', async () => {
|
||||
// Valid private key format
|
||||
const validKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0aQeHIV9vQpZ4
|
||||
UVwW/xhx9zl01UbppLXdoqe3NP9xKfXTCB1YbtJ4GgKIlQqHGLGsLI5ZOE7KxmJe
|
||||
GEwK7ueP4f3WkUlM5C5yTbZ5hSUoR+OFnszFRJJiBXJlw57YAW9+zqKQHYxwve64
|
||||
O64dlgw6pekDYJhXtrUUZ78Lz0GXveJvCrci1M4Xk6/7/p1Ii9PNmbPKqHafdmkF
|
||||
Lf6TXiWPuRDhPuHW7cXyE8xD5ahrNsDuwJyRUk+GS4/oJg0TqLSiD0IPxDH50V5M
|
||||
SfUIB82i+lc1t+OAGwLhjUDuQmJiPv1+9Zvv+HA5PXBCsGXnSADrOOUO6t9q5R9P
|
||||
XbSvAgMBAAECggEADw8Xx9iEv3FvS8hYIRn2ZWM8ObRgbHkFN92NJ/5RvUwgyV03
|
||||
gG8GwVN+7IsVLnIQRyIYEGGJ0ZLZFIq7//Jy0jYUgEGLmXxknuZQn1cQEqqYVyBr
|
||||
G9JrfKkXaDEoP/bZBMvZ0KEO2C9Vq6mY8M0h0GxDT2y6UQnQYjH3+H6Rvhbhh+Ld
|
||||
n8lCJqWoW1t9GOUZ4xLsZ5jEDibcMJJzLBWYRxgHWyECK31/VtEQDKFiUcymrJ3I
|
||||
/zoDEDGbp1gdJHvlCxfSLJ2za7ErtRKRXYFRhZ9QkNSXl1pVFMqRQkedXIcA1/Cs
|
||||
VpUxiIE2JA3hSrv2csjmXoGJKDLVCvZ3CFxKL3u/AQKBgQDf6MxHXN3IDuJNrJP7
|
||||
0gyRbO5d6vcvP/8qiYjtEt2xB2MNt5jDz9Bxl6aKEdNW2+UE0rvXXT6KAMZv9LiF
|
||||
hxr5qiJmmSB8OeGfr0W4FCixGN4BkRNwfT1gUqZgQOrfMOLHNXOksc1CJwHJfROV
|
||||
h6AH+gjtF2BCXnVEHcqtRklk4QKBgQDOOYnLJn1CwgFAyRUYK8LQYKnrLp2cGn7N
|
||||
YH0SLf+VnCu7RCeNr3dm9FoHBCynjkx+qv9kGvCaJuZqEJ7+7IimNUZfDjwXTOJ+
|
||||
pzs8kEPN5EQOcbkmYCTQyOA0YeBuEXcv5xIZRZUYQvKg1xXOe/JhAQ4siVIMhgQL
|
||||
2XR3QwzRDwKBgB7rjZs2VYnuVExGr74lUUAGoZ71WCgt9Du9aYGJfNUriDtTEWAd
|
||||
VT5sKgVqpRwkY/zXujdxGr+K8DZu4vSdHBLcDLQsEBvRZIILTzjwXBRPGMnVe95v
|
||||
Q90+vytbmHshlkbMaVRNQxCjdbf7LbQbLecgRt+5BKxHVwL4u3BZNIqhAoGAas4f
|
||||
PoPOdFfKAMKZL7FLGMhEXLyFsg1JcGRfmByxTNgOJKXpYv5Hl7JLYOvfaiUOUYKI
|
||||
5Dnh5yLdFOaOjnB3iP0KEiSVEwZK0/Vna5JkzFTqImK9QD3SQCtQLXHJLD52EPFR
|
||||
9gRa8N5k68+mIzGDEzPBoC1AajbXFGPxNOwaQQ0CgYEAq0dPYK0TTv3Yez27LzVy
|
||||
RbHkwpE+df4+KhpHbCzUKzfQYo4WTahlR6IzhpOyVQKIptkjuTDyQzkmt0tXEGw3
|
||||
/M3yHa1FcY9IzPrHXHJoOeU1r9ay0GOQUi4FxKkYYWxUCtjOi5xlUxI0ABD8vGGR
|
||||
QbKMrQXRgLd/84nDnY2cYzA=
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
expect(ValidationUtils.isValidPrivateKey(validKey)).toEqual(true);
|
||||
|
||||
// Invalid private key format
|
||||
expect(ValidationUtils.isValidPrivateKey('')).toEqual(false);
|
||||
expect(ValidationUtils.isValidPrivateKey(null as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPrivateKey(undefined as any)).toEqual(false);
|
||||
expect(ValidationUtils.isValidPrivateKey('invalid key')).toEqual(false);
|
||||
expect(ValidationUtils.isValidPrivateKey('-----BEGIN PRIVATE KEY-----')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('validation-utils - validateDomainOptions', async () => {
|
||||
// Valid domain options
|
||||
const validDomainOptions: IDomainOptions = {
|
||||
domainName: 'example.com',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(validDomainOptions).isValid).toEqual(true);
|
||||
|
||||
// Valid domain options with forward
|
||||
const validDomainOptionsWithForward: IDomainOptions = {
|
||||
domainName: 'example.com',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true,
|
||||
forward: {
|
||||
ip: '127.0.0.1',
|
||||
port: 8080
|
||||
}
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(validDomainOptionsWithForward).isValid).toEqual(true);
|
||||
|
||||
// Invalid domain options - no domain name
|
||||
const invalidDomainOptions1: IDomainOptions = {
|
||||
domainName: '',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(invalidDomainOptions1).isValid).toEqual(false);
|
||||
|
||||
// Invalid domain options - invalid domain name
|
||||
const invalidDomainOptions2: IDomainOptions = {
|
||||
domainName: 'inv@lid.com',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(invalidDomainOptions2).isValid).toEqual(false);
|
||||
|
||||
// Invalid domain options - forward missing ip
|
||||
const invalidDomainOptions3: IDomainOptions = {
|
||||
domainName: 'example.com',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true,
|
||||
forward: {
|
||||
ip: '',
|
||||
port: 8080
|
||||
}
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(invalidDomainOptions3).isValid).toEqual(false);
|
||||
|
||||
// Invalid domain options - forward missing port
|
||||
const invalidDomainOptions4: IDomainOptions = {
|
||||
domainName: 'example.com',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true,
|
||||
forward: {
|
||||
ip: '127.0.0.1',
|
||||
port: null as any
|
||||
}
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(invalidDomainOptions4).isValid).toEqual(false);
|
||||
|
||||
// Invalid domain options - invalid forward port
|
||||
const invalidDomainOptions5: IDomainOptions = {
|
||||
domainName: 'example.com',
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true,
|
||||
forward: {
|
||||
ip: '127.0.0.1',
|
||||
port: 99999
|
||||
}
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateDomainOptions(invalidDomainOptions5).isValid).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('validation-utils - validateAcmeOptions', async () => {
|
||||
// Valid ACME options
|
||||
const validAcmeOptions: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: 'admin@example.com',
|
||||
port: 80,
|
||||
httpsRedirectPort: 443,
|
||||
useProduction: false,
|
||||
renewThresholdDays: 30,
|
||||
renewCheckIntervalHours: 24,
|
||||
certificateStore: './certs'
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateAcmeOptions(validAcmeOptions).isValid).toEqual(true);
|
||||
|
||||
// ACME disabled - should be valid regardless of other options
|
||||
const disabledAcmeOptions: IAcmeOptions = {
|
||||
enabled: false
|
||||
};
|
||||
|
||||
// Don't need to verify other fields when ACME is disabled
|
||||
const disabledResult = ValidationUtils.validateAcmeOptions(disabledAcmeOptions);
|
||||
expect(disabledResult.isValid).toEqual(true);
|
||||
|
||||
// Invalid ACME options - missing email
|
||||
const invalidAcmeOptions1: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: '',
|
||||
port: 80
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateAcmeOptions(invalidAcmeOptions1).isValid).toEqual(false);
|
||||
|
||||
// Invalid ACME options - invalid email
|
||||
const invalidAcmeOptions2: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: 'invalid-email',
|
||||
port: 80
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateAcmeOptions(invalidAcmeOptions2).isValid).toEqual(false);
|
||||
|
||||
// Invalid ACME options - invalid port
|
||||
const invalidAcmeOptions3: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: 'admin@example.com',
|
||||
port: 99999
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateAcmeOptions(invalidAcmeOptions3).isValid).toEqual(false);
|
||||
|
||||
// Invalid ACME options - invalid HTTPS redirect port
|
||||
const invalidAcmeOptions4: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: 'admin@example.com',
|
||||
port: 80,
|
||||
httpsRedirectPort: -1
|
||||
};
|
||||
|
||||
expect(ValidationUtils.validateAcmeOptions(invalidAcmeOptions4).isValid).toEqual(false);
|
||||
|
||||
// Invalid ACME options - invalid renew threshold days
|
||||
const invalidAcmeOptions5: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: 'admin@example.com',
|
||||
port: 80,
|
||||
renewThresholdDays: 0
|
||||
};
|
||||
|
||||
// The implementation allows renewThresholdDays of 0, even though the docstring suggests otherwise
|
||||
const validationResult5 = ValidationUtils.validateAcmeOptions(invalidAcmeOptions5);
|
||||
expect(validationResult5.isValid).toEqual(true);
|
||||
|
||||
// Invalid ACME options - invalid renew check interval hours
|
||||
const invalidAcmeOptions6: IAcmeOptions = {
|
||||
enabled: true,
|
||||
accountEmail: 'admin@example.com',
|
||||
port: 80,
|
||||
renewCheckIntervalHours: 0
|
||||
};
|
||||
|
||||
// The implementation should validate this, but let's check the actual result
|
||||
const checkIntervalResult = ValidationUtils.validateAcmeOptions(invalidAcmeOptions6);
|
||||
// Adjust test to match actual implementation behavior
|
||||
expect(checkIntervalResult.isValid !== false ? true : false).toEqual(true);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user