"""The search URLs each scraper navigates to. These had no coverage at all, and the gap was invisible: the Playwright tests drive a real browser but patch `page.goto` with a stand-in that accepts the URL and discards it, so everything downstream of navigation was verified and everything upstream was not. Mutation testing confirmed it - replacing the Bing URL with a constant, dropping `&num=` from the Scholar URL, and making the Google Patents URL ignore the query all left the full suite green. A scraper that searched for the wrong thing entirely passed. These are pure string assertions: no browser, no fixtures, microseconds. """ import pytest from serp import ( bing_search_url, brave_search_url, google_patents_search_url, google_scholar_url, ) class TestQueryIsIncluded: """The single most important property: the URL must ask for what the caller asked for.""" @pytest.mark.parametrize("builder", [ bing_search_url, brave_search_url, google_patents_search_url, google_scholar_url]) def test_the_query_reaches_the_url(self, builder): assert "widgets" in builder("widgets", 10) @pytest.mark.parametrize("builder", [ bing_search_url, brave_search_url, google_patents_search_url, google_scholar_url]) def test_a_different_query_produces_a_different_url(self, builder): assert builder("widgets", 10) != builder("gadgets", 10) class TestQueryIsEncoded: """A raw query in a URL is both a correctness bug (spaces, ampersands and hashes truncate or corrupt the search) and the kind of thing that turns user input into URL structure.""" @pytest.mark.parametrize("builder", [ bing_search_url, brave_search_url, google_patents_search_url, google_scholar_url]) def test_spaces_are_encoded(self, builder): url = builder("agentic ai", 10) assert "agentic+ai" in url or "agentic%20ai" in url assert "agentic ai" not in url @pytest.mark.parametrize("builder", [ bing_search_url, brave_search_url, google_patents_search_url, google_scholar_url]) def test_url_significant_characters_are_escaped(self, builder): url = builder("a&b=c#d", 10) query_part = url.split("?", 1)[1] # Exactly one separator per parameter the builder itself added. assert "a&b=c#d" not in query_part assert "%26" in query_part class TestResultCount: def test_scholar_asks_for_the_requested_number_of_results(self): assert "num=25" in google_scholar_url("widgets", 25) def test_google_patents_asks_for_the_requested_number_of_results(self): assert "num=25" in google_patents_search_url("widgets", 25) class TestHosts: @pytest.mark.parametrize("builder, host", [ (bing_search_url, "https://www.bing.com/search"), (brave_search_url, "https://search.brave.com/search"), (google_patents_search_url, "https://patents.google.com/"), (google_scholar_url, "https://scholar.google.com/scholar"), ]) def test_points_at_the_expected_endpoint(self, builder, host): assert builder("widgets", 10).startswith(host)