scrapy/tests/test_spidermiddleware_httpe...

261 lines
9.1 KiB
Python

from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import pytest
from scrapy.http import Request, Response
from scrapy.spidermiddlewares.httperror import HttpError, HttpErrorMiddleware
from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler
from tests.spiders import MockServerSpider
from tests.utils.decorators import coroutine_test
if TYPE_CHECKING:
from tests.mockserver.http import MockServer
class _HttpErrorSpider(MockServerSpider):
name = "httperror"
bypass_status_codes: set[int] = set()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.mockserver
self.start_urls = [
self.mockserver.url("/status?n=200"),
self.mockserver.url("/status?n=404"),
self.mockserver.url("/status?n=402"),
self.mockserver.url("/status?n=500"),
]
self.failed = set()
self.skipped = set()
self.parsed = set()
async def start(self):
for url in self.start_urls:
yield Request(url, self.parse, errback=self.on_error)
def parse(self, response: Response) -> None:
self.parsed.add(response.url[-3:])
def on_error(self, failure):
if isinstance(failure.value, HttpError):
response = failure.value.response
if response.status in self.bypass_status_codes:
self.skipped.add(response.url[-3:])
return self.parse(response)
# it assumes there is a response attached to failure
self.failed.add(failure.value.response.url[-3:])
return failure
req = Request("http://scrapytest.org")
def _response(request: Request, status_code: int) -> Response:
return Response(request.url, status=status_code, request=request)
@pytest.fixture
def res200() -> Response:
return _response(req, 200)
@pytest.fixture
def res402() -> Response:
return _response(req, 402)
@pytest.fixture
def res404() -> Response:
return _response(req, 404)
class TestHttpErrorMiddleware:
@pytest.fixture
def mw(self) -> HttpErrorMiddleware:
crawler = get_crawler(DefaultSpider)
crawler.spider = crawler._create_spider()
return HttpErrorMiddleware.from_crawler(crawler)
def test_process_spider_input(
self, mw: HttpErrorMiddleware, res200: Response, res404: Response
) -> None:
mw.process_spider_input(res200)
with pytest.raises(HttpError):
mw.process_spider_input(res404)
def test_process_spider_exception(
self, mw: HttpErrorMiddleware, res404: Response
) -> None:
assert mw.process_spider_exception(res404, HttpError(res404)) == ()
assert mw.process_spider_exception(res404, Exception()) is None
def test_handle_httpstatus_list(
self, mw: HttpErrorMiddleware, res404: Response
) -> None:
request = Request(
"http://scrapytest.org", meta={"handle_httpstatus_list": [404]}
)
res = _response(request, 404)
mw.process_spider_input(res)
assert mw.crawler.spider
mw.crawler.spider.handle_httpstatus_list = [404] # type: ignore[attr-defined]
mw.process_spider_input(res404)
class TestHttpErrorMiddlewareSettings:
"""Similar test, but with settings"""
@pytest.fixture
def mw(self) -> HttpErrorMiddleware:
crawler = get_crawler(DefaultSpider, {"HTTPERROR_ALLOWED_CODES": (402,)})
crawler.spider = crawler._create_spider()
return HttpErrorMiddleware.from_crawler(crawler)
def test_process_spider_input(
self,
mw: HttpErrorMiddleware,
res200: Response,
res402: Response,
res404: Response,
) -> None:
mw.process_spider_input(res200)
with pytest.raises(HttpError):
mw.process_spider_input(res404)
mw.process_spider_input(res402)
def test_meta_overrides_settings(self, mw: HttpErrorMiddleware) -> None:
request = Request(
"http://scrapytest.org", meta={"handle_httpstatus_list": [404]}
)
res404 = _response(request, 404)
res402 = _response(request, 402)
mw.process_spider_input(res404)
with pytest.raises(HttpError):
mw.process_spider_input(res402)
def test_spider_override_settings(
self, mw: HttpErrorMiddleware, res402: Response, res404: Response
) -> None:
assert mw.crawler.spider
mw.crawler.spider.handle_httpstatus_list = [404] # type: ignore[attr-defined]
mw.process_spider_input(res404)
with pytest.raises(HttpError):
mw.process_spider_input(res402)
class TestHttpErrorMiddlewareHandleAll:
@pytest.fixture
def mw(self) -> HttpErrorMiddleware:
crawler = get_crawler(DefaultSpider, {"HTTPERROR_ALLOW_ALL": True})
crawler.spider = crawler._create_spider()
return HttpErrorMiddleware.from_crawler(crawler)
def test_process_spider_input(
self,
mw: HttpErrorMiddleware,
res200: Response,
res404: Response,
) -> None:
mw.process_spider_input(res200)
mw.process_spider_input(res404)
def test_meta_overrides_settings(self, mw: HttpErrorMiddleware) -> None:
request = Request(
"http://scrapytest.org", meta={"handle_httpstatus_list": [404]}
)
res404 = _response(request, 404)
res402 = _response(request, 402)
mw.process_spider_input(res404)
with pytest.raises(HttpError):
mw.process_spider_input(res402)
def test_httperror_allow_all_false(self) -> None:
crawler = get_crawler(_HttpErrorSpider)
mw = HttpErrorMiddleware.from_crawler(crawler)
request_httpstatus_false = Request(
"http://scrapytest.org", meta={"handle_httpstatus_all": False}
)
request_httpstatus_true = Request(
"http://scrapytest.org", meta={"handle_httpstatus_all": True}
)
res404 = _response(request_httpstatus_false, 404)
res402 = _response(request_httpstatus_true, 402)
with pytest.raises(HttpError):
mw.process_spider_input(res404)
mw.process_spider_input(res402)
class TestHttpErrorMiddlewareIntegrational:
@coroutine_test
async def test_middleware_works(self, mockserver: MockServer) -> None:
crawler = get_crawler(_HttpErrorSpider)
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, _HttpErrorSpider)
assert not crawler.spider.skipped
assert crawler.spider.parsed == {"200"}
assert crawler.spider.failed == {"404", "402", "500"}
assert crawler.stats
get_value = crawler.stats.get_value
assert get_value("httperror/response_ignored_count") == 3
assert get_value("httperror/response_ignored_status_count/404") == 1
assert get_value("httperror/response_ignored_status_count/402") == 1
assert get_value("httperror/response_ignored_status_count/500") == 1
@coroutine_test
async def test_logging(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(_HttpErrorSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver, bypass_status_codes={402})
assert isinstance(crawler.spider, _HttpErrorSpider)
assert crawler.spider.parsed == {"200", "402"}
assert crawler.spider.skipped == {"402"}
assert crawler.spider.failed == {"404", "500"}
assert "Ignoring response <404" in caplog.text
assert "Ignoring response <500" in caplog.text
assert "Ignoring response <200" not in caplog.text
assert "Ignoring response <402" not in caplog.text
@coroutine_test
async def test_logging_level(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
# HttpError logs ignored responses with level INFO
crawler = get_crawler(_HttpErrorSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, _HttpErrorSpider)
assert crawler.spider.parsed == {"200"}
assert crawler.spider.failed == {"404", "402", "500"}
assert "Ignoring response <402" in caplog.text
assert "Ignoring response <404" in caplog.text
assert "Ignoring response <500" in caplog.text
assert "Ignoring response <200" not in caplog.text
# with level WARNING, we shouldn't capture anything from HttpError
caplog.clear()
crawler = get_crawler(_HttpErrorSpider)
with caplog.at_level(logging.WARNING):
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, _HttpErrorSpider)
assert crawler.spider.parsed == {"200"}
assert crawler.spider.failed == {"404", "402", "500"}
assert "Ignoring response <402" not in caplog.text
assert "Ignoring response <404" not in caplog.text
assert "Ignoring response <500" not in caplog.text
assert "Ignoring response <200" not in caplog.text