a11yanalyze
Version:
A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance
530 lines (411 loc) • 18.1 kB
text/typescript
/**
* URL Validator unit tests
* Tests comprehensive URL validation, error handling, and auto-correction features
*/
import { UrlValidator, UrlErrorFormatter, UrlValidationOptions } from './url-validator';
describe('UrlValidator', () => {
let validator: UrlValidator;
beforeEach(() => {
validator = new UrlValidator();
});
describe('Basic URL Validation', () => {
it('should validate simple HTTP URLs', async () => {
const result = await validator.validate('http://example.com');
expect(result.isValid).toBe(true);
expect(result.normalizedUrl).toBe('http://example.com');
});
it('should validate simple HTTPS URLs', async () => {
const result = await validator.validate('https://example.com');
expect(result.isValid).toBe(true);
expect(result.normalizedUrl).toBe('https://example.com');
});
it('should auto-add HTTPS protocol', async () => {
const result = await validator.validate('example.com');
expect(result.isValid).toBe(true);
expect(result.normalizedUrl).toBe('https://example.com');
});
it('should auto-add HTTP for localhost', async () => {
const result = await validator.validate('localhost:3000');
expect(result.isValid).toBe(true);
expect(result.normalizedUrl).toBe('http://localhost:3000');
});
it('should validate complex URLs', async () => {
const result = await validator.validate('https://sub.example.com:8080/path/to/page?param=value#section');
expect(result.isValid).toBe(true);
expect(result.normalizedUrl).toBe('https://sub.example.com:8080/path/to/page?param=value#section');
});
});
describe('Input Normalization', () => {
it('should fix common typos', async () => {
const typos = [
'htp://example.com',
'htps://example.com',
'http//example.com',
'https//example.com',
];
for (const typo of typos) {
const result = await validator.validate(typo);
expect(result.isValid).toBe(true);
}
});
it('should handle whitespace', async () => {
const result = await validator.validate(' https://example.com ');
expect(result.isValid).toBe(true);
expect(result.normalizedUrl).toBe('https://example.com');
});
it('should add www suggestions', async () => {
const result = await validator.validate('example.com');
expect(result.isValid).toBe(true);
// Should normalize to https://example.com without www
expect(result.normalizedUrl).toBe('https://example.com');
});
});
describe('Protocol Validation', () => {
it('should reject unsupported protocols with strict validator', async () => {
const strictValidator = UrlValidator.strict();
const result = await strictValidator.validate('http://example.com');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('protocol');
expect(result.error?.message).toContain('not allowed');
});
it('should accept HTTP and HTTPS by default', async () => {
const httpResult = await validator.validate('http://example.com');
const httpsResult = await validator.validate('https://example.com');
expect(httpResult.isValid).toBe(true);
expect(httpsResult.isValid).toBe(true);
});
it('should accept additional protocols with lenient validator', async () => {
const lenientValidator = UrlValidator.lenient();
const result = await lenientValidator.validate('ftp://example.com');
expect(result.isValid).toBe(true);
});
});
describe('Domain Validation', () => {
it('should validate localhost', async () => {
const result = await validator.validate('http://localhost');
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('Using localhost - this URL will only work on your local machine');
});
it('should reject localhost with strict validator', async () => {
const strictValidator = UrlValidator.strict();
const result = await strictValidator.validate('http://localhost');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('domain');
});
it('should validate IP addresses', async () => {
const result = await validator.validate('http://192.168.1.1');
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('Using IP address - consider using a domain name for better accessibility');
});
it('should validate IPv6 addresses', async () => {
const result = await validator.validate('http://[::1]');
expect(result.isValid).toBe(true);
});
it('should reject invalid IP addresses', async () => {
const result = await validator.validate('http://999.999.999.999');
expect(result.isValid).toBe(false);
});
it('should reject domains without TLD', async () => {
const result = await validator.validate('http://example');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('domain');
expect(result.error?.message).toContain('missing TLD');
});
it('should warn about suspicious domains', async () => {
const result = await validator.validate('http://bit.ly/test');
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('may be a URL shortener');
});
});
describe('Port Validation', () => {
it('should allow standard ports', async () => {
const httpResult = await validator.validate('http://example.com:80');
const httpsResult = await validator.validate('https://example.com:443');
expect(httpResult.isValid).toBe(true);
expect(httpsResult.isValid).toBe(true);
});
it('should allow non-standard ports by default', async () => {
const result = await validator.validate('http://example.com:8080');
expect(result.isValid).toBe(true);
});
it('should reject non-standard ports with strict validator', async () => {
const strictValidator = UrlValidator.strict();
const result = await strictValidator.validate('http://example.com:8080');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('domain');
expect(result.error?.message).toContain('Non-standard port');
});
});
describe('Security Validation', () => {
it('should warn about HTTP for non-localhost', async () => {
const result = await validator.validate('http://example.com');
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('Consider using HTTPS');
});
it('should reject path traversal patterns', async () => {
const result = await validator.validate('https://example.com/../../../etc/passwd');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('security');
expect(result.error?.message).toContain('path traversal');
});
});
describe('Accessibility Validation', () => {
it('should warn about tracking parameters', async () => {
const trackingUrls = [
'https://example.com?utm_source=test',
'https://example.com?fbclid=123',
'https://example.com?gclid=456',
];
for (const url of trackingUrls) {
const result = await validator.validate(url);
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('tracking parameters');
}
});
it('should warn about text fragments', async () => {
const result = await validator.validate('https://example.com#:~:text=hello');
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('text fragments');
});
it('should warn about very long URLs', async () => {
const longPath = 'a'.repeat(1000);
const result = await validator.validate(`https://example.com/${longPath}`);
expect(result.isValid).toBe(true);
expect(result.warnings).toContain('Very long URLs');
});
});
describe('Format Validation', () => {
it('should reject empty strings', async () => {
const result = await validator.validate('');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('format');
});
it('should reject very long URLs', async () => {
const longUrl = 'https://example.com/' + 'a'.repeat(3000);
const result = await validator.validate(longUrl);
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('format');
expect(result.error?.message).toContain('too long');
});
it('should reject URLs without protocol', async () => {
const validator = new UrlValidator({ autoNormalize: false });
const result = await validator.validate('example.com');
expect(result.isValid).toBe(false);
expect(result.error?.type).toBe('format');
expect(result.error?.message).toContain('valid protocol');
});
});
describe('Auto-correction Suggestions', () => {
it('should suggest protocol additions', async () => {
const validator = new UrlValidator({ autoNormalize: false });
const result = await validator.validate('example.com');
expect(result.isValid).toBe(false);
expect(result.suggestions).toContain('https://example.com');
expect(result.suggestions).toContain('http://example.com');
});
it('should suggest typo corrections', async () => {
const result = await validator.validate('htp://example.com');
// This should auto-correct and be valid
expect(result.isValid).toBe(true);
});
it('should suggest www additions', async () => {
const validator = new UrlValidator({ autoNormalize: false });
const result = await validator.validate('example.com');
expect(result.suggestions).toContain('https://www.example.com');
});
});
describe('Multiple URL Validation', () => {
it('should validate multiple URLs', async () => {
const urls = [
'https://example.com',
'http://localhost:3000',
'invalid-url',
];
const results = await validator.validateMultiple(urls);
expect(results).toHaveLength(3);
expect(results[0]?.isValid).toBe(true);
expect(results[1]?.isValid).toBe(true);
expect(results[2]?.isValid).toBe(false);
});
it('should handle empty array', async () => {
const results = await validator.validateMultiple([]);
expect(results).toHaveLength(0);
});
it('should handle validation errors gracefully', async () => {
const results = await validator.validateMultiple(['', 'https://example.com']);
expect(results).toHaveLength(2);
expect(results[0]?.isValid).toBe(false);
expect(results[1]?.isValid).toBe(true);
});
});
describe('Quick Validation', () => {
it('should quickly validate valid URLs', () => {
expect(validator.isValidUrl('https://example.com')).toBe(true);
expect(validator.isValidUrl('http://localhost')).toBe(true);
});
it('should quickly reject invalid URLs', () => {
expect(validator.isValidUrl('invalid')).toBe(false);
expect(validator.isValidUrl('')).toBe(false);
expect(validator.isValidUrl('ftp://example.com')).toBe(false); // Not in default allowed protocols
});
});
describe('Preset Configurations', () => {
it('should create strict validator', async () => {
const strictValidator = UrlValidator.strict();
// Should reject HTTP
const httpResult = await strictValidator.validate('http://example.com');
expect(httpResult.isValid).toBe(false);
// Should reject localhost
const localhostResult = await strictValidator.validate('https://localhost');
expect(localhostResult.isValid).toBe(false);
});
it('should create lenient validator', async () => {
const lenientValidator = UrlValidator.lenient();
// Should accept FTP
const ftpResult = await lenientValidator.validate('ftp://example.com');
expect(ftpResult.isValid).toBe(true);
});
it('should create development validator', async () => {
const devValidator = UrlValidator.development();
// Should accept localhost
const localhostResult = await devValidator.validate('http://localhost:3000');
expect(localhostResult.isValid).toBe(true);
});
});
describe('Error Handling', () => {
it('should handle malformed input gracefully', async () => {
const result = await validator.validate('not a url at all');
expect(result.isValid).toBe(false);
expect(result.error).toBeDefined();
expect(result.suggestions).toBeDefined();
});
it('should handle special characters', async () => {
const result = await validator.validate('https://example.com/path with spaces');
// Should handle or reject gracefully
expect(result).toBeDefined();
expect(typeof result.isValid).toBe('boolean');
});
it('should handle null and undefined', async () => {
const nullResult = await validator.validate(null as any);
const undefinedResult = await validator.validate(undefined as any);
expect(nullResult.isValid).toBe(false);
expect(undefinedResult.isValid).toBe(false);
});
});
describe('Custom Configuration', () => {
it('should respect custom allowed protocols', async () => {
const customValidator = new UrlValidator({
allowedProtocols: ['https', 'ftp'],
});
const httpsResult = await customValidator.validate('https://example.com');
const ftpResult = await customValidator.validate('ftp://example.com');
const httpResult = await customValidator.validate('http://example.com');
expect(httpsResult.isValid).toBe(true);
expect(ftpResult.isValid).toBe(true);
expect(httpResult.isValid).toBe(false);
});
it('should respect custom max length', async () => {
const customValidator = new UrlValidator({
maxLength: 50,
});
const shortResult = await customValidator.validate('https://example.com');
const longResult = await customValidator.validate('https://example.com/very/long/path/that/exceeds/fifty/characters');
expect(shortResult.isValid).toBe(true);
expect(longResult.isValid).toBe(false);
});
it('should disable auto-normalization when configured', async () => {
const customValidator = new UrlValidator({
autoNormalize: false,
});
const result = await customValidator.validate('example.com');
expect(result.isValid).toBe(false);
expect(result.suggestions).toContain('https://example.com');
});
});
});
describe('UrlErrorFormatter', () => {
describe('Error Formatting', () => {
it('should format validation errors', () => {
const result = {
isValid: false,
error: {
type: 'format' as const,
message: 'Invalid URL format',
details: 'Missing protocol',
input: 'example.com',
fixes: ['Add https://', 'Add http://'],
},
suggestions: ['https://example.com', 'http://example.com'],
};
const formatted = UrlErrorFormatter.formatError(result);
expect(formatted).toContain('❌ Invalid URL format');
expect(formatted).toContain('Details: Missing protocol');
expect(formatted).toContain('💡 Suggestions:');
expect(formatted).toContain('Add https://');
expect(formatted).toContain('🔧 Try these URLs:');
expect(formatted).toContain('https://example.com');
});
it('should return empty string for valid results', () => {
const result = {
isValid: true,
normalizedUrl: 'https://example.com',
};
const formatted = UrlErrorFormatter.formatError(result);
expect(formatted).toBe('');
});
});
describe('Warning Formatting', () => {
it('should format warnings', () => {
const result = {
isValid: true,
normalizedUrl: 'http://example.com',
warnings: ['Consider using HTTPS', 'Domain might be suspicious'],
};
const formatted = UrlErrorFormatter.formatWarnings(result);
expect(formatted).toContain('⚠️ Warnings:');
expect(formatted).toContain('Consider using HTTPS');
expect(formatted).toContain('Domain might be suspicious');
});
it('should return empty string when no warnings', () => {
const result = {
isValid: true,
normalizedUrl: 'https://example.com',
};
const formatted = UrlErrorFormatter.formatWarnings(result);
expect(formatted).toBe('');
});
});
describe('Success Formatting', () => {
it('should format successful validation', () => {
const result = {
isValid: true,
normalizedUrl: 'https://example.com',
};
const formatted = UrlErrorFormatter.formatSuccess(result);
expect(formatted).toContain('✅ URL is valid: https://example.com');
});
it('should include warnings in success message', () => {
const result = {
isValid: true,
normalizedUrl: 'http://example.com',
warnings: ['Consider using HTTPS'],
};
const formatted = UrlErrorFormatter.formatSuccess(result);
expect(formatted).toContain('✅ URL is valid');
expect(formatted).toContain('⚠️ Warnings:');
expect(formatted).toContain('Consider using HTTPS');
});
it('should return empty string for invalid results', () => {
const result = {
isValid: false,
error: {
type: 'format' as const,
message: 'Invalid',
input: 'test',
},
};
const formatted = UrlErrorFormatter.formatSuccess(result);
expect(formatted).toBe('');
});
});
});