7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
364 lines
14 KiB
Python
364 lines
14 KiB
Python
"""
|
|
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 <int:id> or <id>
|
|
url = re.sub(r"\{id\}", "<string:research_id>", url)
|
|
url = re.sub(r"\{key\}", "<path: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/<research_id>/status"),
|
|
("API.RESEARCH_DETAILS", "/api/research/<string:research_id>"),
|
|
("API.RESEARCH_LOGS", "/api/research/<string:research_id>/logs"),
|
|
("API.RESEARCH_REPORT", "/api/report/<string:research_id>"),
|
|
("API.TERMINATE_RESEARCH", "/api/terminate/<string:research_id>"),
|
|
("API.DELETE_RESEARCH", "/api/delete/<string:research_id>"),
|
|
("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
|