"""Unit tests for auth cache: JWT expiry decode and per-provider auth cache.""" import base64 import json import time import pytest import src.services.auth as auth_mod from src.models.cloud_access import AuthResponse from src.services.auth import ( _get_cached, _set_cached, decode_jwt_exp, ) @pytest.fixture(autouse=True) def reset_cache(): auth_mod._cached_auth_by_provider = {} yield auth_mod._cached_auth_by_provider = {} def _make_jwt(exp: int | None = None, payload_extra: dict | None = None) -> str: payload = dict(payload_extra or {}) if exp is not None: payload["exp"] = exp payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=") header_b64 = base64.urlsafe_b64encode(b'{"alg":"HS256"}').decode().rstrip("=") return f"{header_b64}.{payload_b64}.sig" class TestDecodeJwtExp: def test_returns_exp_when_present(self): exp = 1700000000 token = _make_jwt(exp=exp) assert decode_jwt_exp(token) == exp def test_returns_none_when_no_exp(self): token = _make_jwt(payload_extra={"sub": "user"}) assert decode_jwt_exp(token) is None def test_returns_none_for_wrong_number_of_parts(self): assert decode_jwt_exp("one.two") is None assert decode_jwt_exp("one") is None assert decode_jwt_exp("a.b.c.d") is None def test_returns_none_for_invalid_base64(self): token = "a.!!!.sig" assert decode_jwt_exp(token) is None def test_returns_none_for_invalid_json_in_payload(self): bad_b64 = base64.urlsafe_b64encode(b"not json").decode().rstrip("=") token = f"a.{bad_b64}.sig" assert decode_jwt_exp(token) is None def test_handles_padding_in_payload(self): exp = 1234567890 payload_b64 = ( base64.urlsafe_b64encode(json.dumps({"exp": exp}).encode()).decode().rstrip("=") ) token = f"a.{payload_b64}.sig" assert decode_jwt_exp(token) == exp class TestSetCached: def test_caches_when_expires_in_set(self): response = AuthResponse( access_token=None, expires_in=3600, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) assert _get_cached() is not None def test_caches_when_jwt_has_exp(self): future = int(time.time()) + 3600 response = AuthResponse( access_token=_make_jwt(exp=future), expires_in=None, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) assert _get_cached() is not None def test_prefers_jwt_exp_over_expires_in_when_both_present(self): near_future = int(time.time()) + 61 response = AuthResponse( access_token=_make_jwt(exp=near_future), expires_in=3600, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) time.sleep(1.1) assert _get_cached() is None def test_does_not_cache_when_no_expires_in_and_no_valid_jwt(self): response = AuthResponse( access_token="not.a.jwt", expires_in=None, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) assert _get_cached() is None def test_does_not_cache_when_expires_in_zero_and_no_jwt_exp(self): response = AuthResponse( access_token=None, expires_in=0, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) _get_cached() # may return None due to buffer; just ensure no crash def test_overwrites_existing_entry(self): _set_cached( AuthResponse( access_token=None, expires_in=3600, auth0Token=None, observabilityGlobalService="https://old.example.com", s3_access=None, ), ) new_response = AuthResponse( access_token=None, expires_in=3600, auth0Token=None, observabilityGlobalService="https://new.example.com", s3_access=None, ) _set_cached(new_response) cached = _get_cached() assert cached is not None assert cached.observability_global_service == "https://new.example.com" class TestGetCached: def test_returns_none_when_empty(self): assert _get_cached() is None def test_returns_cached_when_not_expired(self): response = AuthResponse( access_token=None, expires_in=3600, auth0Token=None, observabilityGlobalService="https://svc.example.com", s3_access=None, ) _set_cached(response) result = _get_cached() assert result is not None assert result.observability_global_service == "https://svc.example.com" def test_returns_none_when_expired_within_buffer(self): response = AuthResponse( access_token=None, expires_in=1, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) time.sleep(0.01) assert _get_cached() is None def test_evicts_expired_entry(self): response = AuthResponse( access_token=None, expires_in=1, auth0Token=None, observabilityGlobalService=None, s3_access=None, ) _set_cached(response) time.sleep(0.01) _get_cached() # evicts assert _get_cached() is None