208 lines
7.0 KiB
Python
208 lines
7.0 KiB
Python
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
|
|
|
|
def test_root_serves_csp_with_nonce():
|
|
"""Test that GET / returns a CSP header with a nonce and the inline script carries it."""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
csp = response.headers.get("content-security-policy", "")
|
|
assert "nonce-" in csp
|
|
assert "script-src" in csp
|
|
assert "cdn.jsdelivr.net" in csp
|
|
assert "connect-src" in csp
|
|
assert "login.infomaniak.com" in csp
|
|
assert "api.coingecko.com" in csp
|
|
assert "object-src 'none'" in csp
|
|
body = response.text
|
|
assert "<script nonce=" in body
|
|
import re
|
|
match = re.search(r"nonce-([A-Za-z0-9_-]+)", csp)
|
|
assert match is not None
|
|
nonce = match.group(1)
|
|
assert f'nonce="{nonce}"' in body
|
|
|
|
@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"
|
|
|
|
@patch('main.verify_token')
|
|
def test_validate_token_passes_nonce_to_verify(mock_verify_token):
|
|
"""The endpoint must forward the supplied nonce to verify_token (anti-replay binding)."""
|
|
mock_verify_token.return_value = {
|
|
"email": "rene.luria@infomaniak.com",
|
|
"iss": "https://login.infomaniak.com",
|
|
"aud": "test-client-id",
|
|
"nonce": "abc123",
|
|
}
|
|
|
|
response = client.post("/validate-token", json={
|
|
"id_token": "valid.token.here",
|
|
"nonce": "abc123"
|
|
})
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["valid"]
|
|
# verify_token must have been called with the nonce as the second positional arg
|
|
assert mock_verify_token.call_args.args[1] == "abc123"
|
|
|
|
@patch('main.verify_token')
|
|
def test_validate_token_rejects_nonce_mismatch(mock_verify_token):
|
|
"""A mismatched nonce (forced-auth / replay token) is rejected by the backend."""
|
|
mock_verify_token.side_effect = ValueError("Nonce mismatch: token is not bound to this login")
|
|
|
|
response = client.post("/validate-token", json={
|
|
"id_token": "forged.token.here",
|
|
"nonce": "abc123"
|
|
})
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert not data["valid"]
|
|
assert "Nonce mismatch" in data["error"]
|
|
|
|
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" |