docs: update README and add test infrastructure
- Enhance .gitignore with comprehensive Python patterns - Improve README with better setup and testing instructions - Add test infrastructure: * Test requirements file * Environment setup script * Test runner script * Comprehensive test suite * Coverage configuration
This commit is contained in:
14
.coveragerc
Normal file
14
.coveragerc
Normal file
@@ -0,0 +1,14 @@
|
||||
[run]
|
||||
source = .
|
||||
omit =
|
||||
*/venv/*
|
||||
*/tests/*
|
||||
*/.venv/*
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if __name__ == .__main__.:
|
||||
135
.gitignore
vendored
135
.gitignore
vendored
@@ -1 +1,134 @@
|
||||
.envrc
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Additional files from existing .gitignore
|
||||
.envrc
|
||||
45
README.md
45
README.md
@@ -44,20 +44,26 @@ You can try out a live demo at [https://demo-oidc.cl1.parano.ch/](https://demo-o
|
||||
cd oidc-validator
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
2. Set up virtual environment and install dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
./setup_test_env.sh
|
||||
```
|
||||
|
||||
3. Set environment variables:
|
||||
3. Activate the virtual environment:
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
4. Set environment variables:
|
||||
|
||||
```bash
|
||||
export CLIENT_ID=your_oidc_client_id
|
||||
export CLIENT_SECRET=your_oidc_client_secret # Required for token refresh
|
||||
```
|
||||
|
||||
4. Run the application:
|
||||
5. Run the application:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
@@ -118,17 +124,30 @@ The application can be configured using the following environment variables:
|
||||
|
||||
### Running Tests
|
||||
|
||||
Currently, there are no automated tests. You can manually test the endpoints using curl:
|
||||
This project includes a comprehensive test suite. To run the tests:
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8000/health
|
||||
1. Set up the virtual environment:
|
||||
```bash
|
||||
./setup_test_env.sh
|
||||
```
|
||||
|
||||
# Token validation (requires valid JWT token)
|
||||
curl -X POST http://localhost:8000/validate-token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id_token": "your.jwt.token.here"}'
|
||||
```
|
||||
2. Run the tests:
|
||||
|
||||
```bash
|
||||
./run_tests.sh
|
||||
```
|
||||
|
||||
This will run all tests with coverage reporting and generate an HTML coverage report in the `htmlcov/` directory.
|
||||
|
||||
The test suite covers:
|
||||
|
||||
- API endpoint testing
|
||||
- Token validation logic
|
||||
- Error handling
|
||||
- Utility functions
|
||||
- Edge cases
|
||||
|
||||
For more detailed information about testing, see [tests/README.md](tests/README.md).
|
||||
|
||||
### Deployment Notes
|
||||
|
||||
|
||||
5
requirements-test.txt
Normal file
5
requirements-test.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
pytest>=6.2.0
|
||||
pytest-cov>=2.12.0
|
||||
coverage>=5.5
|
||||
httpx>=0.18.0
|
||||
asgi_lifespan>=1.0.1
|
||||
25
run_tests.sh
Executable file
25
run_tests.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# run_tests.sh
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Virtual environment not found. Setting up..."
|
||||
./setup_test_env.sh
|
||||
fi
|
||||
|
||||
# Activate the virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Run tests with coverage
|
||||
echo "Running tests with coverage..."
|
||||
coverage erase # Clear any previous coverage data
|
||||
python -m pytest tests/ --cov=. --cov-config=.coveragerc -v
|
||||
|
||||
# Generate HTML coverage report
|
||||
echo "Generating HTML coverage report..."
|
||||
coverage html
|
||||
|
||||
echo "Coverage report generated in htmlcov/ directory"
|
||||
|
||||
# Deactivate virtual environment
|
||||
deactivate
|
||||
27
setup_test_env.sh
Executable file
27
setup_test_env.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# setup_test_env.sh
|
||||
|
||||
# Remove existing virtual environment if it exists
|
||||
if [ -d "venv" ]; then
|
||||
echo "Removing existing virtual environment..."
|
||||
rm -rf venv
|
||||
fi
|
||||
|
||||
# Create a virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate the virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Upgrade pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# Install application dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Install test dependencies
|
||||
pip install -r requirements-test.txt
|
||||
|
||||
echo "Virtual environment setup complete!"
|
||||
echo "To activate the virtual environment, run:"
|
||||
echo "source venv/bin/activate"
|
||||
92
tests/README.md
Normal file
92
tests/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Testing
|
||||
|
||||
This directory contains tests for the OIDC validator application.
|
||||
|
||||
## Setup
|
||||
|
||||
### Using the setup script (recommended)
|
||||
|
||||
1. Run the setup script:
|
||||
```bash
|
||||
./setup_test_env.sh
|
||||
```
|
||||
|
||||
This will create a virtual environment and install all dependencies.
|
||||
|
||||
### Manual setup
|
||||
|
||||
1. Create and activate a virtual environment:
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
2. Install application dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Install test dependencies:
|
||||
```bash
|
||||
pip install -r requirements-test.txt
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Using the test runner script (recommended)
|
||||
|
||||
```bash
|
||||
./run_tests.sh
|
||||
```
|
||||
|
||||
This will automatically activate the virtual environment and run the tests.
|
||||
|
||||
### Manual execution
|
||||
|
||||
1. Activate the virtual environment:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
2. Run tests:
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run tests with coverage (excluding tests directory)
|
||||
pytest --cov=. --cov-config=.coveragerc
|
||||
|
||||
# Run tests with verbose output
|
||||
pytest -v
|
||||
```
|
||||
|
||||
3. Deactivate the virtual environment when done:
|
||||
```bash
|
||||
deactivate
|
||||
```
|
||||
|
||||
## Coverage Reports
|
||||
|
||||
The test suite generates coverage reports to help you understand how well the code is tested:
|
||||
|
||||
- Terminal coverage report is displayed after running tests
|
||||
- HTML coverage report is generated in the `htmlcov/` directory
|
||||
- The coverage configuration excludes the tests directory itself from coverage statistics
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `conftest.py`: Contains pytest fixtures shared across tests
|
||||
- `test_main.py`: Tests for the main FastAPI application endpoints
|
||||
- `test_token_validation.py`: Tests for token validation and refresh logic
|
||||
- `test_utils.py`: Tests for utility functions
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The tests cover:
|
||||
- Health check endpoint
|
||||
- Token validation endpoint
|
||||
- Token refresh endpoint
|
||||
- Client configuration endpoint
|
||||
- Utility functions for OIDC operations
|
||||
- Error handling for various failure scenarios
|
||||
- Edge cases and invalid inputs
|
||||
48
tests/conftest.py
Normal file
48
tests/conftest.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_client():
|
||||
"""Create a test client for the FastAPI app."""
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
def mock_valid_token_payload():
|
||||
"""Mock payload for a valid JWT token."""
|
||||
return {
|
||||
"iss": "https://login.infomaniak.com",
|
||||
"aud": "test-client-id",
|
||||
"sub": "1234567890",
|
||||
"email": "rene.luria@infomaniak.com",
|
||||
"exp": 9999999999,
|
||||
"iat": 1609459200
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_invalid_token_payload():
|
||||
"""Mock payload for an invalid JWT token."""
|
||||
return {
|
||||
"iss": "https://invalid.issuer.com",
|
||||
"aud": "wrong-client-id",
|
||||
"sub": "1234567890",
|
||||
"email": "unauthorized@example.com",
|
||||
"exp": 9999999999,
|
||||
"iat": 1609459200
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_jwks():
|
||||
"""Mock JWKS for token validation."""
|
||||
return {
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"kid": "test-key-id",
|
||||
"use": "sig",
|
||||
"n": "test-modulus",
|
||||
"e": "AQAB"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tests/test_example.py
Normal file
12
tests/test_example.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test to verify the test environment is set up correctly.
|
||||
"""
|
||||
|
||||
def test_example():
|
||||
"""A simple test to verify the test framework works."""
|
||||
assert True
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_example()
|
||||
print("Test environment verification passed!")
|
||||
153
tests/test_main.py
Normal file
153
tests/test_main.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
|
||||
# Create a test client
|
||||
client = TestClient(app)
|
||||
|
||||
def test_health_check():
|
||||
"""Test the health check endpoint."""
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert "status" in response.json()
|
||||
assert response.json()["status"] == "healthy"
|
||||
|
||||
def test_favicon():
|
||||
"""Test the favicon endpoint."""
|
||||
response = client.get("/favicon.ico")
|
||||
# The favicon endpoint serves the actual favicon file, so we expect a 200
|
||||
# If the file doesn't exist, it would return a 404, but in our case it should exist
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_client_config():
|
||||
"""Test the client configuration endpoint."""
|
||||
response = client.get("/config")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "client_id" in data
|
||||
assert "issuer" in data
|
||||
# Just check that we get a client_id, not necessarily the test value
|
||||
assert isinstance(data["client_id"], str)
|
||||
assert len(data["client_id"]) > 0
|
||||
|
||||
@patch('main.verify_token')
|
||||
def test_validate_token_success(mock_verify_token):
|
||||
"""Test successful token validation."""
|
||||
# Mock the verify_token function to return a valid payload
|
||||
mock_verify_token.return_value = {
|
||||
"email": "rene.luria@infomaniak.com",
|
||||
"iss": "https://login.infomaniak.com",
|
||||
"aud": "test-client-id"
|
||||
}
|
||||
|
||||
response = client.post("/validate-token", json={
|
||||
"id_token": "valid.token.here"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["valid"]
|
||||
assert data["user"] is not None
|
||||
assert data["user"]["email"] == "rene.luria@infomaniak.com"
|
||||
assert data["secret_phrase"] == "Welcome to the secret club!"
|
||||
|
||||
@patch('main.verify_token')
|
||||
def test_validate_token_unauthorized_user(mock_verify_token):
|
||||
"""Test token validation for unauthorized user."""
|
||||
# Mock the verify_token function to return a payload with unauthorized user
|
||||
mock_verify_token.return_value = {
|
||||
"email": "unauthorized@example.com",
|
||||
"iss": "https://login.infomaniak.com",
|
||||
"aud": "test-client-id"
|
||||
}
|
||||
|
||||
response = client.post("/validate-token", json={
|
||||
"id_token": "valid.token.here"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["valid"]
|
||||
assert data["user"] is not None
|
||||
assert data["user"]["email"] == "unauthorized@example.com"
|
||||
assert data["secret_phrase"] is None
|
||||
|
||||
@patch('main.verify_token')
|
||||
def test_validate_token_missing_email(mock_verify_token):
|
||||
"""Test token validation when email is missing from token."""
|
||||
# Mock the verify_token function to return a payload without email
|
||||
mock_verify_token.return_value = {
|
||||
"iss": "https://login.infomaniak.com",
|
||||
"aud": "test-client-id"
|
||||
}
|
||||
|
||||
response = client.post("/validate-token", json={
|
||||
"id_token": "token.without.email"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert not data["valid"]
|
||||
assert data["error"] == "Email not found in token"
|
||||
|
||||
@patch('main.verify_token')
|
||||
def test_validate_token_invalid(mock_verify_token):
|
||||
"""Test validation with an invalid token."""
|
||||
# Mock the verify_token function to raise an exception
|
||||
mock_verify_token.side_effect = ValueError("Invalid token")
|
||||
|
||||
response = client.post("/validate-token", json={
|
||||
"id_token": "invalid.token.here"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert not data["valid"]
|
||||
assert data["error"] == "Invalid token"
|
||||
|
||||
def test_validate_token_empty_request():
|
||||
"""Test validation with empty token."""
|
||||
response = client.post("/validate-token", json={
|
||||
"id_token": ""
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert not data["valid"]
|
||||
|
||||
@patch('main.refresh_token')
|
||||
def test_refresh_token_success(mock_refresh_token):
|
||||
"""Test successful token refresh."""
|
||||
# Mock the refresh_token function
|
||||
mock_refresh_token.return_value = {
|
||||
"access_token": "new-access-token",
|
||||
"id_token": "new-id-token",
|
||||
"expires_in": 3600,
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
|
||||
response = client.post("/refresh-token", json={
|
||||
"refresh_token": "valid-refresh-token"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["access_token"] == "new-access-token"
|
||||
assert data["id_token"] == "new-id-token"
|
||||
assert data["expires_in"] == 3600
|
||||
assert data["token_type"] == "Bearer"
|
||||
|
||||
@patch('main.refresh_token')
|
||||
def test_refresh_token_failure(mock_refresh_token):
|
||||
"""Test token refresh failure."""
|
||||
# Mock the refresh_token function to raise an exception
|
||||
mock_refresh_token.side_effect = ValueError("Invalid refresh token")
|
||||
|
||||
response = client.post("/refresh-token", json={
|
||||
"refresh_token": "invalid-refresh-token"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["error"] == "Invalid refresh token"
|
||||
209
tests/test_token_validation.py
Normal file
209
tests/test_token_validation.py
Normal file
@@ -0,0 +1,209 @@
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import patch, MagicMock
|
||||
import jwt
|
||||
from main import (
|
||||
verify_token,
|
||||
refresh_token,
|
||||
get_well_known_config,
|
||||
get_jwks,
|
||||
get_user_info_from_endpoint,
|
||||
get_userinfo_endpoint,
|
||||
get_token_endpoint
|
||||
)
|
||||
|
||||
def test_verify_token_invalid_input():
|
||||
"""Test verify_token with invalid inputs."""
|
||||
# Test with None
|
||||
with pytest.raises(ValueError, match="Invalid token provided"):
|
||||
verify_token(None)
|
||||
|
||||
# Test with empty string
|
||||
with pytest.raises(ValueError, match="Invalid token provided"):
|
||||
verify_token("")
|
||||
|
||||
# Test with non-string
|
||||
with pytest.raises(ValueError, match="Invalid token provided"):
|
||||
verify_token(123)
|
||||
|
||||
@patch('main.get_jwks')
|
||||
@patch('main.jwt.get_unverified_header')
|
||||
def test_verify_token_missing_kid(mock_get_header, mock_get_jwks):
|
||||
"""Test verify_token when token header is missing 'kid'."""
|
||||
# Mock the token header to not include 'kid'
|
||||
mock_get_header.return_value = {}
|
||||
|
||||
with pytest.raises(ValueError, match="Token header missing 'kid' field"):
|
||||
verify_token("some.token.string")
|
||||
|
||||
@patch('main.get_jwks')
|
||||
@patch('main.jwt.get_unverified_header')
|
||||
def test_verify_token_no_matching_key(mock_get_header, mock_get_jwks):
|
||||
"""Test verify_token when no matching key is found in JWKS."""
|
||||
# Mock the token header to include 'kid'
|
||||
mock_get_header.return_value = {"kid": "nonexistent-key"}
|
||||
|
||||
# Mock JWKS with a different key ID
|
||||
mock_get_jwks.return_value = {
|
||||
"keys": [
|
||||
{
|
||||
"kid": "different-key",
|
||||
"kty": "RSA",
|
||||
"n": "modulus",
|
||||
"e": "AQAB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Unable to find matching key in JWKS"):
|
||||
verify_token("some.token.string")
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_well_known_config_success(mock_get):
|
||||
"""Test successful retrieval of well-known configuration."""
|
||||
# Mock successful response
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"issuer": "https://login.infomaniak.com",
|
||||
"jwks_uri": "https://login.infomaniak.com/.well-known/jwks.json"
|
||||
}
|
||||
mock_response.text = '{"issuer": "https://login.infomaniak.com"}'
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
config = get_well_known_config()
|
||||
assert "issuer" in config
|
||||
assert config["issuer"] == "https://login.infomaniak.com"
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_well_known_config_failure(mock_get):
|
||||
"""Test failed retrieval of well-known configuration."""
|
||||
# Clear cache first
|
||||
get_well_known_config.cache_clear()
|
||||
|
||||
# Mock failed response with requests exception
|
||||
mock_get.side_effect = requests.exceptions.RequestException("Network error")
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to fetch well-known configuration"):
|
||||
get_well_known_config()
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_jwks_success(mock_get):
|
||||
"""Test successful retrieval of JWKS."""
|
||||
# Mock successful response for well-known config
|
||||
with patch('main.get_well_known_config') as mock_config:
|
||||
mock_config.return_value = {
|
||||
"jwks_uri": "https://login.infomaniak.com/.well-known/jwks.json"
|
||||
}
|
||||
|
||||
# Mock successful JWKS response
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"keys": [{"kid": "test-key"}]
|
||||
}
|
||||
mock_response.text = '{"keys": [{"kid": "test-key"}]}'
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
jwks = get_jwks()
|
||||
assert "keys" in jwks
|
||||
assert len(jwks["keys"]) > 0
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_jwks_failure(mock_get):
|
||||
"""Test failed retrieval of JWKS."""
|
||||
# Clear cache first
|
||||
get_jwks.cache_clear()
|
||||
|
||||
# Mock successful response for well-known config
|
||||
with patch('main.get_well_known_config') as mock_config:
|
||||
mock_config.return_value = {
|
||||
"jwks_uri": "https://login.infomaniak.com/.well-known/jwks.json"
|
||||
}
|
||||
|
||||
# Mock failed response with requests exception
|
||||
mock_get.side_effect = requests.exceptions.RequestException("Network error")
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to fetch JWKS"):
|
||||
get_jwks()
|
||||
|
||||
@patch('main.requests.post')
|
||||
def test_refresh_token_success(mock_post):
|
||||
"""Test successful token refresh."""
|
||||
# Mock environment variables
|
||||
with patch.dict('main.os.environ', {'CLIENT_ID': 'test-client', 'CLIENT_SECRET': 'test-secret'}):
|
||||
# Mock successful response
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {
|
||||
"access_token": "new-access-token",
|
||||
"id_token": "new-id-token",
|
||||
"expires_in": 3600,
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with patch('main.get_token_endpoint') as mock_endpoint:
|
||||
mock_endpoint.return_value = "https://login.infomaniak.com/token"
|
||||
|
||||
result = refresh_token("valid-refresh-token")
|
||||
assert "access_token" in result
|
||||
assert result["access_token"] == "new-access-token"
|
||||
|
||||
@patch('main.requests.post')
|
||||
def test_refresh_token_failure(mock_post):
|
||||
"""Test failed token refresh."""
|
||||
# Mock environment variables
|
||||
with patch.dict('main.os.environ', {'CLIENT_ID': 'test-client', 'CLIENT_SECRET': 'test-secret'}):
|
||||
# Mock failed response
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = False
|
||||
mock_response.status_code = 400
|
||||
mock_response.text = "Bad Request"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with patch('main.get_token_endpoint') as mock_endpoint:
|
||||
mock_endpoint.return_value = "https://login.infomaniak.com/token"
|
||||
|
||||
with pytest.raises(ValueError, match="Token refresh failed"):
|
||||
refresh_token("invalid-refresh-token")
|
||||
|
||||
def test_refresh_token_invalid_input():
|
||||
"""Test refresh_token with invalid inputs."""
|
||||
# Test with None
|
||||
with pytest.raises(ValueError, match="Invalid refresh token provided"):
|
||||
refresh_token(None)
|
||||
|
||||
# Test with empty string
|
||||
with pytest.raises(ValueError, match="Invalid refresh token provided"):
|
||||
refresh_token("")
|
||||
|
||||
# Test with non-string
|
||||
with pytest.raises(ValueError, match="Invalid refresh token provided"):
|
||||
refresh_token(123)
|
||||
|
||||
@patch('main.get_token_endpoint')
|
||||
@patch('main.requests.post')
|
||||
def test_refresh_token_missing_secret(mock_post, mock_get_token_endpoint):
|
||||
"""Test refresh_token when client secret is not configured."""
|
||||
# Mock the token endpoint to avoid that error
|
||||
mock_get_token_endpoint.return_value = "https://login.infomaniak.com/token"
|
||||
|
||||
# Mock the response to avoid going further in the function
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
import main
|
||||
original_client_secret = main.CLIENT_SECRET
|
||||
|
||||
# Temporarily set CLIENT_SECRET to empty string
|
||||
main.CLIENT_SECRET = ""
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError, match="Client secret not configured"):
|
||||
refresh_token("valid-refresh-token")
|
||||
finally:
|
||||
# Restore original value
|
||||
main.CLIENT_SECRET = original_client_secret
|
||||
89
tests/test_utils.py
Normal file
89
tests/test_utils.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import patch, MagicMock
|
||||
from main import (
|
||||
get_user_info_from_endpoint,
|
||||
get_userinfo_endpoint,
|
||||
get_token_endpoint
|
||||
)
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_user_info_from_endpoint_success(mock_get):
|
||||
"""Test successful retrieval of user info from endpoint."""
|
||||
# Mock successful response
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"email": "test@example.com",
|
||||
"given_name": "Test",
|
||||
"family_name": "User"
|
||||
}
|
||||
mock_response.text = '{"email": "test@example.com"}'
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with patch('main.get_userinfo_endpoint') as mock_endpoint:
|
||||
mock_endpoint.return_value = "https://login.infomaniak.com/userinfo"
|
||||
|
||||
user_info = get_user_info_from_endpoint("valid-access-token")
|
||||
assert "email" in user_info
|
||||
assert user_info["email"] == "test@example.com"
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_user_info_from_endpoint_failure(mock_get):
|
||||
"""Test failed retrieval of user info from endpoint."""
|
||||
# Mock failed response with requests exception
|
||||
mock_get.side_effect = requests.exceptions.RequestException("Network error")
|
||||
|
||||
with patch('main.get_userinfo_endpoint') as mock_endpoint:
|
||||
mock_endpoint.return_value = "https://login.infomaniak.com/userinfo"
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to fetch user info"):
|
||||
get_user_info_from_endpoint("invalid-access-token")
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_userinfo_endpoint_success(mock_get):
|
||||
"""Test successful retrieval of userinfo endpoint."""
|
||||
# Mock successful response for well-known config
|
||||
with patch('main.get_well_known_config') as mock_config:
|
||||
mock_config.return_value = {
|
||||
"userinfo_endpoint": "https://login.infomaniak.com/userinfo"
|
||||
}
|
||||
|
||||
endpoint = get_userinfo_endpoint()
|
||||
assert endpoint == "https://login.infomaniak.com/userinfo"
|
||||
|
||||
def test_get_userinfo_endpoint_failure():
|
||||
"""Test failed retrieval of userinfo endpoint."""
|
||||
# Clear cache first
|
||||
get_userinfo_endpoint.cache_clear()
|
||||
|
||||
# Mock well-known config without userinfo endpoint
|
||||
with patch('main.get_well_known_config') as mock_config:
|
||||
mock_config.return_value = {}
|
||||
|
||||
with pytest.raises(ValueError, match="Userinfo endpoint not found in well-known configuration"):
|
||||
get_userinfo_endpoint()
|
||||
|
||||
@patch('main.requests.get')
|
||||
def test_get_token_endpoint_success(mock_get):
|
||||
"""Test successful retrieval of token endpoint."""
|
||||
# Mock successful response for well-known config
|
||||
with patch('main.get_well_known_config') as mock_config:
|
||||
mock_config.return_value = {
|
||||
"token_endpoint": "https://login.infomaniak.com/token"
|
||||
}
|
||||
|
||||
endpoint = get_token_endpoint()
|
||||
assert endpoint == "https://login.infomaniak.com/token"
|
||||
|
||||
def test_get_token_endpoint_failure():
|
||||
"""Test failed retrieval of token endpoint."""
|
||||
# Clear cache first
|
||||
get_token_endpoint.cache_clear()
|
||||
|
||||
# Mock well-known config without token endpoint
|
||||
with patch('main.get_well_known_config') as mock_config:
|
||||
mock_config.return_value = {}
|
||||
|
||||
with pytest.raises(ValueError, match="Token endpoint not found in well-known configuration"):
|
||||
get_token_endpoint()
|
||||
Reference in New Issue
Block a user