Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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