from __future__ import annotations import re import warnings from typing import TYPE_CHECKING, Any from urllib.parse import parse_qs, unquote_to_bytes import pytest from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.http import FormRequest, HtmlResponse from scrapy.utils.httpobj import urlparse_cached from scrapy.utils.python import to_unicode from tests.utils.bases.http_request import TestRequestBase if TYPE_CHECKING: from scrapy import Request def _buildresponse(body: bytes | str, **kwargs: Any) -> HtmlResponse: kwargs.setdefault("body", body) kwargs.setdefault("url", "http://example.com") kwargs.setdefault("encoding", "utf-8") return HtmlResponse(**kwargs) def _query_string(req: Request) -> bytes: return req.body if req.method == "POST" else req.url.partition("?")[2].encode() def _qs(req: Request) -> dict[bytes, list[bytes]]: return parse_qs(unquote_to_bytes(_query_string(req)), True) def _qs_unicode(req: Request, encoding: str = "utf-8") -> dict[str, list[str]]: qs = unquote_to_bytes(_query_string(req)).decode(encoding) return parse_qs(qs, True) def _assert_query_equal(first: bytes, second: bytes) -> None: assert sorted(to_unicode(first).split("&")) == sorted(to_unicode(second).split("&")) # FormRequest.from_response() is deprecated in favor of form2request, so the # many tests below that exercise it ignore the resulting deprecation warning. @pytest.mark.filterwarnings("ignore::scrapy.exceptions.ScrapyDeprecationWarning") class TestFormRequest(TestRequestBase): request_class = FormRequest def test_init_not_deprecated(self): # Building a request directly from form data is not deprecated. with warnings.catch_warnings(): warnings.simplefilter("error", ScrapyDeprecationWarning) self.request_class( "http://www.example.com", formdata={"a": "1"}, method="POST" ) self.request_class( "http://www.example.com", method="GET", formdata={"a": "1"} ) def test_from_response_deprecated(self): response = _buildresponse( """
""" ) with pytest.warns( ScrapyDeprecationWarning, match=r"FormRequest\.from_response\(\)" ): self.request_class.from_response(response) def test_empty_formdata(self): r1 = self.request_class("http://www.example.com", formdata={}) assert r1.body == b"" def test_formdata_overrides_querystring(self): data = (("a", "one"), ("a", "two"), ("b", "2")) url = self.request_class( "http://www.example.com/?a=0&b=1&c=3#fragment", method="GET", formdata=data ).url.split("#", maxsplit=1)[0] fs = _qs(self.request_class(url, method="GET", formdata=data)) assert set(fs[b"a"]) == {b"one", b"two"} assert fs[b"b"] == [b"2"] assert fs.get(b"c") is None mapping = {"a": "1", "b": "2"} fs = _qs( self.request_class( "http://www.example.com/", method="GET", formdata=mapping ) ) assert fs[b"a"] == [b"1"] assert fs[b"b"] == [b"2"] def test_default_encoding_bytes(self): # using default encoding (utf-8) data: dict[Any, Any] = {b"one": b"two", b"price": b"\xc2\xa3 100"} r2 = self.request_class("http://www.example.com", formdata=data) assert r2.method == "POST" assert r2.encoding == "utf-8" _assert_query_equal(r2.body, b"price=%C2%A3+100&one=two") assert r2.headers[b"Content-Type"] == b"application/x-www-form-urlencoded" def test_default_encoding_textual_data(self): # using default encoding (utf-8) data = {"µ one": "two", "price": "£ 100"} r2 = self.request_class("http://www.example.com", formdata=data) assert r2.method == "POST" assert r2.encoding == "utf-8" _assert_query_equal(r2.body, b"price=%C2%A3+100&%C2%B5+one=two") assert r2.headers[b"Content-Type"] == b"application/x-www-form-urlencoded" def test_default_encoding_mixed_data(self): # using default encoding (utf-8) data: dict[Any, Any] = {"\u00b5one": b"two", b"price\xc2\xa3": "\u00a3 100"} r2 = self.request_class("http://www.example.com", formdata=data) assert r2.method == "POST" assert r2.encoding == "utf-8" _assert_query_equal(r2.body, b"%C2%B5one=two&price%C2%A3=%C2%A3+100") assert r2.headers[b"Content-Type"] == b"application/x-www-form-urlencoded" def test_custom_encoding_bytes(self): data: dict[Any, Any] = {b"\xb5 one": b"two", b"price": b"\xa3 100"} r2 = self.request_class( "http://www.example.com", formdata=data, encoding="latin1" ) assert r2.method == "POST" assert r2.encoding == "latin1" _assert_query_equal(r2.body, b"price=%A3+100&%B5+one=two") assert r2.headers[b"Content-Type"] == b"application/x-www-form-urlencoded" def test_custom_encoding_textual_data(self): data = {"price": "£ 100"} r3 = self.request_class( "http://www.example.com", formdata=data, encoding="latin1" ) assert r3.encoding == "latin1" assert r3.body == b"price=%A3+100" def test_multi_key_values(self): # using multiples values for a single key data = {"price": "\xa3 100", "colours": ["red", "blue", "green"]} r3 = self.request_class("http://www.example.com", formdata=data) _assert_query_equal( r3.body, b"colours=red&colours=blue&colours=green&price=%C2%A3+100" ) def test_from_response_post(self): response = _buildresponse( b"""""", url="http://www.example.com/this/list.html", ) req = self.request_class.from_response( response, formdata={"one": ["two", "three"], "six": "seven"} ) assert req.method == "POST" assert req.headers[b"Content-type"] == b"application/x-www-form-urlencoded" assert req.url == "http://www.example.com/this/post.php" fs = _qs(req) assert set(fs[b"test"]) == {b"val1", b"val2"} assert set(fs[b"one"]) == {b"two", b"three"} assert fs[b"test2"] == [b"xxx"] assert fs[b"six"] == [b"seven"] def test_from_response_post_nonascii_bytes_utf8(self): response = _buildresponse( b"""""", url="http://www.example.com/this/list.html", ) req = self.request_class.from_response( response, formdata={"one": ["two", "three"], "six": "seven"} ) assert req.method == "POST" assert req.headers[b"Content-type"] == b"application/x-www-form-urlencoded" assert req.url == "http://www.example.com/this/post.php" fs = _qs_unicode(req) assert set(fs["test £"]) == {"val1", "val2"} assert set(fs["one"]) == {"two", "three"} assert fs["test2"] == ["xxx µ"] assert fs["six"] == ["seven"] def test_from_response_post_nonascii_bytes_latin1(self): response = _buildresponse( b"""""", url="http://www.example.com/this/list.html", encoding="latin1", ) req = self.request_class.from_response( response, formdata={"one": ["two", "three"], "six": "seven"} ) assert req.method == "POST" assert req.headers[b"Content-type"] == b"application/x-www-form-urlencoded" assert req.url == "http://www.example.com/this/post.php" fs = _qs_unicode(req, encoding="latin1") assert set(fs["test £"]) == {"val1", "val2"} assert set(fs["one"]) == {"two", "three"} assert fs["test2"] == ["xxx µ"] assert fs["six"] == ["seven"] def test_from_response_post_nonascii_unicode(self): response = _buildresponse( """""", url="http://www.example.com/this/list.html", ) req = self.request_class.from_response( response, formdata={"one": ["two", "three"], "six": "seven"} ) assert req.method == "POST" assert req.headers[b"Content-type"] == b"application/x-www-form-urlencoded" assert req.url == "http://www.example.com/this/post.php" fs = _qs_unicode(req) assert set(fs["test £"]) == {"val1", "val2"} assert set(fs["one"]) == {"two", "three"} assert fs["test2"] == ["xxx µ"] assert fs["six"] == ["seven"] def test_from_response_duplicate_form_key(self): response = _buildresponse("", url="http://www.example.com") req = self.request_class.from_response( response=response, method="GET", formdata=(("foo", "bar"), ("foo", "baz")), ) assert urlparse_cached(req).hostname == "www.example.com" assert urlparse_cached(req).query == "foo=bar&foo=baz" def test_from_response_override_duplicate_form_key(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response, formdata=(("two", "2"), ("two", "4")) ) fs = _qs(req) assert fs[b"one"] == [b"1"] assert fs[b"two"] == [b"2", b"4"] def test_from_response_extra_headers(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response=response, formdata={"one": ["two", "three"], "six": "seven"}, headers={"Accept-Encoding": "gzip,deflate"}, ) assert req.method == "POST" assert req.headers["Content-type"] == b"application/x-www-form-urlencoded" assert req.headers["Accept-Encoding"] == b"gzip,deflate" def test_from_response_get(self): response = _buildresponse( """""", url="http://www.example.com/this/list.html", ) r1 = self.request_class.from_response( response, formdata={"one": ["two", "three"], "six": "seven"} ) assert r1.method == "GET" assert urlparse_cached(r1).hostname == "www.example.com" assert urlparse_cached(r1).path == "/this/get.php" fs = _qs(r1) assert set(fs[b"test"]) == {b"val1", b"val2"} assert set(fs[b"one"]) == {b"two", b"three"} assert fs[b"test2"] == [b"xxx"] assert fs[b"six"] == [b"seven"] def test_from_response_override_params(self): response = _buildresponse( """""" ) req = self.request_class.from_response(response, formdata={"two": "2"}) fs = _qs(req) assert fs[b"one"] == [b"1"] assert fs[b"two"] == [b"2"] def test_from_response_drop_params(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response, formdata={"two": None}, # type: ignore[arg-type] ) fs = _qs(req) assert fs[b"one"] == [b"1"] assert b"two" not in fs def test_from_response_override_method(self): response = _buildresponse( """ """ ) request = FormRequest.from_response(response) assert request.method == "GET" request = FormRequest.from_response(response, method="POST") assert request.method == "POST" # an explicit method=None skips form-method normalization request = FormRequest.from_response(response, method=None) assert request.method == "NONE" def test_from_response_override_url(self): response = _buildresponse( """ """ ) request = FormRequest.from_response(response) assert request.url == "http://example.com/app" request = FormRequest.from_response(response, url="http://foo.bar/absolute") assert request.url == "http://foo.bar/absolute" request = FormRequest.from_response(response, url="/relative") assert request.url == "http://example.com/relative" def test_from_response_case_insensitive(self): response = _buildresponse( """""" ) req = self.request_class.from_response(response) fs = _qs(req) assert fs[b"clickable1"] == [b"clicked1"] assert b"i1" not in fs, fs # xpath in _get_inputs() assert b"clickable2" not in fs, fs # xpath in _get_clickable() def test_from_response_submit_first_clickable(self): response = _buildresponse( """""" ) req = self.request_class.from_response(response, formdata={"two": "2"}) fs = _qs(req) assert fs[b"clickable1"] == [b"clicked1"] assert b"clickable2" not in fs, fs assert fs[b"one"] == [b"1"] assert fs[b"two"] == [b"2"] def test_from_response_submit_not_first_clickable(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response, formdata={"two": "2"}, clickdata={"name": "clickable2"} ) fs = _qs(req) assert fs[b"clickable2"] == [b"clicked2"] assert b"clickable1" not in fs, fs assert fs[b"one"] == [b"1"] assert fs[b"two"] == [b"2"] def test_from_response_dont_submit_image_as_input(self): response = _buildresponse( """""" ) req = self.request_class.from_response(response, dont_click=True) fs = _qs(req) assert fs == {b"i1": [b"i1v"]} def test_from_response_dont_submit_reset_as_input(self): response = _buildresponse( """""" ) req = self.request_class.from_response(response, dont_click=True) fs = _qs(req) assert fs == {b"i1": [b"i1v"], b"i2": [b"i2v"]} def test_from_response_clickdata_does_not_ignore_image(self): response = _buildresponse( """""" ) req = self.request_class.from_response(response) fs = _qs(req) assert fs == {b"i1": [b"i1v"], b"i2": [b"i2v"]} def test_from_response_multiple_clickdata(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response, clickdata={"name": "clickable", "value": "clicked2"} ) fs = _qs(req) assert fs[b"clickable"] == [b"clicked2"] assert fs[b"one"] == [b"clicked1"] assert fs[b"two"] == [b"clicked2"] def test_from_response_unicode_clickdata(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response, clickdata={"name": "price in \u00a3"} ) fs = _qs_unicode(req) assert fs["price in \u00a3"] def test_from_response_unicode_clickdata_latin1(self): response = _buildresponse( """""", encoding="latin1", ) req = self.request_class.from_response( response, clickdata={"name": "price in \u00a5"} ) fs = _qs_unicode(req, encoding="latin1") assert fs["price in \u00a5"] def test_from_response_multiple_forms_clickdata(self): response = _buildresponse( """ """ ) req = self.request_class.from_response( response, formname="form2", clickdata={"name": "clickable"} ) fs = _qs(req) assert fs[b"clickable"] == [b"clicked2"] assert fs[b"field2"] == [b"value2"] assert b"field1" not in fs, fs def test_from_response_override_clickable(self): response = _buildresponse( """""" ) req = self.request_class.from_response( response, formdata={"clickme": "two"}, clickdata={"name": "clickme"} ) fs = _qs(req) assert fs[b"clickme"] == [b"two"] def test_from_response_dont_click(self): response = _buildresponse( """""" ) r1 = self.request_class.from_response(response, dont_click=True) fs = _qs(r1) assert b"clickable1" not in fs, fs assert b"clickable2" not in fs, fs def test_from_response_ambiguous_clickdata(self): response = _buildresponse( """ """ ) with pytest.raises( ValueError, match=r"Multiple elements found .* matching the criteria in clickdata", ): self.request_class.from_response(response, clickdata={"type": "submit"}) def test_from_response_non_matching_clickdata(self): response = _buildresponse( """""" ) with pytest.raises( ValueError, match="No clickable element matching clickdata:" ): self.request_class.from_response( response, clickdata={"nonexistent": "notme"} ) def test_from_response_nr_index_clickdata(self): response = _buildresponse( """ """ ) req = self.request_class.from_response(response, clickdata={"nr": 1}) fs = _qs(req) assert b"clickable2" in fs assert b"clickable1" not in fs def test_from_response_invalid_nr_index_clickdata(self): response = _buildresponse( """ """ ) with pytest.raises( ValueError, match="No clickable element matching clickdata:" ): self.request_class.from_response(response, clickdata={"nr": 1}) def test_from_response_errors_noform(self): response = _buildresponse("""""") with pytest.raises(ValueError, match="No