Skip to main content
Glama
brianirish

Laravel MCP Companion

by brianirish
test_docs_updater.py65.6 kB
"""Unit tests for documentation updater functionality.""" import pytest import json import tempfile import zipfile import io from pathlib import Path from unittest.mock import patch, MagicMock, Mock, mock_open import urllib.error from docs_updater import ( DocsUpdater, ExternalDocsFetcher, MultiSourceDocsUpdater, DocumentationAutoDiscovery, DocumentationSourceType, CommunityPackageFetcher, get_supported_versions, get_cached_supported_versions, SUPPORTED_VERSIONS, DEFAULT_VERSION ) class TestDocsUpdater: """Test core documentation updater functionality.""" def test_init(self, temp_dir): """Test DocsUpdater initialization.""" updater = DocsUpdater(temp_dir, "12.x") assert updater.target_dir == temp_dir assert updater.version == "12.x" assert updater.version_dir == temp_dir / "12.x" assert updater.metadata_dir == temp_dir / "12.x" / ".metadata" assert updater.metadata_file == temp_dir / "12.x" / ".metadata" / "sync_info.json" # Directories should be created assert updater.version_dir.exists() assert updater.metadata_dir.exists() @patch('docs_updater.urllib.request.urlopen') def test_get_latest_commit_success(self, mock_urlopen, docs_updater_instance): """Test successful GitHub API call for latest commit.""" mock_response = MagicMock() mock_response.read.return_value = json.dumps({ "commit": { "sha": "abc123def456", "commit": { "committer": {"date": "2024-01-01T12:00:00Z"}, "message": "Update documentation" }, "html_url": "https://github.com/laravel/docs/commit/abc123def456" } }).encode() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response result = docs_updater_instance.get_latest_commit() assert result["sha"] == "abc123def456" assert result["date"] == "2024-01-01T12:00:00Z" assert result["message"] == "Update documentation" assert result["url"] == "https://github.com/laravel/docs/commit/abc123def456" @patch('docs_updater.urllib.request.urlopen') def test_get_latest_commit_http_error(self, mock_urlopen, docs_updater_instance): """Test GitHub API call with HTTP error.""" mock_urlopen.side_effect = urllib.error.HTTPError( url="test", code=404, msg="Not Found", hdrs=None, fp=None ) with pytest.raises(urllib.error.HTTPError): docs_updater_instance.get_latest_commit() @patch('docs_updater.urllib.request.urlopen') def test_get_latest_commit_rate_limit(self, mock_urlopen, docs_updater_instance): """Test GitHub API rate limiting handling.""" # First call: rate limit error # Second call: success mock_responses = [ urllib.error.HTTPError(url="test", code=403, msg="Rate Limited", hdrs=None, fp=None), MagicMock() ] success_response = MagicMock() success_response.read.return_value = json.dumps({ "commit": { "sha": "abc123", "commit": { "committer": {"date": "2024-01-01T12:00:00Z"}, "message": "Update" }, "html_url": "https://github.com/laravel/docs/commit/abc123" } }).encode() success_response.__enter__ = lambda self: self success_response.__exit__ = lambda self, *args: None mock_responses[1] = success_response mock_urlopen.side_effect = mock_responses # Should retry and succeed with patch('docs_updater.time.sleep'): # Speed up test result = docs_updater_instance.get_latest_commit() assert result["sha"] == "abc123" def test_read_local_metadata_exists(self, docs_updater_instance): """Test reading existing local metadata.""" metadata = { "version": "12.x", "commit_sha": "abc123", "sync_time": "2024-01-01T12:00:00Z" } docs_updater_instance.metadata_file.write_text(json.dumps(metadata)) result = docs_updater_instance.read_local_metadata() assert result == metadata def test_read_local_metadata_not_exists(self, docs_updater_instance): """Test reading non-existent local metadata.""" # Ensure metadata file doesn't exist if docs_updater_instance.metadata_file.exists(): docs_updater_instance.metadata_file.unlink() result = docs_updater_instance.read_local_metadata() assert result == {} def test_read_local_metadata_corrupted(self, docs_updater_instance): """Test reading corrupted local metadata.""" docs_updater_instance.metadata_file.write_text("invalid json {") result = docs_updater_instance.read_local_metadata() assert result == {} def test_write_local_metadata(self, docs_updater_instance): """Test writing local metadata.""" metadata = { "version": "12.x", "commit_sha": "abc123", "sync_time": "2024-01-01T12:00:00Z" } docs_updater_instance.write_local_metadata(metadata) assert docs_updater_instance.metadata_file.exists() written_data = json.loads(docs_updater_instance.metadata_file.read_text()) assert written_data == metadata @patch('docs_updater.shutil.copyfileobj') @patch('docs_updater.urllib.request.urlopen') @patch('docs_updater.tempfile.mkdtemp') @patch('docs_updater.zipfile.ZipFile') def test_download_documentation_success(self, mock_zipfile, mock_mkdtemp, mock_urlopen, mock_copyfileobj, docs_updater_instance, temp_dir): """Test successful documentation download.""" # Create a real temporary directory structure mock_temp_path = temp_dir / "download_test" mock_temp_path.mkdir() # Mock mkdtemp to return our test directory mock_mkdtemp.return_value = str(mock_temp_path) # Mock URL response mock_response = MagicMock() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response # Mock shutil.copyfileobj to create the file def create_zip_file(src, dst): dst.write(b"mock zip content") mock_copyfileobj.side_effect = create_zip_file # Mock zipfile extraction mock_zip_instance = MagicMock() mock_zipfile.return_value.__enter__ = lambda self: mock_zip_instance mock_zipfile.return_value.__exit__ = lambda self, *args: None # Create the expected extracted directory extracted_dir = mock_temp_path / "docs-12.x" extracted_dir.mkdir() (extracted_dir / "test.md").write_text("test content") result = docs_updater_instance.download_documentation() assert result.name == "docs-12.x" @patch.object(DocsUpdater, 'get_latest_commit') def test_needs_update_true(self, mock_get_latest_commit, docs_updater_instance): """Test needs_update when update is needed.""" mock_get_latest_commit.return_value = {"sha": "new123"} # Write old metadata old_metadata = {"version": "12.x", "commit_sha": "old123"} docs_updater_instance.write_local_metadata(old_metadata) assert docs_updater_instance.needs_update() is True @patch.object(DocsUpdater, 'get_latest_commit') def test_needs_update_false(self, mock_get_latest_commit, docs_updater_instance): """Test needs_update when no update is needed.""" mock_get_latest_commit.return_value = {"sha": "same123"} # Write same metadata same_metadata = {"version": "12.x", "commit_sha": "same123"} docs_updater_instance.write_local_metadata(same_metadata) assert docs_updater_instance.needs_update() is False @patch.object(DocsUpdater, 'get_latest_commit') def test_needs_update_no_local_metadata(self, mock_get_latest_commit, docs_updater_instance): """Test needs_update when no local metadata exists.""" mock_get_latest_commit.return_value = {"sha": "abc123"} assert docs_updater_instance.needs_update() is True @patch.object(DocsUpdater, 'get_latest_commit') def test_needs_update_error(self, mock_get_latest_commit, docs_updater_instance): """Test needs_update when error occurs.""" mock_get_latest_commit.side_effect = Exception("Network error") # Should return True (assume update needed on error) assert docs_updater_instance.needs_update() is True @patch.object(DocsUpdater, 'needs_update') @patch.object(DocsUpdater, 'get_latest_commit') @patch.object(DocsUpdater, 'download_documentation') def test_update_success(self, mock_download, mock_get_latest_commit, mock_needs_update, docs_updater_instance): """Test successful documentation update.""" mock_needs_update.return_value = True mock_get_latest_commit.return_value = { "sha": "abc123", "date": "2024-01-01T12:00:00Z", "message": "Update docs", "url": "https://github.com/laravel/docs/commit/abc123" } # Mock downloaded directory with files source_dir = MagicMock(spec=Path) source_dir.parent = MagicMock(spec=Path) # Create mock file mock_file = MagicMock(spec=Path) mock_file.name = "file.md" mock_file.is_dir.return_value = False # Setup source_dir.iterdir to return mock files source_dir.iterdir.return_value = [mock_file] mock_download.return_value = source_dir with patch('docs_updater.shutil.copytree'), \ patch('docs_updater.shutil.copy2'), \ patch('docs_updater.shutil.rmtree'), \ patch('docs_updater.time.strftime', return_value="2024-01-01T12:00:00Z"): result = docs_updater_instance.update(force=False) assert result is True # Should write metadata metadata = docs_updater_instance.read_local_metadata() assert metadata["commit_sha"] == "abc123" @patch.object(DocsUpdater, 'needs_update') def test_update_not_needed(self, mock_needs_update, docs_updater_instance): """Test update when not needed.""" mock_needs_update.return_value = False result = docs_updater_instance.update(force=False) assert result is False @patch.object(DocsUpdater, 'needs_update') def test_update_force(self, mock_needs_update, docs_updater_instance): """Test forced update.""" mock_needs_update.return_value = False with patch.object(docs_updater_instance, 'get_latest_commit') as mock_get_latest_commit, \ patch.object(docs_updater_instance, 'download_documentation') as mock_download: mock_get_latest_commit.return_value = { "sha": "abc123", "date": "2024-01-01T12:00:00Z", "message": "Update", "url": "test" } # Mock source directory source_dir = MagicMock(spec=Path) source_dir.parent = MagicMock(spec=Path) source_dir.iterdir.return_value = [] mock_download.return_value = source_dir with patch('docs_updater.shutil.copytree'), \ patch('docs_updater.shutil.copy2'), \ patch('docs_updater.shutil.rmtree'), \ patch('docs_updater.time.strftime', return_value="2024-01-01T12:00:00Z"): result = docs_updater_instance.update(force=True) assert result is True @patch('docs_updater.urllib.request.urlopen') def test_download_documentation_network_error(self, mock_urlopen, docs_updater_instance): """Test documentation download with network error.""" mock_urlopen.side_effect = urllib.error.URLError("Network error") with pytest.raises(urllib.error.URLError): docs_updater_instance.download_documentation() @patch('docs_updater.shutil.copyfileobj') @patch('docs_updater.urllib.request.urlopen') @patch('docs_updater.tempfile.TemporaryDirectory') def test_download_documentation_zip_error(self, mock_tempdir, mock_urlopen, mock_copyfileobj, docs_updater_instance, temp_dir): """Test documentation download with zip extraction error.""" # Create a real temporary directory structure mock_temp_path = temp_dir / "download_test" mock_temp_path.mkdir() # Mock TemporaryDirectory to return our test directory mock_tempdir_instance = MagicMock() mock_tempdir_instance.__enter__ = lambda self: str(mock_temp_path) mock_tempdir_instance.__exit__ = lambda self, *args: None mock_tempdir.return_value = mock_tempdir_instance # Mock URL response mock_response = MagicMock() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response # Create invalid zip file zip_path = mock_temp_path / "laravel_docs.zip" zip_path.write_text("invalid zip content") with pytest.raises(Exception): docs_updater_instance.download_documentation() @patch.object(DocsUpdater, 'get_latest_commit') @patch.object(DocsUpdater, 'download_documentation') def test_update_with_exception(self, mock_download, mock_get_latest_commit, docs_updater_instance): """Test update when exception occurs during download.""" mock_get_latest_commit.return_value = { "sha": "abc123", "date": "2024-01-01T12:00:00Z", "message": "Update", "url": "test" } mock_download.side_effect = Exception("Download failed") with pytest.raises(Exception) as exc_info: docs_updater_instance.update(force=True) assert "Download failed" in str(exc_info.value) def test_update_copies_subdirectories(self, docs_updater_instance): """Test that update correctly copies subdirectories.""" with patch.object(docs_updater_instance, 'needs_update', return_value=True), \ patch.object(docs_updater_instance, 'get_latest_commit') as mock_commit, \ patch.object(docs_updater_instance, 'download_documentation') as mock_download: mock_commit.return_value = { "sha": "abc123", "date": "2024-01-01T12:00:00Z", "message": "Update", "url": "test" } # Create mock source with subdirectory source_dir = MagicMock(spec=Path) source_dir.parent = MagicMock(spec=Path) mock_subdir = MagicMock(spec=Path) mock_subdir.name = "subdir" mock_subdir.is_dir.return_value = True source_dir.iterdir.return_value = [mock_subdir] mock_download.return_value = source_dir with patch('docs_updater.shutil.copytree') as mock_copytree, \ patch('docs_updater.shutil.rmtree'), \ patch('docs_updater.time.strftime', return_value="2024-01-01T12:00:00Z"): result = docs_updater_instance.update() assert result is True # Verify copytree was called for subdirectory mock_copytree.assert_called() def test_read_write_metadata_cycle(self, docs_updater_instance): """Test reading and writing metadata cycle.""" # Write some metadata metadata = { "version": "12.x", "commit_sha": "abc123", "sync_time": "2024-01-01T12:00:00Z" } docs_updater_instance.write_local_metadata(metadata) # Read it back read_metadata = docs_updater_instance.read_local_metadata() assert read_metadata["version"] == "12.x" assert read_metadata["commit_sha"] == "abc123" class TestExternalDocsFetcher: """Test external documentation fetcher.""" def test_init(self, temp_dir): """Test ExternalDocsFetcher initialization.""" fetcher = ExternalDocsFetcher(temp_dir) assert fetcher.target_dir == temp_dir assert fetcher.external_dir == temp_dir / "external" assert fetcher.external_dir.exists() assert isinstance(fetcher.laravel_services, dict) assert "forge" in fetcher.laravel_services assert "vapor" in fetcher.laravel_services def test_get_service_cache_path(self, external_docs_fetcher): """Test service cache path generation.""" cache_path = external_docs_fetcher.get_service_cache_path("forge") assert cache_path == external_docs_fetcher.external_dir / "forge" assert cache_path.exists() # Should be created def test_get_cache_metadata_path(self, external_docs_fetcher): """Test cache metadata path generation.""" metadata_path = external_docs_fetcher.get_cache_metadata_path("forge") expected = external_docs_fetcher.external_dir / "forge" / ".cache_metadata.json" assert metadata_path == expected def test_is_cache_valid_no_metadata(self, external_docs_fetcher): """Test cache validation when no metadata exists.""" assert external_docs_fetcher.is_cache_valid("forge") is False def test_is_cache_valid_expired(self, external_docs_fetcher): """Test cache validation when cache is expired.""" import time import json import os # Set timestamp to more than cache_duration (86400 seconds) ago old_timestamp = time.time() - (external_docs_fetcher.cache_duration + 1000) metadata = {"cached_at": old_timestamp} # Write metadata directly to bypass save_cache_metadata which updates timestamp metadata_path = external_docs_fetcher.get_cache_metadata_path("forge") with open(metadata_path, 'w') as f: json.dump(metadata, f) # Set file modification time to be expired os.utime(metadata_path, (old_timestamp, old_timestamp)) assert external_docs_fetcher.is_cache_valid("forge") is False def test_is_cache_valid_fresh(self, external_docs_fetcher): """Test cache validation when cache is fresh.""" import time metadata = { "cached_at": time.time(), "success_rate": 1.0 # Required for cache validity (must be >= 0.9) } external_docs_fetcher.save_cache_metadata("forge", metadata) assert external_docs_fetcher.is_cache_valid("forge") is True def test_save_cache_metadata(self, external_docs_fetcher): """Test saving cache metadata.""" metadata = {"service": "forge", "fetched_sections": ["intro"]} external_docs_fetcher.save_cache_metadata("forge", metadata) metadata_path = external_docs_fetcher.get_cache_metadata_path("forge") assert metadata_path.exists() saved_data = json.loads(metadata_path.read_text()) assert saved_data["service"] == "forge" assert saved_data["fetched_sections"] == ["intro"] @patch('docs_updater.urllib.request.urlopen') def test_fetch_laravel_service_docs_success(self, mock_urlopen, external_docs_fetcher): """Test successful service documentation fetching.""" # Mock HTTP response mock_response = MagicMock() mock_response.read.return_value = b"<html><body><h1>Laravel Forge</h1><p>Documentation content</p></body></html>" mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response # Mock auto-discovery to return empty (use manual sections) with patch.object(external_docs_fetcher.auto_discovery, 'discover_sections', return_value=[]): result = external_docs_fetcher.fetch_laravel_service_docs("forge") assert result is True # Check that some files were created service_dir = external_docs_fetcher.get_service_cache_path("forge") md_files = list(service_dir.glob("*.md")) assert len(md_files) > 0 def test_fetch_laravel_service_docs_unknown_service(self, external_docs_fetcher): """Test fetching docs for unknown service.""" result = external_docs_fetcher.fetch_laravel_service_docs("unknown_service") assert result is False @patch('docs_updater.urllib.request.urlopen') def test_fetch_laravel_service_docs_http_error(self, mock_urlopen, external_docs_fetcher): """Test service doc fetching with HTTP errors.""" mock_urlopen.side_effect = urllib.error.HTTPError( url="test", code=404, msg="Not Found", hdrs=None, fp=None ) with patch.object(external_docs_fetcher.auto_discovery, 'discover_sections', return_value=[]): result = external_docs_fetcher.fetch_laravel_service_docs("forge") # Should return False or handle gracefully assert isinstance(result, bool) def test_list_available_services(self, external_docs_fetcher): """Test listing available services.""" services = external_docs_fetcher.list_available_services() assert isinstance(services, list) assert "forge" in services assert "vapor" in services assert "envoyer" in services assert "nova" in services def test_get_service_info(self, external_docs_fetcher): """Test getting service information.""" forge_info = external_docs_fetcher.get_service_info("forge") assert forge_info is not None assert forge_info["name"] == "Laravel Forge" assert "base_url" in forge_info # Unknown service should return None unknown_info = external_docs_fetcher.get_service_info("unknown") assert unknown_info is None def test_html_to_text_conversion(self, external_docs_fetcher): """Test HTML to text conversion.""" html_content = """ <h1>Laravel Forge</h1> <p>This is a <strong>test</strong> paragraph with <em>emphasis</em>.</p> <ul> <li>Item 1</li> <li>Item 2</li> </ul> <code>composer install</code> """ text_content = external_docs_fetcher._html_to_text(html_content) assert "# Laravel Forge" in text_content assert "**test**" in text_content assert "*emphasis*" in text_content assert "- Item 1" in text_content assert "`composer install`" in text_content def test_html_to_text_support_link_filtering(self, external_docs_fetcher): """Test that Support links with email protection are filtered out.""" html_content = """ <div> <a href="https://forge.laravel.com">Laravel Forge home page</a> <a href="/cdn-cgi/l/email-protection#b5d3dac7d2d0f5d9d4c7d4c3d0d99bd6dad8">Support</a> <a href="/dashboard">Dashboard</a> <a href="https://support.laravel.com">Support</a> <a href="/cdn-cgi/l/email-protection#123456">Contact Us</a> </div> """ text_content = external_docs_fetcher._html_to_text(html_content) # Regular links should be converted assert "[Laravel Forge home page](https://forge.laravel.com)" in text_content assert "[Dashboard](/dashboard)" in text_content # Support link without email protection should be kept assert "[Support](https://support.laravel.com)" in text_content # Other link with email protection but not Support text should be kept assert "[Contact Us](/cdn-cgi/l/email-protection#123456)" in text_content # Support links with email protection should have link removed but text kept assert "email-protection#b5d3dac7d2d0f5d9d4c7d4c3d0d99bd6dad8" not in text_content # Check that we have both Support texts - one as plain text, one as a link assert text_content.count("Support") == 2 # Plain text Support + linked Support assert "[Support](https://support.laravel.com)" in text_content # Good Support link kept assert "[Support](/cdn-cgi/l/email-protection" not in text_content # Bad Support link removed def test_process_service_html(self, external_docs_fetcher, sample_html_content): """Test service HTML processing.""" processed = external_docs_fetcher._process_service_html( sample_html_content, "forge", "introduction" ) assert "# Forge - Introduction" in processed assert "Laravel Installation" in processed assert "PHP >= 8.1" in processed assert "composer create-project" in processed def test_save_cache_metadata_write_error(self, external_docs_fetcher): """Test saving cache metadata with write error.""" # Make directory read-only to cause write error external_docs_fetcher.get_service_cache_path("forge") with patch('builtins.open', side_effect=PermissionError("Permission denied")): # Should not raise exception external_docs_fetcher.save_cache_metadata("forge", {"test": "data"}) @patch('docs_updater.urllib.request.urlopen') def test_fetch_service_documentation_success(self, mock_urlopen, external_docs_fetcher): """Test fetching service documentation successfully.""" mock_html = "<html><body><h1>Forge Docs</h1><p>Content</p></body></html>" mock_response = MagicMock() mock_response.read.return_value = mock_html.encode() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response service_config = { "name": "Laravel Forge", "base_url": "https://forge.laravel.com/docs", "sections": ["introduction"], "type": "laravel_service" } service_dir = external_docs_fetcher.get_service_cache_path("forge") result = external_docs_fetcher._fetch_service_documentation("forge", service_config, service_dir) assert result is True # Check that file was created intro_file = service_dir / "introduction.md" assert intro_file.exists() def test_fetch_github_documentation(self, external_docs_fetcher, temp_dir): """Test fetching documentation from GitHub.""" # Create a fake zip file with documentation with tempfile.TemporaryDirectory() as temp_zip_dir: # Create a directory structure that simulates extracted GitHub archive repo_dir = Path(temp_zip_dir) / "repo-main" repo_dir.mkdir() docs_dir = repo_dir / "docs" docs_dir.mkdir() # Create a readme file readme_file = docs_dir / "readme.md" readme_file.write_text("# Test Documentation\n\nThis is test content.") # Create a zip file zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w') as zip_ref: # Add files to zip with the expected structure zip_ref.write(readme_file, arcname="repo-main/docs/readme.md") zip_content = zip_buffer.getvalue() service_config = { "name": "Test Service", "repo": "test/repo", "branch": "main", "path": "docs", "sections": ["readme.md"], "type": DocumentationSourceType.GITHUB_REPO } service_dir = external_docs_fetcher.get_service_cache_path("test") with patch.object(external_docs_fetcher, '_retry_request', return_value=zip_content): result = external_docs_fetcher._fetch_github_documentation("test", service_config, service_dir) assert result is True # Check that the docs directory was copied assert (service_dir / "docs").exists() # Check that file was created inside docs directory assert (service_dir / "docs" / "readme.md").exists() def test_fetch_all_services(self, external_docs_fetcher): """Test fetching all services.""" with patch.object(external_docs_fetcher, 'fetch_laravel_service_docs') as mock_fetch: mock_fetch.side_effect = [True, False, True, True] # forge, vapor, envoyer, nova results = external_docs_fetcher.fetch_all_services(force=False) assert results["forge"] is True assert results["vapor"] is False assert results["envoyer"] is True assert results["nova"] is True assert mock_fetch.call_count == 4 def test_fetch_all_services_force(self, external_docs_fetcher): """Test fetching all services with force flag.""" # Create valid cache for one service import time external_docs_fetcher.save_cache_metadata("forge", {"cached_at": time.time()}) with patch.object(external_docs_fetcher, 'fetch_laravel_service_docs') as mock_fetch: mock_fetch.return_value = True # Force should fetch all services regardless of cache external_docs_fetcher.fetch_all_services(force=True) # All services should be attempted assert mock_fetch.call_count == 4 # forge, vapor, envoyer, nova def test_create_placeholder_documentation_vapor(self, external_docs_fetcher, temp_dir): """Test creating placeholder documentation for Vapor.""" service_config = { "name": "Laravel Vapor", "base_url": "https://docs.vapor.build", "sections": ["getting-started", "projects", "deployments"] } service_dir = external_docs_fetcher.get_service_cache_path("vapor") result = external_docs_fetcher._create_placeholder_documentation("vapor", service_config, service_dir) assert result is True # Check that files were created assert (service_dir / "getting-started.md").exists() assert (service_dir / "projects.md").exists() assert (service_dir / "deployments.md").exists() # Check content of getting-started content = (service_dir / "getting-started.md").read_text() assert "Laravel Vapor - Getting Started" in content assert "requires authentication to access" in content assert "Setting up serverless Laravel applications" in content assert "Configuring AWS Lambda deployment" in content # Check metadata was saved metadata_path = external_docs_fetcher.get_cache_metadata_path("vapor") assert metadata_path.exists() metadata = json.loads(metadata_path.read_text()) assert metadata["type"] == "placeholder" assert metadata["service"] == "vapor" assert metadata["success_rate"] == 1.0 def test_create_placeholder_documentation_envoyer(self, external_docs_fetcher, temp_dir): """Test creating placeholder documentation for Envoyer.""" service_config = { "name": "Laravel Envoyer", "base_url": "https://docs.envoyer.io/docs/1.0", "sections": ["getting-started", "projects", "deployments"] } service_dir = external_docs_fetcher.get_service_cache_path("envoyer") result = external_docs_fetcher._create_placeholder_documentation("envoyer", service_config, service_dir) assert result is True # Check that files were created assert (service_dir / "getting-started.md").exists() assert (service_dir / "projects.md").exists() assert (service_dir / "deployments.md").exists() # Check content of deployments content = (service_dir / "deployments.md").read_text() assert "Laravel Envoyer - Deployments" in content assert "requires authentication to access" in content assert "Configuring deployment hooks" in content assert "Managing deployment history" in content # Check URL replacement assert "Visit [Laravel Envoyer](https://docs.envoyer.io)" in content def test_create_placeholder_documentation_generic_sections(self, external_docs_fetcher, temp_dir): """Test creating placeholder documentation with generic sections.""" service_config = { "name": "Test Service", "base_url": "https://test.example.com/docs", "sections": ["api-reference", "custom-section"] } service_dir = external_docs_fetcher.get_service_cache_path("test_service") result = external_docs_fetcher._create_placeholder_documentation("test_service", service_config, service_dir) assert result is True # Check that files were created assert (service_dir / "api-reference.md").exists() assert (service_dir / "custom-section.md").exists() # Check generic content content = (service_dir / "api-reference.md").read_text() assert "Test Service - Api Reference" in content assert "This section covers api reference functionality" in content assert "requires authentication to access" in content # Should not have specific use cases for unknown service/section assert "Setting up serverless" not in content assert "zero-downtime deployment" not in content class TestDocumentationAutoDiscovery: """Test auto-discovery system.""" def test_init(self): """Test DocumentationAutoDiscovery initialization.""" discovery = DocumentationAutoDiscovery(max_retries=5, request_delay=2.0) assert discovery.max_retries == 5 assert discovery.request_delay == 2.0 @patch('docs_updater.urllib.request.urlopen') def test_discover_forge_sections(self, mock_urlopen): """Test Forge section discovery.""" mock_html = """ <html> <body> <a href="/docs/introduction">Introduction</a> <a href="/docs/servers/management">Server Management</a> <a href="/docs/sites/deployment">Site Deployment</a> <a href="https://external.com">External Link</a> </body> </html> """ mock_response = MagicMock() mock_response.read.return_value = mock_html.encode() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response discovery = DocumentationAutoDiscovery() config = {"base_url": "https://forge.laravel.com/docs"} sections = discovery._discover_forge_sections(config, {}) assert "introduction" in sections assert "servers/management" in sections assert "sites/deployment" in sections assert len(sections) == 3 @patch('docs_updater.urllib.request.urlopen') def test_retry_request_success(self, mock_urlopen): """Test successful retry request.""" mock_response = MagicMock() mock_response.read.return_value = b"success" mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response discovery = DocumentationAutoDiscovery() result = discovery._retry_request("https://example.com") assert result == b"success" @patch('docs_updater.urllib.request.urlopen') def test_retry_request_with_retry(self, mock_urlopen): """Test retry request with initial failure.""" # First call fails, second succeeds responses = [ urllib.error.HTTPError(url="test", code=500, msg="Server Error", hdrs=None, fp=None), MagicMock() ] success_response = MagicMock() success_response.read.return_value = b"success" success_response.__enter__ = lambda self: self success_response.__exit__ = lambda self, *args: None responses[1] = success_response mock_urlopen.side_effect = responses discovery = DocumentationAutoDiscovery() with patch('docs_updater.time.sleep'): # Speed up test result = discovery._retry_request("https://example.com") assert result == b"success" @patch('docs_updater.urllib.request.urlopen') def test_retry_request_404_no_retry(self, mock_urlopen): """Test that 404 errors are not retried.""" mock_urlopen.side_effect = urllib.error.HTTPError( url="test", code=404, msg="Not Found", hdrs=None, fp=None ) discovery = DocumentationAutoDiscovery() with pytest.raises(urllib.error.HTTPError): discovery._retry_request("https://example.com") @patch('docs_updater.urllib.request.urlopen') def test_retry_request_rate_limit(self, mock_urlopen): """Test retry request with rate limiting (429 error).""" # First two calls fail with 429, third succeeds responses = [ urllib.error.HTTPError(url="test", code=429, msg="Too Many Requests", hdrs=None, fp=None), urllib.error.HTTPError(url="test", code=429, msg="Too Many Requests", hdrs=None, fp=None), MagicMock() ] success_response = MagicMock() success_response.read.return_value = b"success" success_response.__enter__ = lambda self: self success_response.__exit__ = lambda self, *args: None responses[2] = success_response mock_urlopen.side_effect = responses discovery = DocumentationAutoDiscovery(max_retries=3) with patch('docs_updater.time.sleep'), \ patch('docs_updater.random.uniform', return_value=1.0): result = discovery._retry_request("https://example.com") assert result == b"success" @patch('docs_updater.urllib.request.urlopen') def test_retry_request_server_error(self, mock_urlopen): """Test retry request with server errors (5xx).""" # First call fails with 500, second succeeds responses = [ urllib.error.HTTPError(url="test", code=503, msg="Service Unavailable", hdrs=None, fp=None), MagicMock() ] success_response = MagicMock() success_response.read.return_value = b"success" success_response.__enter__ = lambda self: self success_response.__exit__ = lambda self, *args: None responses[1] = success_response mock_urlopen.side_effect = responses discovery = DocumentationAutoDiscovery() with patch('docs_updater.time.sleep'), \ patch('docs_updater.random.uniform', return_value=1.0): result = discovery._retry_request("https://example.com") assert result == b"success" @patch('docs_updater.urllib.request.urlopen') def test_retry_request_max_retries_exceeded(self, mock_urlopen): """Test retry request when max retries are exceeded.""" mock_urlopen.side_effect = Exception("Network error") discovery = DocumentationAutoDiscovery(max_retries=2) with patch('docs_updater.time.sleep'), \ patch('docs_updater.random.uniform', return_value=1.0): with pytest.raises(Exception) as exc_info: discovery._retry_request("https://example.com") assert "Network error" in str(exc_info.value) def test_discover_sections_auto_discovery_disabled(self): """Test discover sections when auto-discovery is disabled.""" discovery = DocumentationAutoDiscovery() service_config = {"auto_discovery": False} sections = discovery.discover_sections("forge", service_config) assert sections == [] def test_discover_nova_sections(self): """Test Nova section discovery.""" mock_html = """ <html> <body> <a href="/docs/v4/installation">Installation</a> <a href="/docs/v4/resources">Resources</a> <a href="/docs/v4/filters">Filters</a> </body> </html> """ discovery = DocumentationAutoDiscovery() config = {"base_url": "https://nova.laravel.com/docs"} with patch.object(discovery, '_retry_request', return_value=mock_html.encode()): sections = discovery._discover_nova_sections(config, {}) assert "installation" in sections assert "resources" in sections assert "filters" in sections def test_discover_vapor_sections(self): """Test Vapor section discovery.""" mock_html = """ <html> <body> <a href="/docs/introduction">Introduction</a> <a href="/docs/projects/environments">Environments</a> <a href="/docs/projects/deployments">Deployments</a> </body> </html> """ discovery = DocumentationAutoDiscovery() config = {"base_url": "https://vapor.laravel.com/docs"} with patch.object(discovery, '_retry_request', return_value=mock_html.encode()): sections = discovery._discover_vapor_sections(config, {}) assert "docs/introduction" in sections assert "docs/projects/environments" in sections assert "docs/projects/deployments" in sections def test_discover_envoyer_sections(self): """Test Envoyer section discovery.""" mock_html = """ <html> <body> <ul class="table-of-contents"> <li><a href="/docs/introduction">Introduction</a></li> <li><a href="/docs/servers">Servers</a></li> <li><a href="/docs/deployments">Deployments</a></li> </ul> </body> </html> """ discovery = DocumentationAutoDiscovery() config = {"base_url": "https://envoyer.io/docs"} with patch.object(discovery, '_retry_request') as mock_retry: # First call returns the main page HTML # Subsequent calls return valid content for each section # Content must be >500 chars and contain envoyer-related keywords valid_content = """<html><body> <h1>Envoyer Documentation</h1> <p>Envoyer is a deployment tool for PHP applications that provides zero downtime deployments.</p> <p>This section covers the deployment process, server management, and project configuration.</p> <p>You can configure deployment hooks, manage your servers, set up notifications, and connect your repository.</p> <p>Envoyer ensures your application deployments are smooth and reliable with features like health checks and rollbacks.</p> <p>This documentation will guide you through all aspects of using Envoyer for your deployment needs.</p> <p>Learn about managing projects, configuring servers, setting up deployment scripts, and monitoring your applications.</p> </body></html>""" mock_retry.side_effect = [ mock_html.encode(), # Main page valid_content.encode(), # introduction valid_content.encode(), # servers valid_content.encode() # deployments ] sections = discovery._discover_envoyer_sections(config, {}) assert "introduction" in sections assert "servers" in sections assert "deployments" in sections def test_discover_sections_unknown_service(self): """Test discover sections with unknown service.""" discovery = DocumentationAutoDiscovery() service_config = {"auto_discovery": True} sections = discovery.discover_sections("unknown_service", service_config) assert sections == [] @patch('docs_updater.urllib.request.urlopen') def test_discover_sections_with_exception(self, mock_urlopen): """Test discover sections when exception occurs.""" mock_urlopen.side_effect = Exception("Discovery failed") discovery = DocumentationAutoDiscovery() service_config = {"auto_discovery": True, "base_url": "https://forge.laravel.com/docs"} sections = discovery.discover_sections("forge", service_config) assert sections == [] class TestMultiSourceDocsUpdater: """Test multi-source documentation updater.""" def test_init(self, temp_dir): """Test MultiSourceDocsUpdater initialization.""" updater = MultiSourceDocsUpdater(temp_dir, "12.x") assert updater.target_dir == temp_dir assert updater.version == "12.x" assert isinstance(updater.core_updater, DocsUpdater) assert isinstance(updater.external_fetcher, ExternalDocsFetcher) @patch.object(DocsUpdater, 'update') def test_update_core_docs(self, mock_update, multi_source_updater): """Test core documentation update.""" mock_update.return_value = True result = multi_source_updater.update_core_docs(force=False) assert result is True mock_update.assert_called_once_with(force=False) @patch.object(ExternalDocsFetcher, 'fetch_laravel_service_docs') def test_update_external_docs_specific_services(self, mock_fetch, multi_source_updater): """Test external documentation update for specific services.""" mock_fetch.return_value = True result = multi_source_updater.update_external_docs(services=["forge"], force=False) assert result == {"forge": True} mock_fetch.assert_called_once_with("forge") @patch.object(ExternalDocsFetcher, 'fetch_all_services') def test_update_external_docs_all_services(self, mock_fetch_all, multi_source_updater): """Test external documentation update for all services.""" mock_fetch_all.return_value = {"forge": True, "vapor": False} result = multi_source_updater.update_external_docs(force=False) assert result == {"forge": True, "vapor": False} mock_fetch_all.assert_called_once_with(force=False) def test_update_external_docs_unknown_service(self, multi_source_updater): """Test external documentation update with unknown service.""" result = multi_source_updater.update_external_docs(services=["unknown_service"]) assert result == {"unknown_service": False} @patch.object(CommunityPackageFetcher, 'list_available_packages') @patch.object(ExternalDocsFetcher, 'get_cache_metadata_path') @patch.object(ExternalDocsFetcher, 'get_service_info') @patch.object(ExternalDocsFetcher, 'is_cache_valid') @patch.object(ExternalDocsFetcher, 'list_available_services') @patch.object(DocsUpdater, 'read_local_metadata') def test_get_all_documentation_status_complete(self, mock_core_metadata, mock_list_services, mock_is_cache_valid, mock_get_service_info, mock_get_cache_path, mock_list_packages, multi_source_updater): """Test getting complete documentation status.""" # Mock core documentation status mock_core_metadata.return_value = { "sync_time": "2024-01-15T10:00:00Z", "commit_sha": "abc123" } # Mock external services mock_list_services.return_value = ["forge", "vapor"] mock_is_cache_valid.return_value = True mock_get_service_info.side_effect = [ {"name": "Laravel Forge", "type": "laravel_service"}, {"name": "Laravel Vapor", "type": "laravel_service"} ] # Mock cache metadata paths mock_metadata_path = Mock() mock_metadata_path.exists.return_value = True # Mock stat() to return an object with st_mtime mock_stat = Mock() mock_stat.st_mtime = 1234567890 mock_metadata_path.stat.return_value = mock_stat mock_get_cache_path.return_value = mock_metadata_path # Mock metadata file content with patch('builtins.open', mock_open(read_data='{"cached_at": 1234567890, "success_rate": "100%"}')): with patch('json.load', return_value={"cached_at": 1234567890, "success_rate": "100%"}): # Mock packages mock_list_packages.return_value = [] status = multi_source_updater.get_all_documentation_status() assert "core" in status assert status["core"]["version"] == "12.x" assert status["core"]["available"] is True assert status["core"]["last_updated"] == "2024-01-15T10:00:00Z" assert status["core"]["commit_sha"] == "abc123" assert "external" in status assert "forge" in status["external"] assert status["external"]["forge"]["name"] == "Laravel Forge" assert status["external"]["forge"]["cache_valid"] is True assert "vapor" in status["external"] assert status["external"]["vapor"]["name"] == "Laravel Vapor" @patch.object(CommunityPackageFetcher, 'list_available_packages') @patch.object(ExternalDocsFetcher, 'get_service_info') @patch.object(ExternalDocsFetcher, 'is_cache_valid') @patch.object(ExternalDocsFetcher, 'list_available_services') @patch.object(DocsUpdater, 'read_local_metadata') def test_get_all_documentation_status_no_metadata(self, mock_core_metadata, mock_list_services, mock_is_cache_valid, mock_get_service_info, mock_list_packages, multi_source_updater): """Test status when no metadata exists.""" # Mock no core metadata mock_core_metadata.return_value = {} # Mock external services with no cache mock_list_services.return_value = ["forge"] mock_is_cache_valid.return_value = False mock_get_service_info.return_value = {"name": "Laravel Forge", "type": "laravel_service"} # Mock packages mock_list_packages.return_value = [] # Mock non-existent metadata file with patch.object(multi_source_updater.external_fetcher, 'get_cache_metadata_path') as mock_path: mock_metadata_path = Mock() mock_metadata_path.exists.return_value = False mock_path.return_value = mock_metadata_path status = multi_source_updater.get_all_documentation_status() assert status["core"]["available"] is False assert status["external"]["forge"]["cache_valid"] is False assert status["external"]["forge"]["last_fetched"] == "never" @patch.object(CommunityPackageFetcher, 'list_available_packages') @patch.object(ExternalDocsFetcher, 'list_available_services') @patch.object(DocsUpdater, 'read_local_metadata') def test_get_all_documentation_status_with_errors(self, mock_core_metadata, mock_list_services, mock_list_packages, multi_source_updater): """Test status when errors occur.""" # Mock core metadata error mock_core_metadata.side_effect = Exception("Core metadata error") # Mock external services mock_list_services.return_value = ["forge"] # Mock packages mock_list_packages.return_value = [] # Mock service info error with patch.object(multi_source_updater.external_fetcher, 'get_service_info', side_effect=Exception("Service info error")): status = multi_source_updater.get_all_documentation_status() # Should handle errors gracefully assert "error" in status["core"] assert status["core"]["error"] == "Core metadata error" assert "error" in status["external"]["forge"] class TestVersionSupport: """Test version support functionality.""" @patch('docs_updater.urllib.request.urlopen') def test_get_supported_versions_success(self, mock_urlopen): """Test successful version fetching from GitHub API.""" mock_response_data = [ {"name": "6.x"}, {"name": "7.x"}, {"name": "8.x"}, {"name": "9.x"}, {"name": "10.x"}, {"name": "11.x"}, {"name": "12.x"}, {"name": "master"}, # Should be filtered out {"name": "feature-branch"}, # Should be filtered out ] mock_response = MagicMock() mock_response.read.return_value = json.dumps(mock_response_data).encode() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response versions = get_supported_versions() expected_versions = ["6.x", "7.x", "8.x", "9.x", "10.x", "11.x", "12.x"] assert versions == expected_versions @patch('docs_updater.urllib.request.urlopen') def test_get_supported_versions_no_versions_found(self, mock_urlopen): """Test version fetching when no valid versions found.""" mock_response_data = [ {"name": "master"}, {"name": "develop"}, {"name": "feature-branch"}, ] mock_response = MagicMock() mock_response.read.return_value = json.dumps(mock_response_data).encode() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response versions = get_supported_versions() # Should return fallback list assert "12.x" in versions assert len(versions) > 0 @patch('docs_updater.urllib.request.urlopen') def test_get_supported_versions_old_versions_filtered(self, mock_urlopen): """Test that versions older than 6.x are filtered out.""" mock_response_data = [ {"name": "4.x"}, # Should be filtered out {"name": "5.x"}, # Should be filtered out {"name": "6.x"}, {"name": "7.x"}, ] mock_response = MagicMock() mock_response.read.return_value = json.dumps(mock_response_data).encode() mock_response.__enter__ = lambda self: self mock_response.__exit__ = lambda self, *args: None mock_urlopen.return_value = mock_response versions = get_supported_versions() assert "4.x" not in versions assert "5.x" not in versions assert "6.x" in versions assert "7.x" in versions @patch('docs_updater.urllib.request.urlopen') def test_get_supported_versions_api_error(self, mock_urlopen): """Test version fetching with API error (fallback to hardcoded list).""" mock_urlopen.side_effect = Exception("API Error") versions = get_supported_versions() # Should return fallback list assert isinstance(versions, list) assert len(versions) > 0 assert "12.x" in versions def test_get_cached_supported_versions(self): """Test cached version support.""" # Clear cache first import docs_updater docs_updater._SUPPORTED_VERSIONS_CACHE = None with patch('docs_updater.get_supported_versions', return_value=["test_version"]) as mock_get: # First call should fetch versions1 = get_cached_supported_versions() assert versions1 == ["test_version"] assert mock_get.call_count == 1 # Second call should use cache versions2 = get_cached_supported_versions() assert versions2 == ["test_version"] assert mock_get.call_count == 1 # Not called again def test_default_version_is_latest(self): """Test that default version is the latest supported version.""" # DEFAULT_VERSION should be the last item in SUPPORTED_VERSIONS assert DEFAULT_VERSION == SUPPORTED_VERSIONS[-1] @pytest.mark.slow class TestNetworkOperations: """Test network operations (marked as slow).""" @patch('docs_updater.urllib.request.urlopen') def test_download_with_network_issues(self, mock_urlopen, docs_updater_instance): """Test download handling with various network issues.""" # Simulate network timeout, then success responses = [ Exception("Network timeout"), MagicMock() ] success_response = MagicMock() success_response.__enter__ = lambda self: self success_response.__exit__ = lambda self, *args: None responses[1] = success_response mock_urlopen.side_effect = responses # Should retry and handle the error gracefully with patch('docs_updater.time.sleep'), \ patch('docs_updater.tempfile.TemporaryDirectory'), \ patch('docs_updater.zipfile.ZipFile'): try: docs_updater_instance.download_documentation() except Exception: # Expected to fail after retries, but shouldn't crash the application pass class TestMainFunction: """Test main function and CLI.""" def test_parse_arguments_default(self): """Test argument parsing with defaults.""" import sys test_args = ['docs_updater.py'] with patch.object(sys, 'argv', test_args): from docs_updater import parse_arguments args = parse_arguments() assert args.version == '12.x' # Should use DEFAULT_VERSION assert args.target_dir == './docs' assert args.force is False assert args.check_only is False assert args.services is None def test_parse_arguments_with_options(self): """Test argument parsing with options.""" import sys test_args = [ 'docs_updater.py', '--version', '11.x', '--target-dir', '/custom/path', '--force', '--services', 'forge', 'vapor' ] with patch.object(sys, 'argv', test_args): from docs_updater import parse_arguments args = parse_arguments() assert args.version == '11.x' assert args.target_dir == '/custom/path' assert args.force is True assert args.services == ['forge', 'vapor'] def test_parse_arguments_status(self): """Test argument parsing for status flag.""" import sys test_args = ['docs_updater.py', '--status'] with patch.object(sys, 'argv', test_args): from docs_updater import parse_arguments args = parse_arguments() assert args.status is True def test_parse_arguments_list_services(self): """Test argument parsing for list-services flag.""" import sys test_args = ['docs_updater.py', '--list-services'] with patch.object(sys, 'argv', test_args): from docs_updater import parse_arguments args = parse_arguments() assert args.list_services is True @patch('docs_updater.MultiSourceDocsUpdater') def test_main_update_default(self, mock_updater_class): """Test main function with default update.""" import sys test_args = ['docs_updater.py'] mock_updater = MagicMock() mock_updater.update_all.return_value = { "core": True, "external": {"forge": True, "vapor": True} } mock_updater_class.return_value = mock_updater with patch.object(sys, 'argv', test_args), \ patch('builtins.print'): from docs_updater import main result = main() assert result == 0 mock_updater.update_all.assert_called_once_with(force_core=False, force_external=False, force_packages=False) @patch('docs_updater.MultiSourceDocsUpdater') def test_main_status_command(self, mock_updater_class): """Test main function with status command.""" import sys test_args = ['docs_updater.py', '--status'] mock_updater = MagicMock() mock_updater.get_all_documentation_status.return_value = { "core": { "available": True, "last_updated": "2024-01-01T12:00:00Z", "commit_sha": "abc123" }, "external": { "forge": { "available": True, "name": "Laravel Forge", "cache_valid": True, "type": "laravel_service", "success_rate": 1.0 # Numeric value, not string } }, "packages": {} } mock_updater_class.return_value = mock_updater with patch.object(sys, 'argv', test_args), \ patch('builtins.print'): from docs_updater import main result = main() assert result == 0 mock_updater.get_all_documentation_status.assert_called() @patch('docs_updater.ExternalDocsFetcher') def test_main_list_services_command(self, mock_fetcher_class): """Test main function with list-services command.""" import sys test_args = ['docs_updater.py', '--list-services'] mock_fetcher = MagicMock() mock_fetcher.list_available_services.return_value = ["forge", "vapor"] mock_fetcher_class.return_value = mock_fetcher with patch.object(sys, 'argv', test_args), \ patch('builtins.print'): from docs_updater import main result = main() assert result == 0 mock_fetcher.list_available_services.assert_called_once() @patch('docs_updater.MultiSourceDocsUpdater') def test_main_update_external_service(self, mock_updater_class): """Test main function updating external service.""" import sys test_args = ['docs_updater.py', '--services', 'forge'] mock_updater = MagicMock() # The --services parameter now calls update_all() instead of update_external_docs() mock_updater.update_all.return_value = { 'core': {'11.x': True}, 'external': {'forge': True}, 'packages': {} } mock_updater_class.return_value = mock_updater with patch.object(sys, 'argv', test_args), \ patch('builtins.print'): from docs_updater import main result = main() assert result == 0 mock_updater.update_all.assert_called_with(force_core=False, force_external=False, force_packages=False) def test_main_with_exception(self): """Test main function handling exceptions.""" import sys test_args = ['docs_updater.py'] with patch.object(sys, 'argv', test_args), \ patch('docs_updater.MultiSourceDocsUpdater', side_effect=Exception("Test error")), \ patch('builtins.print'): from docs_updater import main try: main() # If main() doesn't catch the exception, it will propagate assert False, "Expected exception was not raised" except Exception as e: # This is expected - the exception from MultiSourceDocsUpdater propagates assert str(e) == "Test error"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/brianirish/laravel-mcp-companion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server