Skip to content

Add unit tests for properties #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions playwright_stealth/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._stealth_config import StealthConfig

__ALL__ = ["StealthConfig"]
2 changes: 1 addition & 1 deletion playwright_stealth/core/_stealth_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from typing import Dict, Tuple, Optional
import os
from playwright_stealth.properties._properties import Properties
from playwright_stealth.properties import Properties


def from_file(name) -> str:
Expand Down
3 changes: 3 additions & 0 deletions playwright_stealth/properties/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._properties import Properties

__ALL__ = ["Properties"]
21 changes: 10 additions & 11 deletions playwright_stealth/properties/_header_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def __init__(

# # Self generated headers
self.sec_ch_ua = self._generate_sec_ch_ua(brands)
self.sec_ch_ua_mobile = self._generate_sec_ch_ua_mobile()
self.sec_ch_ua_platform = self._generate_sec_ch_ua_platform()
self.sec_ch_ua_form_factors = self.generate_sec_ch_ua_form_factors()
self.sec_ch_ua_mobile = self.generate_sec_ch_ua_mobile()
self.sec_ch_ua_form_factors = self._generate_sec_ch_ua_form_factors()

def _generate_sec_ch_ua_platform(self) -> str:
"""Generates the Sec_Ch_Ua_Platform based on the user agent platform."""
Expand All @@ -66,19 +66,18 @@ def _generate_sec_ch_ua(self, brands: List[dict]) -> str:
)
return merged_brands

def as_dict(self) -> dict:

# Convert all keys to kebab case and return a new dictionary
return {
key.replace("_", "-").lower(): value for key, value in self.__dict__.items()
}

def generate_sec_ch_ua_form_factors(self) -> str:
def _generate_sec_ch_ua_form_factors(self) -> str:
"""Generates the Sec_Ch_Ua_Form_Factors based on the user agent."""

return "desktop"

def generate_sec_ch_ua_mobile(self) -> str:
def _generate_sec_ch_ua_mobile(self) -> str:
"""Generates the Sec_Ch_Ua_Mobile based on the user agent."""

return "?0"

def as_dict(self) -> dict:
# Convert all keys to kebab case and return a new dictionary
return {
key.replace("_", "-").lower(): value for key, value in self.__dict__.items()
}
19 changes: 7 additions & 12 deletions playwright_stealth/properties/_webgl_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,13 @@ class WebGlProperties:
]

def __init__(self):
self.vendor = self._generate_vendor()
self.renderer = self._generate_renderer()
webgl_prop = self._generate_webgl_prop()
self.vendor = webgl_prop["vendor"]
self.renderer = webgl_prop["renderer"]

def _generate_webgl_prop(self):
"""Generates a WebGL property containing both vendor and renderer."""
return random.choice(self.webgl_properties)

def as_dict(self):
return self.__dict__

def _generate_vendor(self) -> str:
"""Generates the vendor based on the user agent."""

return random.choice(self.webgl_properties)["vendor"]

def _generate_renderer(self) -> str:
"""Generates the renderer based on the user agent."""

return random.choice(self.webgl_properties)["renderer"]
Comment on lines -28 to -36
Copy link
Contributor Author

@Zechereh Zechereh Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach allowed for potential mismatches of a vendor and renderer. (i.e AMD as the vendor and a NVIDIA GTX 1660 as the rendered)

Consolidated the logic into one random.choice() call to prevent this.

LMK if this was by design

4 changes: 2 additions & 2 deletions playwright_stealth/stealth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from typing import Union
from playwright.async_api import Page as AsyncPage
from playwright.sync_api import Page as SyncPage
from playwright_stealth.core._stealth_config import StealthConfig
from playwright_stealth.properties._properties import Properties
from playwright_stealth.core import StealthConfig
from playwright_stealth.properties import Properties


def combine_scripts(properties: Properties, config: StealthConfig):
Expand Down
29 changes: 27 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ readme = "README.md"
python = "^3.8"
playwright = "^1"
fake-http-header = "^0.3.5"
pytest-mockito = "^0.0.4"

[tool.poetry.group.dev.dependencies]
agentql = "^1.3.0"
Expand Down
136 changes: 136 additions & 0 deletions tests/unit/header_properties_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import pytest
from fake_http_header import FakeHttpHeader
from playwright_stealth.properties._header_properties import HeaderProperties


@pytest.fixture
def fake_headers():
"""Fixture to generate fake headers with an additional 'origin' key if missing."""
return FakeHttpHeader(domain_code="com", browser="chrome").as_header_dict()


@pytest.fixture
def brands():
"""Fixture for sample brand data."""
return [{"brand": "BrandA", "version": "1"}, {"brand": "BrandB", "version": "2"}]


@pytest.fixture
def header_properties(fake_headers, brands):
"""Fixture to initialize HeaderProperties with fake headers and brands."""
return HeaderProperties(brands=brands, dnt="1", **fake_headers)


def test_initialization(header_properties, fake_headers, brands):
"""Test that HeaderProperties initializes with correct attributes and values."""
assert header_properties.user_agent == fake_headers["User-Agent"]
assert header_properties.accept_language == fake_headers["Accept-language"]
assert header_properties.accept_encoding == fake_headers["Accept-encoding"]
assert header_properties.accept == fake_headers["Accept"]
assert header_properties.referer == fake_headers["Referer"]
assert header_properties.dnt == "1"
assert header_properties.sec_ch_ua == header_properties._generate_sec_ch_ua(brands)
assert (
header_properties.sec_ch_ua_mobile
== header_properties._generate_sec_ch_ua_mobile()
)
assert (
header_properties.sec_ch_ua_platform
== header_properties._generate_sec_ch_ua_platform()
)
assert (
header_properties.sec_ch_ua_form_factors
== header_properties._generate_sec_ch_ua_form_factors()
)


def test_generate_sec_ch_ua_platform(header_properties):
"""Test _generate_sec_ch_ua_platform with various user agents."""
test_cases = [
("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "macOS"),
("Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Windows"),
("Mozilla/5.0 (X11; Linux x86_64)", "Linux"),
("Mozilla/5.0 (Unknown OS)", "Unknown"),
]
for user_agent, expected_platform in test_cases:
header_properties.user_agent = user_agent
assert header_properties._generate_sec_ch_ua_platform() == expected_platform


def test_generate_sec_ch_ua(header_properties, brands):
"""Test _generate_sec_ch_ua generates correct string format."""
ua_string = header_properties._generate_sec_ch_ua(brands)
expected_string = '"BrandA";v="1","BrandB";v="2",'
assert ua_string == expected_string


def test_generate_sec_ch_ua_mobile(header_properties):
"""Test _generate_sec_ch_ua_mobile returns expected value."""
assert header_properties._generate_sec_ch_ua_mobile() == "?0"


def test_generate_sec_ch_ua_form_factors(header_properties):
"""Test _generate_sec_ch_ua_form_factors returns expected value."""
assert header_properties._generate_sec_ch_ua_form_factors() == "desktop"


def test_as_dict(header_properties):
"""Test as_dict converts headers to kebab-case correctly and includes all keys."""
headers_dict = header_properties.as_dict()
expected_keys = {
"user-agent",
"accept-language",
"accept-encoding",
"accept",
"referer",
"dnt",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"sec-ch-ua-form-factors",
}
assert set(headers_dict.keys()) == expected_keys
assert headers_dict["user-agent"] == header_properties.user_agent
assert headers_dict["accept-language"] == header_properties.accept_language
assert headers_dict["accept-encoding"] == header_properties.accept_encoding
assert headers_dict["accept"] == header_properties.accept
assert headers_dict["referer"] == header_properties.referer
assert headers_dict["dnt"] == header_properties.dnt
assert headers_dict["sec-ch-ua"] == header_properties.sec_ch_ua
assert headers_dict["sec-ch-ua-mobile"] == header_properties.sec_ch_ua_mobile
assert headers_dict["sec-ch-ua-platform"] == header_properties.sec_ch_ua_platform
assert (
headers_dict["sec-ch-ua-form-factors"]
== header_properties.sec_ch_ua_form_factors
)


@pytest.mark.parametrize(
"user_agent,expected_platform",
[
("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "macOS"),
("Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Windows"),
("Mozilla/5.0 (X11; Linux x86_64)", "Linux"),
("Mozilla/5.0 (Unknown OS)", "Unknown"),
],
)
def test_generate_sec_ch_ua_platform_parametrized(user_agent, expected_platform):
"""Parametrized test for _generate_sec_ch_ua_platform."""
header_properties = HeaderProperties(
brands=[],
dnt="1",
**{
"User-Agent": user_agent,
"Accept-language": "",
"Accept-encoding": "",
"Accept": "",
"Referer": "",
}
)
assert header_properties._generate_sec_ch_ua_platform() == expected_platform


def test_missing_headers():
"""Test that HeaderProperties raises an error when required headers are missing."""
with pytest.raises(KeyError):
HeaderProperties(brands=[], dnt="1")
99 changes: 99 additions & 0 deletions tests/unit/navigator_properties_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import pytest
from fake_http_header import FakeHttpHeader
from playwright_stealth.properties._navigator_properties import NavigatorProperties


@pytest.fixture
def fake_headers():
"""Fixture to generate fake headers using FakeHttpHeader."""
return FakeHttpHeader(domain_code="com", browser="chrome").as_header_dict()


@pytest.fixture
def brands():
"""Fixture for sample brand data."""
return [
{"brand": "Chromium", "version": "95"},
{"brand": "Google Chrome", "version": "95"},
]


@pytest.fixture
def navigator_properties(fake_headers, brands):
"""Fixture to initialize NavigatorProperties with fake headers and brands."""
return NavigatorProperties(brands=brands, dnt="1", **fake_headers)


def test_initialization(navigator_properties, fake_headers, brands):
"""Test that NavigatorProperties initializes with correct attributes."""
assert navigator_properties.userAgent == fake_headers["User-Agent"]
assert navigator_properties.platform in ["Macintosh", "Windows", "Linux"]
assert navigator_properties.language == "en-US"
assert isinstance(navigator_properties.languages, list)
assert (
navigator_properties.appVersion
== navigator_properties._generate_app_version(fake_headers["User-Agent"])
)
assert navigator_properties.vendor in ["Google Inc.", ""]
assert navigator_properties.deviceMemory == 8
assert navigator_properties.hardwareConcurrency == 8
assert navigator_properties.maxTouchPoints == 0
assert navigator_properties.doNotTrack == "1"
assert navigator_properties.brands == brands
assert navigator_properties.mobile is False


def test_generate_platform(navigator_properties):
"""Test the _generate_platform method with various user agents."""
ua_mac = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
ua_windows = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
ua_linux = "Mozilla/5.0 (X11; Linux x86_64)"
assert navigator_properties._generate_platform(ua_mac) == "Macintosh"
assert navigator_properties._generate_platform(ua_windows) == "Windows"
assert navigator_properties._generate_platform(ua_linux) == "Linux"


def test_generate_languages(navigator_properties):
"""Test the _generate_languages method."""
accept_language = "en-US,en;q=0.9,fr;q=0.8"
expected_languages = ["en-US", "en", "fr"]
assert (
navigator_properties._generate_languages(accept_language) == expected_languages
)


def test_generate_app_version(navigator_properties):
"""Test the _generate_app_version method."""
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
expected_version = "5.0 (Windows NT 10.0; Win64; x64)"
assert navigator_properties._generate_app_version(user_agent) == expected_version


def test_generate_vendor(navigator_properties):
"""Test the _generate_vendor method."""
ua_chrome = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/85.0.4183.102"
ua_firefox = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/80.0"
ua_safari = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15"
assert navigator_properties._generate_vendor(ua_chrome) == "Google Inc."
assert navigator_properties._generate_vendor(ua_firefox) == ""
assert navigator_properties._generate_vendor(ua_safari) == "Google Inc."


def test_as_dict(navigator_properties):
"""Test that as_dict converts the navigator properties to a dictionary correctly."""
navigator_dict = navigator_properties.as_dict()
assert navigator_dict["userAgent"] == navigator_properties.userAgent
assert navigator_dict["platform"] == navigator_properties.platform
assert navigator_dict["language"] == navigator_properties.language
assert navigator_dict["languages"] == navigator_properties.languages
assert navigator_dict["appVersion"] == navigator_properties.appVersion
assert navigator_dict["vendor"] == navigator_properties.vendor
assert navigator_dict["deviceMemory"] == navigator_properties.deviceMemory
assert (
navigator_dict["hardwareConcurrency"]
== navigator_properties.hardwareConcurrency
)
assert navigator_dict["maxTouchPoints"] == navigator_properties.maxTouchPoints
assert navigator_dict["doNotTrack"] == navigator_properties.doNotTrack
assert navigator_dict["brands"] == navigator_properties.brands
assert navigator_dict["mobile"] == navigator_properties.mobile
Loading