Files
LEDMatrix/test/test_http_headers.py
ChuckandClaude Opus 5.5 724673ba0b fix: one retry layer for background fetches; CI installs the web requirements; one Discord invite (#657)
- 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>
2026-09-28 11:08:43 -04:00

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