"""Test JWTAuth.""" import json from typing import Any, Iterable, cast from unittest import mock import jwt import pytest from pytest_mock import MockerFixture from jwtauth import JWTAuth from jwtauth.exceptions import JWTAuthError from tests.types import MakeTokenString @pytest.fixture def auth() -> JWTAuth: """Get fixtured JWTAuth object.""" return JWTAuth(jwks_url="https://test/.well-known/jws", options={}) @pytest.fixture(autouse=True) def mock_jwk_set_fetch(mocker: MockerFixture, jwk_set: dict[str, str]) -> None: """Fixture fetching JWKS.""" class OpenStub: def read(self) -> Any: return json.dumps(jwk_set) mocker.patch( "urllib.request.urlopen", return_value=mock.MagicMock(__enter__=mock.MagicMock(return_value=OpenStub())), ) def test_jwt_auth_get_token_valid( auth: JWTAuth, make_token_string: MakeTokenString ) -> None: """Test JWTAuth validates and decodes a token.""" token_string = make_token_string({}) token = auth.get_token(token_string) assert token == {} @pytest.mark.parametrize( "issuer, payload_iss", [ ("test", "test"), (["test"], "test"), (["test1", "test2"], "test2"), ], ) def test_jwt_auth_get_token_valid_issuer( issuer: str | Iterable[str], payload_iss: str, make_token_string: MakeTokenString, ) -> None: """Test JWTAuth validates issuer.""" auth = JWTAuth( jwks_url="https://test/.well-known/jws", issuer=issuer, options={ "verify_iss": True, "require": ["iss"], }, ) payload = {"iss": payload_iss} token_string = make_token_string(payload) result = auth.get_token(token_string) assert result == payload @pytest.mark.parametrize( "payload", [ {}, {"iss": "invalid"}, ], ) def test_jwt_auth_get_token_invalid_issuer( payload: dict[str, Any], make_token_string: MakeTokenString, ) -> None: """Test JWTAuth identifies invalid issuers.""" auth = JWTAuth( jwks_url="https://test/.well-known/jws", issuer=["test1", "test2"], options={ "verify_iss": True, "require": ["iss"], }, ) token_string = make_token_string(payload) with pytest.raises(JWTAuthError): auth.get_token(token_string) @pytest.mark.anyio async def test_jwt_auth_aget_token_valid( auth: JWTAuth, make_token_string: MakeTokenString ) -> None: """Test JWTAuth async validate and decodes a token.""" token_string = make_token_string({}) token = await auth.aget_token(token_string) assert token == {} def test_jwt_auth_get_token_jwks_connection_error( auth: JWTAuth, mocker: MockerFixture ) -> None: """Test sync token retrieval maps JWKS connection failures.""" mocker.patch.object( auth.jwks_client, "get_signing_key_from_jwt", side_effect=jwt.PyJWKClientConnectionError("network down"), ) with pytest.raises(JWTAuthError) as exc: auth.get_token("token") assert exc.value.code == "jwks_connect_error" assert exc.value.message == "Failed to fetch from JWKS URL" def test_jwt_auth_get_token_jwk_client_error( auth: JWTAuth, mocker: MockerFixture ) -> None: """Test sync token retrieval maps generic JWK client failures.""" mocker.patch.object( auth.jwks_client, "get_signing_key_from_jwt", side_effect=jwt.PyJWKClientError("missing kid"), ) with pytest.raises(JWTAuthError) as exc: auth.get_token("token") assert exc.value.code == "jwk_client_error" assert exc.value.message == "JWK Client Error missing kid" @pytest.mark.anyio async def test_jwt_auth_aget_token_jwks_connection_error( auth: JWTAuth, mocker: MockerFixture ) -> None: """Test async token retrieval maps JWKS connection failures.""" mocker.patch.object( auth.jwks_client, "get_signing_key_from_jwt", side_effect=jwt.PyJWKClientConnectionError("network down"), ) with pytest.raises(JWTAuthError) as exc: await auth.aget_token("token") assert exc.value.code == "jwks_connect_error" assert exc.value.message == "Failed to fetch from JWKS URL" @pytest.mark.anyio async def test_jwt_auth_aget_token_jwk_client_error( auth: JWTAuth, mocker: MockerFixture ) -> None: """Test async token retrieval maps generic JWK client failures.""" mocker.patch.object( auth.jwks_client, "get_signing_key_from_jwt", side_effect=jwt.PyJWKClientError("missing kid"), ) with pytest.raises(JWTAuthError) as exc: await auth.aget_token("token") assert exc.value.code == "jwk_client_error" assert exc.value.message == "JWK Client Error missing kid" def test_jwt_auth_get_token_decode_jwk_client_error( auth: JWTAuth, make_token_string: MakeTokenString, mocker: MockerFixture ) -> None: """Test decode step maps JWK client errors.""" token_string = make_token_string({}) mocker.patch("jwtauth.base.jwt.decode", side_effect=jwt.PyJWKClientError("bad jwk")) with pytest.raises(JWTAuthError) as exc: auth.get_token(token_string) assert exc.value.code == "jwk_client_error" assert exc.value.message == "JWK Client Error bad jwk" def test_jwt_auth_get_token_passes_constructed_options( make_token_string: MakeTokenString, mocker: MockerFixture ) -> None: """Test decode receives an Options object built from provided fields.""" options = { "verify_exp": False, "verify_nbf": False, "require": ["iss"], } auth = JWTAuth(jwks_url="https://test/.well-known/jws", options=options) token_string = make_token_string({}) decode_spy = mocker.patch("jwtauth.base.jwt.decode", return_value={}) auth.get_token(token_string) assert decode_spy.call_args.kwargs["options"] == options def test_jwt_auth_init_preserves_unknown_option_field() -> None: """Test unknown option fields are preserved at runtime.""" auth = JWTAuth( jwks_url="https://test/.well-known/jws", options={ "verify_exp": False, "not_a_real_option": True, }, ) assert cast(dict[str, Any], auth.options)["not_a_real_option"] is True def test_jwt_auth_init_preserves_wrong_typed_option_field() -> None: """Test known option fields are not runtime-validated by Options.""" auth = JWTAuth( jwks_url="https://test/.well-known/jws", options={ "verify_iat": "nope", }, ) assert cast(dict[str, Any], auth.options)["verify_iat"] == "nope"