Fix P2/P3 review findings: DNR redirect format, runtime type guard, cache test setup

This commit is contained in:
2026-05-11 13:54:51 -04:00
parent 726aafef74
commit 0f997b639f
6 changed files with 94 additions and 38 deletions

View File

@@ -1,43 +1,59 @@
import { describe, it, expect } from 'vitest';
import { phishingDetector } from '../src/lib/phishing-detector';
import { UrlVerdict, ThreatType } from '../src/types';
import { describe, it, expect, beforeEach } from 'vitest';
import { urlCache } from '../src/lib/cache';
import { UrlCheckResult, UrlVerdict } from '../src/types';
describe('PhishingDetector (cache test)', () => {
describe('UrlCache', () => {
const sampleResult: UrlCheckResult = {
url: 'https://example.com',
domain: 'example.com',
verdict: UrlVerdict.SAFE,
confidence: 0.95,
threats: [],
cached: false,
latencyMs: 50,
timestamp: Date.now(),
};
describe('analyzeUrl', () => {
it('should return SAFE for legitimate URLs', () => {
const result = phishingDetector.analyzeUrl('https://www.google.com/search?q=test');
expect(result.verdict).toBe(UrlVerdict.SAFE);
});
beforeEach(async () => {
urlCache.clear();
});
it('should detect suspicious TLD', () => {
const result = phishingDetector.analyzeUrl('https://free-prize.tk/claim');
expect(result.threats.some((t) => t.type === ThreatType.DOMAIN_AGE)).toBe(true);
});
it('should return null for missing URL', async () => {
const result = await urlCache.get('https://missing.com');
expect(result).toBeNull();
});
it('should detect typosquatting', () => {
const result = phishingDetector.analyzeUrl('https://goggle.com/login');
expect(result.threats.some((t) => t.type === ThreatType.TYPOSQUAT)).toBe(true);
});
it('should store and retrieve cached result', async () => {
await urlCache.set('https://example.com', sampleResult);
const cached = await urlCache.get('https://example.com');
expect(cached).not.toBeNull();
expect(cached!.cached).toBe(true);
expect(cached!.verdict).toBe(UrlVerdict.SAFE);
});
it('should detect IP address hostname', () => {
const result = phishingDetector.analyzeUrl('http://192.168.1.100/admin');
expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true);
});
it('should normalize URLs by stripping hash and search', async () => {
await urlCache.set('https://example.com/page?foo=bar#section', sampleResult);
const cached = await urlCache.get('https://example.com/page');
expect(cached).not.toBeNull();
});
it('should detect phishing pattern in hostname', () => {
const result = phishingDetector.analyzeUrl('https://login-secure-portal.xyz/account');
expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true);
});
it('should persist and restore from storage', async () => {
await urlCache.set('https://test.com', sampleResult);
await urlCache.persistToStorage();
urlCache.clear();
await urlCache.loadFromStorage();
const cached = await urlCache.get('https://test.com');
expect(cached).not.toBeNull();
});
it('should detect HTTP protocol', () => {
const result = phishingDetector.analyzeUrl('http://example.com/login');
expect(result.threats.some((t) => t.type === ThreatType.MIXED_CONTENT)).toBe(true);
});
it('should evict oldest entry when at max capacity', async () => {
const stats = urlCache.getStats();
expect(stats.max).toBe(5000);
});
it('should return UNKNOWN for malformed URLs', () => {
const result = phishingDetector.analyzeUrl('not-a-real-url');
expect(result.verdict).toBe(UrlVerdict.UNKNOWN);
});
it('should handle malformed URLs gracefully', async () => {
await urlCache.set('not-a-url', sampleResult);
const cached = await urlCache.get('not-a-url');
expect(cached).not.toBeNull();
});
});

View File

@@ -0,0 +1,28 @@
const mockStorage: Record<string, unknown> = {};
const chromeMock = {
storage: {
local: {
set: async (data: Record<string, unknown>) => {
Object.assign(mockStorage, data);
},
get: async (key: string | string[]) => {
if (Array.isArray(key)) {
const result: Record<string, unknown> = {};
for (const k of key) result[k] = mockStorage[k];
return result;
}
return { [key]: mockStorage[key] };
},
remove: async (key: string | string[]) => {
const keys = Array.isArray(key) ? key : [key];
for (const k of keys) delete mockStorage[k];
},
clear: async () => {
Object.keys(mockStorage).forEach((k) => delete mockStorage[k]);
},
},
},
};
(global as any).chrome = chromeMock;