From 2d0fdac69e8c064ad440cec02491e65913238a32 Mon Sep 17 00:00:00 2001 From: "Bendik R. Brenne" Date: Sun, 13 Oct 2024 02:34:25 +0200 Subject: [PATCH] Adds tests and fixtures for authentication client --- tests/conftest.py | 70 +++++-- tests/fixtures/auth_flow.json | 1 + tests/fixtures/bffdata.json | 1 + tests/fixtures/default_credentials.json | 2 +- tests/helpers.py | 26 +++ tests/ruff.toml | 12 +- tests/test_auth.py | 242 ++++++++++++++++++++++++ tests/test_client.py | 54 ++++-- 8 files changed, 376 insertions(+), 32 deletions(-) create mode 100644 tests/fixtures/auth_flow.json create mode 100644 tests/fixtures/bffdata.json create mode 100644 tests/test_auth.py diff --git a/tests/conftest.py b/tests/conftest.py index f692d31..06deacc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from __future__ import annotations import contextlib +from datetime import datetime, timezone import logging import pytest @@ -8,7 +9,7 @@ from podme_api.auth import PodMeDefaultAuthClient, PodMeUserCredentials, SchibstedCredentials from podme_api.client import PodMeClient -from .helpers import load_fixture +from .helpers import load_fixture_json _LOGGER = logging.getLogger(__name__) @@ -16,21 +17,20 @@ @pytest.fixture -async def podme_client(default_credentials): +async def podme_client(default_credentials, user_credentials): """Return PodMeClient.""" @contextlib.asynccontextmanager async def _podme_client( - username: str | None = None, - password: str | None = None, + credentials: SchibstedCredentials | None = None, load_default_credentials: bool = True, + load_default_user_credentials: bool = False, ) -> PodMeClient: - if username is None or password is None: - user_creds = None - else: - user_creds = PodMeUserCredentials(username, password) + user_creds = user_credentials if load_default_user_credentials is True else None auth_client = PodMeDefaultAuthClient(user_credentials=user_creds) - if load_default_credentials: + if credentials is not None: + auth_client.set_credentials(credentials) + elif load_default_credentials: auth_client.set_credentials(default_credentials) client = PodMeClient(auth_client=auth_client, disable_credentials_storage=True) try: @@ -42,7 +42,55 @@ async def _podme_client( return _podme_client +@pytest.fixture +async def podme_default_auth_client(user_credentials, default_credentials): + """Return PodMeDefaultAuthClient.""" + + @contextlib.asynccontextmanager + async def _podme_auth_client( + credentials: SchibstedCredentials | None = None, + load_default_credentials: bool = True, + load_default_user_credentials: bool = True, + ) -> PodMeDefaultAuthClient: + auth_client = PodMeDefaultAuthClient() + + if load_default_user_credentials: + auth_client.user_credentials = user_credentials + if credentials is not None: + auth_client.set_credentials(credentials) + elif load_default_credentials: + auth_client.set_credentials(default_credentials) + + try: + await auth_client.__aenter__() + yield auth_client + finally: + await auth_client.__aexit__(None, None, None) + + return _podme_auth_client + + +@pytest.fixture +def user_credentials(): + return PodMeUserCredentials(email="testuser@example.com", password="securepassword123") + + @pytest.fixture def default_credentials(): - data = load_fixture("default_credentials") - return SchibstedCredentials.from_json(data) + data = load_fixture_json("default_credentials") + data["expiration_time"] = int(datetime.now(tz=timezone.utc).timestamp() + data["expires_in"]) + return SchibstedCredentials.from_dict(data) + + +@pytest.fixture +def expired_credentials(): + data = load_fixture_json("default_credentials") + data["expiration_time"] = int(datetime.now(tz=timezone.utc).timestamp() - 1) + return SchibstedCredentials.from_dict(data) + + +@pytest.fixture +def refreshed_credentials(default_credentials): + data = load_fixture_json("default_credentials") + data["access_token"] = data["access_token"] + "_refreshed" + return SchibstedCredentials.from_dict(data) diff --git a/tests/fixtures/auth_flow.json b/tests/fixtures/auth_flow.json new file mode 100644 index 0000000..5ea19bf --- /dev/null +++ b/tests/fixtures/auth_flow.json @@ -0,0 +1 @@ +{"login_form": "\n\n\n
\n {"client":{"birthdayFormat":"default","css":{"background_color":"","color":""},"domain":"https://podme.com","emailReceiptsEnabled":true,"id":"66fd26cdae6bde57ef206b35","locale":"nb_NO","logoUrl":"//d3iwtia3ndepsv.cloudfront.net/clients/images/logos/66fd26cdae6bde57ef206b35_66fd27c2ebc06.png","logoMarkUrl":"//d3iwtia3ndepsv.cloudfront.net/clients/images/logos/60bf1d46c440077a8ccbfdf4_612e3cde466cb.png","company":"media","merchantId":47099,"merchantName":"Podme","merchantType":"external","name":"New Podme","appType":"web_client","pulseProviderId":"sdrn:schibsted:client:schibsted-account","sessionServiceDomain":"https://id.no.podme.com","teaser":null,"terms":{"clientTermsUrl":"/terms?client_id=66fd26cdae6bde57ef206b35"},"supportUrl":"","defaultClientId":"4d06920474dea26227070000","uriScheme":""},"spidUrl":"https://payment.schibsted.no","reCaptchaSiteKey":"6Le5um4UAAAAABdQhjGRL1lLIVduoKSSuDCpggjg","defaultTermsAgreement":true,"pulse":{"enabled":true,"providerId":"sdrn:schibsted:client:schibsted-account","realm":"spid.no","deployTag":"schacc-v4.5.38","experiments":[],"deployStage":"pro"},"bff":{"host":"https://payment.schibsted.no","env":"pro-no"},"initialState":{"links":{"self":{"href":"/authn/?client_sdrn=sdrn%3Aspid.no%3Aclient%3A66fd26cdae6bde57ef206b35&prompt=select_account&client_id=66fd26cdae6bde57ef206b35&nonce=1b627c68-7b13-4c69-b0dd-f5e4b7fcb3d5&state=2322e273c61b6a46adc08c1c9c9c056dacaa05637dc94a96148df2d895e0d8657cf7db02c2be5952481b2e4c86689aff8f4d923fd92e8d261571b3cb8da91125150282b9026f9058f7655db018e7d3b8247a2090459aee8834da549e92e3339dcc75b3783896cce3ade64727db1b2b99679c1dd87c4175108f2c4e9c1d7afb36"}},"meta":{"layoutOptions":["passwordless-flow"]}},"csrfToken":"yucv27QQ-Tk7aNZFOJjlwdL-5sF9rD2LgZtA"}\n
\n
\n\n\n", "final_html": "\n\n\n\n Oppdag podkaster p\u00e5 Podme.com\n\n\n
\n\n\n", "csrf": {"links": {"self": {"href": "/authn/api/settings/csrf?client_id=66fd26cdae6bde57ef206b35"}}, "data": {"type": "settings", "attributes": {"csrfToken": "W0MURPsg-XnD3Kyb8PW2YbU7Pa8t6li6ZFIk"}}}, "email_status": {"links": {"self": {"href": "/authn/api/identity/email-status?client_id=66fd26cdae6bde57ef206b35"}, "next": {"href": "/login-password"}}, "data": {"type": "email", "attributes": {"email": "testuser@example.com"}}, "meta": {"renderReCaptcha": false}}, "login": {"links": {"self": {"href": "/authn/api/identity/login/?client_id=66fd26cdae6bde57ef206b35"}, "next": {"href": "/success"}}, "meta": {"renderReCaptcha": false, "tracking": {"userIdentifier": "123456", "disabledTracking": true}}}} diff --git a/tests/fixtures/bffdata.json b/tests/fixtures/bffdata.json new file mode 100644 index 0000000..d8feed4 --- /dev/null +++ b/tests/fixtures/bffdata.json @@ -0,0 +1 @@ +{"client":{"birthdayFormat":"default","css":{"background_color":"","color":""},"domain":"https://podme.com","emailReceiptsEnabled":true,"id":"66fd26cdae6bde57ef206b35","locale":"nb_NO","logoUrl":"//d3iwtia3ndepsv.cloudfront.net/clients/images/logos/66fd26cdae6bde57ef206b35_66fd27c2ebc06.png","logoMarkUrl":"//d3iwtia3ndepsv.cloudfront.net/clients/images/logos/60bf1d46c440077a8ccbfdf4_612e3cde466cb.png","company":"media","merchantId":47099,"merchantName":"Podme","merchantType":"external","name":"Podme","appType":"web_client","pulseProviderId":"sdrn:schibsted:client:schibsted-account","sessionServiceDomain":"https://id.no.podme.com","teaser":null,"terms":{"clientTermsUrl":"/terms?client_id=66fd26cdae6bde57ef206b35"},"supportUrl":"","defaultClientId":"4d06920474dea26227070000","uriScheme":""},"spidUrl":"https://payment.schibsted.no","reCaptchaSiteKey":"6Le5um4UAAAAABdQhjGRL1lLIVduoKSSuDCpggjg","defaultTermsAgreement":true,"pulse":{"enabled":true,"providerId":"sdrn:schibsted:client:schibsted-account","realm":"spid.no","deployTag":"schacc-v4.5.38","experiments":[],"deployStage":"pro"},"bff":{"host":"https://payment.schibsted.no","env":"pro-no"},"initialState":{"links":{"self":{"href":"/authn/?client_sdrn=sdrn%3Aspid.no%3Aclient%3A66fd26cdae6bde57ef206b35&prompt=select_account&client_id=66fd26cdae6bde57ef206b35&nonce=5827bfc4-8580-447e-8222-9349102aa5e8&state=243ba7f5ee32d8b25c22a26d1e7f5db520fc6d064388783eb2bcdb7c203450b915e075ab449ebdc232cb0b173933d83d4f5262ca88a9e7cbd84db062103aa8d4694265f677ea354e63f8d291a968b747a667d695d818816232fba32465c5dddbdd5ae42016103e11400e3e9bf079d05b5e515a4556bdf5c275acfd6252718cbe"}},"meta":{"layoutOptions":["passwordless-flow"]}},"csrfToken":"vfZK4v6M-XB9hH8aiNFNepGS0P1I3jWQ-6ik"} diff --git a/tests/fixtures/default_credentials.json b/tests/fixtures/default_credentials.json index 87d4da3..8f43f55 100644 --- a/tests/fixtures/default_credentials.json +++ b/tests/fixtures/default_credentials.json @@ -1 +1 @@ -{"scope":"openid email","user_id":"123456","is_admin":false,"token_type":"Bearer","access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BheW1lbnQuc2NoaWJzdGVkLm5vLyIsImlhdCI6MTcyODUxODQwMCwiZXhwIjo0MTAyNDQ0ODAwLCJhdWQiOiJodHRwczovL3BvZG1lLmNvbSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwZmZmZiIsInVzZXJfaWQiOiIxMjM0NTYiLCJjbGFzcyI6InRva2VuLk9BdXRoVXNlckFjY2Vzc1Rva2VuIiwianRpIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMGZmIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJhenAiOiI2NmZkMjZjZGFlNmJkZTU3ZWYyMDZiMzUiLCJjbGllbnRfaWQiOiI2NmZkMjZjZGFlNmJkZTU3ZWYyMDZiMzUifQ.eXMqS6ymgIthKCDKgGK3ZzFZaY0P-B-2z5QjuZlsIzc","refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BheW1lbnQuc2NoaWJzdGVkLm5vLyIsImlhdCI6MTcyODUxODQwMCwiZXhwIjo0MTAyNDQ0ODAwLCJhdWQiOiJodHRwczovL3BvZG1lLmNvbSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwZmZmZiIsInVzZXJfaWQiOiIxMjM0NTYiLCJjbGFzcyI6InRva2VuLk9BdXRoVXNlclJlZnJlc2hUb2tlbiIsImp0aSI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDBmZiIsInNjb3BlIjoib3BlbmlkIGVtYWlsIiwiYXpwIjoiNjZmZDI2Y2RhZTZiZGU1N2VmMjA2YjM1IiwiY2xpZW50X2lkIjoiNjZmZDI2Y2RhZTZiZGU1N2VmMjA2YjM1IiwiYWp0aSI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDBkZCJ9.M_oRHsskpt0ODJIZJ4x9VlMWIFpTsXkbLucyH1Oj3_Q","expires_in":2373926400,"id_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BheW1lbnQuc2NoaWJzdGVkLm5vLyIsImlhdCI6MTcyODUxODQwMCwiZXhwIjo0MTAyNDQ0ODAwLCJhdWQiOlsiNjZmZDI2Y2RhZTZiZGU1N2VmMjA2YjM1IiwiaHR0cHM6Ly9wb2RtZS5jb20iXSwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDBmZmZmIiwibGVnYWN5X3VzZXJfaWQiOiIxMjM0NTYiLCJjbGFzcyI6InRva2VuLklEVG9rZW4iLCJhenAiOiI2NmZkMjZjZGFlNmJkZTU3ZWYyMDZiMzUiLCJlbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIn0.N-7DexoBVhvLdPLP75cxZrXm0ToiFVCmouV2Ixynp6k","server_time":1728518400,"expiration_time":4102444800,"account_created":null,"email":null} +{"scope":"openid email","user_id":"123456","is_admin":false,"token_type":"Bearer","access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BheW1lbnQuc2NoaWJzdGVkLm5vLyIsImlhdCI6MTcyODUxODQwMCwiZXhwIjo0MTAyNDQ0ODAwLCJhdWQiOiJodHRwczovL3BvZG1lLmNvbSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwZmZmZiIsInVzZXJfaWQiOiIxMjM0NTYiLCJjbGFzcyI6InRva2VuLk9BdXRoVXNlckFjY2Vzc1Rva2VuIiwianRpIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMGZmIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJhenAiOiI2NmZkMjZjZGFlNmJkZTU3ZWYyMDZiMzUiLCJjbGllbnRfaWQiOiI2NmZkMjZjZGFlNmJkZTU3ZWYyMDZiMzUifQ.eXMqS6ymgIthKCDKgGK3ZzFZaY0P-B-2z5QjuZlsIzc","refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BheW1lbnQuc2NoaWJzdGVkLm5vLyIsImlhdCI6MTcyODUxODQwMCwiZXhwIjo0MTAyNDQ0ODAwLCJhdWQiOiJodHRwczovL3BvZG1lLmNvbSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwZmZmZiIsInVzZXJfaWQiOiIxMjM0NTYiLCJjbGFzcyI6InRva2VuLk9BdXRoVXNlclJlZnJlc2hUb2tlbiIsImp0aSI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDBmZiIsInNjb3BlIjoib3BlbmlkIGVtYWlsIiwiYXpwIjoiNjZmZDI2Y2RhZTZiZGU1N2VmMjA2YjM1IiwiY2xpZW50X2lkIjoiNjZmZDI2Y2RhZTZiZGU1N2VmMjA2YjM1IiwiYWp0aSI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDBkZCJ9.M_oRHsskpt0ODJIZJ4x9VlMWIFpTsXkbLucyH1Oj3_Q","expires_in":600,"id_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BheW1lbnQuc2NoaWJzdGVkLm5vLyIsImlhdCI6MTcyODUxODQwMCwiZXhwIjo0MTAyNDQ0ODAwLCJhdWQiOlsiNjZmZDI2Y2RhZTZiZGU1N2VmMjA2YjM1IiwiaHR0cHM6Ly9wb2RtZS5jb20iXSwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDBmZmZmIiwibGVnYWN5X3VzZXJfaWQiOiIxMjM0NTYiLCJjbGFzcyI6InRva2VuLklEVG9rZW4iLCJhenAiOiI2NmZkMjZjZGFlNmJkZTU3ZWYyMDZiMzUiLCJlbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIn0.N-7DexoBVhvLdPLP75cxZrXm0ToiFVCmouV2Ixynp6k","server_time":1704067200,"expiration_time":1704067800,"account_created":null,"email":null} diff --git a/tests/helpers.py b/tests/helpers.py index c0f4eea..660b1af 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -47,6 +47,26 @@ def __init__( match_partial_query=True, repeat=1, ): + """Initialize a CustomRoute instance. + + Args: + method_pattern (str, optional): HTTP method to match. Defaults to ANY. + host_pattern (str, optional): Host to match. Defaults to ANY. + path_pattern (str, optional): Path to match. Defaults to ANY. + body_pattern (str, optional): Body to match. Defaults to ANY. + repeat (int, optional): Number of times to match. Defaults to 1. + path_qs (dict, optional): Query string parameters to match. + match_querystring (bool, optional): Whether to match the query string. + Defaults to True if `path_qs` is not None. + match_partial_query (bool, optional): Whether to match only part of the query string. + + If `True`, the route will match if the query string contains all the specified query + parameters, regardless of other parameters present. + + If `False`, the route will only match if the query string exactly matches the specified + query parameters in `path_qs`. + + """ super().__init__(method_pattern, host_pattern, path_pattern, body_pattern, match_querystring, repeat) if path_qs is not None: self.path_qs = urlencode({k: v for k, v in path_qs.items() if v is not None}) @@ -54,6 +74,12 @@ def __init__( self.match_partial_query = match_partial_query async def matches(self, request): + """Check if the request matches this route. + + Args: + request: The incoming request to match against. + + """ path_to_match = urlparse(request.path_qs) query_to_match = parse_qs(path_to_match.query) parsed_path = urlparse(self.path_pattern) diff --git a/tests/ruff.toml b/tests/ruff.toml index 7993bad..4440f8a 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -2,14 +2,16 @@ extend = "../pyproject.toml" lint.extend-select = [ - "PT", # Use @pytest.fixture without parentheses + "PT", # Use @pytest.fixture without parentheses ] lint.extend-ignore = [ - "S101", - "SLF001", - "TCH002", - "PLR2004", + "S101", + "S105", + "S106", + "SLF001", + "TCH002", + "PLR2004", ] lint.pylint.max-branches = 13 diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..ee2d7f3 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,242 @@ +"""Tests for PodMeClient.""" + +from __future__ import annotations + +from asyncio import sleep +import json +import logging +from urllib.parse import quote_plus + +from aiohttp import ClientResponse +from aiohttp.web_response import json_response +from aresponses import ResponsesMockServer +import pytest +from yarl import URL + +from podme_api import PodMeClient, PodMeDefaultAuthClient +from podme_api.const import PODME_AUTH_BASE_URL, PODME_AUTH_RETURN_URL, PODME_BASE_URL +from podme_api.exceptions import ( + PodMeApiAuthenticationError, + PodMeApiConnectionError, + PodMeApiConnectionTimeoutError, + PodMeApiError, +) + +from .helpers import load_fixture_json + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + + +async def test_async_get_access_token_with_valid_credentials(podme_default_auth_client, default_credentials): + async with podme_default_auth_client() as auth_client: + access_token = await auth_client.async_get_access_token() + assert access_token == default_credentials.access_token + + async with PodMeDefaultAuthClient(credentials=default_credentials) as auth_client: + access_token = await auth_client.async_get_access_token() + assert access_token == default_credentials.access_token + + +async def test_async_get_access_token_with_expired_credentials( + aresponses: ResponsesMockServer, podme_default_auth_client, expired_credentials, refreshed_credentials +): + aresponses.add( + URL(PODME_BASE_URL).host, + "/auth/refreshSchibstedSession", + "GET", + json_response(data=refreshed_credentials.to_dict()), + ) + async with podme_default_auth_client(credentials=expired_credentials) as auth_client: + access_token = await auth_client.async_get_access_token() + assert access_token == refreshed_credentials.access_token + + +async def test_async_get_access_token_without_credentials(podme_client, podme_default_auth_client): + async with podme_client(load_default_credentials=False) as client: + client: PodMeClient + with pytest.raises(PodMeApiAuthenticationError): + await client.auth_client.async_get_access_token() + + async with podme_default_auth_client( + load_default_user_credentials=False, load_default_credentials=False + ) as auth_client: + with pytest.raises(PodMeApiAuthenticationError): + await auth_client.async_get_access_token() + + +async def test_authorize_success( + aresponses: ResponsesMockServer, podme_default_auth_client, default_credentials, user_credentials +): + auth_flow = load_fixture_json("auth_flow") + + # GET oauth/authorize + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/oauth/authorize", + "GET", + aresponses.Response(body=auth_flow["login_form"]), + repeat=2, + ) + + # POST authn/api/settings/csrf + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/authn/api/settings/csrf", + "GET", + json_response(data=auth_flow["csrf"]), + repeat=2, + ) + + # POST authn/api/identity/email-status + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/authn/api/identity/email-status", + "POST", + json_response(data=auth_flow["email_status"]), + repeat=2, + ) + + # POST authn/api/identity/login/ + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/authn/api/identity/login/", + "POST", + json_response(data=auth_flow["login"]), + repeat=2, + ) + + # POST authn/identity/finish/ + # redirect_state = quote_plus(json.dumps({"")) + redirect_state = f"%7B%22returnUrl%22%3A%22{quote_plus(PODME_AUTH_RETURN_URL)}%22%7D" + redirect_qs = f"code=testCode&state={redirect_state}" + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/authn/identity/finish/", + "POST", + aresponses.Response( + status=302, + headers={ + "Location": f"{PODME_BASE_URL}/auth/handleSchibstedLogin?{redirect_qs}", + }, + ), + repeat=2, + ) + + # GET https://podme.com/auth/handleSchibstedLogin + default_credentials_json = default_credentials.to_json() + default_credentials_dict = default_credentials.to_dict() + mock_response = aresponses.Response( + status=307, + headers={ + "Location": PODME_AUTH_RETURN_URL, + }, + ) + mock_response.set_cookie("jwt-cred", default_credentials_json) + aresponses.add( + URL(PODME_BASE_URL).host, + "/auth/handleSchibstedLogin", + "GET", + mock_response, + repeat=2, + ) + + aresponses.add( + URL(PODME_AUTH_RETURN_URL).host, + URL(PODME_AUTH_RETURN_URL).path, + "GET", + aresponses.Response( + body=auth_flow["final_html"], + ), + repeat=2, + ) + async with podme_default_auth_client(load_default_credentials=False) as auth_client: + token = await auth_client.async_get_access_token() + credentials = auth_client.get_credentials() + assert credentials == default_credentials_dict + assert token == default_credentials.access_token + credentials = await auth_client.authorize(user_credentials) + assert credentials == default_credentials + + +async def test_refresh_token_success( + aresponses: ResponsesMockServer, podme_default_auth_client, default_credentials, refreshed_credentials +): + # Mock the refresh token endpoint + aresponses.add( + URL(PODME_BASE_URL).host, + "/auth/refreshSchibstedSession", + "GET", + json_response(data=refreshed_credentials.to_dict()), + ) + async with podme_default_auth_client() as auth_client: + new_credentials = await auth_client.refresh_token(default_credentials) + assert new_credentials == refreshed_credentials + + +async def test_refresh_token_failure(aresponses: ResponsesMockServer, podme_default_auth_client): + # Mock the refresh token endpoint to return an error + aresponses.add( + URL(PODME_BASE_URL).host, + "/auth/refreshSchibstedSession", + "GET", + aresponses.Response(text="Unauthorized", status=401), + ) + + async with podme_default_auth_client() as auth_client: + with pytest.raises(PodMeApiConnectionError): + await auth_client.refresh_token() + + +async def test_get_set_credentials(podme_default_auth_client, default_credentials): + async with podme_default_auth_client(load_default_credentials=False) as auth_client: + auth_client.set_credentials(default_credentials) + retrieved_credentials = auth_client.get_credentials() + assert retrieved_credentials == default_credentials.to_dict() + + # Set credentials using a dictionary + new_creds_dict = default_credentials.to_dict() + new_creds_dict["user_id"] = "123456" + auth_client.set_credentials(new_creds_dict) + retrieved_new_credentials = auth_client.get_credentials() + assert retrieved_new_credentials == new_creds_dict + + # Set credentials using a JSON string + new_creds_json = json.dumps(new_creds_dict) + auth_client.set_credentials(new_creds_json) + retrieved_json_credentials = auth_client.get_credentials() + assert retrieved_json_credentials == new_creds_dict + + +async def test_request_timeout(aresponses: ResponsesMockServer, podme_default_auth_client): + # Mock a timeout by not responding + # Faking a timeout by sleeping + async def response_handler(_: ClientResponse): + """Response handler for this test.""" + await sleep(1) + return aresponses.Response(body="Helluu") # pragma: no cover + + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/oauth/authorize", + "GET", + response_handler, + ) + async with podme_default_auth_client() as auth_client: + auth_client.request_timeout = 0.1 + with pytest.raises(PodMeApiConnectionTimeoutError): + await auth_client._request("oauth/authorize") + + +async def test_request_bad_request(aresponses: ResponsesMockServer, podme_default_auth_client): + # Mock a 400 Bad Request response + aresponses.add( + URL(PODME_AUTH_BASE_URL).host, + "/invalid/endpoint", + "GET", + aresponses.Response(text="Bad Request", status=400), + ) + async with podme_default_auth_client() as auth_client: + with pytest.raises(PodMeApiError) as exc_info: + await auth_client._request("invalid/endpoint") + assert "Bad request syntax or unsupported method" in str(exc_info.value) diff --git a/tests/test_client.py b/tests/test_client.py index 2204d77..17eaf46 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from datetime import time import logging from unittest.mock import AsyncMock @@ -42,24 +43,22 @@ PODME_API_PATH = URL(PODME_API_URL).path -@pytest.mark.parametrize( - ("username", "password"), - [ - ("testuser@example.com", "qwerty123"), - ], -) -async def test_username(aresponses: ResponsesMockServer, podme_client, username: str, password: str): - # fixture_name = "ipcheck" - # fixture = load_fixture_json(fixture_name) +def test_version(): + from podme_api.__version__ import __version__ + + assert __version__ == "0.0.0" + + +async def test_username(aresponses: ResponsesMockServer, podme_client, default_credentials, user_credentials): aresponses.add( URL(PODME_API_URL).host, f"{PODME_API_PATH}/user", "GET", - Response(body=username), + Response(body=user_credentials.email), ) - async with podme_client(username, password) as client: + async with podme_client(credentials=default_credentials, load_default_user_credentials=True) as client: result = await client.get_username() - assert result == username + assert result == user_credentials.email async def test_get_user_subscription(aresponses: ResponsesMockServer, podme_client): @@ -179,6 +178,30 @@ async def test_get_category(aresponses: ResponsesMockServer, podme_client, categ assert result.id == category_id +async def test_get_category_nonexistent(aresponses: ResponsesMockServer, podme_client): + fixture = load_fixture_json("cms_categories") + aresponses.add( + URL(PODME_API_URL).host, + f"{PODME_API_PATH}/cms/categories", + "GET", + json_response(data=fixture), + ) + aresponses.add( + URL(PODME_API_URL).host, + f"{PODME_API_PATH}/cms/categories?region={PodMeRegion.NO.value}", + "GET", + json_response(data=fixture), + match_querystring=True, + ) + + async with podme_client() as client: + client: PodMeClient + with pytest.raises(PodMeApiError): + await client.get_category(0) + with pytest.raises(PodMeApiError): + await client.get_category("1") + + @pytest.mark.parametrize( ("region_name", "category_key"), [ @@ -324,6 +347,7 @@ async def test_podcast_subscription(aresponses: ResponsesMockServer, podme_clien ("episode_id", "progress"), [ (4125238, "00:00:10"), + (4125238, time(second=10)), (4125238, None), ], ) @@ -513,7 +537,7 @@ async def test_timeout(aresponses: ResponsesMockServer, podme_client): async def response_handler(_: aiohttp.ClientResponse): """Response handler for this test.""" await asyncio.sleep(2) - return aresponses.Response(body="Helluu") + return aresponses.Response(body="Helluu") # pragma: no cover aresponses.add( URL(PODME_API_URL).host, @@ -616,8 +640,8 @@ async def test_session_close(): client.session.close.assert_called_once() -async def test_context_manager(): - async with PodMeDefaultAuthClient() as auth_client: +async def test_context_manager(podme_default_auth_client): + async with podme_default_auth_client() as auth_client: assert isinstance(auth_client, PodMeDefaultAuthClient) async with PodMeClient(auth_client=auth_client) as client: assert isinstance(client, PodMeClient)