mirror of
https://github.com/laramies/theHarvester.git
synced 2026-09-11 04:07:41 +02:00
247 lines
8.1 KiB
Python
247 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
|
|
from theHarvester.discovery import whoisxml
|
|
from theHarvester.discovery.constants import MissingKey
|
|
from theHarvester.lib.core import FetcherResponse
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncIterator
|
|
|
|
|
|
class ProviderSession:
|
|
def __init__(self) -> None:
|
|
self.exited = False
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def provider_session(monkeypatch: pytest.MonkeyPatch) -> ProviderSession:
|
|
session = ProviderSession()
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def fake_open_session(**_kwargs: Any) -> AsyncIterator[object]:
|
|
try:
|
|
yield session
|
|
finally:
|
|
session.exited = True
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'open_session', fake_open_session)
|
|
return session
|
|
|
|
|
|
@pytest.mark.provider_contract('whoisxml')
|
|
@pytest.mark.asyncio
|
|
async def test_response_body_is_not_logged_and_scoped_records_are_returned(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
provider_session: ProviderSession,
|
|
) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
calls: list[dict[str, Any]] = []
|
|
responses = [
|
|
FetcherResponse(
|
|
{
|
|
'secret': 'provider-secret-payload',
|
|
'result': {
|
|
'count': 2,
|
|
'nextPageSearchAfter': 'www.example.com',
|
|
'records': [
|
|
{'domain': 'API.Example.COM.'},
|
|
{'domain': 'outside.test'},
|
|
],
|
|
},
|
|
},
|
|
200,
|
|
{},
|
|
),
|
|
FetcherResponse(
|
|
{
|
|
'result': {
|
|
'count': 1,
|
|
'nextPageSearchAfter': '',
|
|
'records': [{'domain': 'www.example.com'}],
|
|
},
|
|
},
|
|
200,
|
|
{},
|
|
),
|
|
]
|
|
|
|
async def fake_fetch(**kwargs: Any) -> FetcherResponse:
|
|
calls.append(kwargs)
|
|
return responses.pop(0)
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'fetch', fake_fetch)
|
|
search = whoisxml.SearchWhoisXML('example.com', 3)
|
|
report = await search.process(proxy=True)
|
|
|
|
assert await search.get_hostnames() == {'api.example.com', 'www.example.com'}
|
|
assert report is None
|
|
assert [call['url'] for call in calls] == ['https://subdomains.whoisxmlapi.com/api/v2'] * 2
|
|
assert all(call['session'] is provider_session for call in calls)
|
|
assert [call['params'] for call in calls] == [
|
|
{'apiKey': 'test-key', 'domainName': 'example.com'},
|
|
{'apiKey': 'test-key', 'domainName': 'example.com', 'searchAfter': 'www.example.com'},
|
|
]
|
|
assert all(call['json'] is True and call['include_metadata'] is True for call in calls)
|
|
assert provider_session.exited is True
|
|
|
|
|
|
@pytest.mark.parametrize('key', [None, '', ' '])
|
|
def test_missing_or_blank_key_fails_closed(monkeypatch: pytest.MonkeyPatch, key: str | None) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: key))
|
|
with pytest.raises(MissingKey):
|
|
whoisxml.SearchWhoisXML('example.com', 10)
|
|
|
|
|
|
@pytest.mark.parametrize('limit', [0, -1, True, 1.5])
|
|
def test_limit_must_be_positive(monkeypatch: pytest.MonkeyPatch, limit: Any) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
with pytest.raises(ValueError, match='positive integer'):
|
|
whoisxml.SearchWhoisXML('example.com', limit)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('response', 'status', 'reason'),
|
|
[
|
|
(None, 'failed', 'transport-error'),
|
|
(FetcherResponse({}, 401, {}), 'failed', 'access-denied'),
|
|
(FetcherResponse({}, 429, {}), 'rate-limited', 'http-429'),
|
|
(FetcherResponse({}, 503, {}), 'failed', 'http-503'),
|
|
(FetcherResponse([], 200, {}), 'failed', 'invalid-response'),
|
|
(FetcherResponse({'result': {'records': 'many'}}, 200, {}), 'failed', 'invalid-response'),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_provider_failures_are_truthful(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
response: FetcherResponse | None,
|
|
status: str,
|
|
reason: str,
|
|
) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
|
|
async def fake_fetch(**_kwargs: Any) -> FetcherResponse | None:
|
|
return response
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'fetch', fake_fetch)
|
|
search = whoisxml.SearchWhoisXML('example.com', 10)
|
|
report = await search.process()
|
|
|
|
assert report.status == status
|
|
assert report.stop_reason == reason
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_malformed_rows_preserve_valid_partial_results(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
|
|
async def fake_fetch(**_kwargs: Any) -> FetcherResponse:
|
|
return FetcherResponse(
|
|
{
|
|
'result': {
|
|
'count': 2,
|
|
'nextPageSearchAfter': '',
|
|
'records': [{'domain': 'ok.example.com'}, {'domain': 7}],
|
|
}
|
|
},
|
|
200,
|
|
{},
|
|
)
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'fetch', fake_fetch)
|
|
search = whoisxml.SearchWhoisXML('example.com', 10)
|
|
report = await search.process()
|
|
|
|
assert await search.get_hostnames() == {'ok.example.com'}
|
|
assert report.status == 'failed'
|
|
assert report.stop_reason == 'invalid-response'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_later_page_failure_preserves_partial_results(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
responses = [
|
|
FetcherResponse(
|
|
{
|
|
'result': {
|
|
'count': 1,
|
|
'nextPageSearchAfter': 'next.example.com',
|
|
'records': [{'domain': 'api.example.com'}],
|
|
}
|
|
},
|
|
200,
|
|
{},
|
|
),
|
|
FetcherResponse({}, 429, {}),
|
|
]
|
|
|
|
async def fake_fetch(**_kwargs: Any) -> FetcherResponse:
|
|
return responses.pop(0)
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'fetch', fake_fetch)
|
|
search = whoisxml.SearchWhoisXML('example.com', 10)
|
|
report = await search.process()
|
|
|
|
assert await search.get_hostnames() == {'api.example.com'}
|
|
assert report.status == 'rate-limited'
|
|
assert report.stop_reason == 'http-429'
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('domains', 'expected_status'),
|
|
[
|
|
(('api.example.com', 'mail.example.com'), 'partial'),
|
|
(('outside.test', 'other.test'), 'failed'),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_repeated_cursor_status_reflects_retained_evidence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
domains: tuple[str, str],
|
|
expected_status: str,
|
|
) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
responses = [
|
|
FetcherResponse(
|
|
{'result': {'nextPageSearchAfter': 'same', 'records': [{'domain': domains[0]}]}},
|
|
200,
|
|
{},
|
|
),
|
|
FetcherResponse(
|
|
{'result': {'nextPageSearchAfter': 'same', 'records': [{'domain': domains[1]}]}},
|
|
200,
|
|
{},
|
|
),
|
|
]
|
|
|
|
async def fake_fetch(**_kwargs: Any) -> FetcherResponse:
|
|
return responses.pop(0)
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'fetch', fake_fetch)
|
|
report = await whoisxml.SearchWhoisXML('example.com', 10).process()
|
|
|
|
assert report.status == expected_status
|
|
assert report.stop_reason == 'repeated-cursor'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancellation_closes_provider_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
provider_session: ProviderSession,
|
|
) -> None:
|
|
monkeypatch.setattr(whoisxml.Core, 'whoisxml_key', staticmethod(lambda: 'test-key'))
|
|
|
|
async def fake_fetch(**_kwargs: Any) -> FetcherResponse:
|
|
raise asyncio.CancelledError('operator-stop')
|
|
|
|
monkeypatch.setattr(whoisxml.AsyncFetcher, 'fetch', fake_fetch)
|
|
|
|
with pytest.raises(asyncio.CancelledError, match='operator-stop'):
|
|
await whoisxml.SearchWhoisXML('example.com', 10).process()
|
|
assert provider_session.exited is True
|