diff --git a/packages/modules/vehicles/saic_ismart/api.py b/packages/modules/vehicles/saic_ismart/api.py index 97da753d08..b0fff800b9 100644 --- a/packages/modules/vehicles/saic_ismart/api.py +++ b/packages/modules/vehicles/saic_ismart/api.py @@ -24,6 +24,8 @@ from typing import Any, Dict, Optional, Tuple from urllib.parse import urlencode +from requests.exceptions import HTTPError + from modules.common import req from modules.common.component_state import CarState from modules.vehicles.saic_ismart import crypto @@ -94,10 +96,16 @@ def _send(method: str, path_with_query: str, config: SaicIsmartConfiguration, headers["event-id"] = event_id session = req.get_http_session() - response = session.request( - method, f"{BASE_URL}{path_with_query}", - data=encrypted_body if encrypted_body else None, headers=headers, timeout=15) - response.raise_for_status() + try: + response = session.request( + method, f"{BASE_URL}{path_with_query}", + data=encrypted_body if encrypted_body else None, headers=headers, timeout=15) + except HTTPError as exc: + if exc.response is not None and exc.response.status_code in (401, 403): + # Session kann auch schon auf HTTP-Ebene abgelehnt werden, nicht nur über code= in der Antwort. + raise SaicAuthenticationError( + f"{path_with_query}: Session abgelaufen (HTTP {exc.response.status_code})") from exc + raise body_text = response.text resp_ts = response.headers.get("APP-SEND-DATE") diff --git a/packages/modules/vehicles/saic_ismart/api_test.py b/packages/modules/vehicles/saic_ismart/api_test.py index a70a87a348..87e14c7866 100644 --- a/packages/modules/vehicles/saic_ismart/api_test.py +++ b/packages/modules/vehicles/saic_ismart/api_test.py @@ -3,6 +3,7 @@ from unittest.mock import Mock import pytest +from requests.exceptions import HTTPError from modules.vehicles.saic_ismart import api from modules.vehicles.saic_ismart.config import SaicIsmartConfiguration @@ -95,3 +96,19 @@ def test_wake_vehicle_auth_error_triggers_relogin(self, monkeypatch): assert result.soc == 55.5 assert wake_mock.call_count == 2 + + +class TestSendHttpLevelAuthError: + def test_http_401_raises_authentication_error_not_http_error(self, monkeypatch): + # Die Cloud lehnt eine ungueltige Session manchmal schon auf HTTP-Ebene ab (zB nach + # Login in der iSMART-App auf dem Handy), nicht nur ueber das eigene code-Feld in einer + # 200-Antwort - muss als SaicAuthenticationError erkannt werden, damit der automatische + # Relogin in _call_with_relogin() greift (siehe Discussion zu wiederholtem 401 ohne Relogin). + # Der Session-Hook in req.py ruft raise_for_status() bereits in session.request() auf. + response = Mock(status_code=401, headers={}) + session = Mock() + session.request = Mock(side_effect=HTTPError("401 Client Error", response=response)) + monkeypatch.setattr(api.req, "get_http_session", Mock(return_value=session)) + + with pytest.raises(api.SaicAuthenticationError): + api._send("GET", "/vehicle/charging/mgmtData?vin=xxx", _config(), "token")