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")]