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:
170
tests/integration/services/test_happ_parser.py
Normal file
170
tests/integration/services/test_happ_parser.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Integration tests for h-app parser service.
|
||||
|
||||
Tests client metadata fetching with mocked HTTP responses.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
|
||||
class TestHAppParserIntegration:
|
||||
"""Integration tests for h-app metadata parsing."""
|
||||
|
||||
@pytest.fixture
|
||||
def happ_parser_with_mock_fetcher(self):
|
||||
"""Create h-app parser with mocked HTML fetcher."""
|
||||
from gondulf.services.happ_parser import HAppParser
|
||||
|
||||
html = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test App</title></head>
|
||||
<body>
|
||||
<div class="h-app">
|
||||
<h1 class="p-name">Example Application</h1>
|
||||
<img class="u-logo" src="https://app.example.com/logo.png" alt="Logo">
|
||||
<a class="u-url" href="https://app.example.com">Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
mock_fetcher = Mock()
|
||||
mock_fetcher.fetch = Mock(return_value=html)
|
||||
|
||||
return HAppParser(html_fetcher=mock_fetcher)
|
||||
|
||||
def test_fetch_and_parse_happ_metadata(self, happ_parser_with_mock_fetcher):
|
||||
"""Test fetching and parsing h-app microformat."""
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
happ_parser_with_mock_fetcher.fetch_and_parse("https://app.example.com")
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.name == "Example Application"
|
||||
assert result.logo == "https://app.example.com/logo.png"
|
||||
|
||||
def test_parse_page_without_happ(self, mock_urlopen):
|
||||
"""Test parsing page without h-app returns fallback."""
|
||||
from gondulf.services.happ_parser import HAppParser
|
||||
from gondulf.services.html_fetcher import HTMLFetcherService
|
||||
|
||||
# Setup mock to return page without h-app
|
||||
html = b'<html><head><title>Plain Page</title></head><body>No h-app</body></html>'
|
||||
mock_response = MagicMock()
|
||||
mock_response.read.return_value = html
|
||||
mock_response.status = 200
|
||||
mock_response.__enter__ = Mock(return_value=mock_response)
|
||||
mock_response.__exit__ = Mock(return_value=False)
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
fetcher = HTMLFetcherService()
|
||||
parser = HAppParser(html_fetcher=fetcher)
|
||||
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
parser.fetch_and_parse("https://app.example.com")
|
||||
)
|
||||
|
||||
# Should return fallback metadata using domain
|
||||
assert result is not None
|
||||
assert "example.com" in result.name.lower() or result.name == "Plain Page"
|
||||
|
||||
def test_fetch_timeout_returns_fallback(self, mock_urlopen_timeout):
|
||||
"""Test HTTP timeout returns fallback metadata."""
|
||||
from gondulf.services.happ_parser import HAppParser
|
||||
from gondulf.services.html_fetcher import HTMLFetcherService
|
||||
|
||||
fetcher = HTMLFetcherService()
|
||||
parser = HAppParser(html_fetcher=fetcher)
|
||||
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
parser.fetch_and_parse("https://slow-app.example.com")
|
||||
)
|
||||
|
||||
# Should return fallback metadata
|
||||
assert result is not None
|
||||
# Should use domain as fallback name
|
||||
assert "slow-app.example.com" in result.name or result.url == "https://slow-app.example.com"
|
||||
|
||||
|
||||
class TestClientMetadataCaching:
|
||||
"""Tests for client metadata caching behavior."""
|
||||
|
||||
def test_metadata_fetched_from_url(self, mock_urlopen_with_happ):
|
||||
"""Test metadata is actually fetched from URL."""
|
||||
from gondulf.services.happ_parser import HAppParser
|
||||
from gondulf.services.html_fetcher import HTMLFetcherService
|
||||
|
||||
fetcher = HTMLFetcherService()
|
||||
parser = HAppParser(html_fetcher=fetcher)
|
||||
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
parser.fetch_and_parse("https://app.example.com")
|
||||
)
|
||||
|
||||
# urlopen should have been called
|
||||
mock_urlopen_with_happ.assert_called()
|
||||
|
||||
|
||||
class TestHAppMicroformatVariants:
|
||||
"""Tests for various h-app microformat formats."""
|
||||
|
||||
@pytest.fixture
|
||||
def create_parser_with_html(self):
|
||||
"""Factory to create parser with specific HTML content."""
|
||||
def _create(html_content):
|
||||
from gondulf.services.happ_parser import HAppParser
|
||||
|
||||
mock_fetcher = Mock()
|
||||
mock_fetcher.fetch = Mock(return_value=html_content)
|
||||
|
||||
return HAppParser(html_fetcher=mock_fetcher)
|
||||
return _create
|
||||
|
||||
def test_parse_happ_with_minimal_data(self, create_parser_with_html):
|
||||
"""Test parsing h-app with only name."""
|
||||
html = '''
|
||||
<html>
|
||||
<body>
|
||||
<div class="h-app">
|
||||
<span class="p-name">Minimal App</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
parser = create_parser_with_html(html)
|
||||
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
parser.fetch_and_parse("https://minimal.example.com")
|
||||
)
|
||||
|
||||
assert result.name == "Minimal App"
|
||||
|
||||
def test_parse_happ_with_logo_relative_url(self, create_parser_with_html):
|
||||
"""Test parsing h-app with relative logo URL."""
|
||||
html = '''
|
||||
<html>
|
||||
<body>
|
||||
<div class="h-app">
|
||||
<span class="p-name">Relative Logo App</span>
|
||||
<img class="u-logo" src="/logo.png">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
parser = create_parser_with_html(html)
|
||||
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
parser.fetch_and_parse("https://relative.example.com")
|
||||
)
|
||||
|
||||
assert result.name == "Relative Logo App"
|
||||
# Logo should be resolved to absolute URL
|
||||
assert result.logo is not None
|
||||
Reference in New Issue
Block a user