feat(test): add Phase 5b integration and E2E tests

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>
This commit is contained in:
2025-11-21 22:22:04 -07:00
parent 01dcaba86b
commit e1f79af347
19 changed files with 4387 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
"""
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