""" Test JavaScript URL configuration consistency This tests that the JavaScript URL configuration matches the backend routes """ import re from pathlib import Path import pytest from local_deep_research.web.routes.route_registry import ( get_all_routes, ) class TestJavaScriptURLConfiguration: """Test that JavaScript URL configuration matches backend routes""" @pytest.fixture def js_urls_content(self): """Load the JavaScript URLs configuration file""" js_file_path = ( Path(__file__).parent.parent.parent / "src" / "local_deep_research" / "web" / "static" / "js" / "config" / "urls.js" ) with open(js_file_path, "r") as f: return f.read() def extract_js_urls(self, js_content): """Extract URL patterns from JavaScript content""" urls = {} # More robust extraction pattern that handles multiline and comments # Extract the URLS object urls_match = re.search( r"const\s+URLS\s*=\s*\{(.*?)\n\};", js_content, re.DOTALL ) if not urls_match: return urls urls_content = urls_match.group(1) # Extract each section sections = { "API": r"API:\s*\{(.*?)\n\s*\}", "PAGES": r"PAGES:\s*\{(.*?)\n\s*\}", "HISTORY_API": r"HISTORY_API:\s*\{(.*?)\n\s*\}", "SETTINGS_API": r"SETTINGS_API:\s*\{(.*?)\n\s*\}", "METRICS_API": r"METRICS_API:\s*\{(.*?)\n\s*\}", } for section_name, section_pattern in sections.items(): section_match = re.search(section_pattern, urls_content, re.DOTALL) if section_match: section_content = section_match.group(1) # Extract key-value pairs, handling comments and multiline kv_pattern = r"(\w+):\s*'([^']+)'(?:\s*,|\s*(?://[^\n]*)?\s*$)" for match in re.finditer(kv_pattern, section_content): key, url = match.groups() urls[f"{section_name}.{key}"] = url return urls def normalize_url_pattern(self, url): """Normalize URL patterns for comparison""" # Replace {id} with Flask-style or url = re.sub(r"\{id\}", "", url) url = re.sub(r"\{key\}", "", url) url = re.sub(r"\{(\w+)\}", r"<\1>", url) return url def test_js_urls_exist(self, js_urls_content): """Test that the JavaScript URLs file exists and has content""" assert js_urls_content assert "URLS" in js_urls_content assert "URLBuilder" in js_urls_content def test_critical_api_urls_match_backend(self, js_urls_content): """Test that critical API URLs in JS match backend routes""" js_urls = self.extract_js_urls(js_urls_content) backend_routes = get_all_routes() # Create a map of backend routes for easy lookup backend_map = {} for route in backend_routes: # Normalize the path for comparison path = route["path"] backend_map[path] = route # Critical URLs that must match critical_mappings = [ ("API.START_RESEARCH", "/api/start_research"), ("API.RESEARCH_STATUS", "/api/research//status"), ("API.RESEARCH_DETAILS", "/api/research/"), ("API.RESEARCH_LOGS", "/api/research//logs"), ("API.RESEARCH_REPORT", "/api/report/"), ("API.TERMINATE_RESEARCH", "/api/terminate/"), ("API.DELETE_RESEARCH", "/api/delete/"), ("API.HISTORY", "/history/api"), ("PAGES.HOME", "/"), ("PAGES.HISTORY", "/history/"), ("PAGES.SETTINGS", "/settings/"), ("PAGES.METRICS", "/metrics/"), ] for js_key, expected_backend_path in critical_mappings: assert js_key in js_urls, f"Missing JS URL key: {js_key}" js_url = js_urls[js_key] normalized_js_url = self.normalize_url_pattern(js_url) # Check if the URL exists in backend (with some flexibility for parameter names) found = False for backend_path in backend_map: if self.urls_match(normalized_js_url, backend_path): found = True break assert found, ( f"JS URL {js_key}='{js_url}' not found in backend routes" ) def urls_match(self, js_url, backend_url): """Check if two URLs match, accounting for parameter differences""" # Normalize both URLs js_parts = js_url.strip("/").split("/") backend_parts = backend_url.strip("/").split("/") if len(js_parts) != len(backend_parts): return False for js_part, backend_part in zip(js_parts, backend_parts, strict=False): # If either part is a parameter, consider it a match if "<" in js_part or "<" in backend_part: continue # Otherwise, parts must match exactly if js_part != backend_part: return False return True def test_url_builder_methods_exist(self, js_urls_content): """Test that URLBuilder utility methods are defined""" expected_methods = [ "build", "buildWithReplacements", "progressPage", "resultsPage", "detailsPage", "researchStatus", "researchDetails", "researchLogs", "researchReport", "terminateResearch", "deleteResearch", "extractResearchId", "getCurrentPageType", ] for method in expected_methods: assert ( f"{method}(" in js_urls_content or f"{method}:" in js_urls_content ), f"URLBuilder method '{method}' not found" def test_no_hardcoded_urls_in_pattern(self, js_urls_content): """Test that URLs follow consistent patterns""" js_urls = self.extract_js_urls(js_urls_content) # Check for consistent parameter naming for key, url in js_urls.items(): # Research ID parameters should use {id} if "research" in key.lower() and "{" in url: assert "{id}" in url or "{research_id}" in url, ( f"Research URL {key} should use {{id}} parameter: {url}" ) # No URLs should have trailing slashes except root paths if ( url != "/" and not url.endswith("/{id}") and not url.endswith("/{key}") ): # Allow trailing slash for directory-like paths if url.count("/") > 2: assert not url.endswith("//"), ( f"URL {key} has double slash: {url}" ) def test_api_url_patterns(self, js_urls_content): """Test that API URLs follow RESTful patterns""" js_urls = self.extract_js_urls(js_urls_content) api_urls = {k: v for k, v in js_urls.items() if k.startswith("API.")} for key, url in api_urls.items(): # API URLs should start with /api (with some exceptions) if key not in ["API.HISTORY", "API.HEALTH"]: # Known exceptions assert url.startswith("/api"), ( f"API URL {key} should start with /api: {url}" ) # Check REST patterns if "DELETE" in key: # Delete operations should have an ID parameter assert "{id}" in url, ( f"DELETE URL {key} should have ID parameter: {url}" ) if "SAVE" in key or "CREATE" in key: # Save/Create operations typically don't have ID in URL if ( "RATING" not in key ): # Exception for ratings which need research ID assert "{id}" not in url, ( f"CREATE/SAVE URL {key} shouldn't have ID in path: {url}" ) def test_settings_api_consistency(self, js_urls_content): """Test that settings API URLs are consistent""" js_urls = self.extract_js_urls(js_urls_content) settings_urls = { k: v for k, v in js_urls.items() if k.startswith("SETTINGS_API.") } # All settings API URLs should start with /settings/ for key, url in settings_urls.items(): assert url.startswith("/settings/"), ( f"Settings API URL {key} should start with /settings/: {url}" ) # Check that CRUD operations exist assert "SETTINGS_API.GET_SETTING" in js_urls assert "SETTINGS_API.UPDATE_SETTING" in js_urls assert "SETTINGS_API.DELETE_SETTING" in js_urls def test_metrics_api_consistency(self, js_urls_content): """Test that metrics API URLs are consistent""" js_urls = self.extract_js_urls(js_urls_content) metrics_urls = { k: v for k, v in js_urls.items() if k.startswith("METRICS_API.") } # All metrics API URLs should start with /metrics/ for key, url in metrics_urls.items(): assert url.startswith("/metrics/"), ( f"Metrics API URL {key} should start with /metrics/: {url}" ) # Check research-specific metrics URLs have ID parameter research_metrics = [ "RESEARCH", "RESEARCH_TIMELINE", "RESEARCH_SEARCH", "RATINGS_GET", "RATINGS_SAVE", "RESEARCH_COSTS", ] for metric in research_metrics: key = f"METRICS_API.{metric}" if key in js_urls: assert "{id}" in js_urls[key], ( f"Metrics URL {key} should have {{id}} parameter: {js_urls[key]}" ) def test_page_routes_consistency(self, js_urls_content): """Test that page routes are consistent""" js_urls = self.extract_js_urls(js_urls_content) # Pages with IDs should have {id} parameter id_pages = ["PROGRESS", "RESULTS", "DETAILS"] for page in id_pages: key = f"PAGES.{page}" if key in js_urls: assert "{id}" in js_urls[key], ( f"Page URL {key} should have {{id}} parameter: {js_urls[key]}" ) # Root pages should end with / root_pages = ["HOME", "HISTORY", "SETTINGS", "METRICS"] for page in root_pages: key = f"PAGES.{page}" if key in js_urls and page != "HOME": # HOME is just '/' assert js_urls[key].endswith("/"), ( f"Root page URL {key} should end with /: {js_urls[key]}" ) def test_url_builder_convenience_methods_match_urls(self, js_urls_content): """Test that URLBuilder convenience methods use correct URL constants""" # Extract method implementations method_checks = [ ("progressPage", "URLS.PAGES.PROGRESS"), ("resultsPage", "URLS.PAGES.RESULTS"), ("detailsPage", "URLS.PAGES.DETAILS"), ("researchStatus", "URLS.API.RESEARCH_STATUS"), ("researchDetails", "URLS.API.RESEARCH_DETAILS"), ("researchLogs", "URLS.API.RESEARCH_LOGS"), ("researchReport", "URLS.API.RESEARCH_REPORT"), ("terminateResearch", "URLS.API.TERMINATE_RESEARCH"), ("deleteResearch", "URLS.API.DELETE_RESEARCH"), ] for method_name, expected_url_const in method_checks: # Find the method implementation method_pattern = rf"{method_name}\([^)]*\)\s*\{{[^}}]*\}}" method_match = re.search(method_pattern, js_urls_content, re.DOTALL) assert method_match, f"Method {method_name} not found" method_body = method_match.group(0) assert expected_url_const in method_body, ( f"Method {method_name} should use {expected_url_const}" ) def test_no_duplicate_urls(self, js_urls_content): """Test that there are no duplicate URL patterns""" js_urls = self.extract_js_urls(js_urls_content) # Normalize URLs for comparison normalized_urls = {} for key, url in js_urls.items(): normalized = self.normalize_url_pattern(url) if normalized in normalized_urls: # Allow some known duplicates allowed_duplicates = [ # RESTful APIs use same URL with different methods ("SETTINGS_API.GET_SETTING", "SETTINGS_API.UPDATE_SETTING"), ( "SETTINGS_API.UPDATE_SETTING", "SETTINGS_API.DELETE_SETTING", ), ("SETTINGS_API.GET_SETTING", "SETTINGS_API.DELETE_SETTING"), ("METRICS_API.RATINGS_GET", "METRICS_API.RATINGS_SAVE"), # History API might share patterns with main API ("HISTORY_API", "API"), ] # Check if this is an allowed duplicate is_allowed = False for pattern1, pattern2 in allowed_duplicates: if ( key.startswith(pattern1) and normalized_urls[normalized].startswith(pattern2) ) or ( key.startswith(pattern2) and normalized_urls[normalized].startswith(pattern1) ): is_allowed = True break if not is_allowed: pytest.fail( f"Duplicate URL pattern found:\n" f" {key} = {url}\n" f" {normalized_urls[normalized]} = {url}\n" f" (normalized: {normalized})" ) normalized_urls[normalized] = key