import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { safeFetch, SsrfBlockedError } from '../../security/safe-fetch.js';
describe('safeFetch — DNS rebinding defense', () => {
    let originalFetch;
    let mockFetch;
    beforeEach(() => {
        originalFetch = globalThis.fetch;
        mockFetch = vi.fn();
        globalThis.fetch = mockFetch;
    });
    afterEach(() => {
        globalThis.fetch = originalFetch;
    });
    it('proceeds with public IP when DNS resolves to a public address', async () => {
        mockFetch.mockResolvedValueOnce(new Response('ok', { status: 200 }));
        const resolver = vi.fn().mockResolvedValue({ safe: true, resolvedIp: '8.8.8.8' });
        const res = await safeFetch('https://example.com/x', { _resolver: resolver });
        expect(res.status).toBe(200);
        expect(resolver).toHaveBeenCalledWith('example.com');
        expect(mockFetch).toHaveBeenCalledTimes(1);
    });
    it('rejects when DNS resolves to a private IP (DNS rebinding defeated)', async () => {
        const resolver = vi.fn().mockResolvedValue({
            safe: false,
            resolvedIp: '127.0.0.1',
            error: 'Resolved IP 127.0.0.1 is in a private range',
        });
        await expect(safeFetch('https://attacker-controlled.tld/probe', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
        expect(mockFetch).not.toHaveBeenCalled();
    });
    it('rejects when DNS resolves to AWS metadata IP', async () => {
        const resolver = vi.fn().mockResolvedValue({
            safe: false,
            resolvedIp: '169.254.169.254',
            error: 'Resolved IP 169.254.169.254 is in a private range',
        });
        await expect(safeFetch('https://looks-public.com/', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
    });
    it('best-effort mode: proceeds when resolver throws (default)', async () => {
        mockFetch.mockResolvedValueOnce(new Response('ok'));
        const resolver = vi.fn().mockRejectedValue(new Error('DNS module unavailable'));
        const res = await safeFetch('https://example.com/', { _resolver: resolver });
        expect(res.status).toBe(200);
    });
    it('strict mode: rejects when resolver throws and requireDnsCheck=true', async () => {
        const resolver = vi.fn().mockRejectedValue(new Error('DNS module unavailable'));
        await expect(safeFetch('https://example.com/', {
            _resolver: resolver,
            requireDnsCheck: true,
        })).rejects.toBeInstanceOf(SsrfBlockedError);
        expect(mockFetch).not.toHaveBeenCalled();
    });
    it('rejects URL-string bypass forms before any DNS lookup', async () => {
        const resolver = vi.fn();
        await expect(safeFetch('http://2130706433/', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
        await expect(safeFetch('http://[::ffff:127.0.0.1]/', { _resolver: resolver })).rejects.toBeInstanceOf(SsrfBlockedError);
        expect(resolver).not.toHaveBeenCalled();
    });
    it('re-validates AND re-resolves every redirect hop', async () => {
        // Hop 1: example.com (public) -> 302 to evil.com
        mockFetch.mockResolvedValueOnce(new Response(null, { status: 302, headers: { Location: 'https://evil.com/inner' } }));
        // Hop 2 should never be issued because resolver flags evil.com as private.
        const resolver = vi
            .fn()
            .mockImplementationOnce(async () => ({ safe: true, resolvedIp: '8.8.8.8' }))
            .mockImplementationOnce(async () => ({
            safe: false,
            resolvedIp: '10.0.0.5',
            error: 'private',
        }));
        await expect(safeFetch('https://example.com/start', {
            _resolver: resolver,
            followRedirects: true,
        })).rejects.toBeInstanceOf(SsrfBlockedError);
        expect(resolver).toHaveBeenCalledTimes(2);
        expect(mockFetch).toHaveBeenCalledTimes(1); // only the first hop went out
    });
    it('default (no follow) returns the redirect response unmodified', async () => {
        mockFetch.mockResolvedValueOnce(new Response(null, { status: 302, headers: { Location: 'https://elsewhere.com/' } }));
        const resolver = vi.fn().mockResolvedValue({ safe: true, resolvedIp: '8.8.8.8' });
        const res = await safeFetch('https://example.com/', { _resolver: resolver });
        expect(res.status).toBe(302);
        expect(mockFetch).toHaveBeenCalledTimes(1);
        // No re-resolve because we didn't follow the redirect.
        expect(resolver).toHaveBeenCalledTimes(1);
    });
    it('honors maxRedirects bound', async () => {
        mockFetch.mockResolvedValue(new Response(null, { status: 302, headers: { Location: 'https://example.com/loop' } }));
        const resolver = vi.fn().mockResolvedValue({ safe: true, resolvedIp: '8.8.8.8' });
        await expect(safeFetch('https://example.com/start', {
            _resolver: resolver,
            followRedirects: true,
            maxRedirects: 2,
        })).rejects.toBeInstanceOf(SsrfBlockedError);
    });
});
//# sourceMappingURL=safe-fetch.test.js.map