File size: 3,136 Bytes
232eb80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"""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)