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:
1
tests/e2e/__init__.py
Normal file
1
tests/e2e/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""End-to-end tests for Gondulf IndieAuth server."""
|
||||
390
tests/e2e/test_complete_auth_flow.py
Normal file
390
tests/e2e/test_complete_auth_flow.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
End-to-end tests for complete IndieAuth authentication flow.
|
||||
|
||||
Tests the full authorization code flow from initial request through token exchange.
|
||||
Uses TestClient-based flow simulation per Phase 5b clarifications.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from tests.conftest import extract_code_from_redirect
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_app(monkeypatch, tmp_path):
|
||||
"""Create app for E2E 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 e2e_client(e2e_app):
|
||||
"""Create test client for E2E tests."""
|
||||
with TestClient(e2e_app) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_happ_for_e2e():
|
||||
"""Mock h-app parser for E2E tests."""
|
||||
from gondulf.services.happ_parser import ClientMetadata
|
||||
|
||||
metadata = ClientMetadata(
|
||||
name="E2E Test App",
|
||||
url="https://app.example.com",
|
||||
logo="https://app.example.com/logo.png"
|
||||
)
|
||||
|
||||
with patch('gondulf.services.happ_parser.HAppParser.fetch_and_parse', new_callable=AsyncMock) as mock:
|
||||
mock.return_value = metadata
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestCompleteAuthorizationFlow:
|
||||
"""E2E tests for complete authorization code flow."""
|
||||
|
||||
def test_full_authorization_to_token_flow(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test complete flow: authorization request -> consent -> token exchange."""
|
||||
# Step 1: Authorization request
|
||||
auth_params = {
|
||||
"response_type": "code",
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "e2e_test_state_12345",
|
||||
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
}
|
||||
|
||||
auth_response = e2e_client.get("/authorize", params=auth_params)
|
||||
|
||||
# Should show consent page
|
||||
assert auth_response.status_code == 200
|
||||
assert "text/html" in auth_response.headers["content-type"]
|
||||
|
||||
# Step 2: Submit consent form
|
||||
consent_data = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "e2e_test_state_12345",
|
||||
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
}
|
||||
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data=consent_data,
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# Should redirect with authorization code
|
||||
assert consent_response.status_code == 302
|
||||
location = consent_response.headers["location"]
|
||||
assert location.startswith("https://app.example.com/callback")
|
||||
assert "code=" in location
|
||||
assert "state=e2e_test_state_12345" in location
|
||||
|
||||
# Step 3: Extract authorization code
|
||||
auth_code = extract_code_from_redirect(location)
|
||||
assert auth_code is not None
|
||||
|
||||
# Step 4: Exchange code for token
|
||||
token_response = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": auth_code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
|
||||
# Should receive access token
|
||||
assert token_response.status_code == 200
|
||||
token_data = token_response.json()
|
||||
assert "access_token" in token_data
|
||||
assert token_data["token_type"] == "Bearer"
|
||||
assert token_data["me"] == "https://user.example.com"
|
||||
|
||||
def test_authorization_flow_preserves_state(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test that state parameter is preserved throughout the flow."""
|
||||
state = "unique_state_for_csrf_protection"
|
||||
|
||||
# Authorization request
|
||||
auth_response = e2e_client.get("/authorize", params={
|
||||
"response_type": "code",
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": state,
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
})
|
||||
|
||||
assert auth_response.status_code == 200
|
||||
assert state in auth_response.text
|
||||
|
||||
# Consent submission
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": state,
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# State should be in redirect
|
||||
location = consent_response.headers["location"]
|
||||
assert f"state={state}" in location
|
||||
|
||||
def test_multiple_concurrent_flows(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test multiple authorization flows can run concurrently."""
|
||||
flows = []
|
||||
|
||||
# Start 3 authorization flows
|
||||
for i in range(3):
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": f"flow_{i}",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": f"https://user{i}.example.com",
|
||||
"scope": "",
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
code = extract_code_from_redirect(consent_response.headers["location"])
|
||||
flows.append((code, f"https://user{i}.example.com"))
|
||||
|
||||
# Exchange all codes - each should work
|
||||
for code, expected_me in flows:
|
||||
token_response = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
|
||||
assert token_response.status_code == 200
|
||||
assert token_response.json()["me"] == expected_me
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestErrorScenariosE2E:
|
||||
"""E2E tests for error scenarios."""
|
||||
|
||||
def test_invalid_client_id_error_page(self, e2e_client):
|
||||
"""Test invalid client_id shows error page."""
|
||||
response = e2e_client.get("/authorize", params={
|
||||
"client_id": "http://insecure.example.com", # HTTP not allowed
|
||||
"redirect_uri": "http://insecure.example.com/callback",
|
||||
"response_type": "code",
|
||||
})
|
||||
|
||||
assert response.status_code == 400
|
||||
# Should show error page, not redirect
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
def test_expired_code_rejected(self, e2e_client, e2e_app, mock_happ_for_e2e):
|
||||
"""Test expired authorization code is rejected."""
|
||||
from gondulf.dependencies import get_code_storage
|
||||
from gondulf.storage import CodeStore
|
||||
|
||||
# Create code storage with very short TTL
|
||||
short_ttl_storage = CodeStore(ttl_seconds=0) # Expire immediately
|
||||
|
||||
# Store a code that will expire immediately
|
||||
code = "expired_test_code_12345"
|
||||
metadata = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "test",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"created_at": 1000000000,
|
||||
"expires_at": 1000000001,
|
||||
"used": False
|
||||
}
|
||||
short_ttl_storage.store(f"authz:{code}", metadata, ttl=0)
|
||||
|
||||
e2e_app.dependency_overrides[get_code_storage] = lambda: short_ttl_storage
|
||||
|
||||
# Wait a tiny bit for expiration
|
||||
import time
|
||||
time.sleep(0.01)
|
||||
|
||||
# Try to exchange expired code
|
||||
response = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"]["error"] == "invalid_grant"
|
||||
|
||||
e2e_app.dependency_overrides.clear()
|
||||
|
||||
def test_code_cannot_be_reused(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test authorization code single-use enforcement."""
|
||||
# Get a valid code
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
code = extract_code_from_redirect(consent_response.headers["location"])
|
||||
|
||||
# First exchange should succeed
|
||||
response1 = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
assert response1.status_code == 200
|
||||
|
||||
# Second exchange should fail
|
||||
response2 = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
assert response2.status_code == 400
|
||||
|
||||
def test_wrong_client_id_rejected(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test token exchange with wrong client_id is rejected."""
|
||||
# Get a code for one client
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
code = extract_code_from_redirect(consent_response.headers["location"])
|
||||
|
||||
# Try to exchange with different client_id
|
||||
response = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://different-app.example.com", # Wrong client
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"]["error"] == "invalid_client"
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestTokenUsageE2E:
|
||||
"""E2E tests for token usage after obtaining it."""
|
||||
|
||||
def test_obtained_token_has_correct_format(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test the token obtained through E2E flow has correct format."""
|
||||
# Complete the flow
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
code = extract_code_from_redirect(consent_response.headers["location"])
|
||||
|
||||
token_response = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
|
||||
assert token_response.status_code == 200
|
||||
token_data = token_response.json()
|
||||
|
||||
# Verify token has correct format
|
||||
assert "access_token" in token_data
|
||||
assert len(token_data["access_token"]) >= 32 # Should be substantial
|
||||
assert token_data["token_type"] == "Bearer"
|
||||
assert token_data["me"] == "https://user.example.com"
|
||||
|
||||
def test_token_response_includes_all_fields(self, e2e_client, mock_happ_for_e2e):
|
||||
"""Test token response includes all required IndieAuth fields."""
|
||||
# Complete the flow
|
||||
consent_response = e2e_client.post(
|
||||
"/authorize/consent",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "profile",
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
code = extract_code_from_redirect(consent_response.headers["location"])
|
||||
|
||||
token_response = e2e_client.post("/token", data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
})
|
||||
|
||||
assert token_response.status_code == 200
|
||||
token_data = token_response.json()
|
||||
|
||||
# All required IndieAuth fields
|
||||
assert "access_token" in token_data
|
||||
assert "token_type" in token_data
|
||||
assert "me" in token_data
|
||||
assert "scope" in token_data
|
||||
260
tests/e2e/test_error_scenarios.py
Normal file
260
tests/e2e/test_error_scenarios.py
Normal 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
|
||||
Reference in New Issue
Block a user