mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
- BackgroundDataService: the session adapter retried connection errors 3x inside each attempt of the service's own retry loop (up to 16 connection attempts per request on a dead network). The adapter no longer retries; ESPN date chunks, which bypass the loop and skip a failed chunk, get a small connection retry of their own (_ConnectionRetryingSession). - CI installs web_interface/requirements.txt. The brotli header test now checks its intent (core never hand-sets br; requests may advertise it when a decoder is installed) instead of failing whenever brotli is present. - Every Discord link uses the LEDMatrix server's invite (RdrC37rEag). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
4.7 KiB
Python
110 lines
4.7 KiB
Python
"""One set of HTTP headers for core's ESPN/data requests.
|
|
|
|
The logo downloader and the background data service each carried their own
|
|
header dict with a placeholder User-Agent (``yourusername/LEDMatrix;
|
|
contact@example.com``) -- the kind of nonconforming token ESPN began 403ing
|
|
around 2026-08-04 -- and a hand-set ``Accept-Encoding: ... br`` although brotli
|
|
is not installed, so a ``br`` body could not have been decoded.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.common.api_helper import DEFAULT_HTTP_HEADERS, USER_AGENT, APIHelper
|
|
|
|
|
|
def _lower_keys(headers):
|
|
return {k.lower(): v for k, v in headers.items()}
|
|
|
|
|
|
class TestSharedHeaders:
|
|
def test_user_agent_names_the_project(self):
|
|
assert USER_AGENT == 'LEDMatrix/1.0 (+https://github.com/ChuckBuilds/LEDMatrix)'
|
|
assert DEFAULT_HTTP_HEADERS['User-Agent'] == USER_AGENT
|
|
|
|
def test_no_hand_set_accept_encoding(self):
|
|
assert 'accept-encoding' not in _lower_keys(DEFAULT_HTTP_HEADERS)
|
|
|
|
def test_is_read_only(self):
|
|
with pytest.raises(TypeError):
|
|
DEFAULT_HTTP_HEADERS['User-Agent'] = 'x' # type: ignore[index]
|
|
|
|
def test_api_helper_sends_the_same_user_agent(self):
|
|
assert APIHelper().session.headers['User-Agent'] == USER_AGENT
|
|
|
|
def test_api_helper_does_not_hand_set_brotli(self):
|
|
# Accept-Encoding is left to requests, which advertises br only when
|
|
# a brotli decoder is installed (flask-compress pulls one in, so the
|
|
# web requirements do). What must not happen is core adding it by
|
|
# hand, advertising a body the client may not be able to read.
|
|
import requests
|
|
assert 'accept-encoding' not in _lower_keys(DEFAULT_HTTP_HEADERS)
|
|
assert (APIHelper().session.headers.get('Accept-Encoding')
|
|
== requests.utils.default_headers()['Accept-Encoding'])
|
|
|
|
def test_logo_helper_sends_the_same_user_agent(self):
|
|
from src.common.logo_helper import LogoHelper
|
|
assert LogoHelper(display_width=64, display_height=32).session.headers['User-Agent'] == USER_AGENT
|
|
|
|
|
|
class TestLogoDownloaderHeaders:
|
|
def test_uses_the_shared_headers(self):
|
|
from src.logo_downloader import LogoDownloader
|
|
headers = LogoDownloader().headers
|
|
assert headers['User-Agent'] == USER_AGENT
|
|
assert 'accept-encoding' not in _lower_keys(headers)
|
|
assert 'yourusername' not in str(headers)
|
|
|
|
def test_instance_headers_are_a_private_copy(self):
|
|
from src.logo_downloader import LogoDownloader
|
|
downloader = LogoDownloader()
|
|
downloader.headers['X-Test'] = '1'
|
|
assert 'X-Test' not in DEFAULT_HTTP_HEADERS
|
|
assert 'X-Test' not in LogoDownloader().headers
|
|
|
|
def test_logo_request_sends_the_user_agent_and_asks_for_an_image(self, tmp_path):
|
|
from src.logo_downloader import LogoDownloader
|
|
downloader = LogoDownloader()
|
|
downloader.session.get = MagicMock(side_effect=RuntimeError("stop"))
|
|
downloader.download_logo("http://x/a.png", tmp_path / "A.png", "A")
|
|
sent = _lower_keys(downloader.session.get.call_args.kwargs['headers'])
|
|
assert sent['user-agent'] == USER_AGENT
|
|
assert sent['accept'].startswith('image/')
|
|
assert 'accept-encoding' not in sent
|
|
|
|
|
|
class TestBackgroundDataServiceHeaders:
|
|
def test_uses_the_shared_headers(self):
|
|
from src.background_data_service import BackgroundDataService
|
|
service = BackgroundDataService(MagicMock(), max_workers=1)
|
|
try:
|
|
headers = service.default_headers
|
|
assert headers['User-Agent'] == USER_AGENT
|
|
assert 'accept-encoding' not in _lower_keys(headers)
|
|
assert 'yourusername' not in str(headers)
|
|
finally:
|
|
service.shutdown(wait=False)
|
|
|
|
|
|
class TestResolverHeaders:
|
|
def test_dynamic_team_resolver_sends_the_user_agent(self):
|
|
from unittest.mock import patch
|
|
from src.dynamic_team_resolver import DynamicTeamResolver
|
|
DynamicTeamResolver._rankings_cache = {}
|
|
DynamicTeamResolver._cache_timestamp = 0
|
|
try:
|
|
with patch('src.dynamic_team_resolver.requests.get',
|
|
side_effect=RuntimeError("stop")) as get:
|
|
DynamicTeamResolver().resolve_teams(["AP_TOP_5"])
|
|
assert get.call_args.kwargs['headers']['User-Agent'] == USER_AGENT
|
|
finally:
|
|
DynamicTeamResolver._rankings_cache = {}
|
|
DynamicTeamResolver._cache_timestamp = 0
|
|
|
|
def test_odds_manager_uses_the_shared_headers(self):
|
|
from src.base_odds_manager import BaseOddsManager
|
|
headers = BaseOddsManager(MagicMock()).session.headers
|
|
for name, value in DEFAULT_HTTP_HEADERS.items():
|
|
assert headers[name] == value
|