"""Тесты HttpCheckboxClient. Сеть замокана через respx — реальных запросов не делает.""" from __future__ import annotations import json import uuid import httpx import pytest import respx from httpx import Response from app.core.config import Settings from app.services.checkbox.client import ( CheckboxCredentials, CheckboxError, CheckboxUnavailableError, ) from app.services.checkbox.http_client import HttpCheckboxClient BASE = "https://api.checkbox.test" CREDS = CheckboxCredentials(cash_register_id=uuid.uuid4(), license_key="lic-123", pin_code="1111") ETTN = { "id": "e-1", "status": "CREATED", "ettnNumber": "20450123456789", "totalSum": 120000, "description": "", "recipientPhone": "", "employee": {"id": "x", "dateCreated": "2026-09-23"}, "cashRegister": {"id": "y", "dateCreated": "2026-09-23"}, "lastCheckDate": "2026-09-23", "dateCreated": "2026-09-23", } def _client() -> HttpCheckboxClient: settings = Settings( secret_key="test-secret-key", encryption_key="dGVzdC1lbmNyeXB0aW9uLWtleS0zMi1ieXRlcyEh", checkbox_base_url=BASE, ) # type: ignore[arg-type] return HttpCheckboxClient(settings) def _signin(token: str = "tok-1") -> respx.Route: return respx.post(f"{BASE}/api/v1/cashier/signinPinCode").mock( return_value=Response(200, json={"access_token": token}) ) @respx.mock async def test_create_signs_in_and_sends_headers() -> None: signin = _signin() create = respx.post(f"{BASE}/api/v1/ettn").mock(return_value=Response(200, json=ETTN)) ettn = await _client().create_ettn(CREDS, {"provider": "novapost"}) assert ettn.id == "e-1" and ettn.ettn_number == "20450123456789" assert json.loads(signin.calls.last.request.content) == {"pin_code": "1111"} assert signin.calls.last.request.headers["X-License-Key"] == "lic-123" sent = create.calls.last.request assert sent.headers["Authorization"] == "Bearer tok-1" assert sent.headers["X-License-Key"] == "lic-123" assert sent.headers["X-Client-Name"] == "lux_fiscal" @respx.mock async def test_token_is_cached_between_calls() -> None: signin = _signin() respx.get(f"{BASE}/api/v1/ettn/e-1").mock(return_value=Response(200, json=ETTN)) client = _client() await client.get_ettn(CREDS, "e-1") await client.get_ettn(CREDS, "e-1") assert signin.call_count == 1 @respx.mock async def test_401_triggers_single_resign_in() -> None: signin = _signin("tok-2") route = respx.get(f"{BASE}/api/v1/ettn/e-1").mock( side_effect=[Response(401, json={"message": "expired"}), Response(200, json=ETTN)] ) ettn = await _client().get_ettn(CREDS, "e-1") assert ettn.status == "CREATED" assert signin.call_count == 2 assert route.calls.last.request.headers["Authorization"] == "Bearer tok-2" @respx.mock async def test_422_raises_checkbox_error_with_message() -> None: _signin() respx.post(f"{BASE}/api/v1/ettn").mock( return_value=Response( 422, json={ "message": "Validation error", "detail": [{"loc": ["body", "payments"], "msg": "bad", "type": "x"}], }, ) ) with pytest.raises(CheckboxError, match="body.payments: bad") as exc_info: await _client().create_ettn(CREDS, {}) assert not isinstance(exc_info.value, CheckboxUnavailableError) @respx.mock async def test_timeout_and_5xx_are_unavailable() -> None: _signin() respx.post(f"{BASE}/api/v1/ettn").mock(side_effect=httpx.ReadTimeout("slow")) with pytest.raises(CheckboxUnavailableError): await _client().create_ettn(CREDS, {}) respx.post(f"{BASE}/api/v1/ettn").mock(return_value=Response(503, text="down")) with pytest.raises(CheckboxUnavailableError): await _client().create_ettn(CREDS, {}) @respx.mock async def test_find_skips_cancelled_and_other_waybills() -> None: _signin() respx.get(f"{BASE}/api/v1/ettn").mock( return_value=Response( 200, json=[ {**ETTN, "id": "old", "status": "CANCELLED"}, {**ETTN, "id": "other", "ettnNumber": "111"}, {**ETTN, "id": "match"}, ], ) ) found = await _client().find_ettn(CREDS, "20450123456789") assert found is not None and found.id == "match" @respx.mock async def test_bad_pin_raises() -> None: respx.post(f"{BASE}/api/v1/cashier/signinPinCode").mock( return_value=Response(400, json={"message": "Невірний пін-код"}) ) with pytest.raises(CheckboxError, match="Невірний пін-код"): await _client().sign_in(CREDS)