Skip to content

Commit

Permalink
Enable test step in got-episodes-api-python for ex-10 and ex-11 (#6)
Browse files Browse the repository at this point in the history
* fix references and prepare for tests

* restructure and fix pyinit

* add controller tests

* Add missing authorization check on endpoints

* Add missing authorization check on endpoints, ex-10

* update with testrun command

* add tests folder for ex-10

* add note on jwt python validation

* add note on jwt python validation

* remove unused import

* Add test for ex-10 python readme

---------

Co-authored-by: Lars Kåre Skjørestad <[email protected]>
  • Loading branch information
steinsiv and larskaare authored Dec 20, 2023
1 parent 659afc6 commit 7d60269
Show file tree
Hide file tree
Showing 25 changed files with 385 additions and 43 deletions.
3 changes: 2 additions & 1 deletion ex-10/doc/swapping_tech_for_episodes_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Steps:
- Execute tests

```shell
TBA
pytest
```
- In `./src/core/config.py` update the API audience to reflect your API (AppSettings)
- Follow the pattern in the code for this; use only the GUID not the whole URI
Expand Down Expand Up @@ -79,4 +79,5 @@ Steps:
## Discuss security issues and good practices

- How "close" does the api (NodeJS/Python) need to be, on a contract level?
- Where is JWT validation happening in the Python code?

6 changes: 6 additions & 0 deletions ex-10/got-episodes-api-python/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
addopts = -p no:cacheprovider
console_output_style = progress
pythonpath = src
testpaths = tests
python_files = test_*.py
2 changes: 1 addition & 1 deletion ex-10/got-episodes-api-python/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The is the Episodes API. For authenticated requests the api will return a list o
## Test

```sh
TBA
▶ pytest
```

## Run
Expand Down
3 changes: 2 additions & 1 deletion ex-10/got-episodes-api-python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pydantic
requests
authlib
pydantic-settings
pytest
pytest
httpx
2 changes: 1 addition & 1 deletion ex-10/got-episodes-api-python/rest.http
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Test episodes API and/or Client

@bearer = ey...
@episodes_url=https://musical-space-barnacle-qv9r64xp5264gg-3100.preview.app.github.dev/api/episodes
@episodes_url=
#@episodes_url=http://localhost:3100/api/episodes

# Episodes getAll
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# controllers.py
from venv import logger
from fastapi import HTTPException
from typing import List
from data.models import Episode
Expand Down
8 changes: 4 additions & 4 deletions ex-10/got-episodes-api-python/src/routes/episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,18 @@ def get_all_episodes(token: str = Depends(get_token_header)):
return episodes

@router.get("/episodes/{episode_id}", response_model=Episode)
def get_episode(episode_id: str):
def get_episode(episode_id: str, token: str = Depends(get_token_header)):
return episodes_controller.get_episode(episode_id)

@router.post("/episodes", response_model=Episode, status_code=status.HTTP_201_CREATED)
def add_episode(episode: Episode):
def add_episode(episode: Episode, token: str = Depends(get_token_header)):
return episodes_controller.add_episode(episode)

@router.put("/episodes/{episode_id}", response_model=Episode)
def update_episode(episode_id: str, episode: Episode = Body(...)):
def update_episode(episode_id: str, episode: Episode = Body(...), token: str = Depends(get_token_header)):
return episodes_controller.update_episode(episode_id, episode)

@router.delete("/episodes/{episode_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_episode(episode_id: str):
def delete_episode(episode_id: str, token: str = Depends(get_token_header)):
episodes_controller.delete_episode(episode_id)
return {"msg": f"Episode with ID {episode_id} has been deleted."}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest
from controller.episodes_controller import get_all_episodes, get_episode, add_episode, update_episode, delete_episode
from data.got_demo_data import episodes
from data.models import Episode

@pytest.fixture
def patchenv(monkeypatch):
test_episodes = [
{"id": "1", "title": 'Winter is coming', "season": 1},
{"id": "2", "title": 'The Kingsroad', "season": 1},
{"id": "3", "title": 'Lord Snow', "season": 1},
{"id": "4", "title": 'Cripples, Bastards and Broken Things', "season": 1},
{"id": "5", "title": 'The Wolf and the Lion', "season": 1},
{"id": "6", "title": 'A Golden Crown', "season": 1},
{"id": "7", "title": 'You Win or You Die', "season": 1},
{"id": "8", "title": 'The Pointy End', "season": 1},
{"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)
yield monkeypatch

def test_get_all_episodes(patchenv):
response = get_all_episodes()
assert response == episodes

def test_get_episode():
episode_id = "1"
response = get_episode(episode_id)
assert response["id"] == episode_id

def test_add_episode(patchenv):
episode_data = Episode(
id = "0",
title = "Test Episode",
season = 1001,
)
response = add_episode(episode_data)
assert response["id"] == "11"

def test_update_episode(patchenv):
episode_id = "10"
episode_data = Episode(
id = episode_id,
title = "Test Episode",
season = 42,
)
response = update_episode(episode_id, episode_data)
assert response["id"] == episode_data.id
assert response["season"] == episode_data.season

def test_delete_episode(patchenv):
episode_id = "1"
delete_episode(episode_id)
with pytest.raises(Exception):
get_episode(episode_id)

7 changes: 0 additions & 7 deletions ex-10/got-episodes-api-python/tests/core/auth_test.py

This file was deleted.

44 changes: 44 additions & 0 deletions ex-10/got-episodes-api-python/tests/core/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
import pytest
import uvicorn
from pydantic import HttpUrl
from core.config import AppSettings, get_settings, get_claims_options, get_uvicorn_config

# monkeypatch the environment
@pytest.fixture
def patchenv(monkeypatch):
# Use monkeypatch to modify the environment variable
monkeypatch.setenv('TENANT_ID', 'test_tenant_id')
monkeypatch.setenv('PORT', '7777')
monkeypatch.setenv('HOST', 'test_host')

yield monkeypatch

def test_valid_app_settings(patchenv):
config = get_settings()
assert isinstance(config, AppSettings)
assert config.tenant_id == 'test_tenant_id'
assert config.port == 7777
assert config.host == 'test_host'
# Generated
assert config.jwks_uri == HttpUrl(f"https://login.microsoftonline.com/{config.tenant_id}/discovery/v2.0/keys")
assert config.api_audience == f"api://f6a763f4-932d-4784-8122-f2b526bb2364"
assert config.issuer == HttpUrl(f"https://sts.windows.net/{config.tenant_id}/")

def test_missing_environment_variables():
with pytest.raises(KeyError):
get_settings()

def test_get_uvicorn_config(patchenv):
patchenv.setenv('PYTHON_ENV', 'development')
uvicorn_config = get_uvicorn_config()
assert isinstance(uvicorn_config,uvicorn.Config)
assert uvicorn_config.log_level == 'debug'

def test_get_claims_options(patchenv):
expected_claims_options = {
"iss": {"essential": True, "value": f"https://sts.windows.net/{os.environ['TENANT_ID']}/",},
"aud": {"essential": True, "value": f"api://f6a763f4-932d-4784-8122-f2b526bb2364"},
}
result_claims_options = get_claims_options()
assert result_claims_options == expected_claims_options
17 changes: 17 additions & 0 deletions ex-10/got-episodes-api-python/tests/data/test_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
from data.models import Episode
from data.got_demo_data import episodes

@pytest.fixture
def sample_episode():
return Episode(id="1", title="Winter is Coming", season=1)

def test_create_valid_episode(sample_episode):
assert sample_episode.id == "1"
assert sample_episode.title == "Winter is Coming"
assert sample_episode.season == 1

def test_valid_episodes_data():
assert len(episodes) == 10, "There should be 10 episodes"
ids = [episode['id'] for episode in episodes]
assert len(ids) == len(set(ids)), "All ids should be unique"
45 changes: 45 additions & 0 deletions ex-10/got-episodes-api-python/tests/routes/test_episodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
from fastapi.testclient import TestClient
from routes.episodes import get_all_episodes
from main import app

client = TestClient(app)

@pytest.fixture
def patchenv(monkeypatch):
# Use monkeypatch to modify the environment variable
monkeypatch.setenv('TENANT_ID', 'test_tenant_id')
monkeypatch.setenv('CLIENT_ID', 'test_client_id')
monkeypatch.setenv('CLIENT_SECRET', 'test_client_secret')
monkeypatch.setenv('EPISODES_API_URI', 'test_episodes_api_uri')
monkeypatch.setenv('QUOTES_API_URL', 'https://test_quotes_api.url')
monkeypatch.setenv('QUOTES_API_URI', 'test_quotes_api_uri')
monkeypatch.setenv('PORT', '7777')
monkeypatch.setenv('HOST', 'test_host')

def mock_get_token_endpoint(well_known_conf_url: str):
return "https://login.microsoftonline.com/test_tenant_id/oauth2/v2.0/token"

def mock_get_all_episodes():
sample_episodes = [
{"id": 1, "title": "Episode 1", "season": 1},
{"id": 2, "title": "Episode 2", "season": 2},
]
return sample_episodes

monkeypatch.setattr("core.auth.get_token_endpoint", mock_get_token_endpoint)
monkeypatch.setattr("controller.episodes_controller.get_all_episodes", mock_get_all_episodes)

yield monkeypatch

def test_missing_authorization_header():
response = client.get("/api/episodes")
assert response.status_code == 403

def test_get_all_episodes(patchenv):
expected = [
{'id': 1, 'title': 'Episode 1', 'season': 1},
{'id': 2, 'title': 'Episode 2', 'season': 2}
]
response = get_all_episodes("mock_access_token")
assert response == expected
6 changes: 6 additions & 0 deletions ex-11/got-episodes-api-python/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
addopts = -p no:cacheprovider
console_output_style = progress
pythonpath = src
testpaths = tests
python_files = test_*.py
7 changes: 3 additions & 4 deletions ex-11/got-episodes-api-python/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The is the Episodes API. For authenticated requests the api will return a list o
## Test

```sh
TBA
▶ pytest
```

## Run
Expand All @@ -38,8 +38,7 @@ Expects the following environment variables to execute properly
source "$CFG_ENV_FILE_DIRECTORY/<your_episodes_env_file>"
```

### Run with `main.py`
### Run with `main.py` in folder `./src`
```
▶ cd src
▶ ./main.py
▶ src/main.py
```
3 changes: 2 additions & 1 deletion ex-11/got-episodes-api-python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pydantic
requests
authlib
pydantic-settings
pytest
pytest
httpx
2 changes: 1 addition & 1 deletion ex-11/got-episodes-api-python/rest.http
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Test episodes API and/or Client

@bearer = ey...
@episodes_url=https://musical-space-barnacle-qv9r64xp5264gg-3100.preview.app.github.dev/api/episodes
@episodes_url=
#@episodes_url=http://localhost:3100/api/episodes

# Episodes getAll
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# controllers.py
from venv import logger
import requests
from fastapi import HTTPException
from typing import List
Expand Down
8 changes: 4 additions & 4 deletions ex-11/got-episodes-api-python/src/routes/episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ def get_all_episodes(token: str = Depends(get_token_header)):
return episodes_and_quote

@router.get("/episodes/{episode_id}", response_model=Episode)
def get_episode(episode_id: str):
def get_episode(episode_id: str, token: str = Depends(get_token_header)):
return episodes_controller.get_episode(episode_id)

@router.post("/episodes", response_model=Episode, status_code=status.HTTP_201_CREATED)
def add_episode(episode: Episode):
def add_episode(episode: Episode, token: str = Depends(get_token_header)):
return episodes_controller.add_episode(episode)

@router.put("/episodes/{episode_id}", response_model=Episode)
def update_episode(episode_id: str, episode: Episode = Body(...)):
def update_episode(episode_id: str, episode: Episode = Body(...), token: str = Depends(get_token_header)):
return episodes_controller.update_episode(episode_id, episode)

@router.delete("/episodes/{episode_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_episode(episode_id: str):
def delete_episode(episode_id: str, token: str = Depends(get_token_header)):
episodes_controller.delete_episode(episode_id)
return {"msg": f"Episode with ID {episode_id} has been deleted."}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest
from controller.episodes_controller import get_all_episodes, get_episode, add_episode, update_episode, delete_episode
from data.got_demo_data import episodes
from data.models import Episode

@pytest.fixture
def patchenv(monkeypatch):
test_episodes = [
{"id": "1", "title": 'Winter is coming', "season": 1},
{"id": "2", "title": 'The Kingsroad', "season": 1},
{"id": "3", "title": 'Lord Snow', "season": 1},
{"id": "4", "title": 'Cripples, Bastards and Broken Things', "season": 1},
{"id": "5", "title": 'The Wolf and the Lion', "season": 1},
{"id": "6", "title": 'A Golden Crown', "season": 1},
{"id": "7", "title": 'You Win or You Die', "season": 1},
{"id": "8", "title": 'The Pointy End', "season": 1},
{"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)
yield monkeypatch

def test_get_all_episodes(patchenv):
response = get_all_episodes()
assert response == episodes

def test_get_episode():
episode_id = "1"
response = get_episode(episode_id)
assert response["id"] == episode_id

def test_add_episode(patchenv):
episode_data = Episode(
id = "0",
title = "Test Episode",
season = 1001,
)
response = add_episode(episode_data)
assert response["id"] == "11"

def test_update_episode(patchenv):
episode_id = "10"
episode_data = Episode(
id = episode_id,
title = "Test Episode",
season = 42,
)
response = update_episode(episode_id, episode_data)
assert response["id"] == episode_data.id
assert response["season"] == episode_data.season

def test_delete_episode(patchenv):
episode_id = "1"
delete_episode(episode_id)
with pytest.raises(Exception):
get_episode(episode_id)

Loading

0 comments on commit 7d60269

Please sign in to comment.