Skip to content

Commit

Permalink
Merge pull request #106 from Colin-b/feature/base_exc
Browse files Browse the repository at this point in the history
Add base exception
  • Loading branch information
Colin-b authored Jan 7, 2025
2 parents 5344e3d + 9ead1a9 commit feca18c
Show file tree
Hide file tree
Showing 13 changed files with 76 additions and 48 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
- Exceptions issued by `httpx_auth` are now inheriting from `httpx_auth.HttpxAuthException`, itself inheriting from `httpx.HTTPError`, instead of `Exception`.

### Added
- Explicit support for python `3.13`.
Expand Down
2 changes: 2 additions & 0 deletions httpx_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
InvalidToken,
TokenExpiryNotProvided,
InvalidGrantRequest,
HttpxAuthException,
)
from httpx_auth.version import __version__

Expand Down Expand Up @@ -67,6 +68,7 @@
"JsonTokenFileCache",
"TokenMemoryCache",
"AWS4Auth",
"HttpxAuthException",
"GrantNotProvided",
"TimeoutOccurred",
"AuthenticationFailed",
Expand Down
33 changes: 19 additions & 14 deletions httpx_auth/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,42 @@
import httpx


class AuthenticationFailed(Exception):
class HttpxAuthException(httpx.HTTPError): ...


class AuthenticationFailed(HttpxAuthException):
"""User was not authenticated."""

def __init__(self):
Exception.__init__(self, "User was not authenticated.")
HttpxAuthException.__init__(self, "User was not authenticated.")


class TimeoutOccurred(Exception):
class TimeoutOccurred(HttpxAuthException):
"""No response within timeout interval."""

def __init__(self, timeout: float):
Exception.__init__(
HttpxAuthException.__init__(
self, f"User authentication was not received within {timeout} seconds."
)


class InvalidToken(Exception):
class InvalidToken(HttpxAuthException):
"""Token is invalid."""

def __init__(self, token_name: str):
Exception.__init__(self, f"{token_name} is invalid.")
HttpxAuthException.__init__(self, f"{token_name} is invalid.")


class GrantNotProvided(Exception):
class GrantNotProvided(HttpxAuthException):
"""Grant was not provided."""

def __init__(self, grant_name: str, dictionary_without_grant: dict):
Exception.__init__(
HttpxAuthException.__init__(
self, f"{grant_name} not provided within {dictionary_without_grant}."
)


class InvalidGrantRequest(Exception):
class InvalidGrantRequest(HttpxAuthException):
"""
If the request failed client authentication or is invalid, the authorization server returns an error response as described in https://tools.ietf.org/html/rfc6749#section-5.2
"""
Expand Down Expand Up @@ -64,7 +67,7 @@ class InvalidGrantRequest(Exception):
}

def __init__(self, response: Union[httpx.Response, dict]):
Exception.__init__(self, InvalidGrantRequest.to_message(response))
HttpxAuthException.__init__(self, InvalidGrantRequest.to_message(response))

@staticmethod
def to_message(response: Union[httpx.Response, dict]) -> str:
Expand Down Expand Up @@ -114,17 +117,19 @@ def _pop(key: str) -> str:
return message


class StateNotProvided(Exception):
class StateNotProvided(HttpxAuthException):
"""State was not provided."""

def __init__(self, dictionary_without_state: dict):
Exception.__init__(
HttpxAuthException.__init__(
self, f"state not provided within {dictionary_without_state}."
)


class TokenExpiryNotProvided(Exception):
class TokenExpiryNotProvided(HttpxAuthException):
"""Token expiry was not provided."""

def __init__(self, token_body: dict):
Exception.__init__(self, f"Expiry (exp) is not provided in {token_body}.")
HttpxAuthException.__init__(
self, f"Expiry (exp) is not provided in {token_body}."
)
3 changes: 3 additions & 0 deletions tests/features/token_cache/test_json_token_file_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import pathlib

import httpx
import pytest
import jwt

Expand Down Expand Up @@ -78,6 +79,8 @@ def failing_dump(*args):
with pytest.raises(httpx_auth.AuthenticationFailed) as exception_info:
same_cache.get_token("key1")
assert str(exception_info.value) == "User was not authenticated."
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)

assert caplog.messages == [
"Cannot save tokens.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"example_parameter": "example_value",
},
match_content=b"grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
Expand All @@ -190,7 +190,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client(

tab.assert_success()

time.sleep(10)
time.sleep(2)
tab = browser_mock.add_response(
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
Expand Down Expand Up @@ -240,7 +240,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
Expand All @@ -260,7 +260,7 @@ async def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token

tab.assert_success()

time.sleep(10)
time.sleep(2)

# response for refresh token grant
httpx_mock.add_response(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,12 @@ def test_with_invalid_request_error_uses_custom_failure(
)

with httpx.Client() as client:
with pytest.raises(httpx_auth.InvalidGrantRequest):
with pytest.raises(httpx_auth.InvalidGrantRequest) as exception_info:
client.get("https://authorized_only", auth=auth)

assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)

tab.assert_failure(
"invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
)
Expand All @@ -166,7 +169,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"example_parameter": "example_value",
},
match_content=b"grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
Expand All @@ -185,7 +188,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client(

tab.assert_success()

time.sleep(10)
time.sleep(2)
tab = browser_mock.add_response(
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
Expand Down Expand Up @@ -234,7 +237,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token_refre
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
Expand All @@ -254,7 +257,7 @@ def test_oauth2_authorization_code_flow_is_able_to_reuse_client_with_token_refre

tab.assert_success()

time.sleep(10)
time.sleep(2)

# response for refresh token grant
httpx_mock.add_response(
Expand Down Expand Up @@ -631,6 +634,8 @@ def test_empty_token_is_invalid(
str(exception_info.value)
== "access_token not provided within {'access_token': '', 'token_type': 'example', 'expires_in': 3600, 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA', 'example_parameter': 'example_value'}."
)
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)
tab.assert_success()


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"example_parameter": "example_value",
},
match_content=b"code_verifier=MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
Expand All @@ -187,7 +187,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client(
await client.get("https://authorized_only", auth=auth)

tab.assert_success()
time.sleep(10)
time.sleep(2)
tab = browser_mock.add_response(
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
Expand Down Expand Up @@ -236,7 +236,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
Expand All @@ -255,7 +255,7 @@ async def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
await client.get("https://authorized_only", auth=auth)

tab.assert_success()
time.sleep(10)
time.sleep(2)

httpx_mock.add_response(
method="POST",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"example_parameter": "example_value",
},
match_content=b"code_verifier=MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA",
Expand All @@ -182,7 +182,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client(
client.get("https://authorized_only", auth=auth)

tab.assert_success()
time.sleep(10)
time.sleep(2)
tab = browser_mock.add_response(
opened_url="https://provide_code?response_type=code&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=ce9c755b41b5e3c5b64c70598715d5de271023a53f39a67a70215d265d11d2bfb6ef6e9c701701e998e69cbdbf2cee29fd51d2a950aa05f59a20cf4a646099d5",
Expand Down Expand Up @@ -230,7 +230,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
Expand All @@ -249,7 +249,7 @@ def test_oauth2_pkce_flow_is_able_to_reuse_client_with_token_refresh(
client.get("https://authorized_only", auth=auth)

tab.assert_success()
time.sleep(10)
time.sleep(2)

httpx_mock.add_response(
method="POST",
Expand Down
14 changes: 13 additions & 1 deletion tests/oauth2/implicit/test_oauth2_implicit_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ def open(self, url, new):
str(exception_info.value)
== "User authentication was not received within 0.1 seconds."
)
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)


def test_browser_error(token_cache, httpx_mock: HTTPXMock, monkeypatch):
Expand Down Expand Up @@ -380,6 +382,8 @@ def open(self, url, new):
str(exception_info.value)
== "User authentication was not received within 0.1 seconds."
)
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)


def test_state_change(token_cache, httpx_mock: HTTPXMock, browser_mock: BrowserMock):
Expand Down Expand Up @@ -416,9 +420,13 @@ def test_empty_token_is_invalid(token_cache, browser_mock: BrowserMock):
)

with httpx.Client() as client:
with pytest.raises(httpx_auth.InvalidToken, match=" is invalid."):
with pytest.raises(
httpx_auth.InvalidToken, match=" is invalid."
) as exception_info:
client.get("https://authorized_only", auth=auth)

assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)
tab.assert_success()


Expand All @@ -435,6 +443,8 @@ def test_token_without_expiry_is_invalid(token_cache, browser_mock: BrowserMock)
client.get("https://authorized_only", auth=auth)

assert str(exception_info.value) == "Expiry (exp) is not provided in None."
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)
tab.assert_success()


Expand Down Expand Up @@ -701,6 +711,8 @@ def test_oauth2_implicit_flow_post_failure_if_state_is_not_provided(
str(exception_info.value)
== f"state not provided within {{'access_token': ['{token}']}}."
)
assert isinstance(exception_info.value, httpx_auth.HttpxAuthException)
assert isinstance(exception_info.value, httpx.HTTPError)
tab.assert_failure(f"state not provided within {{'access_token': ['{token}']}}.")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client(
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"example_parameter": "example_value",
},
match_content=b"grant_type=password&username=test_user&password=test_pwd&scope=openid",
Expand All @@ -91,7 +91,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client(
async with httpx.AsyncClient() as client:
await client.get("https://authorized_only", auth=auth)

time.sleep(10)
time.sleep(2)

httpx_mock.add_response(
method="POST",
Expand Down Expand Up @@ -139,7 +139,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client_with_tok
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 10,
"expires_in": 2,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
Expand All @@ -160,7 +160,7 @@ async def test_oauth2_password_credentials_flow_is_able_to_reuse_client_with_tok
async with httpx.AsyncClient() as client:
await client.get("https://authorized_only", auth=auth)

time.sleep(10)
time.sleep(2)

httpx_mock.add_response(
method="POST",
Expand Down
Loading

0 comments on commit feca18c

Please sign in to comment.