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
@@ -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__.:
|
||||||
+133
@@ -1 +1,134 @@
|
|||||||
|
# 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
|
.envrc
|
||||||
@@ -44,20 +44,26 @@ You can try out a live demo at [https://demo-oidc.cl1.parano.ch/](https://demo-o
|
|||||||
cd oidc-validator
|
cd oidc-validator
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
2. Set up virtual environment and install dependencies:
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
export CLIENT_ID=your_oidc_client_id
|
export CLIENT_ID=your_oidc_client_id
|
||||||
export CLIENT_SECRET=your_oidc_client_secret # Required for token refresh
|
export CLIENT_SECRET=your_oidc_client_secret # Required for token refresh
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run the application:
|
5. Run the application:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
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
|
### 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
|
1. Set up the virtual environment:
|
||||||
# Health check
|
```bash
|
||||||
curl http://localhost:8000/health
|
./setup_test_env.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Token validation (requires valid JWT token)
|
2. Run the tests:
|
||||||
curl -X POST http://localhost:8000/validate-token \
|
|
||||||
-H "Content-Type: application/json" \
|
```bash
|
||||||
-d '{"id_token": "your.jwt.token.here"}'
|
./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
|
### Deployment Notes
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+25
@@ -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
|
||||||
Executable
+27
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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!")
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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