mirror of https://github.com/scrapy/scrapy.git
261 lines
9.1 KiB
Python
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
|