diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ee49fd12..5f4d1627 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,7 +34,10 @@ jobs: - name: Verify dependencies # Jinja, https://data.safetycli.com/v/70612/97c - run: python -m safety check --ignore 70612 + # urllib3, https://data.safetycli.com/v/77744/97c, only when using python 3.8. Consider upgrading. + # setuptools, https://data.safetycli.com/v/76752/97c, only when using python 3.8. Consider upgrading. + # regex, https://data.safetycli.com/v/78558/97c, only when using python 3.8. Consider upgrading. + run: python -m safety check --ignore 70612 --ignore 77744 --ignore 77745 --ignore 76752 --ignore 78558 - name: Verify code style run: python -m flake8 -v diff --git a/examples/oauth/oauth_app.py b/examples/oauth/oauth_app.py index c42614a2..be890f66 100644 --- a/examples/oauth/oauth_app.py +++ b/examples/oauth/oauth_app.py @@ -78,8 +78,6 @@ def index(): "onboarding.write", ] - global client - authorized, authorization_url = client.setup_oauth( client_id, client_secret, @@ -99,8 +97,6 @@ def index(): @app.route("/callback") def callback(*args, **kwargs): - global client - url = request.url.replace("http", "https") # Fake https for the examples app only. DON'T DO THIS IN YOUR CODE! client.setup_oauth_authorization_response(url) body = "

Oauth client is setup

" diff --git a/mollie/api/client.py b/mollie/api/client.py index ebe9dc9d..a96ff00d 100644 --- a/mollie/api/client.py +++ b/mollie/api/client.py @@ -355,9 +355,26 @@ def setup_oauth_authorization_response(self, authorization_response: str) -> Non ) self.set_token(token) - # TODO Implement https://docs.mollie.com/reference/oauth2/revoke-token - # def revoke_oauth_token(self, token, type_hint): - # ... + def revoke_oauth_token(self, client_id: str, token: str, type_hint: str) -> requests.Response: + """ + :param client_id: The client ID (string) + :param token: The access token to revoke (string) + :param type_hint: The type of the token, either 'access_token' or 'refresh_token' (string) + Revoking a refresh token revokes all access tokens that were created using the same authorization. + """ + if not hasattr(self, "_oauth_client"): + raise RequestSetupError("You need to setup OAuth before you can revoke a token.") + + return self._oauth_client.request( + method="DELETE", + url=self.OAUTH_TOKEN_URL, + data={ + "token": token, + "token_type_hint": type_hint, + "client_id": client_id, + "client_secret": self.client_secret, + }, + ) def _setup_retry(self) -> None: """Configure a retry behaviour on the HTTP client.""" diff --git a/tests/conftest.py b/tests/conftest.py index 144f130d..1925d3a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def client(): return client -@pytest.fixture(scope="session") +@pytest.fixture() def oauth_token(): """Return a valid oauth token for resuming an existing OAuth client.""" token = { diff --git a/tests/responses/error_bad_request.json b/tests/responses/error_bad_request.json new file mode 100644 index 00000000..c85fc139 --- /dev/null +++ b/tests/responses/error_bad_request.json @@ -0,0 +1,4 @@ +{ + "error": "invalid_grant", + "error_description": "Authorization code doesn't exist or is invalid for the client" +} \ No newline at end of file diff --git a/tests/responses/revoke_token.json b/tests/responses/revoke_token.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 0a466cbf..c8d5fef0 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -1,6 +1,8 @@ +import json import re import time from datetime import datetime +from http import HTTPStatus import pytest import requests.adapters @@ -433,6 +435,58 @@ def test_unauthorized_oauth_client_should_return_authorization_url(mocker, respo ), "A client without initial token should return a correct authorization url" +def test_revoke_oauth_token_returns_request_setup_error_if_not_oauth_client(): + client = Client() + with pytest.raises(RequestSetupError) as excinfo: + client.revoke_oauth_token("client_id", "access_123", "access_token") + assert str(excinfo.value) == "You need to setup OAuth before you can revoke a token." + + +def test_revoke_oauth_token_succeeds(mocker, oauth_token, response): + client = Client() + client.setup_oauth( + client_id="client_id", + client_secret="client_secret", + redirect_uri="https://example.com/callback", + scope=("organizations.read",), + token=oauth_token, + set_token=mocker.Mock(), + ) + mocked_request = response.delete( + "https://api.mollie.com/oauth2/tokens", "revoke_token", status=HTTPStatus.NO_CONTENT + ) + result = client.revoke_oauth_token("client_id", oauth_token, "access_token") + + assert mocked_request.call_count == 1 + assert result.status_code == HTTPStatus.NO_CONTENT + + +def test_revoke_oauth_token_returns_error_response(mocker, oauth_token, response): + client = Client() + client.setup_oauth( + client_id="client_id", + client_secret="client_secret", + redirect_uri="https://example.com/callback", + scope=("organizations.read",), + token=oauth_token, + set_token=mocker.Mock(), + ) + + mocked_request = response.delete( + "https://api.mollie.com/oauth2/tokens", "error_bad_request", status=HTTPStatus.BAD_REQUEST + ) + result = client.revoke_oauth_token("client_id", oauth_token, "access_token") + + content = json.loads(result.content) + + assert mocked_request.call_count == 1 + assert result.status_code == HTTPStatus.BAD_REQUEST + assert content == { + "error": "invalid_grant", + "error_description": "Authorization code doesn't exist or is invalid for the client", + } + + def test_enable_testmode_globally_access_token(response): mocked_request = response.get( "https://api.mollie.com/v2/methods", "methods_list", match=[matchers.query_string_matcher("testmode=true")]