From 1855cbb85ca94e94662d8f29d087191395a9fd64 Mon Sep 17 00:00:00 2001 From: "Carl Sutton (dogmatic69)" Date: Thu, 6 Jan 2022 23:10:06 +0100 Subject: [PATCH] feat: oAuth support Due to breaking changes introduced by v2 for oAuth support, some hacking things done with depreciation warnings supporting backwards compatibility. When using secrets version will default to V2 which is not yet tested / implemented for all endpoints. Will be supported in the coming PRs in non-breaking way as possible. This will resolve #dogmatic69/nordigen-homeassistant/7 once the HA integration is updated --- .gitignore | 3 +- Makefile | 13 +++-- README.md | 7 ++- VERSION | 2 +- nordigen/__init__.py | 34 +++++++++-- nordigen/client.py | 102 +++++++++++++++++++++------------ nordigen/oauth.py | 45 +++++++++++++++ pyproject.toml | 2 +- tests/__init__.py | 15 ++++- tests/test_account.py | 22 ++++++-- tests/test_agreements.py | 18 +++--- tests/test_aspsps.py | 4 +- tests/test_auth.py | 28 +++++++++ tests/test_base.py | 39 +++++++------ tests/test_client_wrapper.py | 24 +++++++- tests/test_oauth.py | 106 +++++++++++++++++++++++++++++++++++ tests/test_requisitions.py | 16 +++--- 17 files changed, 384 insertions(+), 96 deletions(-) create mode 100644 nordigen/oauth.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_oauth.py diff --git a/.gitignore b/.gitignore index 2e23ec2..2f2cf84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +.python *egg* -client.py +examples/client.py **/__pycache__ .coverage htmlcov diff --git a/Makefile b/Makefile index aa62141..3952e4a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: build -build: +build: venv python setup.py sdist .PHONY: isort @@ -16,13 +16,13 @@ flake8: .PHONY: test test: - pytest + pytest --ignore=examples/ .PHONY: ci -ci: isort black flake8 test +ci: venv isort black flake8 test .PHONY: ci-fix -ci-fix: +ci-fix: venv isort ./nordigen ./tests ./examples black ./nordigen ./tests ./examples @@ -32,7 +32,7 @@ dev: $(MAKE) ci .PHONY: install-pip -install-pip: +install-pip: venv python -m pip install --upgrade pip .PHONY: install-dev @@ -47,3 +47,6 @@ install-deploy: install-pip deploy: build twine upload --verbose dist/* +.PHONY: venv +venv: + . .python/bin/activate \ No newline at end of file diff --git a/README.md b/README.md index 0afbe44..fd661b6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Nordigen API Client +[![GitHub](https://img.shields.io/github/license/dogmatic69/nordigen-python)](LICENSE) +[![CodeFactor](https://www.codefactor.io/repository/github/dogmatic69/nordigen-python/badge)](https://www.codefactor.io/repository/github/dogmatic69/nordigen-python) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dogmatic69_nordigen-python&metric=alert_status)](https://sonarcloud.io/dashboard?id=dogmatic69_nordigen-python) +[![CI](https://github.com/dogmatic69/nordigen-python/actions/workflows/master.yaml/badge.svg)](https://github.com/dogmatic69/nordigen-python/actions/workflows/master.yaml) + Nordigen is a (always*) free banking API that takes advantage of the EU PSD2 regulations. They connect to banks in over 30 countries using real banking API's (no screen scraping). @@ -28,7 +33,7 @@ pip install nordigen-python ## Usage -Some more indepth working examples can be found in `./examples`. Also check out the test cases for usage examples. +Some more in-depth working examples can be found in `./examples`. Also check out the test cases for usage examples. Create a client instance diff --git a/VERSION b/VERSION index 8294c18..fdd0e66 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.2 \ No newline at end of file +0.2.0b1 \ No newline at end of file diff --git a/nordigen/__init__.py b/nordigen/__init__.py index 7a2acb8..8a9bc00 100644 --- a/nordigen/__init__.py +++ b/nordigen/__init__.py @@ -1,13 +1,35 @@ -from nordigen.client import AccountClient, AgreementsClient, AspspsClient, RequisitionsClient +import warnings +from apiclient import HeaderAuthentication, NoAuthentication + +from nordigen.client import AccountClient, AgreementsClient, AspspsClient, AuthClient, RequisitionsClient +from nordigen.oauth import OAuthAuthentication + + +def Client(token=None, request_strategy=None, secret_id=None, secret_key=None, version=None): + if token: + warnings.warn("Use Client(secret_id=xxx, secret_key=xxx) instead of token", DeprecationWarning) + + if not token and (not secret_id or not secret_key): + raise ValueError("secret_id and secret_key must be provided") -def Client(token, request_strategy=None): def instance(): return instance - instance.aspsps = AspspsClient(token=token, request_strategy=request_strategy) - instance.agreements = AgreementsClient(token=token, request_strategy=request_strategy) - instance.account = AccountClient(token=token, request_strategy=request_strategy) - instance.requisitions = RequisitionsClient(token=token, request_strategy=request_strategy) + auth = HeaderAuthentication(scheme="Token", token=token) + if not token: + version = "v2" if not version else version + auth = OAuthAuthentication( + body={ + "secret_id": secret_id, + "secret_key": secret_key, + }, + client=AuthClient(auth=NoAuthentication(), request_strategy=request_strategy, version=version), + ) + + instance.aspsps = AspspsClient(auth=auth, request_strategy=request_strategy, version=version) + instance.agreements = AgreementsClient(auth=auth, request_strategy=request_strategy, version=version) + instance.account = AccountClient(auth=auth, request_strategy=request_strategy, version=version) + instance.requisitions = RequisitionsClient(auth=auth, request_strategy=request_strategy, version=version) return instance diff --git a/nordigen/client.py b/nordigen/client.py index 2001f81..2b5a96f 100644 --- a/nordigen/client.py +++ b/nordigen/client.py @@ -1,6 +1,6 @@ import urllib -from apiclient import APIClient, HeaderAuthentication, JsonResponseHandler +from apiclient import APIClient, JsonResponseHandler def next_page_by_url(response, previous_page_url): @@ -19,7 +19,7 @@ def next_page_by_url(response, previous_page_url): class NordigenClient(APIClient): - def __init__(self, token, scheme='https', host='ob.nordigen.com', base='/api', request_strategy=None): + def __init__(self, auth, scheme="https", host="ob.nordigen.com", base="/api", request_strategy=None, version="v2"): """Nordigen client base class. Args: @@ -32,10 +32,11 @@ def __init__(self, token, scheme='https', host='ob.nordigen.com', base='/api', r self.request_strategy = request_strategy self.scheme = scheme self.host = host - self.base = base + version = f"/{version}" if version else "" + self.base = f"{base}{version}" super(NordigenClient, self).__init__( - authentication_method=HeaderAuthentication(scheme='Token', token=token), + authentication_method=auth, response_handler=JsonResponseHandler, request_strategy=request_strategy, ) @@ -50,73 +51,97 @@ def url(self, fragment, url_args={}): Returns: str """ - url_args = ('?' + urllib.parse.urlencode(url_args)) if url_args else '' - return f'{self.scheme}://{self.host}{self.base}/{fragment}/{url_args}' + url_args = ("?" + urllib.parse.urlencode(url_args)) if url_args else "" + return f"{self.scheme}://{self.host}{self.base}/{fragment}/{url_args}" + + +class AuthClient(NordigenClient): + def token(self, secret_id, secret_key): + url = self.url(fragment="token/new") + return self.post( + url, + data={ + "secret_id": secret_id, + "secret_key": secret_key, + }, + ) + + def refresh(self, refresh_token): + url = self.url(fragment="token/refresh") + return self.post( + url, + data={ + "refresh_token": refresh_token, + }, + ) class AccountClient(NordigenClient): def info(self, id): - url = self.url(fragment=f'accounts/{id}') + url = self.url(fragment=f"accounts/{id}") return self.get(url) def balances(self, id): - url = self.url(fragment=f'accounts/{id}/balances') + url = self.url(fragment=f"accounts/{id}/balances") return self.get(url) def details(self, id): - url = self.url(fragment=f'accounts/{id}/details') + url = self.url(fragment=f"accounts/{id}/details") return self.get(url) def transactions(self, id): - url = self.url(fragment=f'accounts/{id}/transactions') + url = self.url(fragment=f"accounts/{id}/transactions") return self.get(url) class AgreementsClient(NordigenClient): def create(self, enduser_id, aspsp_id, historical_days=30): - url = self.url(fragment='agreements/enduser') - return self.post(url, data={ - "max_historical_days": historical_days, - "enduser_id": enduser_id, - "aspsp_id": aspsp_id, - }) + url = self.url(fragment="agreements/enduser") + return self.post( + url, + data={ + "max_historical_days": historical_days, + "enduser_id": enduser_id, + "aspsp_id": aspsp_id, + }, + ) def by_enduser_id(self, enduser_id, limit=None, offset=None): url_args = dict(enduser_id=enduser_id, limit=limit, offset=offset) url_args = {k: v for k, v in url_args.items() if v} - url = self.url(fragment='agreements/enduser', url_args=url_args) + url = self.url(fragment="agreements/enduser", url_args=url_args) return self.get(url) def by_id(self, id): - url = self.url(fragment=f'agreements/enduser/{id}') + url = self.url(fragment=f"agreements/enduser/{id}") return self.get(url) def remove(self, id): - url = self.url(fragment=f'agreements/enduser/{id}') + url = self.url(fragment=f"agreements/enduser/{id}") return self.delete(url) def accept(self, id): - url = self.url(fragment=f'agreements/enduser/{id}/accept') - return self.put(url, { - "user_agent": "user-agent", - "ip_address": "127.0.0.1" - }) + url = self.url(fragment=f"agreements/enduser/{id}/accept") + return self.put(url, {"user_agent": "user-agent", "ip_address": "127.0.0.1"}) def text(self, id): - url = self.url(fragment=f'agreements/enduser/{id}/text') + url = self.url(fragment=f"agreements/enduser/{id}/text") return self.get(url) class AspspsClient(NordigenClient): def by_country(self, country): - url = self.url(fragment='aspsps', url_args={ - 'country': country, - }) + url = self.url( + fragment="aspsps", + url_args={ + "country": country, + }, + ) return self.get(url) def by_id(self, id): - url = self.url(fragment=f'aspsps/{id}') + url = self.url(fragment=f"aspsps/{id}") return self.get(url) @@ -125,19 +150,19 @@ def list(self, limit=None, offset=None): url_args = dict(limit=limit, offset=offset) url_args = {k: v for k, v in url_args.items() if v} - url = self.url(fragment='requisitions', url_args=url_args) + url = self.url(fragment="requisitions", url_args=url_args) return self.get(url) def by_id(self, id): - url = self.url(fragment=f'requisitions/{id}') + url = self.url(fragment=f"requisitions/{id}") return self.get(url) def remove(self, id): - url = self.url(fragment=f'requisitions/{id}') + url = self.url(fragment=f"requisitions/{id}") return self.delete(url) def create(self, redirect, enduser_id, reference, agreements=[], language=None): - url = self.url(fragment='requisitions') + url = self.url(fragment="requisitions") data = { "redirect": redirect, "agreements": agreements, @@ -150,7 +175,10 @@ def create(self, redirect, enduser_id, reference, agreements=[], language=None): return self.post(url, data=data) def initiate(self, id, aspsp_id): - url = self.url(fragment=f'requisitions/{id}/links') - return self.post(url, { - "aspsp_id": aspsp_id, - }) + url = self.url(fragment=f"requisitions/{id}/links") + return self.post( + url, + { + "aspsp_id": aspsp_id, + }, + ) diff --git a/nordigen/oauth.py b/nordigen/oauth.py new file mode 100644 index 0000000..447154a --- /dev/null +++ b/nordigen/oauth.py @@ -0,0 +1,45 @@ +from time import time + +from apiclient.authentication_methods import BaseAuthenticationMethod + + +class OAuthAuthentication(BaseAuthenticationMethod): + """Authentication using secret_id and secret_key.""" + + _access_token = None + _token_expiration = None + _refresh_token = None + _refresh_expiration = None + + def __init__( + self, + body, + client, + expiry_margin=10, + ): + """Initialize OAuthAuthentication.""" + self._client = client + self._body = body + self._expiry_margin = expiry_margin + + def get_headers(self): + self.refresh_token() + return { + "Authorization": f"Bearer {self._access_token}", + } + + def refresh_token(self): + if self._token_expiration and self._token_expiration >= int(time()): + return True + + if self._refresh_expiration and self._refresh_expiration >= int(time()): + ret = self._client.refresh(refresh_token=self._refresh_token) + self._access_token = ret.get("access") + self._token_expiration = int(time()) + int(ret.get("access_expires")) - self._expiry_margin + return True + + ret = self._client.token(**self._body) + self._access_token = ret.get("access") + self._refresh_token = ret.get("refresh") + self._token_expiration = int(time()) + int(ret.get("access_expires")) - self._expiry_margin + self._refresh_expiration = int(time()) + int(ret.get("refresh_expires")) - self._expiry_margin diff --git a/pyproject.toml b/pyproject.toml index c18f3b5..2d88e8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] line-length=120 -py36=true \ No newline at end of file +py36=true diff --git a/tests/__init__.py b/tests/__init__.py index 5c1ebd1..a213107 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,5 +5,16 @@ from nordigen import Client -def test_client(token="secret-token", request_strategy=Mock(spec=BaseRequestStrategy)): - return Client(token=token, request_strategy=request_strategy) +def test_client( + token=None, + request_strategy=Mock(spec=BaseRequestStrategy), + secret_id="secret-id", + secret_key="secret-key", +): + args = dict( + token=token, + request_strategy=request_strategy, + secret_id=secret_id, + secret_key=secret_key, + ) + return Client(**args) diff --git a/tests/test_account.py b/tests/test_account.py index ffa835b..8945895 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,4 +1,7 @@ import unittest +from unittest.mock import Mock + +from apiclient.request_strategies import BaseRequestStrategy from . import test_client @@ -7,25 +10,36 @@ class TestAccountClient(unittest.TestCase): def test_info(self): client = test_client().account client.info("foobar-id") - client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/accounts/foobar-id/", params=None) + client.request_strategy.get.assert_called_with( + "https://ob.nordigen.com/api/v2/accounts/foobar-id/", params=None + ) def test_balances(self): client = test_client().account client.balances("foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/accounts/foobar-id/balances/", params=None + "https://ob.nordigen.com/api/v2/accounts/foobar-id/balances/", params=None ) def test_details(self): client = test_client().account client.details("foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/accounts/foobar-id/details/", params=None + "https://ob.nordigen.com/api/v2/accounts/foobar-id/details/", params=None ) def test_transactions(self): client = test_client().account client.transactions("foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/accounts/foobar-id/transactions/", params=None + "https://ob.nordigen.com/api/v2/accounts/foobar-id/transactions/", params=None ) + + +class TestAccountClientErrors(unittest.TestCase): + def test_info(self): + client = test_client(request_strategy=Mock(spec=BaseRequestStrategy)).account + client.info("foobar-id") + + # mock urllib3.connectionpool._make_request and raise an exception + client.request_strategy.get.side_effect = Exception("test") diff --git a/tests/test_agreements.py b/tests/test_agreements.py index cf76f64..a5907d8 100644 --- a/tests/test_agreements.py +++ b/tests/test_agreements.py @@ -3,31 +3,31 @@ from . import test_client -class TestAspspsClient(unittest.TestCase): +class TestAgreementsClient(unittest.TestCase): def test_by_enduser_id(self): client = test_client().agreements client.by_enduser_id(enduser_id="foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/?enduser_id=foobar-id", params=None + "https://ob.nordigen.com/api/v2/agreements/enduser/?enduser_id=foobar-id", params=None ) def test_by_enduser_id_pagination(self): client = test_client().agreements client.by_enduser_id(enduser_id="foobar-id", limit=1) client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/?enduser_id=foobar-id&limit=1", params=None + "https://ob.nordigen.com/api/v2/agreements/enduser/?enduser_id=foobar-id&limit=1", params=None ) client.by_enduser_id(enduser_id="foobar-id", offset=5) client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/?enduser_id=foobar-id&offset=5", params=None + "https://ob.nordigen.com/api/v2/agreements/enduser/?enduser_id=foobar-id&offset=5", params=None ) def test_create(self): client = test_client().agreements client.create(enduser_id="foobar-id", aspsp_id="fizzbuzz-id", historical_days=45) client.request_strategy.post.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/", + "https://ob.nordigen.com/api/v2/agreements/enduser/", data={"max_historical_days": 45, "enduser_id": "foobar-id", "aspsp_id": "fizzbuzz-id"}, params=None, ) @@ -36,21 +36,21 @@ def test_by_id(self): client = test_client().agreements client.by_id("foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/foobar-id/", params=None + "https://ob.nordigen.com/api/v2/agreements/enduser/foobar-id/", params=None ) def test_remove(self): client = test_client().agreements client.remove("foobar-id") client.request_strategy.delete.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/foobar-id/", params=None + "https://ob.nordigen.com/api/v2/agreements/enduser/foobar-id/", params=None ) def test_accept(self): client = test_client().agreements client.accept("foobar-id") client.request_strategy.put.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/foobar-id/accept/", + "https://ob.nordigen.com/api/v2/agreements/enduser/foobar-id/accept/", data={"user_agent": "user-agent", "ip_address": "127.0.0.1"}, params=None, ) @@ -59,5 +59,5 @@ def test_text(self): client = test_client().agreements client.text("foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/agreements/enduser/foobar-id/text/", params=None + "https://ob.nordigen.com/api/v2/agreements/enduser/foobar-id/text/", params=None ) diff --git a/tests/test_aspsps.py b/tests/test_aspsps.py index ebac4ec..024a0a1 100644 --- a/tests/test_aspsps.py +++ b/tests/test_aspsps.py @@ -7,9 +7,9 @@ class TestAspspsClient(unittest.TestCase): def test_by_country(self): client = test_client().aspsps client.by_country("SE") - client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/aspsps/?country=SE", params=None) + client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/v2/aspsps/?country=SE", params=None) def test_by_id(self): client = test_client().aspsps client.by_id("foo-bar-id") - client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/aspsps/foo-bar-id/", params=None) + client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/v2/aspsps/foo-bar-id/", params=None) diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..d620b06 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,28 @@ +import unittest + +from . import test_client + + +class TestAuthClient(unittest.TestCase): + def test_token(self): + client = test_client().aspsps.get_authentication_method()._client + client.token(secret_id="secret_id", secret_key="secret_key") + client.request_strategy.post.assert_called_with( + "https://ob.nordigen.com/api/v2/token/new/", + data={ + "secret_id": "secret_id", + "secret_key": "secret_key", + }, + params=None, + ) + + def test_refresh(self): + client = test_client().aspsps.get_authentication_method()._client + client.refresh(refresh_token="refresh_token") + client.request_strategy.post.assert_called_with( + "https://ob.nordigen.com/api/v2/token/refresh/", + data={ + "refresh_token": "refresh_token", + }, + params=None, + ) diff --git a/tests/test_base.py b/tests/test_base.py index a6685fb..597e043 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,12 +1,15 @@ import unittest -from nordigen.client import NordigenClient as Client -from nordigen.client import next_page_by_url +from apiclient import HeaderAuthentication + +from nordigen.client import NordigenClient, next_page_by_url + +header_auth = HeaderAuthentication(scheme="Token", token="fizz-buzz") class TestBaseAuth(unittest.TestCase): - def test_token(self): - client = Client(token="fizz-buzz") + def test_token_auth(self): + client = NordigenClient(auth=header_auth) self.assertEqual( client.get_default_headers(), @@ -24,42 +27,42 @@ def test_pagination(self): class TestBaseUrl(unittest.TestCase): def test_url_host(self): - client = Client(token="asdf", host="localhost") + client = NordigenClient(auth=None, host="localhost") result = client.url("foo") - self.assertEqual(result, "https://localhost/api/foo/") + self.assertEqual(result, "https://localhost/api/v2/foo/") def test_url_scheme(self): - client = Client(token="asdf", scheme="http") + client = NordigenClient(auth=None, scheme="sftp") result = client.url("foo") - self.assertEqual(result, "http://ob.nordigen.com/api/foo/") + self.assertEqual(result, "sftp://ob.nordigen.com/api/v2/foo/") def test_url_base(self): - client = Client(token="asdf", base="") + client = NordigenClient(auth=None, base="") result = client.url("foo") - self.assertEqual(result, "https://ob.nordigen.com/foo/") + self.assertEqual(result, "https://ob.nordigen.com/v2/foo/") - client = Client(token="asdf", base="/some/thing/here") + client = NordigenClient(auth=None, base="/some/thing/here") result = client.url("foo") - self.assertEqual(result, "https://ob.nordigen.com/some/thing/here/foo/") + self.assertEqual(result, "https://ob.nordigen.com/some/thing/here/v2/foo/") def test_url_basic(self): - client = Client(token="asdf") + client = NordigenClient(auth=None) result = client.url("foo") - self.assertEqual(result, "https://ob.nordigen.com/api/foo/") + self.assertEqual(result, "https://ob.nordigen.com/api/v2/foo/") result = client.url("foo/bar") - self.assertEqual(result, "https://ob.nordigen.com/api/foo/bar/") + self.assertEqual(result, "https://ob.nordigen.com/api/v2/foo/bar/") def test_url_args(self): - client = Client(token="asdf") + client = NordigenClient(auth=None) result = client.url("foo", url_args={}) - self.assertEqual(result, "https://ob.nordigen.com/api/foo/") + self.assertEqual(result, "https://ob.nordigen.com/api/v2/foo/") result = client.url("foo", url_args={"fizz": "buzz"}) - self.assertEqual(result, "https://ob.nordigen.com/api/foo/?fizz=buzz") + self.assertEqual(result, "https://ob.nordigen.com/api/v2/foo/?fizz=buzz") diff --git a/tests/test_client_wrapper.py b/tests/test_client_wrapper.py index 6229aa5..fa50de3 100644 --- a/tests/test_client_wrapper.py +++ b/tests/test_client_wrapper.py @@ -9,13 +9,33 @@ class TestClient(unittest.TestCase): def test_basic(self): - client = Client(token="whoo", request_strategy=Mock(spec=BaseRequestStrategy)) + client = Client(secret_id="secret", secret_key="secret", request_strategy=Mock(spec=BaseRequestStrategy)) self.assertEqual(client, client()) def test_instances(self): - client = Client(token="whoo", request_strategy=Mock(spec=BaseRequestStrategy)) + client = Client(secret_id="secret", secret_key="secret", request_strategy=Mock(spec=BaseRequestStrategy)) self.assertIsInstance(client.aspsps, NordigenClient) self.assertIsInstance(client.agreements, NordigenClient) self.assertIsInstance(client.account, NordigenClient) self.assertIsInstance(client.requisitions, NordigenClient) + + def test_pool(self): + client = Client(secret_id="secret", secret_key="secret", request_strategy=Mock(spec=BaseRequestStrategy)) + self.assertIsInstance(client.aspsps, NordigenClient) + + def test_warning_for_depreciated_config(self): + with self.assertWarns(DeprecationWarning): + Client(token="token") + + def test_exception_missing_secret_id(self): + with self.assertRaises(ValueError): + Client(secret_key="secret") + + def test_exception_missing_secret_key(self): + with self.assertRaises(ValueError): + Client(secret_id="secret") + + def test_exception_missing_secret_id_and_secret_key(self): + with self.assertRaises(ValueError): + Client() diff --git a/tests/test_oauth.py b/tests/test_oauth.py new file mode 100644 index 0000000..3dfb8df --- /dev/null +++ b/tests/test_oauth.py @@ -0,0 +1,106 @@ +import unittest +from unittest.mock import Mock, patch + +from nordigen.oauth import OAuthAuthentication + + +class TestOAuthAuthentication(unittest.TestCase): + def test_perform_initial_auth(self): + auth = OAuthAuthentication( + expiry_margin=999, + body={}, + client="client", + ) + + assert auth._client == "client" + + def test_valid_token_does_not_refresh(self): + mocked_client = Mock() + auth = OAuthAuthentication( + expiry_margin=999, + body={}, + client=mocked_client, + ) + + auth._token_expiration = 99999999999 + + assert auth.refresh_token() is True + + mocked_client.token.assert_not_called() + mocked_client.refresh.assert_not_called() + + @patch("nordigen.oauth.time") + def test_expired_token_refreshes(self, mocked_time): + mocked_time.return_value = 1234.567 + mocked_client = Mock() + mocked_client.refresh.return_value = { + "access": "access-token-yeahaw", + "access_expires": 50, # expires in 50 seconds + } + + auth = OAuthAuthentication( + expiry_margin=999, + body={"foo": "bar"}, + client=mocked_client, + ) + + auth._refresh_expiration = 1250 + auth._refresh_token = "super-refresh-token" + + assert auth._access_token is None + assert auth._token_expiration is None + + assert auth.refresh_token() is True + + mocked_client.refresh.assert_called_once_with(refresh_token="super-refresh-token") + + assert auth._access_token == "access-token-yeahaw" + assert auth._token_expiration == 1234 + 50 - 999 + + @patch("nordigen.oauth.time") + def test_expired_refresh_token(self, mocked_time): + mocked_time.return_value = 1234.567 + mocked_client = Mock() + mocked_client.token.return_value = { + "access": "access-token-yeahaw", + "access_expires": 50, # expires in 50 seconds + "refresh": "refresh-token-yeahaw", + "refresh_expires": 60, # expires in 60 seconds + } + + auth = OAuthAuthentication( + expiry_margin=1, + body={"foo": "bar"}, + client=mocked_client, + ) + + auth._refresh_expiration = 1200 # expired 34 seconds ago + auth._refresh_token = "super-old-refresh-token" + auth._token_expiration = 1200 # expired 34 seconds ago + auth._access_token = "super-old-access-token" + + assert auth.refresh_token() is None + + mocked_client.token.assert_called_once_with(foo="bar") + + assert auth._access_token == "access-token-yeahaw" + assert auth._token_expiration == 1234 + 50 - 1 + assert auth._refresh_token == "refresh-token-yeahaw" + assert auth._refresh_expiration == 1234 + 60 - 1 + + @patch("nordigen.oauth.time") + def test_get_headers(self, mocked_time): + mocked_time.return_value = 1234.567 + + auth = OAuthAuthentication( + expiry_margin=999, + body={}, + client="client", + ) + auth._token_expiration = 9999 + auth._access_token = "valid-access-token" + + expected = { + "Authorization": "Bearer valid-access-token", + } + assert auth.get_headers() == expected diff --git a/tests/test_requisitions.py b/tests/test_requisitions.py index 118388e..82ab6d5 100644 --- a/tests/test_requisitions.py +++ b/tests/test_requisitions.py @@ -8,14 +8,16 @@ def test_list(self): client = test_client().requisitions client.list() - client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/requisitions/", params=None) + client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/v2/requisitions/", params=None) client.list(limit=1) - client.request_strategy.get.assert_called_with("https://ob.nordigen.com/api/requisitions/?limit=1", params=None) + client.request_strategy.get.assert_called_with( + "https://ob.nordigen.com/api/v2/requisitions/?limit=1", params=None + ) client.list(offset=5) client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/requisitions/?offset=5", params=None + "https://ob.nordigen.com/api/v2/requisitions/?offset=5", params=None ) def test_by_id(self): @@ -23,7 +25,7 @@ def test_by_id(self): client.by_id("foobar-id") client.request_strategy.get.assert_called_with( - "https://ob.nordigen.com/api/requisitions/foobar-id/", params=None + "https://ob.nordigen.com/api/v2/requisitions/foobar-id/", params=None ) def test_remove(self): @@ -31,7 +33,7 @@ def test_remove(self): client.remove("foobar-id") client.request_strategy.delete.assert_called_with( - "https://ob.nordigen.com/api/requisitions/foobar-id/", params=None + "https://ob.nordigen.com/api/v2/requisitions/foobar-id/", params=None ) def test_create(self): @@ -48,7 +50,7 @@ def test_create(self): ) client.request_strategy.post.assert_called_with( - "https://ob.nordigen.com/api/requisitions/", + "https://ob.nordigen.com/api/v2/requisitions/", data={ "redirect": "redirect", "agreements": "agreements", @@ -64,7 +66,7 @@ def test_initiate(self): client.initiate("foobar-id", "aspsp_id") client.request_strategy.post.assert_called_with( - "https://ob.nordigen.com/api/requisitions/foobar-id/links/", + "https://ob.nordigen.com/api/v2/requisitions/foobar-id/links/", data={ "aspsp_id": "aspsp_id", },