Files
demo-oidc/tests/test_main.py
T
herel 9c7f2bde11 ref(frontend): Replace Bootstrap with Tailwind CSS
Swap the Bootstrap CDN for a self-hosted Tailwind CSS build, mirroring
the myice setup: standalone Tailwind CLI v3.4.17, a one-off build-css.sh
script, and a gitignored generated style.css.

- Add tailwind.config.js, style-input.css, build-css.sh at repo root
- Rewrite templates/index.html: drop Bootstrap link/script, link
  /style.css, convert all classes to Tailwind utilities; showAlert now
  uses a class lookup so the JIT scanner detects dynamic classes
- Add /style.css FileResponse route in main.py; drop cdn.jsdelivr.net
  from script-src/style-src in the CSP
- Flip test assertion to assert cdn.jsdelivr.net is absent from CSP

The generated style.css and tailwindcss binary are gitignored; run
./build-css.sh to regenerate (or --watch for dev).
2026-07-08 14:10:37 +02:00

221 lines
7.4 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" not 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
def test_root_csp_nonce_is_unique_per_request():
"""Test that each GET / response gets a fresh CSP nonce (not module-level/static)."""
import re
n1 = re.search(
r"nonce-([A-Za-z0-9_-]+)",
client.get("/").headers.get("content-security-policy", ""),
).group(1)
n2 = re.search(
r"nonce-([A-Za-z0-9_-]+)",
client.get("/").headers.get("content-security-policy", ""),
).group(1)
assert n1 != n2
@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"