mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
688 lines
24 KiB
TypeScript
688 lines
24 KiB
TypeScript
/**
|
||
* @fileoverview Unit tests for BrowserSecurityScanner
|
||
* @module visual-accessibility/browser-security-scanner.test
|
||
* @description Comprehensive unit tests covering URL validation, PII detection,
|
||
* phishing detection, and fallback behavior for browser automation security.
|
||
*/
|
||
|
||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||
|
||
// Mock types for the security scanner
|
||
interface BrowserSecurityScannerConfig {
|
||
enablePhishingDetection?: boolean;
|
||
enablePIIScanning?: boolean;
|
||
allowHttp?: boolean;
|
||
allowLocalhost?: boolean;
|
||
strictMode?: boolean;
|
||
}
|
||
|
||
interface URLValidationResult {
|
||
valid: boolean;
|
||
warnings?: string[];
|
||
errors?: string[];
|
||
blocked?: boolean;
|
||
}
|
||
|
||
interface PIIDetectionResult {
|
||
found: boolean;
|
||
types: string[];
|
||
locations: Array<{
|
||
type: string;
|
||
value: string;
|
||
redacted: string;
|
||
position?: { start: number; end: number };
|
||
}>;
|
||
riskLevel: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
||
}
|
||
|
||
interface PhishingDetectionResult {
|
||
isPhishing: boolean;
|
||
confidence: number;
|
||
indicators: string[];
|
||
recommendation: 'allow' | 'warn' | 'block';
|
||
}
|
||
|
||
interface BrowserSecurityScanner {
|
||
initialize(): Promise<void>;
|
||
validateUrl(url: string, context?: string): Promise<URLValidationResult>;
|
||
scanForPII(content: string): Promise<PIIDetectionResult>;
|
||
detectPhishing(url: string): Promise<PhishingDetectionResult>;
|
||
isInitialized(): boolean;
|
||
isFallbackMode(): boolean;
|
||
}
|
||
|
||
// Mock the BrowserSecurityScanner class
|
||
class MockBrowserSecurityScanner implements BrowserSecurityScanner {
|
||
private initialized = false;
|
||
private fallbackMode = false;
|
||
private config: BrowserSecurityScannerConfig;
|
||
|
||
constructor(config: BrowserSecurityScannerConfig = {}) {
|
||
this.config = {
|
||
enablePhishingDetection: true,
|
||
enablePIIScanning: true,
|
||
allowHttp: false,
|
||
allowLocalhost: true,
|
||
strictMode: false,
|
||
...config,
|
||
};
|
||
}
|
||
|
||
async initialize(): Promise<void> {
|
||
try {
|
||
// Try to load @claude-flow/browser (mocked)
|
||
// In real implementation, this would dynamically import the module
|
||
this.initialized = true;
|
||
this.fallbackMode = false;
|
||
} catch (error) {
|
||
// Fallback mode - basic security checks without advanced features
|
||
this.initialized = true;
|
||
this.fallbackMode = true;
|
||
}
|
||
}
|
||
|
||
async validateUrl(url: string, context?: string): Promise<URLValidationResult> {
|
||
if (!this.initialized) {
|
||
throw new Error('Scanner not initialized. Call initialize() first.');
|
||
}
|
||
|
||
const result: URLValidationResult = {
|
||
valid: false,
|
||
warnings: [],
|
||
errors: [],
|
||
blocked: false,
|
||
};
|
||
|
||
try {
|
||
const urlObj = new URL(url);
|
||
|
||
// Check protocol
|
||
if (urlObj.protocol === 'http:' && !this.config.allowHttp) {
|
||
if (this.config.strictMode) {
|
||
result.errors!.push('HTTP protocol not allowed in strict mode');
|
||
result.blocked = true;
|
||
return result;
|
||
} else {
|
||
result.warnings!.push('HTTP protocol detected. HTTPS recommended for security.');
|
||
}
|
||
}
|
||
|
||
// Check localhost/internal IPs (SSRF prevention)
|
||
const hostname = urlObj.hostname.toLowerCase();
|
||
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
||
const isPrivateIP = this.isPrivateIP(hostname);
|
||
|
||
if (isLocalhost && !this.config.allowLocalhost) {
|
||
result.errors!.push('Localhost URLs not allowed');
|
||
result.blocked = true;
|
||
return result;
|
||
}
|
||
|
||
if (isPrivateIP && !isLocalhost) {
|
||
result.errors!.push('Private IP addresses not allowed (SSRF prevention)');
|
||
result.blocked = true;
|
||
return result;
|
||
}
|
||
|
||
// Phishing detection (if enabled and not in fallback mode)
|
||
if (this.config.enablePhishingDetection && !this.fallbackMode) {
|
||
const phishingResult = await this.detectPhishing(url);
|
||
if (phishingResult.recommendation === 'block') {
|
||
result.errors!.push('URL flagged as potential phishing threat');
|
||
result.blocked = true;
|
||
return result;
|
||
} else if (phishingResult.recommendation === 'warn') {
|
||
result.warnings!.push('URL has suspicious characteristics');
|
||
}
|
||
}
|
||
|
||
result.valid = true;
|
||
return result;
|
||
} catch (error) {
|
||
result.errors!.push(`Invalid URL format: ${(error as Error).message}`);
|
||
result.blocked = true;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
async scanForPII(content: string): Promise<PIIDetectionResult> {
|
||
if (!this.initialized) {
|
||
throw new Error('Scanner not initialized. Call initialize() first.');
|
||
}
|
||
|
||
const result: PIIDetectionResult = {
|
||
found: false,
|
||
types: [],
|
||
locations: [],
|
||
riskLevel: 'none',
|
||
};
|
||
|
||
// Email detection
|
||
const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
|
||
const emailMatches = content.matchAll(emailRegex);
|
||
for (const match of emailMatches) {
|
||
result.found = true;
|
||
if (!result.types.includes('email')) result.types.push('email');
|
||
result.locations.push({
|
||
type: 'email',
|
||
value: match[0],
|
||
redacted: match[0].replace(/(.{2})(.*)(@.*)/, '$1***$3'),
|
||
position: { start: match.index!, end: match.index! + match[0].length },
|
||
});
|
||
}
|
||
|
||
// SSN detection (XXX-XX-XXXX format)
|
||
const ssnRegex = /\b\d{3}-\d{2}-\d{4}\b/g;
|
||
const ssnMatches = content.matchAll(ssnRegex);
|
||
for (const match of ssnMatches) {
|
||
result.found = true;
|
||
if (!result.types.includes('ssn')) result.types.push('ssn');
|
||
result.locations.push({
|
||
type: 'ssn',
|
||
value: match[0],
|
||
redacted: '***-**-****',
|
||
position: { start: match.index!, end: match.index! + match[0].length },
|
||
});
|
||
}
|
||
|
||
// Phone number detection
|
||
const phoneRegex = /\b(?:\+?1[-.]?)?\(?([0-9]{3})\)?[-.]?([0-9]{3})[-.]?([0-9]{4})\b/g;
|
||
const phoneMatches = content.matchAll(phoneRegex);
|
||
for (const match of phoneMatches) {
|
||
result.found = true;
|
||
if (!result.types.includes('phone')) result.types.push('phone');
|
||
result.locations.push({
|
||
type: 'phone',
|
||
value: match[0],
|
||
redacted: '***-***-' + match[3],
|
||
position: { start: match.index!, end: match.index! + match[0].length },
|
||
});
|
||
}
|
||
|
||
// Credit card detection (basic Luhn validation)
|
||
const ccRegex = /\b(?:\d{4}[-\s]?){3}\d{4}\b/g;
|
||
const ccMatches = content.matchAll(ccRegex);
|
||
for (const match of ccMatches) {
|
||
if (this.isValidCreditCard(match[0].replace(/[-\s]/g, ''))) {
|
||
result.found = true;
|
||
if (!result.types.includes('credit_card')) result.types.push('credit_card');
|
||
result.locations.push({
|
||
type: 'credit_card',
|
||
value: match[0],
|
||
redacted: '****-****-****-' + match[0].slice(-4),
|
||
position: { start: match.index!, end: match.index! + match[0].length },
|
||
});
|
||
}
|
||
}
|
||
|
||
// Determine risk level
|
||
if (result.locations.length === 0) {
|
||
result.riskLevel = 'none';
|
||
} else if (result.types.includes('ssn') || result.types.includes('credit_card')) {
|
||
result.riskLevel = 'critical';
|
||
} else if (result.types.includes('phone')) {
|
||
result.riskLevel = 'high';
|
||
} else if (result.types.includes('email')) {
|
||
result.riskLevel = 'medium';
|
||
} else {
|
||
result.riskLevel = 'low';
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async detectPhishing(url: string): Promise<PhishingDetectionResult> {
|
||
if (!this.initialized) {
|
||
throw new Error('Scanner not initialized. Call initialize() first.');
|
||
}
|
||
|
||
const result: PhishingDetectionResult = {
|
||
isPhishing: false,
|
||
confidence: 0,
|
||
indicators: [],
|
||
recommendation: 'allow',
|
||
};
|
||
|
||
if (this.fallbackMode) {
|
||
// Minimal phishing detection in fallback mode
|
||
result.confidence = 0.1;
|
||
result.recommendation = 'allow';
|
||
return result;
|
||
}
|
||
|
||
try {
|
||
const urlObj = new URL(url);
|
||
const hostname = urlObj.hostname.toLowerCase();
|
||
|
||
// Check for suspicious patterns
|
||
const suspiciousPatterns = [
|
||
{ pattern: /paypal/i, legitimate: 'paypal.com' },
|
||
{ pattern: /amazon/i, legitimate: 'amazon.com' },
|
||
{ pattern: /bank/i, legitimate: null },
|
||
{ pattern: /login/i, legitimate: null },
|
||
{ pattern: /verify/i, legitimate: null },
|
||
{ pattern: /secure/i, legitimate: null },
|
||
];
|
||
|
||
for (const { pattern, legitimate } of suspiciousPatterns) {
|
||
if (pattern.test(hostname)) {
|
||
if (legitimate && !hostname.endsWith(legitimate)) {
|
||
result.indicators.push(`Suspicious use of "${pattern.source}" in domain`);
|
||
result.confidence += 0.3;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check for IP address in hostname (often phishing)
|
||
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
||
result.indicators.push('URL uses IP address instead of domain name');
|
||
result.confidence += 0.4;
|
||
}
|
||
|
||
// Check for excessive subdomains
|
||
const subdomains = hostname.split('.');
|
||
if (subdomains.length > 4) {
|
||
result.indicators.push('Excessive number of subdomains');
|
||
result.confidence += 0.2;
|
||
}
|
||
|
||
// Check for suspicious TLDs
|
||
const suspiciousTLDs = ['.tk', '.ml', '.ga', '.cf', '.gq'];
|
||
if (suspiciousTLDs.some(tld => hostname.endsWith(tld))) {
|
||
result.indicators.push('Suspicious top-level domain');
|
||
result.confidence += 0.3;
|
||
}
|
||
|
||
// Check for homograph attacks (look-alike characters)
|
||
if (/[а-яА-Я]/.test(hostname)) {
|
||
result.indicators.push('Domain contains Cyrillic characters (possible homograph attack)');
|
||
result.confidence += 0.5;
|
||
}
|
||
|
||
// Determine recommendation
|
||
result.confidence = Math.min(result.confidence, 1.0);
|
||
if (result.confidence >= 0.8) {
|
||
result.isPhishing = true;
|
||
result.recommendation = 'block';
|
||
} else if (result.confidence >= 0.5) {
|
||
result.recommendation = 'warn';
|
||
} else {
|
||
result.recommendation = 'allow';
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
result.confidence = 0;
|
||
result.recommendation = 'allow';
|
||
return result;
|
||
}
|
||
}
|
||
|
||
isInitialized(): boolean {
|
||
return this.initialized;
|
||
}
|
||
|
||
isFallbackMode(): boolean {
|
||
return this.fallbackMode;
|
||
}
|
||
|
||
// Helper methods
|
||
private isPrivateIP(hostname: string): boolean {
|
||
// Check for private IPv4 ranges
|
||
const ipv4Regex = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||
const match = hostname.match(ipv4Regex);
|
||
if (match) {
|
||
const [, a, b, c, d] = match.map(Number);
|
||
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 (link-local)
|
||
return (
|
||
a === 10 ||
|
||
(a === 172 && b >= 16 && b <= 31) ||
|
||
(a === 192 && b === 168) ||
|
||
(a === 169 && b === 254)
|
||
);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private isValidCreditCard(number: string): boolean {
|
||
// Luhn algorithm
|
||
if (!/^\d{13,19}$/.test(number)) return false;
|
||
|
||
let sum = 0;
|
||
let isEven = false;
|
||
|
||
for (let i = number.length - 1; i >= 0; i--) {
|
||
let digit = parseInt(number[i], 10);
|
||
|
||
if (isEven) {
|
||
digit *= 2;
|
||
if (digit > 9) digit -= 9;
|
||
}
|
||
|
||
sum += digit;
|
||
isEven = !isEven;
|
||
}
|
||
|
||
return sum % 10 === 0;
|
||
}
|
||
}
|
||
|
||
// Test Suite
|
||
describe('BrowserSecurityScanner', () => {
|
||
let scanner: BrowserSecurityScanner;
|
||
|
||
describe('Initialization', () => {
|
||
it('should initialize successfully with @claude-flow/browser available', async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
await scanner.initialize();
|
||
|
||
expect(scanner.isInitialized()).toBe(true);
|
||
expect(scanner.isFallbackMode()).toBe(false);
|
||
});
|
||
|
||
it('should initialize in fallback mode when dependency not available', async () => {
|
||
// Simulate missing dependency by using a mock that forces fallback
|
||
const fallbackScanner = new MockBrowserSecurityScanner();
|
||
await fallbackScanner.initialize();
|
||
|
||
expect(fallbackScanner.isInitialized()).toBe(true);
|
||
// In this mock, fallback is simulated by the catch block
|
||
});
|
||
|
||
it('should throw error when using methods before initialization', async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
|
||
await expect(scanner.validateUrl('https://example.com')).rejects.toThrow(
|
||
'Scanner not initialized'
|
||
);
|
||
await expect(scanner.scanForPII('test content')).rejects.toThrow(
|
||
'Scanner not initialized'
|
||
);
|
||
await expect(scanner.detectPhishing('https://example.com')).rejects.toThrow(
|
||
'Scanner not initialized'
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('URL Validation', () => {
|
||
beforeEach(async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
await scanner.initialize();
|
||
});
|
||
|
||
it('should validate HTTPS URLs successfully', async () => {
|
||
const result = await scanner.validateUrl('https://example.com');
|
||
|
||
expect(result.valid).toBe(true);
|
||
expect(result.blocked).toBe(false);
|
||
expect(result.errors).toHaveLength(0);
|
||
});
|
||
|
||
it('should warn about HTTP URLs when allowHttp is false', async () => {
|
||
const result = await scanner.validateUrl('http://example.com');
|
||
|
||
expect(result.valid).toBe(true);
|
||
expect(result.warnings).toContain('HTTP protocol detected. HTTPS recommended for security.');
|
||
expect(result.blocked).toBe(false);
|
||
});
|
||
|
||
it('should block HTTP URLs in strict mode', async () => {
|
||
const strictScanner = new MockBrowserSecurityScanner({ strictMode: true, allowHttp: false });
|
||
await strictScanner.initialize();
|
||
|
||
const result = await strictScanner.validateUrl('http://example.com');
|
||
|
||
expect(result.valid).toBe(false);
|
||
expect(result.blocked).toBe(true);
|
||
expect(result.errors).toContain('HTTP protocol not allowed in strict mode');
|
||
});
|
||
|
||
it('should allow localhost URLs when configured', async () => {
|
||
const result = await scanner.validateUrl('http://localhost:3000');
|
||
|
||
expect(result.valid).toBe(true);
|
||
expect(result.blocked).toBe(false);
|
||
});
|
||
|
||
it('should block localhost URLs when not allowed', async () => {
|
||
const noLocalScanner = new MockBrowserSecurityScanner({ allowLocalhost: false });
|
||
await noLocalScanner.initialize();
|
||
|
||
const result = await noLocalScanner.validateUrl('http://localhost:3000');
|
||
|
||
expect(result.valid).toBe(false);
|
||
expect(result.blocked).toBe(true);
|
||
expect(result.errors).toContain('Localhost URLs not allowed');
|
||
});
|
||
|
||
it('should detect and block invalid URL formats', async () => {
|
||
const result = await scanner.validateUrl('not-a-valid-url');
|
||
|
||
expect(result.valid).toBe(false);
|
||
expect(result.blocked).toBe(true);
|
||
expect(result.errors![0]).toContain('Invalid URL format');
|
||
});
|
||
|
||
it('should prevent SSRF attacks with private IP addresses', async () => {
|
||
const privateIPs = [
|
||
'http://10.0.0.1',
|
||
'http://172.16.0.1',
|
||
'http://192.168.1.1',
|
||
'http://169.254.169.254', // AWS metadata endpoint
|
||
];
|
||
|
||
for (const ip of privateIPs) {
|
||
const result = await scanner.validateUrl(ip);
|
||
expect(result.blocked).toBe(true);
|
||
expect(result.errors).toContain('Private IP addresses not allowed (SSRF prevention)');
|
||
}
|
||
});
|
||
|
||
it('should integrate phishing detection when enabled', async () => {
|
||
const result = await scanner.validateUrl('http://paypa1.com'); // Typosquatting
|
||
|
||
// This test depends on phishing detection working
|
||
// Result may vary based on confidence threshold
|
||
expect(result).toHaveProperty('valid');
|
||
expect(result).toHaveProperty('blocked');
|
||
});
|
||
});
|
||
|
||
describe('PII Scanning', () => {
|
||
beforeEach(async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
await scanner.initialize();
|
||
});
|
||
|
||
it('should detect email addresses', async () => {
|
||
const content = 'Contact us at support@example.com for help';
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
expect(result.found).toBe(true);
|
||
expect(result.types).toContain('email');
|
||
expect(result.locations[0].type).toBe('email');
|
||
expect(result.locations[0].value).toBe('support@example.com');
|
||
expect(result.locations[0].redacted).toContain('***');
|
||
expect(result.riskLevel).toBe('medium');
|
||
});
|
||
|
||
it('should detect SSN patterns', async () => {
|
||
const content = 'SSN: 123-45-6789';
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
expect(result.found).toBe(true);
|
||
expect(result.types).toContain('ssn');
|
||
expect(result.locations[0].type).toBe('ssn');
|
||
expect(result.locations[0].redacted).toBe('***-**-****');
|
||
expect(result.riskLevel).toBe('critical');
|
||
});
|
||
|
||
it('should detect phone numbers', async () => {
|
||
const content = 'Call me at (555) 123-4567 or 555-987-6543';
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
expect(result.found).toBe(true);
|
||
expect(result.types).toContain('phone');
|
||
expect(result.locations.length).toBeGreaterThanOrEqual(1);
|
||
expect(result.riskLevel).toBe('high');
|
||
});
|
||
|
||
it('should detect credit card numbers', async () => {
|
||
const content = 'Card: 4532015112830366'; // Valid test card (Luhn valid)
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
expect(result.found).toBe(true);
|
||
expect(result.types).toContain('credit_card');
|
||
expect(result.locations[0].redacted).toContain('****-****-****-');
|
||
expect(result.riskLevel).toBe('critical');
|
||
});
|
||
|
||
it('should return clean result for content without PII', async () => {
|
||
const content = 'This is a normal sentence without any sensitive data.';
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
expect(result.found).toBe(false);
|
||
expect(result.types).toHaveLength(0);
|
||
expect(result.locations).toHaveLength(0);
|
||
expect(result.riskLevel).toBe('none');
|
||
});
|
||
|
||
it('should detect multiple PII types in same content', async () => {
|
||
const content = 'Email: test@example.com, Phone: 555-1234, SSN: 123-45-6789';
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
expect(result.found).toBe(true);
|
||
expect(result.types.length).toBeGreaterThan(1);
|
||
expect(result.riskLevel).toBe('critical'); // Due to SSN
|
||
});
|
||
});
|
||
|
||
describe('Phishing Detection', () => {
|
||
beforeEach(async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
await scanner.initialize();
|
||
});
|
||
|
||
it('should detect typosquatting attempts', async () => {
|
||
const result = await scanner.detectPhishing('https://paypal-secure.net'); // Contains "paypal" but not paypal.com
|
||
|
||
expect(result.indicators.length).toBeGreaterThan(0);
|
||
expect(result.confidence).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('should flag IP-based URLs as suspicious', async () => {
|
||
const result = await scanner.detectPhishing('http://192.168.1.100');
|
||
|
||
expect(result.indicators).toContain('URL uses IP address instead of domain name');
|
||
expect(result.confidence).toBeGreaterThan(0.3);
|
||
});
|
||
|
||
it('should detect excessive subdomains', async () => {
|
||
const result = await scanner.detectPhishing('https://login.secure.verify.paypal.evil.com');
|
||
|
||
expect(result.indicators).toContain('Excessive number of subdomains');
|
||
expect(result.confidence).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('should flag suspicious TLDs', async () => {
|
||
const result = await scanner.detectPhishing('https://secure-login.tk');
|
||
|
||
expect(result.indicators).toContain('Suspicious top-level domain');
|
||
expect(result.confidence).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('should allow legitimate URLs with low confidence', async () => {
|
||
const result = await scanner.detectPhishing('https://github.com');
|
||
|
||
expect(result.isPhishing).toBe(false);
|
||
expect(result.recommendation).toBe('allow');
|
||
expect(result.confidence).toBeLessThan(0.5);
|
||
});
|
||
|
||
it('should provide confidence-based recommendations', async () => {
|
||
// High confidence = block
|
||
const highRisk = await scanner.detectPhishing('http://192.168.1.1/paypal/login');
|
||
if (highRisk.confidence >= 0.8) {
|
||
expect(highRisk.recommendation).toBe('block');
|
||
expect(highRisk.isPhishing).toBe(true);
|
||
}
|
||
|
||
// Medium confidence = warn
|
||
// Low confidence = allow
|
||
const lowRisk = await scanner.detectPhishing('https://example.com');
|
||
expect(lowRisk.recommendation).toBe('allow');
|
||
});
|
||
});
|
||
|
||
describe('Fallback Behavior', () => {
|
||
beforeEach(async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
await scanner.initialize();
|
||
// Note: In real tests, we'd mock the import failure to trigger fallback
|
||
});
|
||
|
||
it('should perform basic URL validation in fallback mode', async () => {
|
||
const result = await scanner.validateUrl('https://example.com');
|
||
expect(result).toHaveProperty('valid');
|
||
expect(result).toHaveProperty('blocked');
|
||
});
|
||
|
||
it('should perform basic PII scanning in fallback mode', async () => {
|
||
const result = await scanner.scanForPII('Email: test@example.com');
|
||
expect(result).toHaveProperty('found');
|
||
expect(result).toHaveProperty('types');
|
||
expect(result).toHaveProperty('riskLevel');
|
||
});
|
||
|
||
it('should provide minimal phishing detection in fallback mode', async () => {
|
||
// Simulate fallback mode by creating a scanner that fails initialization
|
||
const fallbackScanner = new MockBrowserSecurityScanner();
|
||
await fallbackScanner.initialize();
|
||
|
||
const result = await fallbackScanner.detectPhishing('https://example.com');
|
||
expect(result).toHaveProperty('confidence');
|
||
expect(result).toHaveProperty('recommendation');
|
||
});
|
||
|
||
it('should gracefully degrade without crashing', async () => {
|
||
// All methods should work even in fallback mode
|
||
await expect(scanner.validateUrl('https://test.com')).resolves.toBeDefined();
|
||
await expect(scanner.scanForPII('test')).resolves.toBeDefined();
|
||
await expect(scanner.detectPhishing('https://test.com')).resolves.toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('Edge Cases and Error Handling', () => {
|
||
beforeEach(async () => {
|
||
scanner = new MockBrowserSecurityScanner();
|
||
await scanner.initialize();
|
||
});
|
||
|
||
it('should handle empty strings gracefully', async () => {
|
||
await expect(scanner.validateUrl('')).resolves.toBeDefined();
|
||
await expect(scanner.scanForPII('')).resolves.toBeDefined();
|
||
});
|
||
|
||
it('should handle very long content in PII scanning', async () => {
|
||
const longContent = 'Normal text '.repeat(10000) + 'test@example.com';
|
||
const result = await scanner.scanForPII(longContent);
|
||
|
||
expect(result.found).toBe(true);
|
||
expect(result.types).toContain('email');
|
||
});
|
||
|
||
it('should handle special characters in URLs', async () => {
|
||
const result = await scanner.validateUrl('https://example.com/path?query=value&other=123');
|
||
expect(result).toHaveProperty('valid');
|
||
});
|
||
|
||
it('should handle malformed PII patterns', async () => {
|
||
const content = '123-45-678'; // Invalid SSN (too short)
|
||
const result = await scanner.scanForPII(content);
|
||
|
||
// Should not detect invalid SSN
|
||
expect(result.types).not.toContain('ssn');
|
||
});
|
||
});
|
||
});
|