Add comprehensive integration and end-to-end test suites: - Integration tests for API flows (authorization, token, verification) - Integration tests for middleware chain and security headers - Integration tests for domain verification services - E2E tests for complete authentication flows - E2E tests for error scenarios and edge cases - Shared test fixtures and utilities in conftest.py - Rename Dockerfile to Containerfile for Podman compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
261 lines
9.0 KiB
Python
261 lines
9.0 KiB
Python
"""
|
|
End-to-end tests for error scenarios and edge cases.
|
|
|
|
Tests various error conditions and ensures proper error handling throughout the system.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def error_app(monkeypatch, tmp_path):
|
|
"""Create app for error scenario testing."""
|
|
db_path = tmp_path / "test.db"
|
|
|
|
monkeypatch.setenv("GONDULF_SECRET_KEY", "a" * 32)
|
|
monkeypatch.setenv("GONDULF_BASE_URL", "https://auth.example.com")
|
|
monkeypatch.setenv("GONDULF_DATABASE_URL", f"sqlite:///{db_path}")
|
|
monkeypatch.setenv("GONDULF_DEBUG", "true")
|
|
|
|
from gondulf.main import app
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def error_client(error_app):
|
|
"""Create test client for error scenario tests."""
|
|
with TestClient(error_app) as client:
|
|
yield client
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestAuthorizationErrors:
|
|
"""E2E tests for authorization endpoint errors."""
|
|
|
|
def test_missing_all_parameters(self, error_client):
|
|
"""Test authorization request with no parameters."""
|
|
response = error_client.get("/authorize")
|
|
|
|
assert response.status_code == 400
|
|
|
|
def test_http_client_id_rejected(self, error_client):
|
|
"""Test HTTP (non-HTTPS) client_id is rejected."""
|
|
response = error_client.get("/authorize", params={
|
|
"client_id": "http://insecure.example.com",
|
|
"redirect_uri": "http://insecure.example.com/callback",
|
|
"response_type": "code",
|
|
"state": "test",
|
|
})
|
|
|
|
assert response.status_code == 400
|
|
assert "https" in response.text.lower()
|
|
|
|
def test_mismatched_redirect_uri_domain(self, error_client):
|
|
"""Test redirect_uri must match client_id domain."""
|
|
response = error_client.get("/authorize", params={
|
|
"client_id": "https://legitimate-app.example.com",
|
|
"redirect_uri": "https://evil-site.example.com/steal",
|
|
"response_type": "code",
|
|
"state": "test",
|
|
})
|
|
|
|
assert response.status_code == 400
|
|
|
|
def test_invalid_response_type_redirects(self, error_client):
|
|
"""Test invalid response_type redirects with error."""
|
|
response = error_client.get("/authorize", params={
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"response_type": "implicit", # Not supported
|
|
"state": "test123",
|
|
}, follow_redirects=False)
|
|
|
|
assert response.status_code == 302
|
|
location = response.headers["location"]
|
|
assert "error=unsupported_response_type" in location
|
|
assert "state=test123" in location
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestTokenEndpointErrors:
|
|
"""E2E tests for token endpoint errors."""
|
|
|
|
def test_invalid_grant_type(self, error_client):
|
|
"""Test unsupported grant_type returns error."""
|
|
response = error_client.post("/token", data={
|
|
"grant_type": "client_credentials",
|
|
"code": "some_code",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
})
|
|
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["detail"]["error"] == "unsupported_grant_type"
|
|
|
|
def test_missing_grant_type(self, error_client):
|
|
"""Test missing grant_type returns validation error."""
|
|
response = error_client.post("/token", data={
|
|
"code": "some_code",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
})
|
|
|
|
# FastAPI validation error
|
|
assert response.status_code == 422
|
|
|
|
def test_nonexistent_code(self, error_client):
|
|
"""Test nonexistent authorization code returns error."""
|
|
response = error_client.post("/token", data={
|
|
"grant_type": "authorization_code",
|
|
"code": "completely_made_up_code_12345",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
})
|
|
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["detail"]["error"] == "invalid_grant"
|
|
|
|
def test_get_method_not_allowed(self, error_client):
|
|
"""Test GET method not allowed on token endpoint."""
|
|
response = error_client.get("/token")
|
|
|
|
assert response.status_code == 405
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestVerificationErrors:
|
|
"""E2E tests for verification endpoint errors."""
|
|
|
|
def test_invalid_me_url(self, error_client):
|
|
"""Test invalid me URL format."""
|
|
response = error_client.post(
|
|
"/api/verify/start",
|
|
data={"me": "not-a-url"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
assert data["error"] == "invalid_me_url"
|
|
|
|
def test_invalid_code_verification(self, error_client):
|
|
"""Test verification with invalid code."""
|
|
response = error_client.post(
|
|
"/api/verify/code",
|
|
data={"domain": "example.com", "code": "000000"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestSecurityErrorHandling:
|
|
"""E2E tests for security-related error handling."""
|
|
|
|
def test_xss_in_state_escaped(self, error_client):
|
|
"""Test XSS attempt in state parameter is escaped."""
|
|
xss_payload = "<script>alert('xss')</script>"
|
|
|
|
response = error_client.get("/authorize", params={
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"response_type": "token", # Will error and redirect
|
|
"state": xss_payload,
|
|
}, follow_redirects=False)
|
|
|
|
# Should redirect with error
|
|
assert response.status_code == 302
|
|
location = response.headers["location"]
|
|
# Script tags should be URL encoded, not raw
|
|
assert "<script>" not in location
|
|
|
|
def test_errors_have_security_headers(self, error_client):
|
|
"""Test error responses include security headers."""
|
|
response = error_client.get("/authorize") # Missing params = error
|
|
|
|
assert response.status_code == 400
|
|
assert "X-Frame-Options" in response.headers
|
|
assert response.headers["X-Frame-Options"] == "DENY"
|
|
|
|
def test_error_response_is_json_for_api(self, error_client):
|
|
"""Test API error responses are JSON formatted."""
|
|
response = error_client.post("/token", data={
|
|
"grant_type": "authorization_code",
|
|
"code": "invalid",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
})
|
|
|
|
assert response.status_code == 400
|
|
# Should be JSON
|
|
assert "application/json" in response.headers["content-type"]
|
|
data = response.json()
|
|
assert "detail" in data
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestEdgeCases:
|
|
"""E2E tests for edge cases."""
|
|
|
|
def test_empty_scope_accepted(self, error_client):
|
|
"""Test empty scope is accepted."""
|
|
from unittest.mock import AsyncMock, patch
|
|
from gondulf.services.happ_parser import ClientMetadata
|
|
|
|
metadata = ClientMetadata(
|
|
name="Test App",
|
|
url="https://app.example.com",
|
|
logo=None
|
|
)
|
|
|
|
with patch('gondulf.services.happ_parser.HAppParser.fetch_and_parse', new_callable=AsyncMock) as mock:
|
|
mock.return_value = metadata
|
|
|
|
response = error_client.get("/authorize", params={
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"response_type": "code",
|
|
"state": "test",
|
|
"code_challenge": "abc123",
|
|
"code_challenge_method": "S256",
|
|
"me": "https://user.example.com",
|
|
"scope": "", # Empty scope
|
|
})
|
|
|
|
# Should show consent page
|
|
assert response.status_code == 200
|
|
|
|
def test_very_long_state_handled(self, error_client):
|
|
"""Test very long state parameter is handled."""
|
|
from unittest.mock import AsyncMock, patch
|
|
from gondulf.services.happ_parser import ClientMetadata
|
|
|
|
metadata = ClientMetadata(
|
|
name="Test App",
|
|
url="https://app.example.com",
|
|
logo=None
|
|
)
|
|
|
|
long_state = "x" * 1000
|
|
|
|
with patch('gondulf.services.happ_parser.HAppParser.fetch_and_parse', new_callable=AsyncMock) as mock:
|
|
mock.return_value = metadata
|
|
|
|
response = error_client.get("/authorize", params={
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"response_type": "code",
|
|
"state": long_state,
|
|
"code_challenge": "abc123",
|
|
"code_challenge_method": "S256",
|
|
"me": "https://user.example.com",
|
|
})
|
|
|
|
# Should handle without error
|
|
assert response.status_code == 200
|