mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 18:43:11 +02:00
Split 18+ Python files that exceeded 500 lines into smaller modules with helper files (prefixed with _). All functions are re-exported from the original modules to maintain backward compatibility with test patches and external imports. Files split: - moviepy_showcase.py (1212 -> 302 + 3 helpers) - anki_generator.py (1174 -> 473 + 4 helpers) - test_analyze_chess_game.py (1152 -> 361 + 2 parts) - poker_modifier_app.py (1024 -> 263 + 2 helpers) - transcribe_fw.py (1007 -> 342 + 3 helpers) - music_generator.py (1002 -> 319 + 2 helpers) - translator.py (951 -> 442 + 2 helpers) - cinema_planner.py (893 -> 369 + 2 helpers) - lichess_bot/main.py (757 -> 495 + _game_logic.py) - test_translator.py (725 -> 289 + part2 + conftest) - test_lichess_api.py (680 -> 475 + part2) - learning_pipe.py (668 -> 375 + 2 helpers) - cache.py (655 -> 360 + _cache_decks.py) - analyze_chess_game.py (632 -> 463 + _move_analysis.py) - visualize_q02.py (609 -> 371 + helper) - repo_explorer.py (602 -> 347 + 2 helpers) - keyboard_coop/main.py (515 -> 416 + _dictionary.py) - scanning.py (501 -> 314 + _enforce_loop.py) All tests pass: 144 lichess_bot (100% branch coverage), 243 others. No new lint errors introduced.
476 lines
17 KiB
Python
476 lines
17 KiB
Python
"""Unit tests for lichess_bot lichess_api module."""
|
|
|
|
from http import HTTPStatus
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import chess
|
|
import pytest
|
|
import requests
|
|
|
|
from python_pkg.lichess_bot.lichess_api import LichessAPI
|
|
|
|
_TERMINATION_MSG = "Test termination"
|
|
|
|
|
|
class _TestTerminationError(Exception):
|
|
"""Custom exception to break out of infinite loops in tests."""
|
|
|
|
|
|
class TestLichessAPIInit:
|
|
"""Tests for LichessAPI initialization."""
|
|
|
|
def test_init_creates_session_with_headers(self) -> None:
|
|
"""Test initialization creates session with proper headers."""
|
|
api = LichessAPI("test_token")
|
|
|
|
expected = "test_token"
|
|
assert api.token == expected
|
|
assert "Bearer test_token" in api.session.headers["Authorization"]
|
|
assert "application/json" in api.session.headers["Accept"]
|
|
|
|
def test_init_with_custom_session(self) -> None:
|
|
"""Test initialization with custom session."""
|
|
custom_session = requests.Session()
|
|
api = LichessAPI("test_token", session=custom_session)
|
|
|
|
assert api.session is custom_session
|
|
|
|
|
|
class TestRequest:
|
|
"""Tests for _request method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance with mocked session."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_request_success(self, api: LichessAPI) -> None:
|
|
"""Test successful request logs appropriately."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
|
|
with patch.object(api.session, "request", return_value=mock_response):
|
|
result = api._request("GET", "http://test.com")
|
|
|
|
assert result == mock_response
|
|
|
|
def test_request_error_logs_body(self, api: LichessAPI) -> None:
|
|
"""Test error response logs body snippet."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.BAD_REQUEST
|
|
mock_response.text = "Error message here"
|
|
|
|
with patch.object(api.session, "request", return_value=mock_response):
|
|
result = api._request("GET", "http://test.com")
|
|
|
|
assert result == mock_response
|
|
|
|
def test_request_error_no_body(self, api: LichessAPI) -> None:
|
|
"""Test error response without body."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
|
mock_response.text = None
|
|
|
|
with patch.object(api.session, "request", return_value=mock_response):
|
|
result = api._request("GET", "http://test.com")
|
|
|
|
assert result == mock_response
|
|
|
|
def test_request_raises_for_status(self, api: LichessAPI) -> None:
|
|
"""Test request raises for status when flag is set."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.NOT_FOUND
|
|
mock_response.text = ""
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError()
|
|
|
|
with (
|
|
patch.object(api.session, "request", return_value=mock_response),
|
|
pytest.raises(requests.HTTPError),
|
|
):
|
|
api._request("GET", "http://test.com", raise_for_status=True)
|
|
|
|
def test_request_exception_handling(self, api: LichessAPI) -> None:
|
|
"""Test request handles exceptions."""
|
|
with (
|
|
patch.object(
|
|
api.session, "request", side_effect=requests.ConnectionError()
|
|
),
|
|
pytest.raises(requests.ConnectionError),
|
|
):
|
|
api._request("GET", "http://test.com")
|
|
|
|
|
|
class TestStreamEvents:
|
|
"""Tests for stream_events method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_stream_events_yields_json_lines(self, api: LichessAPI) -> None:
|
|
"""Test stream_events yields parsed JSON lines."""
|
|
# stream_events has a while True loop, so we need to break out of it
|
|
# by raising an exception after yielding our test data
|
|
|
|
def iter_lines_with_stop(*_args: object, **_kwargs: object) -> list[str]:
|
|
"""Return lines then signal generator to stop."""
|
|
return ['{"type": "challenge"}', "", '{"type": "gameStart"}']
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
mock_response.iter_lines = iter_lines_with_stop
|
|
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
mock_response.__exit__ = MagicMock(return_value=False)
|
|
|
|
call_count = 0
|
|
|
|
def mock_request(*_args: object, **_kwargs: object) -> MagicMock:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count > 1:
|
|
# Break out of while True on second iteration
|
|
raise _TestTerminationError(_TERMINATION_MSG)
|
|
return mock_response
|
|
|
|
events_collected: list[dict] = []
|
|
with (
|
|
patch.object(api, "_request", side_effect=mock_request),
|
|
pytest.raises(_TestTerminationError),
|
|
):
|
|
events_collected.extend(api.stream_events())
|
|
|
|
assert len(events_collected) == 2
|
|
assert events_collected[0]["type"] == "challenge"
|
|
assert events_collected[1]["type"] == "gameStart"
|
|
|
|
def test_stream_events_skips_invalid_json(self, api: LichessAPI) -> None:
|
|
"""Test stream_events skips non-JSON lines."""
|
|
|
|
def iter_lines_with_invalid(*_args: object, **_kwargs: object) -> list[str]:
|
|
return ['{"type": "challenge"}', "not json", '{"type": "gameStart"}']
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
mock_response.iter_lines = iter_lines_with_invalid
|
|
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
mock_response.__exit__ = MagicMock(return_value=False)
|
|
|
|
call_count = 0
|
|
|
|
def mock_request(*_args: object, **_kwargs: object) -> MagicMock:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count > 1:
|
|
raise _TestTerminationError(_TERMINATION_MSG)
|
|
return mock_response
|
|
|
|
events_collected: list[dict] = []
|
|
with (
|
|
patch.object(api, "_request", side_effect=mock_request),
|
|
pytest.raises(_TestTerminationError),
|
|
):
|
|
events_collected.extend(api.stream_events())
|
|
|
|
assert len(events_collected) == 2
|
|
|
|
def test_stream_events_handles_rate_limit(self, api: LichessAPI) -> None:
|
|
"""Test stream_events backs off on rate limit."""
|
|
mock_response_429 = MagicMock()
|
|
mock_response_429.status_code = HTTPStatus.TOO_MANY_REQUESTS
|
|
mock_response_429.raise_for_status.side_effect = requests.HTTPError(
|
|
response=MagicMock(status_code=HTTPStatus.TOO_MANY_REQUESTS)
|
|
)
|
|
mock_response_429.__enter__ = MagicMock(return_value=mock_response_429)
|
|
mock_response_429.__exit__ = MagicMock(return_value=False)
|
|
|
|
def iter_lines_ok(*_args: object, **_kwargs: object) -> list[str]:
|
|
return ['{"type": "test"}']
|
|
|
|
mock_response_ok = MagicMock()
|
|
mock_response_ok.status_code = HTTPStatus.OK
|
|
mock_response_ok.iter_lines = iter_lines_ok
|
|
mock_response_ok.__enter__ = MagicMock(return_value=mock_response_ok)
|
|
mock_response_ok.__exit__ = MagicMock(return_value=False)
|
|
|
|
call_count = 0
|
|
|
|
def mock_request(*_args: object, **_kwargs: object) -> MagicMock:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
return mock_response_429
|
|
if call_count == 2:
|
|
return mock_response_ok
|
|
raise _TestTerminationError(_TERMINATION_MSG)
|
|
|
|
events_collected: list[dict] = []
|
|
with (
|
|
patch.object(api, "_request", side_effect=mock_request),
|
|
patch("python_pkg.lichess_bot.lichess_api.time.sleep"),
|
|
pytest.raises(_TestTerminationError),
|
|
):
|
|
events_collected.extend(api.stream_events())
|
|
|
|
assert len(events_collected) == 1
|
|
assert call_count == 3 # 429 + OK + termination
|
|
|
|
|
|
class TestChallenges:
|
|
"""Tests for challenge-related methods."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_accept_challenge(self, api: LichessAPI) -> None:
|
|
"""Test accepting a challenge."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
|
|
with patch.object(api, "_request", return_value=mock_response) as mock_req:
|
|
api.accept_challenge("test_challenge_id")
|
|
|
|
mock_req.assert_called_once()
|
|
call_args = mock_req.call_args
|
|
assert "test_challenge_id" in call_args[0][1]
|
|
assert call_args[1]["raise_for_status"] is True
|
|
|
|
def test_decline_challenge(self, api: LichessAPI) -> None:
|
|
"""Test declining a challenge."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
|
|
with patch.object(api, "_request", return_value=mock_response) as mock_req:
|
|
api.decline_challenge("test_challenge_id", reason="tooSlow")
|
|
|
|
mock_req.assert_called_once()
|
|
call_args = mock_req.call_args
|
|
assert "decline" in call_args[0][1]
|
|
assert call_args[1]["data"]["reason"] == "tooSlow"
|
|
|
|
|
|
class TestParseGameFullEvent:
|
|
"""Tests for _parse_game_full_event method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_parse_game_full_as_white(self, api: LichessAPI) -> None:
|
|
"""Test parsing gameFull event when playing as white."""
|
|
event = {
|
|
"white": {"id": "my_user"},
|
|
"black": {"id": "opponent"},
|
|
"state": {"moves": "e2e4 e7e5"},
|
|
}
|
|
board = chess.Board()
|
|
|
|
with patch.object(api, "get_my_user_id", return_value="my_user"):
|
|
color = api._parse_game_full_event(event, board, "unknown")
|
|
|
|
assert color == "white"
|
|
assert len(board.move_stack) == 2
|
|
|
|
def test_parse_game_full_as_black(self, api: LichessAPI) -> None:
|
|
"""Test parsing gameFull event when playing as black."""
|
|
event = {
|
|
"white": {"id": "opponent"},
|
|
"black": {"id": "my_user"},
|
|
"state": {"moves": ""},
|
|
}
|
|
board = chess.Board()
|
|
|
|
with patch.object(api, "get_my_user_id", return_value="my_user"):
|
|
color = api._parse_game_full_event(event, board, "unknown")
|
|
|
|
assert color == "black"
|
|
|
|
def test_parse_game_full_as_spectator(self, api: LichessAPI) -> None:
|
|
"""Test parsing gameFull event when not a player (spectator/unknown)."""
|
|
event = {
|
|
"white": {"id": "player1"},
|
|
"black": {"id": "player2"},
|
|
"state": {"moves": "e2e4"},
|
|
}
|
|
board = chess.Board()
|
|
|
|
# User ID doesn't match either player
|
|
with patch.object(api, "get_my_user_id", return_value="spectator"):
|
|
color = api._parse_game_full_event(event, board, "default_color")
|
|
|
|
# Should keep the original color passed in
|
|
assert color == "default_color"
|
|
assert len(board.move_stack) == 1
|
|
|
|
|
|
class TestJoinGameStream:
|
|
"""Tests for join_game_stream method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_join_game_stream(self, api: LichessAPI) -> None:
|
|
"""Test joining a game stream."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
event = json.dumps(
|
|
{
|
|
"type": "gameFull",
|
|
"white": {"id": "my_user"},
|
|
"black": {"id": "opponent"},
|
|
"state": {"moves": ""},
|
|
}
|
|
)
|
|
mock_response.iter_lines.return_value = iter([event])
|
|
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
mock_response.__exit__ = MagicMock(return_value=False)
|
|
|
|
with (
|
|
patch.object(api, "_request", return_value=mock_response),
|
|
patch.object(api, "get_my_user_id", return_value="my_user"),
|
|
):
|
|
board, color = api.join_game_stream("game123", None)
|
|
|
|
assert color == "white"
|
|
assert board.fen() == chess.STARTING_FEN
|
|
|
|
|
|
class TestStreamGameEvents:
|
|
"""Tests for stream_game_events method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_stream_game_events_yields_events(self, api: LichessAPI) -> None:
|
|
"""Test stream_game_events yields parsed events."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
mock_response.iter_lines.return_value = iter(
|
|
['{"type": "gameFull"}', '{"type": "gameState"}']
|
|
)
|
|
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
|
mock_response.__exit__ = MagicMock(return_value=False)
|
|
|
|
with patch.object(api, "_request", return_value=mock_response):
|
|
events = list(api.stream_game_events("game123"))
|
|
|
|
assert len(events) == 2
|
|
|
|
|
|
class TestMakeMove:
|
|
"""Tests for make_move method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_make_move_success(self, api: LichessAPI) -> None:
|
|
"""Test successful move submission."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
|
|
with patch.object(api, "_request", return_value=mock_response):
|
|
api.make_move("game123", chess.Move.from_uci("e2e4"))
|
|
|
|
def test_make_move_conflict_raises(self, api: LichessAPI) -> None:
|
|
"""Test move submission with conflict."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.CONFLICT
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError()
|
|
|
|
with (
|
|
patch.object(api, "_request", return_value=mock_response),
|
|
pytest.raises(requests.HTTPError),
|
|
):
|
|
api.make_move("game123", chess.Move.from_uci("e2e4"))
|
|
|
|
def test_make_move_rate_limit_retries(self, api: LichessAPI) -> None:
|
|
"""Test move submission retries on rate limit."""
|
|
mock_response_429 = MagicMock()
|
|
mock_response_429.status_code = HTTPStatus.TOO_MANY_REQUESTS
|
|
|
|
mock_response_ok = MagicMock()
|
|
mock_response_ok.status_code = HTTPStatus.OK
|
|
|
|
call_count = 0
|
|
|
|
def mock_request(*_args: object, **_kwargs: object) -> MagicMock:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
return mock_response_429
|
|
return mock_response_ok
|
|
|
|
with (
|
|
patch.object(api, "_request", side_effect=mock_request),
|
|
patch("python_pkg.lichess_bot.lichess_api.time.sleep"),
|
|
):
|
|
api.make_move("game123", chess.Move.from_uci("e2e4"))
|
|
|
|
assert call_count == 2
|
|
|
|
def test_make_move_bad_request_raises(self, api: LichessAPI) -> None:
|
|
"""Test move submission with bad request raises but returns."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.BAD_REQUEST
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError()
|
|
|
|
with (
|
|
patch.object(api, "_request", return_value=mock_response),
|
|
pytest.raises(requests.HTTPError),
|
|
):
|
|
api.make_move("game123", chess.Move.from_uci("e2e4"))
|
|
|
|
|
|
class TestGetGameState:
|
|
"""Tests for get_game_state method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_get_game_state_returns_none(self, api: LichessAPI) -> None:
|
|
"""Test deprecated get_game_state returns None."""
|
|
result = api.get_game_state("game123")
|
|
assert result is None
|
|
|
|
|
|
class TestGetMyUserId:
|
|
"""Tests for get_my_user_id method."""
|
|
|
|
@pytest.fixture
|
|
def api(self) -> LichessAPI:
|
|
"""Create API instance."""
|
|
return LichessAPI("test_token")
|
|
|
|
def test_get_my_user_id_success(self, api: LichessAPI) -> None:
|
|
"""Test getting user ID successfully."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.OK
|
|
mock_response.json.return_value = {"id": "my_username"}
|
|
|
|
with patch.object(api, "_request", return_value=mock_response):
|
|
user_id = api.get_my_user_id()
|
|
|
|
assert user_id == "my_username"
|
|
|
|
def test_get_my_user_id_failure(self, api: LichessAPI) -> None:
|
|
"""Test getting user ID when request fails."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = HTTPStatus.UNAUTHORIZED
|
|
|
|
with patch.object(api, "_request", return_value=mock_response):
|
|
user_id = api.get_my_user_id()
|
|
|
|
assert user_id is None
|