/** * Unit tests for the fuzzing module. */ import { FuzzSession } from '../../src/modules/fuzzing/domain/entities/FuzzSession'; import { FuzzResult } from '../../src/modules/fuzzing/domain/entities/FuzzResult'; import { FuzzIntensity } from '../../src/modules/fuzzing/domain/value-objects/FuzzIntensity'; import { FuzzStrategy } from '../../src/modules/fuzzing/domain/value-objects/FuzzStrategy'; import { Seed } from '../../src/modules/fuzzing/domain/value-objects/Seed'; import { FuzzPayload } from '../../src/modules/fuzzing/domain/value-objects/FuzzPayload'; import { EmptyValueStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy'; import { OversizedStringStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy'; import { SpecialCharsStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy'; import { TypeMismatchStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy'; import { BoundaryValueStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy'; import { FuzzingEngineAdapter } from '../../src/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter'; import { detectInputType } from '../../src/modules/fuzzing/infrastructure/adapters/InputTypeDetector'; import { IState } from '../../src/core/interfaces'; function makeState(domSnapshot = ''): IState { return { id: 'state1', url: 'http://test.com', title: 'Test', timestamp: Date.now(), domSnapshot, visitCount: 1, }; } // ─── Value Objects ──────────────────────────────────────────────────────────── describe('FuzzIntensity', () => { it('creates low intensity', () => { expect(FuzzIntensity.low().value).toBe('low'); }); it('creates from string', () => { expect(FuzzIntensity.fromString('high').value).toBe('high'); }); it('throws for invalid intensity', () => { expect(() => FuzzIntensity.fromString('extreme')).toThrow(); }); it('equality works', () => { expect(FuzzIntensity.medium().equals(FuzzIntensity.medium())).toBe(true); expect(FuzzIntensity.low().equals(FuzzIntensity.high())).toBe(false); }); }); describe('FuzzStrategy', () => { it('creates all strategies', () => { expect(FuzzStrategy.empty().value).toBe('empty'); expect(FuzzStrategy.specialChars().value).toBe('special_chars'); expect(FuzzStrategy.boundary().value).toBe('boundary'); }); it('throws for invalid strategy', () => { expect(() => FuzzStrategy.fromString('invalid')).toThrow(); }); }); describe('Seed', () => { it('creates valid seed', () => { expect(Seed.create(42).value).toBe(42); }); it('throws for negative seed', () => { expect(() => Seed.create(-1)).toThrow(); }); it('throws for non-integer', () => { expect(() => Seed.create(1.5)).toThrow(); }); it('creates from timestamp', () => { const seed = Seed.fromTimestamp(); expect(seed.value).toBeGreaterThan(0); }); }); describe('FuzzPayload', () => { it('creates payload with value and strategy', () => { const p = FuzzPayload.create("' OR 1=1 --", 'special_chars'); expect(p.value).toBe("' OR 1=1 --"); expect(p.strategy).toBe('special_chars'); }); }); // ─── FuzzSession Aggregate ─────────────────────────────────────────────────── describe('FuzzSession', () => { it('creates a fuzz session with domain event', () => { const result = FuzzSession.create({ crawlSessionId: 'crawl-1', intensity: 'medium', seed: 12345, }); if (!result.ok) throw new Error('Expected Ok'); const session = result.value; expect(session.intensity.value).toBe('medium'); expect(session.seed.value).toBe(12345); expect(session.status).toBe('running'); expect(session.actionsExecuted).toBe(0); expect(session.domainEvents).toHaveLength(1); expect(session.domainEvents[0]!.eventName).toBe('fuzz.started'); }); it('fails with invalid intensity', () => { const result = FuzzSession.create({ crawlSessionId: 'crawl-1', intensity: 'extreme' as 'low', seed: 1, }); expect(result.ok).toBe(false); }); it('fails with negative seed', () => { const result = FuzzSession.create({ crawlSessionId: 'crawl-1', intensity: 'low', seed: -1, }); expect(result.ok).toBe(false); }); it('tracks vulnerability detection', () => { const r1 = FuzzSession.create({ crawlSessionId: 'c1', intensity: 'high', seed: 1 }); if (!r1.ok) throw new Error('Expected Ok'); const session = r1.value; session.clearEvents(); const fuzzResult = FuzzResult.create({ sessionId: session.id.toString(), stateId: 'state1', selector: '#email', payload: "' OR 1=1 --", strategy: 'special_chars', anomalyType: 'xss_reflection', severity: 'critical', description: 'XSS reflected', }); session.recordVulnerability(fuzzResult); expect(session.vulnerabilitiesFound).toBe(1); expect(session.actionsExecuted).toBe(1); expect(session.domainEvents[0]!.eventName).toBe('fuzz.vulnerability_detected'); }); it('completes session with event', () => { const r2 = FuzzSession.create({ crawlSessionId: 'c1', intensity: 'low', seed: 1 }); if (!r2.ok) throw new Error('Expected Ok'); const session = r2.value; session.clearEvents(); session.complete(); expect(session.status).toBe('completed'); expect(session.completedAt).toBeDefined(); expect(session.domainEvents[0]!.eventName).toBe('fuzz.completed'); }); }); // ─── Strategies ─────────────────────────────────────────────────────────────── describe('EmptyValueStrategy', () => { const s = new EmptyValueStrategy(); it('applies to all types', () => { expect(s.appliesTo('email')).toBe(true); expect(s.appliesTo('number')).toBe(true); expect(s.appliesTo('select')).toBe(true); }); it('returns empty/whitespace values', () => { const vals = s.values(); expect(vals).toContain(''); expect(vals).toContain(' '); expect(vals).toContain('\t'); }); }); describe('OversizedStringStrategy', () => { it('applies to text, email, password, textarea', () => { const s = new OversizedStringStrategy('medium'); expect(s.appliesTo('text')).toBe(true); expect(s.appliesTo('email')).toBe(true); expect(s.appliesTo('number')).toBe(false); }); it('low = 256 chars', () => { const s = new OversizedStringStrategy('low'); expect(s.values()[0]?.length).toBe(256); }); it('medium = 1024 chars', () => { const s = new OversizedStringStrategy('medium'); expect(s.values()[0]?.length).toBe(1024); }); it('high = 10000+ chars', () => { const s = new OversizedStringStrategy('high'); expect(s.values()[0]!.length).toBeGreaterThan(10000); }); }); describe('SpecialCharsStrategy', () => { const s = new SpecialCharsStrategy(); it('applies to text, email, search, textarea', () => { expect(s.appliesTo('text')).toBe(true); expect(s.appliesTo('email')).toBe(true); expect(s.appliesTo('number')).toBe(false); }); it('includes SQL injection payload', () => { expect(s.values()).toContain("' OR 1=1 --"); }); it('includes XSS payload', () => { expect(s.values()).toContain(''); }); }); describe('TypeMismatchStrategy', () => { const s = new TypeMismatchStrategy(); it('applies to typed fields', () => { expect(s.appliesTo('email')).toBe(true); expect(s.appliesTo('number')).toBe(true); expect(s.appliesTo('text')).toBe(false); }); it('returns mismatch for email', () => { expect(s.values('email')).toContain('not-an-email'); }); it('returns mismatch for number', () => { expect(s.values('number')).toContain('abc'); }); }); describe('BoundaryValueStrategy', () => { const s = new BoundaryValueStrategy(); it('applies to number and date only', () => { expect(s.appliesTo('number')).toBe(true); expect(s.appliesTo('date')).toBe(true); expect(s.appliesTo('text')).toBe(false); }); it('returns boundary numbers', () => { const vals = s.values('number'); expect(vals).toContain('0'); expect(vals).toContain('2147483647'); expect(vals).toContain('-2147483648'); }); it('returns boundary dates', () => { const vals = s.values('date'); expect(vals).toContain('1900-01-01'); expect(vals).toContain('2099-12-31'); }); }); // ─── InputTypeDetector ──────────────────────────────────────────────────────── describe('detectInputType (module adapter)', () => { it('detects email from inputType', () => { expect(detectInputType({ inputType: 'email' })).toBe('email'); }); it('detects password', () => { expect(detectInputType({ inputType: 'password' })).toBe('password'); }); it('detects textarea', () => { expect(detectInputType({ tagName: 'textarea' })).toBe('textarea'); }); it('infers email from name', () => { expect(detectInputType({ name: 'email_address' })).toBe('email'); }); it('falls back to text', () => { expect(detectInputType({})).toBe('text'); }); }); // ─── FuzzingEngineAdapter ───────────────────────────────────────────────────── describe('FuzzingEngineAdapter', () => { it('generates fuzz actions from DOM with input fields', () => { const engine = new FuzzingEngineAdapter({ intensity: 'low', seed: 42 }); const dom = `
`; const state = makeState(dom); const actions = engine.generateFuzzActions(dom, state); expect(actions.length).toBeGreaterThan(0); expect(actions.every((a) => a.type === 'fill')).toBe(true); expect(actions.every((a) => a.stateId === 'state1')).toBe(true); }); it('generates more actions at high intensity', () => { const low = new FuzzingEngineAdapter({ intensity: 'low', seed: 1 }); const high = new FuzzingEngineAdapter({ intensity: 'high', seed: 1 }); const dom = ``; const state = makeState(dom); expect(high.generateFuzzActions(dom, state).length).toBeGreaterThan( low.generateFuzzActions(dom, state).length ); }); it('returns empty for DOM with no inputs', () => { const engine = new FuzzingEngineAdapter({ intensity: 'medium', seed: 1 }); const dom = `No forms here