/** * Unit tests for scope enforcement and auth in PlaywrightAgent. * These tests use the private helper methods indirectly via the agent's behavior * by testing the isExcludedPath, isExternalLink, and isAllowedUrl logic * through subclassing or direct method exposure. * * Since these methods are private, we test observable behavior via * discoverActions and executeAction with mock pages or just verify config logic. */ import { ExplorationConfig, DEFAULT_EXPLORATION_CONFIG } from '../../src/core/ExplorationConfig'; describe('ExplorationConfig', () => { it('has sensible defaults', () => { const cfg = { ...DEFAULT_EXPLORATION_CONFIG }; expect(cfg.maxStates).toBe(50); expect(cfg.maxDepth).toBe(5); expect(cfg.actionDelayMs).toBe(500); expect(cfg.sessionTimeoutMs).toBe(300000); expect(cfg.fuzzingEnabled).toBe(true); expect(cfg.fuzzingIntensity).toBe('medium'); expect(cfg.auth).toBeNull(); expect(cfg.excludedPaths).toEqual([]); expect(cfg.excludedSelectors).toEqual([]); }); it('accepts cookies auth config', () => { const config: ExplorationConfig = { ...DEFAULT_EXPLORATION_CONFIG, auth: { type: 'cookies', cookies: [{ name: 'session', value: 'abc', domain: 'localhost' }], }, }; expect(config.auth?.type).toBe('cookies'); }); it('accepts headers auth config', () => { const config: ExplorationConfig = { ...DEFAULT_EXPLORATION_CONFIG, auth: { type: 'headers', headers: { Authorization: 'Bearer token123' }, }, }; expect(config.auth?.type).toBe('headers'); }); it('accepts login_flow auth config', () => { const config: ExplorationConfig = { ...DEFAULT_EXPLORATION_CONFIG, auth: { type: 'login_flow', loginUrl: 'http://app.com/login', usernameSelector: 'input[name="email"]', passwordSelector: 'input[name="password"]', submitSelector: 'button[type="submit"]', username: 'user@test.com', password: 'secret', }, }; expect(config.auth?.type).toBe('login_flow'); }); }); // Helper to test URL-based scope rules (extracted for testability) describe('Scope URL rules', () => { function isExcludedPath(urlOrPath: string, excludedPaths: string[]): boolean { if (excludedPaths.length === 0) return false; try { const parsed = new URL(urlOrPath, 'http://placeholder'); return excludedPaths.some((p) => parsed.pathname.startsWith(p)); } catch { return false; } } function isExternalLink(href: string, currentUrl: string, allowedDomains: string[]): boolean { if (allowedDomains.length === 0) return false; try { const base = new URL(currentUrl); const target = new URL(href, base.origin); return !allowedDomains.includes(target.hostname); } catch { return false; } } it('excludes paths correctly', () => { expect(isExcludedPath('http://app.com/logout', ['/logout'])).toBe(true); expect(isExcludedPath('http://app.com/home', ['/logout'])).toBe(false); expect(isExcludedPath('http://app.com/admin/users', ['/admin'])).toBe(true); }); it('allows paths when no exclusions', () => { expect(isExcludedPath('http://app.com/logout', [])).toBe(false); }); it('detects external links', () => { expect(isExternalLink('http://external.com/page', 'http://myapp.com', ['myapp.com'])).toBe(true); expect(isExternalLink('/page', 'http://myapp.com', ['myapp.com'])).toBe(false); expect(isExternalLink('http://myapp.com/page', 'http://myapp.com', ['myapp.com'])).toBe(false); }); it('allows all links when no allowedDomains', () => { expect(isExternalLink('http://external.com/page', 'http://myapp.com', [])).toBe(false); }); });