Sync DNS2 production changes - removed obsolete test suite and refactored structure
This commit is contained in:
@@ -1,727 +0,0 @@
|
||||
const {
|
||||
ValidationError,
|
||||
validateDNSRecord,
|
||||
validateDockerDeployment,
|
||||
validateFilePath,
|
||||
validateVolumePath,
|
||||
validateURL,
|
||||
validateToken,
|
||||
validateServiceConfig,
|
||||
sanitizeString,
|
||||
isValidPort,
|
||||
isPrivateIP,
|
||||
} = require('../input-validator');
|
||||
|
||||
// Helper: extract .errors from ValidationError
|
||||
function getErrors(fn) {
|
||||
try {
|
||||
fn();
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ValidationError', () => {
|
||||
test('creates error with message and field', () => {
|
||||
const err = new ValidationError('bad input', 'name');
|
||||
expect(err.message).toBe('bad input');
|
||||
expect(err.field).toBe('name');
|
||||
});
|
||||
|
||||
test('has statusCode 400', () => {
|
||||
expect(new ValidationError('x').statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('has name "ValidationError"', () => {
|
||||
expect(new ValidationError('x').name).toBe('ValidationError');
|
||||
});
|
||||
|
||||
test('defaults field to null', () => {
|
||||
expect(new ValidationError('x').field).toBeNull();
|
||||
});
|
||||
|
||||
test('is instance of Error', () => {
|
||||
expect(new ValidationError('x')).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDNSRecord', () => {
|
||||
const valid = { subdomain: 'myapp', ip: '192.168.1.1' };
|
||||
|
||||
describe('valid inputs', () => {
|
||||
test('accepts valid subdomain and ip', () => {
|
||||
const result = validateDNSRecord(valid);
|
||||
expect(result.subdomain).toBe('myapp');
|
||||
expect(result.ip).toBe('192.168.1.1');
|
||||
});
|
||||
|
||||
test('returns sanitized lowercase output', () => {
|
||||
const result = validateDNSRecord({ subdomain: 'MyApp', ip: '10.0.0.1' });
|
||||
expect(result.subdomain).toBe('myapp');
|
||||
});
|
||||
|
||||
test('defaults ttl to 3600 when not provided', () => {
|
||||
expect(validateDNSRecord(valid).ttl).toBe(3600);
|
||||
});
|
||||
|
||||
test('accepts explicit ttl', () => {
|
||||
expect(validateDNSRecord({ ...valid, ttl: 300 }).ttl).toBe(300);
|
||||
});
|
||||
|
||||
test('accepts IPv6 addresses', () => {
|
||||
const result = validateDNSRecord({ subdomain: 'test', ip: '::1' });
|
||||
expect(result.ip).toBe('::1');
|
||||
});
|
||||
|
||||
test('accepts valid domain', () => {
|
||||
const result = validateDNSRecord({ ...valid, domain: 'example.local' });
|
||||
expect(result.domain).toBe('example.local');
|
||||
});
|
||||
|
||||
test('returns null domain when not provided', () => {
|
||||
expect(validateDNSRecord(valid).domain).toBeNull();
|
||||
});
|
||||
|
||||
test('lowercases and trims subdomain in output', () => {
|
||||
const result = validateDNSRecord({ subdomain: 'MyApp', ip: '10.0.0.1' });
|
||||
expect(result.subdomain).toBe('myapp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('subdomain validation', () => {
|
||||
test('rejects missing subdomain', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ ip: '1.2.3.4' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects non-string subdomain', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: 123, ip: '1.2.3.4' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects subdomain starting with hyphen', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: '-bad', ip: '1.2.3.4' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects subdomain ending with hyphen', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: 'bad-', ip: '1.2.3.4' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts single-character subdomain', () => {
|
||||
expect(validateDNSRecord({ subdomain: 'a', ip: '1.2.3.4' }).subdomain).toBe('a');
|
||||
});
|
||||
|
||||
test('accepts subdomain with hyphens in middle', () => {
|
||||
expect(validateDNSRecord({ subdomain: 'my-app', ip: '1.2.3.4' }).subdomain).toBe('my-app');
|
||||
});
|
||||
|
||||
test('rejects subdomain exceeding 63 characters', () => {
|
||||
const long = 'a'.repeat(64);
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: long, ip: '1.2.3.4' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injection prevention', () => {
|
||||
const chars = [';', '&', '|', '`', '$', '(', ')', '<', '>', '\n', '\r', '\\'];
|
||||
chars.forEach(char => {
|
||||
test(`rejects "${char === '\n' ? '\\n' : char === '\r' ? '\\r' : char}" in subdomain`, () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: `test${char}bad`, ip: '1.2.3.4' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IP validation', () => {
|
||||
test('rejects missing IP', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: 'test' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects invalid IP format', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: 'test', ip: '999.999.999.999' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects non-string IP', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: 'test', ip: 12345 }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks private IP when blockPrivateIPs is true', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ subdomain: 'test', ip: '192.168.1.1', blockPrivateIPs: true }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('allows private IP when blockPrivateIPs is absent', () => {
|
||||
expect(validateDNSRecord({ subdomain: 'test', ip: '192.168.1.1' }).ip).toBe('192.168.1.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TTL validation', () => {
|
||||
test('rejects TTL below 60', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ ...valid, ttl: 10 }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects TTL above 86400', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ ...valid, ttl: 100000 }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects non-numeric TTL', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ ...valid, ttl: 'abc' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts TTL at lower boundary (60)', () => {
|
||||
expect(validateDNSRecord({ ...valid, ttl: 60 }).ttl).toBe(60);
|
||||
});
|
||||
|
||||
test('accepts TTL at upper boundary (86400)', () => {
|
||||
expect(validateDNSRecord({ ...valid, ttl: 86400 }).ttl).toBe(86400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error aggregation', () => {
|
||||
test('returns multiple errors for multiple invalid fields', () => {
|
||||
const err = getErrors(() => validateDNSRecord({ ttl: 1 }));
|
||||
expect(err.errors.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test('throws ValidationError with .errors array', () => {
|
||||
const err = getErrors(() => validateDNSRecord({}));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
expect(Array.isArray(err.errors)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDockerDeployment', () => {
|
||||
const valid = { name: 'myapp', image: 'nginx:latest' };
|
||||
|
||||
describe('valid inputs', () => {
|
||||
test('accepts valid name and image', () => {
|
||||
const result = validateDockerDeployment(valid);
|
||||
expect(result.name).toBe('myapp');
|
||||
expect(result.image).toBe('nginx:latest');
|
||||
});
|
||||
|
||||
test('returns defaults for optional fields', () => {
|
||||
const result = validateDockerDeployment(valid);
|
||||
expect(result.ports).toEqual([]);
|
||||
expect(result.volumes).toEqual([]);
|
||||
expect(result.environment).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('container name validation', () => {
|
||||
test('rejects missing name', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ image: 'nginx' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects name starting with special char', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ name: '-bad', image: 'nginx' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects name exceeding 255 characters', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ name: 'a'.repeat(256), image: 'nginx' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts name with underscores, periods, hyphens', () => {
|
||||
const result = validateDockerDeployment({ name: 'my_app.v1-test', image: 'nginx' });
|
||||
expect(result.name).toBe('my_app.v1-test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('image validation', () => {
|
||||
test('rejects missing image', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ name: 'app' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts simple image', () => {
|
||||
expect(validateDockerDeployment({ name: 'a', image: 'alpine' }).image).toBe('alpine');
|
||||
});
|
||||
|
||||
test('accepts image with tag', () => {
|
||||
expect(validateDockerDeployment({ name: 'a', image: 'nginx:latest' }).image).toBe('nginx:latest');
|
||||
});
|
||||
|
||||
test('accepts fully qualified image', () => {
|
||||
const result = validateDockerDeployment({ name: 'a', image: 'docker.io/library/nginx:1.21' });
|
||||
expect(result.image).toBe('docker.io/library/nginx:1.21');
|
||||
});
|
||||
|
||||
test('rejects image with semicolon', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ name: 'a', image: 'nginx;rm -rf /' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects image with $( subshell', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ name: 'a', image: 'nginx$(evil)' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects image exceeding 512 characters', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ name: 'a', image: 'a'.repeat(513) }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ports validation', () => {
|
||||
test('rejects non-array ports', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, ports: 'bad' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts string port format "8080:80"', () => {
|
||||
const result = validateDockerDeployment({ ...valid, ports: ['8080:80'] });
|
||||
expect(result.ports).toEqual(['8080:80']);
|
||||
});
|
||||
|
||||
test('accepts port format with protocol "8080:80/tcp"', () => {
|
||||
const result = validateDockerDeployment({ ...valid, ports: ['8080:80/tcp'] });
|
||||
expect(result.ports).toEqual(['8080:80/tcp']);
|
||||
});
|
||||
|
||||
test('rejects invalid port format', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, ports: ['bad'] }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects port numbers > 65535', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, ports: ['70000:80'] }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects port numbers < 1', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, ports: ['0:80'] }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts numeric port values', () => {
|
||||
const result = validateDockerDeployment({ ...valid, ports: [8080] });
|
||||
expect(result.ports).toEqual([8080]);
|
||||
});
|
||||
|
||||
test('rejects non-string non-number port values', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, ports: [{}] }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('volumes validation', () => {
|
||||
test('rejects non-array volumes', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, volumes: 'bad' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects non-string volume entries', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, volumes: [123] }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment validation', () => {
|
||||
test('rejects non-object environment', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, environment: 'bad' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects array as environment', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, environment: [] }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects invalid env var names', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, environment: { '1BAD': 'val' } }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts valid env var names', () => {
|
||||
const result = validateDockerDeployment({ ...valid, environment: { MY_VAR: 'test', _under: '1' } });
|
||||
expect(result.environment).toEqual({ MY_VAR: 'test', _under: '1' });
|
||||
});
|
||||
|
||||
test('accepts string, number, boolean values', () => {
|
||||
const env = { A: 'str', B: 42, C: true };
|
||||
const result = validateDockerDeployment({ ...valid, environment: env });
|
||||
expect(result.environment).toEqual(env);
|
||||
});
|
||||
|
||||
test('rejects object values', () => {
|
||||
const err = getErrors(() => validateDockerDeployment({ ...valid, environment: { X: { nested: true } } }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFilePath', () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
test('rejects empty path', () => {
|
||||
expect(() => validateFilePath('')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects null path', () => {
|
||||
expect(() => validateFilePath(null)).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects path with ~', () => {
|
||||
expect(() => validateFilePath('~/secrets')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
// On Windows, path.normalize resolves '..' so the normalized path may not contain '..'
|
||||
// On Linux, '/app/../etc/passwd' normalizes to '/etc/passwd' which is blocked
|
||||
test('blocks C:\\Windows path', () => {
|
||||
expect(() => validateFilePath('C:\\Windows\\System32')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks C:\\Program Files path', () => {
|
||||
expect(() => validateFilePath('C:\\Program Files\\test')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
if (!isWindows) {
|
||||
test('rejects path with ..', () => {
|
||||
expect(() => validateFilePath('/app/../etc/passwd')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks /etc path', () => {
|
||||
expect(() => validateFilePath('/etc/passwd')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks /proc path', () => {
|
||||
expect(() => validateFilePath('/proc/self/environ')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks /sys path', () => {
|
||||
expect(() => validateFilePath('/sys/class')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks /root path', () => {
|
||||
expect(() => validateFilePath('/root/.ssh')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks /var/run path', () => {
|
||||
expect(() => validateFilePath('/var/run/docker.sock')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks /var/lib/docker path', () => {
|
||||
expect(() => validateFilePath('/var/lib/docker/containers')).toThrow(ValidationError);
|
||||
});
|
||||
}
|
||||
|
||||
test('returns normalized path for valid input', () => {
|
||||
const testPath = isWindows ? 'D:\\app\\data\\config' : '/app/data/config';
|
||||
const result = validateFilePath(testPath);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('enforces allowedBasePaths when specified', () => {
|
||||
const testPath = isWindows ? 'D:\\app\\data' : '/app/data';
|
||||
const allowedBase = isWindows ? 'D:\\opt' : '/opt';
|
||||
expect(() => validateFilePath(testPath, [allowedBase])).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts path within allowedBasePaths', () => {
|
||||
const testPath = isWindows ? 'D:\\opt\\myapp\\config' : '/opt/myapp/config';
|
||||
const allowedBase = isWindows ? 'D:\\opt' : '/opt';
|
||||
const result = validateFilePath(testPath, [allowedBase]);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('accepts any path when allowedBasePaths is empty', () => {
|
||||
const testPath = isWindows ? 'D:\\app\\data' : '/app/data';
|
||||
const result = validateFilePath(testPath, []);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateVolumePath', () => {
|
||||
test('rejects invalid volume format', () => {
|
||||
const errors = validateVolumePath('not-a-volume', 0);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('rejects container path with ..', () => {
|
||||
const errors = validateVolumePath('/app/data:/../etc:ro', 0);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('accepts valid modes: ro, rw, z, Z', () => {
|
||||
['ro', 'rw', 'z', 'Z'].forEach(mode => {
|
||||
const errors = validateVolumePath(`/app/data:/container/path:${mode}`, 0);
|
||||
// Filter to only mode-related errors
|
||||
const modeErrors = errors.filter(e => e.field && e.field.includes('mode'));
|
||||
expect(modeErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('accepts valid volume without mode', () => {
|
||||
const errors = validateVolumePath('/app/data:/container/path', 0);
|
||||
// Should have no container path errors
|
||||
const containerErrors = errors.filter(e => e.field && e.field.includes('containerPath'));
|
||||
expect(containerErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateURL', () => {
|
||||
test('rejects empty URL', () => {
|
||||
expect(() => validateURL('')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects null URL', () => {
|
||||
expect(() => validateURL(null)).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts valid https URL', () => {
|
||||
expect(validateURL('https://example.com')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
test('accepts valid http URL', () => {
|
||||
expect(validateURL('http://example.com')).toBe('http://example.com');
|
||||
});
|
||||
|
||||
test('rejects non-URL strings', () => {
|
||||
expect(() => validateURL('not a url')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks localhost when blockPrivate is true', () => {
|
||||
expect(() => validateURL('http://localhost:3000', { blockPrivate: true })).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks 127.0.0.1 when blockPrivate is true', () => {
|
||||
expect(() => validateURL('http://127.0.0.1:3000', { blockPrivate: true })).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('blocks private IPs when blockPrivate is true', () => {
|
||||
expect(() => validateURL('http://192.168.1.1', { blockPrivate: true })).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('allows private IPs when blockPrivate is false', () => {
|
||||
expect(validateURL('http://192.168.1.1')).toBe('http://192.168.1.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateToken', () => {
|
||||
test('rejects empty token', () => {
|
||||
expect(() => validateToken('')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects null token', () => {
|
||||
expect(() => validateToken(null)).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects token shorter than 8 chars', () => {
|
||||
expect(() => validateToken('short')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects token longer than 512 chars', () => {
|
||||
expect(() => validateToken('a'.repeat(513))).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects token with semicolon', () => {
|
||||
expect(() => validateToken('token123;evil')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects token with $( subshell', () => {
|
||||
expect(() => validateToken('token123$(evil)')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects token with &&', () => {
|
||||
expect(() => validateToken('token123&&evil')).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts valid alphanumeric token', () => {
|
||||
expect(validateToken('abcdef12345678')).toBe('abcdef12345678');
|
||||
});
|
||||
|
||||
test('trims whitespace', () => {
|
||||
expect(validateToken(' abcdef12345678 ')).toBe('abcdef12345678');
|
||||
});
|
||||
|
||||
test('accepts token at minimum length (8)', () => {
|
||||
expect(validateToken('12345678')).toBe('12345678');
|
||||
});
|
||||
|
||||
test('accepts token at maximum length (512)', () => {
|
||||
const token = 'a'.repeat(512);
|
||||
expect(validateToken(token)).toBe(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateServiceConfig', () => {
|
||||
const valid = { id: 'my-service', name: 'My Service' };
|
||||
|
||||
test('accepts valid service config', () => {
|
||||
const result = validateServiceConfig(valid);
|
||||
expect(result.id).toBe('my-service');
|
||||
});
|
||||
|
||||
test('rejects missing ID', () => {
|
||||
const err = getErrors(() => validateServiceConfig({ name: 'Test' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects invalid ID format', () => {
|
||||
const err = getErrors(() => validateServiceConfig({ id: 'bad id!', name: 'Test' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects missing name', () => {
|
||||
const err = getErrors(() => validateServiceConfig({ id: 'test' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('rejects name exceeding 100 chars', () => {
|
||||
const err = getErrors(() => validateServiceConfig({ id: 'test', name: 'a'.repeat(101) }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('validates URL when present', () => {
|
||||
const err = getErrors(() => validateServiceConfig({ ...valid, url: 'not a url' }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('validates port when present', () => {
|
||||
const err = getErrors(() => validateServiceConfig({ ...valid, port: 99999 }));
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
});
|
||||
|
||||
test('accepts valid URL and port', () => {
|
||||
const result = validateServiceConfig({ ...valid, url: 'http://example.com', port: 8080 });
|
||||
expect(result.id).toBe('my-service');
|
||||
});
|
||||
|
||||
test('aggregates multiple errors', () => {
|
||||
const err = getErrors(() => validateServiceConfig({}));
|
||||
expect(err.errors.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidPort', () => {
|
||||
test('accepts port 1', () => {
|
||||
expect(isValidPort(1)).toBe(true);
|
||||
});
|
||||
|
||||
test('accepts port 65535', () => {
|
||||
expect(isValidPort(65535)).toBe(true);
|
||||
});
|
||||
|
||||
test('rejects port 0', () => {
|
||||
expect(isValidPort(0)).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects port 65536', () => {
|
||||
expect(isValidPort(65536)).toBe(false);
|
||||
});
|
||||
|
||||
test('accepts string port "8080"', () => {
|
||||
expect(isValidPort('8080')).toBe(true);
|
||||
});
|
||||
|
||||
test('rejects NaN', () => {
|
||||
expect(isValidPort('abc')).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects negative port', () => {
|
||||
expect(isValidPort(-1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPrivateIP', () => {
|
||||
test('detects 10.x.x.x as private', () => {
|
||||
expect(isPrivateIP('10.0.0.1')).toBe(true);
|
||||
expect(isPrivateIP('10.255.255.255')).toBe(true);
|
||||
});
|
||||
|
||||
test('detects 172.16-31.x.x as private', () => {
|
||||
expect(isPrivateIP('172.16.0.1')).toBe(true);
|
||||
expect(isPrivateIP('172.31.255.255')).toBe(true);
|
||||
});
|
||||
|
||||
test('does not flag 172.15.x.x as private', () => {
|
||||
expect(isPrivateIP('172.15.0.1')).toBe(false);
|
||||
});
|
||||
|
||||
test('does not flag 172.32.x.x as private', () => {
|
||||
expect(isPrivateIP('172.32.0.1')).toBe(false);
|
||||
});
|
||||
|
||||
test('detects 192.168.x.x as private', () => {
|
||||
expect(isPrivateIP('192.168.1.1')).toBe(true);
|
||||
});
|
||||
|
||||
test('detects 127.x.x.x as private', () => {
|
||||
expect(isPrivateIP('127.0.0.1')).toBe(true);
|
||||
expect(isPrivateIP('127.255.255.255')).toBe(true);
|
||||
});
|
||||
|
||||
test('detects 169.254.x.x as private', () => {
|
||||
expect(isPrivateIP('169.254.1.1')).toBe(true);
|
||||
});
|
||||
|
||||
test('detects ::1 as private', () => {
|
||||
expect(isPrivateIP('::1')).toBe(true);
|
||||
});
|
||||
|
||||
test('detects fc00: as private', () => {
|
||||
expect(isPrivateIP('fc00::1')).toBe(true);
|
||||
});
|
||||
|
||||
test('detects fe80: as private', () => {
|
||||
expect(isPrivateIP('fe80::1')).toBe(true);
|
||||
});
|
||||
|
||||
test('identifies 8.8.8.8 as public', () => {
|
||||
expect(isPrivateIP('8.8.8.8')).toBe(false);
|
||||
});
|
||||
|
||||
test('identifies 1.1.1.1 as public', () => {
|
||||
expect(isPrivateIP('1.1.1.1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeString', () => {
|
||||
test('escapes < to <', () => {
|
||||
expect(sanitizeString('<script>')).toBe('<script>');
|
||||
});
|
||||
|
||||
test('escapes > to >', () => {
|
||||
expect(sanitizeString('a>b')).toBe('a>b');
|
||||
});
|
||||
|
||||
test('escapes single quote to '', () => {
|
||||
expect(sanitizeString("it's")).toBe('it's');
|
||||
});
|
||||
|
||||
test('escapes double quote to "', () => {
|
||||
expect(sanitizeString('say "hi"')).toBe('say "hi"');
|
||||
});
|
||||
|
||||
test('truncates to maxLength', () => {
|
||||
expect(sanitizeString('hello world', 5)).toBe('hello');
|
||||
});
|
||||
|
||||
test('returns empty string for non-string input', () => {
|
||||
expect(sanitizeString(123)).toBe('');
|
||||
expect(sanitizeString(null)).toBe('');
|
||||
expect(sanitizeString(undefined)).toBe('');
|
||||
});
|
||||
|
||||
test('uses default maxLength of 1000', () => {
|
||||
const long = 'a'.repeat(1500);
|
||||
expect(sanitizeString(long).length).toBe(1000);
|
||||
});
|
||||
|
||||
test('returns safe strings unchanged', () => {
|
||||
expect(sanitizeString('hello world')).toBe('hello world');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user