From 70dbc8beb36ad4bbdd9ea7d16d2ba44d02937539 Mon Sep 17 00:00:00 2001 From: Stein A Sivertsen Date: Tue, 2 Jan 2024 12:20:42 +0100 Subject: [PATCH] Add test for error handling in episodes api (#8) * add test for quote error handling in episodes api * add missing environment variables * pass on all signingkeys to validator * remove redundant logging --- .../src/controller/episodes_controller.py | 13 +++++++----- .../controller/test_episodes_controller.py | 20 +++++++++++++++++-- .../tests/core/test_core.py | 5 +++-- .../src/utils/TokenValidator.cs | 17 ++++++---------- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/ex-11/got-episodes-api-python/src/controller/episodes_controller.py b/ex-11/got-episodes-api-python/src/controller/episodes_controller.py index a6374ea..25046d6 100644 --- a/ex-11/got-episodes-api-python/src/controller/episodes_controller.py +++ b/ex-11/got-episodes-api-python/src/controller/episodes_controller.py @@ -1,5 +1,5 @@ -import requests from fastapi import HTTPException +from requests import get from typing import List from data.models import Episode from data.got_demo_data import episodes @@ -15,10 +15,13 @@ def get_random_quote(obo_token: str): config = get_settings() quote_endpoint = f"{ config.quotes_api_url }api/quote" quote_headers = {"Authorization": f"Bearer {obo_token}"} - logger.warning(f"{quote_endpoint = }") - quote = requests.get(url= quote_endpoint, headers = quote_headers) - logger.info(f"Got a quote: {quote} {repr(quote)}") - return quote.json() + logger.info(f"{quote_endpoint = }") + try: + quote = get(url= quote_endpoint, headers = quote_headers).json() + logger.info(f"Got a quote: {quote} {repr(quote)}") + except Exception: + quote = {"title": "Quote error"} + return quote def get_episode(episode_id: str) -> Episode: episode = next((ep for ep in episodes if ep['id'] == episode_id), None) diff --git a/ex-11/got-episodes-api-python/tests/controller/test_episodes_controller.py b/ex-11/got-episodes-api-python/tests/controller/test_episodes_controller.py index b4e0fd0..d8dd8e4 100644 --- a/ex-11/got-episodes-api-python/tests/controller/test_episodes_controller.py +++ b/ex-11/got-episodes-api-python/tests/controller/test_episodes_controller.py @@ -1,10 +1,17 @@ import pytest -from controller.episodes_controller import get_all_episodes, get_episode, add_episode, update_episode, delete_episode +from unittest.mock import Mock +from controller.episodes_controller import get_all_episodes, get_episode, add_episode, update_episode, delete_episode, get_random_quote from data.got_demo_data import episodes from data.models import Episode @pytest.fixture def patchenv(monkeypatch): + monkeypatch.setenv('QUOTES_API_URL', 'https://test_quotes_api.url') + monkeypatch.setenv('TENANT_ID', '123') + monkeypatch.setenv('CLIENT_ID', '123') + monkeypatch.setenv('CLIENT_SECRET', '123') + monkeypatch.setenv('EPISODES_API_URI', 'api://123') + monkeypatch.setenv('QUOTES_API_URI', 'api://123') test_episodes = [ {"id": "1", "title": 'Winter is coming', "season": 1}, {"id": "2", "title": 'The Kingsroad', "season": 1}, @@ -17,8 +24,14 @@ def patchenv(monkeypatch): {"id": "9", "title": 'Bealor', "season": 1}, {"id": "10", "title": 'Fire and Blood', "season": 1}, ] - monkeypatch.setattr("data.got_demo_data.episodes", test_episodes) monkeypatch.setattr("controller.episodes_controller.episodes", test_episodes) + + def mock_requests_get(*args, **kwargs): + mock_response = Mock() + mock_response.json.side_effect = Exception("Triggered exception") + return mock_response + monkeypatch.setattr("controller.episodes_controller.get", mock_requests_get) + yield monkeypatch def test_get_all_episodes(patchenv): @@ -56,3 +69,6 @@ def test_delete_episode(patchenv): with pytest.raises(Exception): get_episode(episode_id) +def test_get_random_quote(patchenv): + quote = get_random_quote("test_obo_token") + assert quote == {"title": "Quote error"} \ No newline at end of file diff --git a/ex-11/got-episodes-api-python/tests/core/test_core.py b/ex-11/got-episodes-api-python/tests/core/test_core.py index f251b0a..35c836e 100644 --- a/ex-11/got-episodes-api-python/tests/core/test_core.py +++ b/ex-11/got-episodes-api-python/tests/core/test_core.py @@ -16,7 +16,7 @@ def patchenv(monkeypatch): monkeypatch.setenv('QUOTES_API_URI', 'test_quotes_api_uri') monkeypatch.setenv('PORT', '7777') monkeypatch.setenv('HOST', 'test_host') - + yield monkeypatch def test_valid_app_settings(patchenv): @@ -35,7 +35,8 @@ def test_valid_app_settings(patchenv): assert config.api_audience == f"api://{config.episodes_api_uri}" assert config.issuer == HttpUrl(f"https://sts.windows.net/{config.tenant_id}/") -def test_missing_environment_variables(): +def test_missing_environment_variables(patchenv): + patchenv.delenv('TENANT_ID') with pytest.raises(KeyError): get_settings() diff --git a/ex-11/got-quote-api-dotnet/src/utils/TokenValidator.cs b/ex-11/got-quote-api-dotnet/src/utils/TokenValidator.cs index f1bc2f7..edd0fca 100644 --- a/ex-11/got-quote-api-dotnet/src/utils/TokenValidator.cs +++ b/ex-11/got-quote-api-dotnet/src/utils/TokenValidator.cs @@ -43,25 +43,21 @@ public bool IsValidToken(string token, TokenValidationParameters validationParam { // Check issuer if (jwtToken.Issuer != validationParameters.ValidIssuer) { - _logger.LogError("Issuer is invalid"); throw new SecurityTokenInvalidIssuerException("Issuer is invalid"); }; // Check audience if (jwtToken.Audiences.All(a => a != validationParameters.ValidAudience)) { - _logger.LogError("Audience is invalid"); throw new SecurityTokenInvalidAudienceException("Audience is invalid"); }; // Check signature and validate - var signingKey = validationParameters.IssuerSigningKeys.FirstOrDefault(); - if (signingKey == null) { - _logger.LogError("Signing key is invalid"); - throw new SecurityTokenInvalidSigningKeyException("Signing key is invalid"); + if (!validationParameters.IssuerSigningKeys.Any()) { + throw new SecurityTokenInvalidSigningKeyException("No signing keys!"); }; var validationParametersWithSigningKey = new TokenValidationParameters { - IssuerSigningKey = signingKey, + IssuerSigningKeys = validationParameters.IssuerSigningKeys, ValidateIssuerSigningKey = true, ValidateAudience = false, ValidateIssuer = false, @@ -69,23 +65,22 @@ public bool IsValidToken(string token, TokenValidationParameters validationParam }; _ = tokenHandler.ValidateToken(token, validationParametersWithSigningKey, out _); - // Check valid timeframe + // Check valid timeframes if (jwtToken.ValidFrom > DateTime.UtcNow || jwtToken.ValidTo < DateTime.UtcNow) { - _logger.LogError("Token is not valid in timeframe"); throw new SecurityTokenInvalidLifetimeException("Token is not valid in timeframe"); }; // Check scope if (jwtToken.Claims.All(c => c.Type != "scp" || !c.Value.Split(' ').Any(s => s == "Quote.Read"))) { - _logger.LogError("Token does not contain correct scope"); throw new SecurityTokenInvalidLifetimeException("Token does not contain correct scope"); }; // All checks passed return true; } - catch + catch (Exception e) { + _logger.LogError("Invalid token: {e.Message}", e.Message); return false; } }