chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:55 +08:00
commit 7a0da7932b
2985 changed files with 1049377 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""API Tests for Local Deep Research."""
+120
View File
@@ -0,0 +1,120 @@
"""
Fixtures for library API tests.
Provides test data and helpers for testing library/collection/RAG endpoints.
"""
import hashlib
import uuid
import pytest
from local_deep_research.database.models.library import (
DocumentStatus,
)
@pytest.fixture
def sample_source_type_data():
"""Sample source type data for test documents."""
return {
"id": str(uuid.uuid4()),
"name": "test_upload",
"display_name": "Test Upload",
"description": "Test documents for API tests",
"icon": "fas fa-file",
}
@pytest.fixture
def sample_collection_data():
"""Sample collection data for tests."""
return {
"name": f"Test Collection {uuid.uuid4().hex[:8]}",
"description": "A test collection for API tests",
"type": "user_uploads",
}
@pytest.fixture
def sample_document_data():
"""Sample document data for tests."""
content = "This is sample text content for testing API endpoints."
return {
"id": str(uuid.uuid4()),
"document_hash": hashlib.sha256(content.encode()).hexdigest(),
"file_size": len(content),
"file_type": "text",
"text_content": content,
"title": "Test Document for API",
"status": DocumentStatus.COMPLETED,
}
def create_test_collection(client, name=None, description=None):
"""
Helper to create a collection via API.
Args:
client: Flask test client (authenticated)
name: Collection name (optional, auto-generated if not provided)
description: Collection description (optional)
Returns:
dict: Created collection data or None if failed
"""
payload = {
"name": name or f"Test Collection {uuid.uuid4().hex[:8]}",
"description": description or "Test collection",
"type": "user_uploads",
}
response = client.post(
"/library/api/collections",
json=payload,
content_type="application/json",
)
if response.status_code == 200:
data = response.get_json()
if data.get("success"):
return data.get("collection")
return None
def delete_test_collection(client, collection_id):
"""
Helper to delete a collection via API.
Args:
client: Flask test client (authenticated)
collection_id: ID of collection to delete
Returns:
bool: True if deleted successfully
"""
response = client.delete(f"/library/api/collections/{collection_id}")
return response.status_code == 200
@pytest.fixture
def create_collection_helper(authenticated_client):
"""Fixture that returns a helper function to create collections."""
created_collections = []
def _create(name=None, description=None):
collection = create_test_collection(
authenticated_client, name, description
)
if collection:
created_collections.append(collection["id"])
return collection
yield _create
# Cleanup: delete all created collections
for coll_id in created_collections:
try:
delete_test_collection(authenticated_client, coll_id)
except Exception:
pass
+92
View File
@@ -0,0 +1,92 @@
"""
Quick fix to ensure search engine settings are loaded for test user.
"""
import os
import sys
from pathlib import Path
from loguru import logger
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from local_deep_research.database.encrypted_db import db_manager
from local_deep_research.settings.manager import (
SettingsManager,
)
def fix_search_engines_for_user(username: str, password: str):
"""Ensure search engine settings are loaded for a user."""
logger.info(f"Fixing search engine settings for user: {username}")
# Open user database
engine = db_manager.open_user_database(username, password)
if not engine:
logger.error("Failed to open user database")
return False
# Get database session
Session = db_manager.Session
if not Session:
logger.error("Failed to get database session factory")
return False
with Session() as db_session:
try:
# Create settings manager
settings_manager = SettingsManager(db_session)
# Check if we need to load defaults
from local_deep_research.database.models import Setting
search_engine_count = (
db_session.query(Setting)
.filter(Setting.key.like("search.engine.%.display_name"))
.count()
)
if search_engine_count == 0:
logger.info(
"No search engine settings found. Loading defaults..."
)
settings_manager.load_from_defaults_file(commit=True)
logger.info("Default settings loaded successfully")
else:
logger.info(
f"Found {search_engine_count} search engine settings"
)
# Verify settings were loaded
search_engines = (
db_session.query(Setting)
.filter(Setting.key.like("search.engine.%.display_name"))
.all()
)
logger.info(f"Search engines after fix: {len(search_engines)}")
for engine in search_engines[:5]: # Show first 5
logger.info(f" - {engine.key}: {engine.value}")
return True
except Exception:
logger.exception("Failed to fix search engines")
db_session.rollback()
return False
if __name__ == "__main__":
import os
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "true"
# Fix for test user
fix_search_engines_for_user(
"testuser", "testpassword123"
) # pragma: allowlist secret
# Also fix for any other common test users
fix_search_engines_for_user("apitest_user", "apitest_pass123")
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Add research_mode column to existing databases.
This script adds the missing research_mode column to TokenUsage and SearchCall tables.
"""
import os
import sqlite3
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from loguru import logger
from local_deep_research.config.paths import get_user_data_directory
def add_research_mode_column(db_path: str):
"""Add research_mode column to tables if it doesn't exist."""
try:
conn = sqlite3.connect(db_path)
except Exception:
logger.exception(f"Error connecting to {db_path}")
return
try:
cursor = conn.cursor()
# Check if column exists in token_usage
cursor.execute("PRAGMA table_info(token_usage)")
columns = [col[1] for col in cursor.fetchall()]
if "research_mode" not in columns:
logger.info(
f"Adding research_mode column to token_usage in {db_path}"
)
cursor.execute(
"ALTER TABLE token_usage ADD COLUMN research_mode VARCHAR(50)"
)
conn.commit()
else:
logger.info(
f"research_mode column already exists in token_usage in {db_path}"
)
# Check if column exists in search_calls
cursor.execute("PRAGMA table_info(search_calls)")
columns = [col[1] for col in cursor.fetchall()]
if "research_mode" not in columns:
logger.info(
f"Adding research_mode column to search_calls in {db_path}"
)
cursor.execute(
"ALTER TABLE search_calls ADD COLUMN research_mode VARCHAR(50)"
)
conn.commit()
else:
logger.info(
f"research_mode column already exists in search_calls in {db_path}"
)
logger.info(f"Successfully updated {db_path}")
except Exception:
logger.exception(f"Error updating {db_path}")
finally:
conn.close()
def migrate_all_user_databases():
"""Migrate all user databases to add research_mode column."""
# Get base data directory
from local_deep_research.config.paths import get_data_directory
base_dir = get_data_directory()
# Look for user directories
user_dirs = []
if base_dir.exists():
for item in base_dir.iterdir():
if item.is_dir() and item.name.startswith("user_"):
user_dirs.append(item)
if not user_dirs:
logger.warning("No user directories found")
return
logger.info(f"Found {len(user_dirs)} user directories to migrate")
for user_dir in user_dirs:
db_path = user_dir / f"{user_dir.name}_encrypted.db"
if db_path.exists():
logger.info(f"Migrating database for {user_dir.name}")
add_research_mode_column(str(db_path))
else:
logger.warning(f"No database found in {user_dir}")
if __name__ == "__main__":
# Enable debug logging
logger.remove()
# diagnose=False: don't dump frame-local credentials in tracebacks
# (#4185 / #4384). This script touches user databases and SQLCipher
# passwords live in frame locals during migration.
logger.add(sys.stdout, level="INFO", diagnose=False)
# Allow unencrypted for testing
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "true"
# Run migration
migrate_all_user_databases()
# Also migrate specific test user if needed
if len(sys.argv) > 1:
username = sys.argv[1]
user_dir = get_user_data_directory(username)
db_path = user_dir / f"{user_dir.name}_encrypted.db"
if db_path.exists():
logger.info(f"Migrating specific user: {username}")
add_research_mode_column(str(db_path))
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Populate search engine settings in the database
"""
import os
import sys
from pathlib import Path
from loguru import logger
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from local_deep_research.database.models import Setting
from local_deep_research.utilities.db_utils import get_db_session
def populate_search_engines():
"""Populate search engine settings in the database"""
# Define search engines with their properties
search_engines = {
"searxng": {
"display_name": "SearXNG",
"description": "Privacy-focused metasearch engine",
"strengths": ["Privacy", "No tracking", "Multiple sources"],
},
"google": {
"display_name": "Google",
"description": "Google search engine",
"strengths": ["Comprehensive", "Fast", "Relevant results"],
},
"bing": {
"display_name": "Bing",
"description": "Microsoft Bing search engine",
"strengths": ["Good for technical queries", "Image search", "News"],
},
"duckduckgo": {
"display_name": "DuckDuckGo",
"description": "Privacy-focused search engine",
"strengths": ["Privacy", "No tracking", "Instant answers"],
},
"wikipedia": {
"display_name": "Wikipedia",
"description": "Wikipedia search",
"strengths": ["Encyclopedic content", "Reliable", "Detailed"],
},
"arxiv": {
"display_name": "arXiv",
"description": "Scientific paper repository",
"strengths": ["Academic papers", "Research", "Preprints"],
},
"pubmed": {
"display_name": "PubMed",
"description": "Medical research database",
"strengths": ["Medical research", "Clinical studies", "Healthcare"],
},
"semantic_scholar": {
"display_name": "Semantic Scholar",
"description": "AI-powered research tool",
"strengths": [
"Academic search",
"Citation analysis",
"AI insights",
],
},
}
session = get_db_session()
try:
# Add web search engines
for engine_name, props in search_engines.items():
# Display name
display_setting = Setting(
key=f"search.engine.web.{engine_name}.display_name",
value=props["display_name"],
type="SEARCH",
category="search",
name=f"{engine_name} Display Name",
description=f"Display name for {engine_name}",
)
existing = (
session.query(Setting)
.filter_by(key=display_setting.key)
.first()
)
if not existing:
session.add(display_setting)
logger.info(f"Added {display_setting.key}")
# Description
desc_setting = Setting(
key=f"search.engine.web.{engine_name}.description",
value=props["description"],
type="SEARCH",
category="search",
name=f"{engine_name} Description",
description=f"Description for {engine_name}",
)
existing = (
session.query(Setting).filter_by(key=desc_setting.key).first()
)
if not existing:
session.add(desc_setting)
logger.info(f"Added {desc_setting.key}")
# Strengths
strengths_setting = Setting(
key=f"search.engine.web.{engine_name}.strengths",
value=props["strengths"],
type="SEARCH",
category="search",
name=f"{engine_name} Strengths",
description=f"Strengths of {engine_name}",
)
existing = (
session.query(Setting)
.filter_by(key=strengths_setting.key)
.first()
)
if not existing:
session.add(strengths_setting)
logger.info(f"Added {strengths_setting.key}")
# Commit all changes
session.commit()
logger.info("Successfully populated search engine settings")
# Verify by querying
search_settings = (
session.query(Setting)
.filter(
Setting.type == "SEARCH", Setting.key.contains("display_name")
)
.all()
)
logger.info(f"Total search engine settings: {len(search_settings)}")
for setting in search_settings:
logger.info(f" {setting.key}: {setting.value}")
except Exception:
logger.exception("Error populating search engines")
session.rollback()
finally:
session.close()
if __name__ == "__main__":
# Need to set this for test user database
os.environ["LDR_CURRENT_USER"] = "testuser"
populate_search_engines()
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
Runner script for API tests.
Usage:
python run_api_tests.py # Run all API tests
python run_api_tests.py search # Run search engines test only
"""
import subprocess
import sys
from pathlib import Path
def run_all_tests():
"""Run all API tests."""
print("Running all API tests...")
result = subprocess.run(
[sys.executable, "test_all_apis.py"], cwd=str(Path(__file__).parent)
)
return result.returncode
def run_search_engines_test():
"""Run search engines API test."""
print("Running search engines API test...")
result = subprocess.run(
[sys.executable, "test_search_engines_api.py"],
cwd=str(Path(__file__).parent),
)
return result.returncode
def main():
"""Main entry point."""
if len(sys.argv) > 1 and sys.argv[1] == "search":
return run_search_engines_test()
return run_all_tests()
if __name__ == "__main__":
sys.exit(main())
+53
View File
@@ -0,0 +1,53 @@
"""
Run basic API tests that are known to work.
"""
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "true"
from loguru import logger
from test_comprehensive_apis import (
test_auth_apis,
test_history_apis,
test_settings_apis,
)
def run_working_tests():
"""Run only the APIs that are known to work."""
logger.info("Running basic API tests...\n")
try:
# Initialize tester and authenticate
tester = test_auth_apis()
# Run working test suites
test_settings_apis(tester)
test_history_apis(tester)
logger.info("\n✅ Basic API tests passed successfully!")
return True
except Exception:
logger.exception("Test failed")
return False
if __name__ == "__main__":
# Setup logging
logger.remove()
# diagnose=False: don't dump frame-local credentials in tracebacks
# (#4185 / #4384). API tests hold api_key and password locals.
logger.add(
sys.stdout,
level="INFO",
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
diagnose=False,
)
success = run_working_tests()
sys.exit(0 if success else 1)
+254
View File
@@ -0,0 +1,254 @@
"""
Comprehensive API Tests for Local Deep Research.
This test suite checks all API endpoints for proper authentication,
response formats, and basic functionality.
"""
import json
from loguru import logger
class TestAllAPIs:
"""Test suite for all API endpoints."""
def test_auth_apis(self, authenticated_client):
"""Test authentication APIs."""
# Check auth status
response = authenticated_client.get("/auth/check")
assert response.status_code == 200
data = json.loads(response.data)
assert data["authenticated"] is True
assert "username" in data
# Test integrity check
response = authenticated_client.get("/auth/integrity-check")
assert response.status_code == 200
data = json.loads(response.data)
assert "integrity" in data
assert "username" in data
def test_settings_apis(self, authenticated_client):
"""Test settings APIs."""
# Get all settings
response = authenticated_client.get("/settings/api")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "success"
assert "settings" in data
# Get categories
response = authenticated_client.get("/settings/api/categories")
assert response.status_code == 200
data = json.loads(response.data)
assert "categories" in data
# Get types
response = authenticated_client.get("/settings/api/types")
assert response.status_code == 200
data = json.loads(response.data)
assert "types" in data
# Get available models
response = authenticated_client.get("/settings/api/available-models")
assert response.status_code == 200
data = json.loads(response.data)
assert (
"providers" in data or "models" in data
) # API might return either
# Get available search engines - THIS IS FAILING
logger.info("Testing search engines API...")
response = authenticated_client.get(
"/settings/api/available-search-engines"
)
logger.info(f"Search engines response: {response.status_code}")
if response.status_code != 200:
logger.error(f"Search engines error: {response.data}")
assert response.status_code == 200
data = json.loads(response.data)
assert "engines" in data or "engine_options" in data
# Get warnings
response = authenticated_client.get("/settings/api/warnings")
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(
data, (list, dict)
) # API might return list or dict with warnings
# Test get/set specific setting
response = authenticated_client.get("/settings/api/llm.temperature")
if response.status_code == 404:
# Setting doesn't exist, create it
response = authenticated_client.put(
"/settings/api/llm.temperature",
json={"value": 0.7},
content_type="application/json",
)
assert response.status_code in [200, 201]
# Get data location
response = authenticated_client.get("/settings/api/data-location")
assert response.status_code == 200
data = json.loads(response.data)
assert (
"data_directory" in data or "data_dir" in data
) # API might return either
assert "is_custom" in data
def test_research_apis(self, authenticated_client):
"""Test research APIs."""
# Get research history
response = authenticated_client.get("/history/api")
assert response.status_code == 200
data = json.loads(response.data)
assert "items" in data or "history" in data # API might return either
# Start a test research
research_data = {
"query": "API test research query",
"model": "gpt-3.5-turbo",
"search_engines": ["searxng"],
"local_context": 2000,
"web_context": 2000,
"temperature": 0.7,
}
response = authenticated_client.post(
"/research/api/start",
json=research_data,
content_type="application/json",
)
# The research start API might fail in test environment due to
# missing LLM configuration or other issues. We'll accept any
# response as long as it's a valid HTTP response.
assert response.status_code in [200, 400, 404, 500]
# If successful, test the other endpoints
if response.status_code == 200:
data = json.loads(response.data)
if "research_id" in data:
research_id = data["research_id"]
# Check status
response = authenticated_client.get(
f"/research/api/status/{research_id}"
)
assert response.status_code == 200
# Terminate research
response = authenticated_client.post(
f"/research/api/terminate/{research_id}"
)
assert response.status_code in [200, 404]
def test_metrics_apis(self, authenticated_client):
"""Test metrics APIs."""
# Get metrics summary
response = authenticated_client.get("/metrics/api/metrics")
assert response.status_code == 200
data = json.loads(response.data)
assert "metrics" in data or "summary" in data # API might return either
# Get enhanced metrics
response = authenticated_client.get("/metrics/api/metrics/enhanced")
assert response.status_code == 200
# Get star reviews
response = authenticated_client.get("/metrics/api/star-reviews")
assert response.status_code == 200
# Get pricing info
response = authenticated_client.get("/metrics/api/pricing")
assert response.status_code == 200
data = json.loads(response.data)
assert "pricing" in data or "models" in data # API might return either
# Get cost analytics
response = authenticated_client.get("/metrics/api/cost-analytics")
assert response.status_code == 200
def test_benchmark_apis(self, authenticated_client):
"""Test benchmark APIs."""
# Get benchmark history
response = authenticated_client.get("/benchmark/api/history")
assert response.status_code == 200
data = json.loads(response.data)
assert "runs" in data or "history" in data # API might return either
# Get saved configs
response = authenticated_client.get("/benchmark/api/configs")
assert response.status_code == 200
data = json.loads(response.data)
assert "configs" in data
# Check running benchmark
response = authenticated_client.get("/benchmark/api/running")
assert response.status_code == 200
data = json.loads(response.data)
assert (
"is_running" in data or "success" in data
) # API might return either
# Validate config
test_config = {
"name": "Test Config",
"queries": ["test query"],
"models": ["gpt-3.5-turbo"],
"search_engines": ["searxng"],
}
response = authenticated_client.post(
"/benchmark/api/validate-config",
json=test_config,
content_type="application/json",
)
assert response.status_code == 200
def test_history_apis(self, authenticated_client):
"""Test history APIs."""
response = authenticated_client.get("/history/api")
assert response.status_code == 200
data = json.loads(response.data)
assert "items" in data or "history" in data # API might return either
# Get the actual history list
history_data = data.get("items", data.get("history", []))
assert isinstance(history_data, list)
def test_config_apis(self, authenticated_client):
"""Test configuration APIs."""
# Get current config - api_bp is registered with /research/api prefix
response = authenticated_client.get(
"/research/api/settings/current-config"
)
assert response.status_code == 200
data = json.loads(response.data)
assert "success" in data # API returns success status
if data.get("success"):
assert "config" in data # Config is nested under config key
# Get public config - This endpoint may not exist
response = authenticated_client.get("/api/config")
if response.status_code == 404:
# Endpoint doesn't exist, skip test
pass
else:
assert response.status_code == 200
data = json.loads(response.data)
assert "version" in data or "available_models" in data
def test_health_check_apis(self, authenticated_client):
"""Test health check APIs."""
# Check Ollama status - the api_bp is registered with /research/api prefix
response = authenticated_client.get("/research/api/check/ollama_status")
assert response.status_code == 200
data = json.loads(response.data)
assert (
"running" in data or "available" in data
) # API might return either
# Check Ollama model
response = authenticated_client.get("/research/api/check/ollama_model")
# This might return 400 if no model specified
assert response.status_code in [200, 400]
+453
View File
@@ -0,0 +1,453 @@
#!/usr/bin/env python3
"""
Comprehensive test suite that tests ALL API endpoints in the codebase.
This ensures we have coverage for every single endpoint.
"""
import json
import time
import pytest
from loguru import logger
class TestAllEndpoints:
"""Comprehensive API test suite using Flask test client."""
def test_all_auth_endpoints(self, authenticated_client):
"""Test ALL authentication endpoints."""
logger.info("\n=== Testing ALL Authentication Endpoints ===")
# Test endpoints
endpoints = [
("GET", "/auth/check", None, 200),
("GET", "/auth/integrity-check", None, 200),
]
for method, endpoint, data, expected_status in endpoints:
if method == "GET":
response = authenticated_client.get(endpoint)
else:
response = authenticated_client.post(
endpoint,
json=data,
content_type="application/json",
)
assert response.status_code == expected_status, (
f"{endpoint} failed: {response.data}"
)
logger.info("✅ All Auth endpoints tested")
def test_all_settings_endpoints(self, authenticated_client):
"""Test ALL settings endpoints."""
logger.info("\n=== Testing ALL Settings Endpoints ===")
# Basic GET endpoints
get_endpoints = [
"/settings/api",
"/settings/api/categories",
"/settings/api/types",
"/settings/api/ui_elements",
"/settings/api/available-models",
"/settings/api/available-search-engines",
"/settings/api/warnings",
"/settings/api/ollama-status",
"/settings/api/rate-limiting/status",
"/settings/api/data-location",
]
for endpoint in get_endpoints:
response = authenticated_client.get(endpoint)
assert response.status_code == 200, (
f"{endpoint} failed: {response.data}"
)
# Test bulk get
response = authenticated_client.get(
"/settings/api/bulk?keys=llm.provider,llm.model"
)
assert response.status_code == 200, f"Bulk get failed: {response.data}"
# Test setting CRUD (use an allow-listed prefix so the
# namespace gate permits creation of this throwaway key).
test_key = "llm.endpoint_test_value"
test_value = {"value": "test_endpoint_123", "editable": True}
# Create/Update
response = authenticated_client.put(
f"/settings/api/{test_key}",
json=test_value,
content_type="application/json",
)
assert response.status_code in [200, 201], (
f"Create setting failed: {response.data}"
)
# Get specific setting
data = json.loads(response.data)
if isinstance(data, dict) and "setting" in data:
actual_key = data["setting"].get("key", test_key)
else:
actual_key = test_key
response = authenticated_client.get(f"/settings/api/{actual_key}")
assert response.status_code == 200, (
f"Get setting failed: {response.data}"
)
# Delete
response = authenticated_client.delete(f"/settings/api/{actual_key}")
assert response.status_code in [200, 204, 404], (
f"Delete setting failed: {response.data}"
)
# Test rate limiting reset
response = authenticated_client.post(
"/settings/api/rate-limiting/engines/test/reset"
)
assert response.status_code in [200, 404], (
f"Rate limit reset failed: {response.data}"
)
# Test cleanup
response = authenticated_client.post(
"/settings/api/rate-limiting/cleanup"
)
assert response.status_code == 200, (
f"Rate limit cleanup failed: {response.data}"
)
logger.info("✅ All Settings endpoints tested")
def test_all_research_endpoints(self, authenticated_client):
"""Test ALL research-related endpoints."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
logger.info("\n=== Testing ALL Research Endpoints ===")
# Set up logging.
try:
logger.level("MILESTONE", no=26, color="<magenta><bold>")
except (TypeError, ValueError):
pass # Level already registered
# Test history endpoint first
response = authenticated_client.get("/history/api")
assert response.status_code == 200, (
f"History API failed: {response.data}"
)
# Start research via main endpoint
research_data = {
"query": "Test all endpoints comprehensive",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama2",
"search_engine": "searxng",
"max_results": 5,
"time_period": "y",
"iterations": 1,
"questions_per_iteration": 2,
"strategy": "rapid",
}
# Test /api/start_research endpoint
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
assert response.status_code == 200, (
f"Start research failed: {response.data}"
)
data = json.loads(response.data)
assert "research_id" in data
research_id = data["research_id"]
# Give it a moment to start
time.sleep(0.5) # allow: unmarked-sleep
# Test various research endpoints
research_endpoints = [
("GET", f"/api/research/{research_id}", None, 200),
("GET", f"/api/research/{research_id}/status", None, 200),
("GET", f"/research/api/status/{research_id}", None, 200),
("GET", f"/history/status/{research_id}", None, 200),
("GET", f"/history/details/{research_id}", None, 200),
("GET", f"/history/log_count/{research_id}", None, 200),
]
for method, endpoint, req_data, expected_status in research_endpoints:
if method == "GET":
response = authenticated_client.get(endpoint)
else:
response = authenticated_client.post(
endpoint,
json=req_data,
content_type="application/json",
)
# Some endpoints might return 404 if research is still initializing
assert response.status_code in [expected_status, 404], (
f"{endpoint} failed: {response.data}"
)
# Test research API routes
response = authenticated_client.get(
"/research/api/settings/current-config"
)
assert response.status_code == 200, (
f"Current config failed: {response.data}"
)
# Test Ollama checks
response = authenticated_client.get("/research/api/check/ollama_status")
assert response.status_code == 200, (
f"Ollama status check failed: {response.data}"
)
# Terminate research
response = authenticated_client.post(f"/api/terminate/{research_id}")
assert response.status_code in [200, 400, 404], (
f"Terminate research failed: {response.data}"
)
# Test alternate terminate endpoint
response = authenticated_client.post(
f"/research/api/terminate/{research_id}"
)
assert response.status_code in [200, 400, 404], (
f"API terminate research failed: {response.data}"
)
logger.info("✅ All Research endpoints tested")
return research_id
def test_all_metrics_endpoints(self, authenticated_client):
"""Test ALL metrics endpoints."""
logger.info("\n=== Testing ALL Metrics Endpoints ===")
# Basic metrics endpoints
metrics_endpoints = [
"/metrics/api/metrics",
"/metrics/api/metrics/enhanced",
"/metrics/api/pricing",
"/metrics/api/cost-analytics",
"/metrics/api/star-reviews",
"/metrics/api/rate-limiting",
"/metrics/api/rate-limiting/current",
]
for endpoint in metrics_endpoints:
response = authenticated_client.get(endpoint)
assert response.status_code == 200, (
f"{endpoint} failed: {response.data}"
)
# Test specific model pricing
response = authenticated_client.get(
"/metrics/api/pricing/gpt-3.5-turbo"
)
assert response.status_code in [200, 404], (
f"Model pricing failed: {response.data}"
)
# Test cost calculation
cost_data = {
"model_name": "gpt-3.5-turbo",
"prompt_tokens": 1000,
"completion_tokens": 500,
}
response = authenticated_client.post(
"/metrics/api/cost-calculation",
json=cost_data,
content_type="application/json",
)
assert response.status_code == 200, (
f"Cost calculation failed: {response.data}"
)
logger.info("✅ All Metrics endpoints tested")
def test_all_benchmark_endpoints(self, authenticated_client):
"""Test ALL benchmark endpoints."""
logger.info("\n=== Testing ALL Benchmark Endpoints ===")
# Basic benchmark endpoints
benchmark_endpoints = [
"/benchmark/api/history",
"/benchmark/api/configs",
"/benchmark/api/running",
"/benchmark/api/search-quality",
]
for endpoint in benchmark_endpoints:
response = authenticated_client.get(endpoint)
assert response.status_code == 200, (
f"{endpoint} failed: {response.data}"
)
# Test config validation
test_config = {
"name": "Test Benchmark Config",
"queries": ["test query 1", "test query 2"],
"models": ["gpt-3.5-turbo"],
"search_engines": ["searxng"],
"iterations": 1,
}
response = authenticated_client.post(
"/benchmark/api/validate-config",
json=test_config,
content_type="application/json",
)
assert response.status_code == 200, (
f"Validate config failed: {response.data}"
)
# Test simple benchmark start (but don't actually run it)
simple_benchmark = {
"query": "test benchmark",
"models": ["gpt-3.5-turbo"],
"search_engines": ["searxng"],
}
# Note: We won't actually start a benchmark as it's resource intensive
# Just verify the endpoint exists
response = authenticated_client.post(
"/benchmark/api/start-simple",
json=simple_benchmark,
content_type="application/json",
)
# Accept 409 (conflict) if another benchmark is running
assert response.status_code in [200, 409, 400], (
f"Start simple benchmark status: {response.status_code}, {response.data}"
)
logger.info("✅ All Benchmark endpoints tested")
def test_all_history_endpoints(self, authenticated_client):
"""Test ALL history endpoints."""
logger.info("\n=== Testing ALL History Endpoints ===")
# Basic history endpoint
response = authenticated_client.get("/history/api")
assert response.status_code == 200, (
f"History API failed: {response.data}"
)
logger.info("✅ All History endpoints tested")
@pytest.mark.requires_llm
def test_all_api_v1_endpoints(self, authenticated_client):
"""Test ALL API v1 endpoints."""
logger.info("\n=== Testing ALL API v1 Endpoints ===")
# Health check (doesn't require auth)
response = authenticated_client.get("/api/v1/health")
assert response.status_code == 200, (
f"Health check failed: {response.data}"
)
# API documentation
response = authenticated_client.get("/api/v1/")
assert response.status_code == 200, f"API docs failed: {response.data}"
# Quick summary
summary_data = {"query": "What is machine learning?", "max_tokens": 100}
response = authenticated_client.post(
"/api/v1/quick_summary",
json=summary_data,
content_type="application/json",
)
assert response.status_code in [200, 404, 500], (
f"Quick summary status: {response.status_code}"
)
# Generate report
report_data = {
"query": "Test report generation",
"research_type": "comprehensive",
}
response = authenticated_client.post(
"/api/v1/generate_report",
json=report_data,
content_type="application/json",
)
assert response.status_code in [200, 404, 500], (
f"Generate report status: {response.status_code}"
)
# Analyze documents. The endpoint searches a named local collection;
# it does not accept an inline "documents" key (that now yields a 400).
doc_data = {
"query": "Summarize this document",
"collection_name": "test_collection",
}
response = authenticated_client.post(
"/api/v1/analyze_documents",
json=doc_data,
content_type="application/json",
)
assert response.status_code in [200, 404, 500], (
f"Analyze documents status: {response.status_code}"
)
logger.info("✅ All API v1 endpoints tested")
def test_miscellaneous_endpoints(self, authenticated_client):
"""Test miscellaneous endpoints not covered in other categories."""
logger.info("\n=== Testing Miscellaneous Endpoints ===")
# Test static file serving
response = authenticated_client.get("/static/css/style.css")
assert response.status_code in [200, 404], "Static file endpoint failed"
# Test favicon
response = authenticated_client.get("/favicon.ico")
assert response.status_code in [200, 404], "Favicon endpoint failed"
# Test pages that should exist
page_endpoints = [
"/", # Home page
"/history", # History page
"/settings", # Settings page
"/metrics/", # Metrics dashboard
"/benchmark/", # Benchmark dashboard
]
for endpoint in page_endpoints:
response = authenticated_client.get(endpoint)
assert response.status_code == 200, (
f"Page {endpoint} failed with status {response.status_code}"
)
logger.info("✅ All miscellaneous endpoints tested")
def test_error_handling(self, authenticated_client):
"""Test error handling for invalid endpoints and methods."""
logger.info("\n=== Testing Error Handling ===")
# Test non-existent endpoint
response = authenticated_client.get("/api/does-not-exist")
assert response.status_code == 404, (
f"Non-existent endpoint should return 404, got {response.status_code}"
)
# Test wrong method
response = authenticated_client.delete("/auth/check")
assert response.status_code in [404, 405], (
f"Wrong method should return 404/405, got {response.status_code}"
)
# Test invalid JSON
response = authenticated_client.post(
"/api/start_research",
data="invalid json",
content_type="application/json",
)
assert response.status_code in [400, 500], (
f"Invalid JSON should return 400/500, got {response.status_code}"
)
logger.info("✅ Error handling tested")
+103
View File
@@ -0,0 +1,103 @@
"""
Basic API tests - only test endpoints that should respond quickly.
Focus on verifying the API is working without doing actual research.
"""
import pytest
from flask import json
class TestBasicAPI:
"""Test basic API functionality."""
def test_health_check(self, authenticated_client):
"""Test health check endpoint."""
response = authenticated_client.get("/api/v1/health")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "ok"
assert "timestamp" in data
def test_api_documentation(self, authenticated_client):
"""Test API documentation endpoint."""
response = authenticated_client.get("/api/v1/")
assert response.status_code == 200
data = json.loads(response.data)
assert data["api_version"] == "v1"
assert "endpoints" in data
assert len(data["endpoints"]) >= 3
def test_error_handling(self, authenticated_client):
"""Test error handling for malformed requests."""
# Test missing query parameter
response = authenticated_client.post(
"/api/v1/quick_summary",
json={},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
assert "required" in data["error"].lower()
# Test missing collection_name
response = authenticated_client.post(
"/api/v1/analyze_documents",
json={"query": "test"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
@pytest.mark.requires_llm
def test_api_structure(
self, authenticated_client, setup_database_for_all_tests
):
"""Test that API accepts properly formatted requests."""
payload = {
"query": "test",
"search_tool": "wikipedia",
"iterations": 1,
"temperature": 0.7,
}
print(
f"\n[DEBUG] Sending request to /api/v1/quick_summary with payload: {payload}"
)
# Send request with proper format
response = authenticated_client.post(
"/api/v1/quick_summary",
json=payload,
content_type="application/json",
)
print(f"[DEBUG] Response status code: {response.status_code}")
print(
f"[DEBUG] Response data: {response.data.decode()[:500]}"
) # First 500 chars
if response.status_code == 500:
# Print full error details for debugging
try:
error_data = json.loads(response.data)
print(f"[DEBUG] Error response JSON: {error_data}")
except Exception:
print(f"[DEBUG] Raw error response: {response.data.decode()}")
# The API should accept the request format
# It might return 200 with processing started, or 400 if there's a validation error
assert response.status_code in [200, 202, 400]
if response.status_code == 400:
# Check that error message is informative
data = json.loads(response.data)
assert "error" in data
def test_unauthenticated_access(self, client):
"""Test that unauthenticated requests are rejected."""
response = client.get("/api/v1/health")
# API endpoints might allow unauthenticated access for health checks
# or might redirect to login
assert response.status_code in [200, 302, 401]
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Test API response contracts.
These tests verify that API responses have the expected structure.
Breaking these contracts will break client integrations.
"""
import pytest
class TestResearchStatusValues:
"""
Verify that research status values are consistent.
Clients depend on these exact string values.
"""
def test_research_status_enum_values(self):
"""
Verify ResearchStatus enum has expected values.
Clients check for these exact string values in responses.
Changing them will break client code.
"""
from local_deep_research.database.models.research import (
ResearchStatus,
)
# These are the status values clients depend on
expected_values = {
"pending",
"in_progress",
"completed",
"failed",
"cancelled",
"suspended",
}
actual_values = {status.value for status in ResearchStatus}
# Check no expected values were removed
missing = expected_values - actual_values
assert not missing, (
f"ResearchStatus is missing expected values: {missing}\n"
"Clients depend on these status values.\n"
"Removing them will break client integrations."
)
def test_research_mode_enum_values(self):
"""
Verify ResearchMode enum has expected values.
Clients use these values when starting research.
"""
from local_deep_research.database.models.research import (
ResearchMode,
)
# These are the mode values clients depend on
expected_values = {
"quick",
"detailed",
}
actual_values = {mode.value for mode in ResearchMode}
missing = expected_values - actual_values
assert not missing, (
f"ResearchMode is missing expected values: {missing}\n"
"Clients depend on these mode values."
)
class TestResponseStructures:
"""
Verify that models have expected columns for API responses.
These are the fields that clients depend on.
"""
def test_research_has_required_columns(self):
"""
Verify Research model has required columns for API responses.
These columns are serialized in API responses.
"""
from local_deep_research.database.models.research import Research
required_columns = {
"id",
"query",
"status",
"mode",
"created_at",
}
actual_columns = set(Research.__table__.columns.keys())
missing = required_columns - actual_columns
assert not missing, (
f"Research model is missing required columns: {missing}\n"
"These columns are expected in API responses."
)
def test_user_settings_has_required_columns(self):
"""
Verify UserSettings model has required columns for API responses.
"""
from local_deep_research.database.models import UserSettings
required_columns = {"id", "key", "value", "category"}
actual_columns = set(UserSettings.__table__.columns.keys())
missing = required_columns - actual_columns
assert not missing, (
f"UserSettings model is missing required columns: {missing}\n"
"These columns are expected in settings API responses."
)
if __name__ == "__main__":
pytest.main([__file__, "-v"])
+114
View File
@@ -0,0 +1,114 @@
"""
pytest-compatible API tests for REST API endpoints.
Tests basic functionality and programmatic access integration.
"""
import json
class TestRestAPIBasic:
"""Basic tests for REST API endpoints."""
def test_health_check(self, authenticated_client):
"""Test the health check endpoint returns OK status."""
response = authenticated_client.get("/api/v1/health")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "ok"
assert "timestamp" in data
assert isinstance(data["timestamp"], (int, float))
def test_api_documentation(self, authenticated_client):
"""Test the API documentation endpoint returns proper structure."""
response = authenticated_client.get("/api/v1/")
assert response.status_code == 200
data = json.loads(response.data)
assert data["api_version"] == "v1"
assert "description" in data
assert "endpoints" in data
assert len(data["endpoints"]) >= 3
# Verify endpoint structure
endpoints = data["endpoints"]
if isinstance(endpoints, list):
# For list format, just check basic fields
for endpoint in endpoints:
assert "path" in endpoint or "endpoint" in endpoint
assert "method" in endpoint or "methods" in endpoint
assert "description" in endpoint
# auth_required field is optional in API v1
else:
# For dict format
for endpoint in endpoints.values():
assert "path" in endpoint
assert "methods" in endpoint
assert "description" in endpoint
assert "requires_auth" in endpoint
def test_quick_summary_validation(self, authenticated_client):
"""Test quick_summary endpoint validates required fields."""
# Test missing query
response = authenticated_client.post(
"/api/v1/quick_summary",
json={},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
assert "query" in data["error"].lower()
def test_analyze_documents_validation(self, authenticated_client):
"""Test analyze_documents endpoint validates required fields."""
# Test missing collection_name
response = authenticated_client.post(
"/api/v1/analyze_documents",
json={"query": "test query"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
assert "collection_name" in data["error"].lower()
# Test missing query
response = authenticated_client.post(
"/api/v1/analyze_documents",
json={"collection_name": "test_collection"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
assert "query" in data["error"].lower()
def test_generate_report_validation(self, authenticated_client):
"""Test generate_report endpoint validates required fields."""
# Test missing research_id or query
response = authenticated_client.post(
"/api/v1/generate_report",
json={},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
# Check for either research_id or query in error message
error_msg = data["error"].lower()
assert (
"research_id" in error_msg
or "query" in error_msg
or "required" in error_msg
)
def test_error_response_format(self, authenticated_client):
"""Test that error responses follow consistent format."""
# Trigger a 404 error
response = authenticated_client.get("/api/v1/nonexistent_endpoint")
assert response.status_code == 404
if response.content_type == "application/json":
data = json.loads(response.data)
assert "error" in data or "message" in data
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Test the browser's research creation endpoint specifically
"""
from flask import json
class TestBrowserEndpoint:
"""Test browser-specific endpoints."""
def test_start_research_endpoint(self, authenticated_client):
"""Test the browser endpoint /api/start_research."""
research_data = {
"query": "Test from browser endpoint",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama2",
"search_engine": "searxng",
"max_results": 10,
"time_period": "y",
"iterations": 1,
"questions_per_iteration": 3,
"strategy": "source-based",
"local_context": 2000,
"web_context": 2000,
"temperature": 0.7,
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
assert response.status_code in [200, 202]
data = json.loads(response.data)
if response.status_code == 200:
assert data.get("status") in ["success", "processing"]
if "research_id" in data:
assert isinstance(data["research_id"], (str, int))
def test_research_status_endpoint(self, authenticated_client):
"""Test research status endpoint."""
# First create a research
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: FIX_ASSERTION).
research_data = {
"query": "Test research for status check",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama2",
"search_engine": "wikipedia",
"iterations": 1,
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
if response.status_code == 200:
data = json.loads(response.data)
research_id = data.get("research_id")
if research_id:
# Check status
status_response = authenticated_client.get(
f"/research/api/status/{research_id}"
)
assert status_response.status_code in [200, 404]
if status_response.status_code == 200:
status_data = json.loads(status_response.data)
assert "status" in status_data
def test_endpoint_requires_authentication(self, client):
"""Test that endpoint requires authentication."""
research_data = {
"query": "Test without auth",
"mode": "quick",
}
response = client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
# Should either redirect to login or return 401
assert response.status_code in [302, 401]
if response.status_code == 302:
assert "/auth/login" in response.location
+442
View File
@@ -0,0 +1,442 @@
"""
Tests for Collection CRUD API endpoints.
Tests the following endpoints:
- GET /library/api/collections
- POST /library/api/collections
- PUT /library/api/collections/<id>
- DELETE /library/api/collections/<id>
"""
import uuid
# Import helpers from conftest
from .conftest import delete_test_collection
class TestGetCollections:
"""Tests for GET /library/api/collections endpoint."""
def test_get_collections_unauthenticated(self, client):
"""Unauthenticated request is rejected."""
response = client.get("/library/api/collections")
# Should redirect to login or return 401
assert response.status_code in [302, 401]
def test_get_collections_authenticated_empty(self, authenticated_client):
"""Returns collections list (may include default Library)."""
response = authenticated_client.get("/library/api/collections")
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "collections" in data
assert isinstance(data["collections"], list)
def test_get_collections_with_data(
self, authenticated_client, create_collection_helper
):
"""Returns created collections."""
# Create a test collection
created = create_collection_helper(
name="API Test Collection",
description="Testing GET endpoint",
)
assert created is not None
response = authenticated_client.get("/library/api/collections")
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
# Find our created collection
collection_names = [c["name"] for c in data["collections"]]
assert "API Test Collection" in collection_names
def test_get_collections_includes_metadata(
self, authenticated_client, create_collection_helper
):
"""Collections include required metadata fields."""
created = create_collection_helper()
assert created is not None
response = authenticated_client.get("/library/api/collections")
assert response.status_code == 200
data = response.get_json()
assert len(data["collections"]) > 0
# Check metadata fields
coll = data["collections"][0]
assert "id" in coll
assert "name" in coll
assert "description" in coll
assert "created_at" in coll
assert "collection_type" in coll
assert "document_count" in coll
class TestCreateCollection:
"""Tests for POST /library/api/collections endpoint."""
def test_create_collection_unauthenticated(self, client):
"""Unauthenticated request is rejected."""
response = client.post(
"/library/api/collections",
json={"name": "Test", "description": "Test"},
content_type="application/json",
)
assert response.status_code in [302, 401]
def test_create_collection_success(self, authenticated_client):
"""Successfully creates a new collection."""
unique_name = f"Test Collection {uuid.uuid4().hex[:8]}"
response = authenticated_client.post(
"/library/api/collections",
json={
"name": unique_name,
"description": "A test collection",
"type": "user_uploads",
},
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert data["collection"]["name"] == unique_name
assert data["collection"]["description"] == "A test collection"
assert "id" in data["collection"]
# Cleanup
delete_test_collection(authenticated_client, data["collection"]["id"])
def test_create_collection_defaults_private(self, authenticated_client):
"""A new collection with no is_public flag defaults to private."""
name = f"Priv {uuid.uuid4().hex[:8]}"
resp = authenticated_client.post(
"/library/api/collections",
json={"name": name},
content_type="application/json",
)
assert resp.status_code == 200
coll = resp.get_json()["collection"]
assert coll["is_public"] is False
delete_test_collection(authenticated_client, coll["id"])
def test_create_and_toggle_collection_is_public(self, authenticated_client):
"""is_public can be set at create and flipped via PUT, and is
reflected in the list API."""
name = f"Pub {uuid.uuid4().hex[:8]}"
resp = authenticated_client.post(
"/library/api/collections",
json={"name": name, "is_public": True},
content_type="application/json",
)
assert resp.status_code == 200
coll = resp.get_json()["collection"]
cid = coll["id"]
assert coll["is_public"] is True
# List reflects it
listing = authenticated_client.get(
"/library/api/collections"
).get_json()["collections"]
match = next(c for c in listing if c["id"] == cid)
assert match["is_public"] is True
# The collection-details load path (documents endpoint) also exposes
# is_public so the details-page toggle can reflect current state.
docs = authenticated_client.get(
f"/library/api/collections/{cid}/documents"
)
assert docs.status_code == 200
assert docs.get_json()["collection"]["is_public"] is True
# Flip back to private via PUT
put = authenticated_client.put(
f"/library/api/collections/{cid}",
json={"is_public": False},
content_type="application/json",
)
assert put.status_code == 200
assert put.get_json()["collection"]["is_public"] is False
delete_test_collection(authenticated_client, cid)
def test_create_collection_empty_name(self, authenticated_client):
"""Rejects collection with empty name."""
response = authenticated_client.post(
"/library/api/collections",
json={"name": "", "description": "No name"},
content_type="application/json",
)
assert response.status_code == 400
data = response.get_json()
assert data["success"] is False
assert (
"required" in data["error"].lower()
or "name" in data["error"].lower()
)
def test_create_collection_whitespace_name(self, authenticated_client):
"""Rejects collection with whitespace-only name."""
response = authenticated_client.post(
"/library/api/collections",
json={"name": " ", "description": "Whitespace name"},
content_type="application/json",
)
assert response.status_code == 400
def test_create_collection_duplicate_name(self, authenticated_client):
"""Rejects collection with duplicate name."""
unique_name = f"Duplicate Test {uuid.uuid4().hex[:8]}"
# Create first collection
response1 = authenticated_client.post(
"/library/api/collections",
json={"name": unique_name, "description": "First"},
content_type="application/json",
)
assert response1.status_code == 200
collection_id = response1.get_json()["collection"]["id"]
try:
# Try to create duplicate
response2 = authenticated_client.post(
"/library/api/collections",
json={"name": unique_name, "description": "Duplicate"},
content_type="application/json",
)
assert response2.status_code == 400
data = response2.get_json()
assert data["success"] is False
assert "exists" in data["error"].lower()
finally:
# Cleanup
delete_test_collection(authenticated_client, collection_id)
def test_create_collection_without_description(self, authenticated_client):
"""Creates collection without description."""
unique_name = f"No Desc Collection {uuid.uuid4().hex[:8]}"
response = authenticated_client.post(
"/library/api/collections",
json={"name": unique_name},
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert data["collection"]["name"] == unique_name
# Cleanup
delete_test_collection(authenticated_client, data["collection"]["id"])
class TestUpdateCollection:
"""Tests for PUT /library/api/collections/<id> endpoint."""
def test_update_collection_unauthenticated(self, client):
"""Unauthenticated request is rejected."""
response = client.put(
"/library/api/collections/some-id",
json={"name": "Updated"},
content_type="application/json",
)
assert response.status_code in [302, 401]
def test_update_collection_name(
self, authenticated_client, create_collection_helper
):
"""Successfully updates collection name."""
created = create_collection_helper(name="Original Name")
assert created is not None
new_name = f"Updated Name {uuid.uuid4().hex[:8]}"
response = authenticated_client.put(
f"/library/api/collections/{created['id']}",
json={"name": new_name},
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert data["collection"]["name"] == new_name
def test_update_collection_description(
self, authenticated_client, create_collection_helper
):
"""Successfully updates collection description."""
created = create_collection_helper(description="Original description")
assert created is not None
response = authenticated_client.put(
f"/library/api/collections/{created['id']}",
json={"description": "Updated description"},
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert data["collection"]["description"] == "Updated description"
def test_update_collection_not_found(self, authenticated_client):
"""Returns 404 for non-existent collection."""
fake_id = str(uuid.uuid4())
response = authenticated_client.put(
f"/library/api/collections/{fake_id}",
json={"name": "Updated"},
content_type="application/json",
)
assert response.status_code == 404
data = response.get_json()
assert data["success"] is False
assert "not found" in data["error"].lower()
def test_update_collection_duplicate_name(
self, authenticated_client, create_collection_helper
):
"""Rejects update with duplicate name."""
# Create two collections
coll1 = create_collection_helper(
name=f"Collection A {uuid.uuid4().hex[:8]}"
)
coll2 = create_collection_helper(
name=f"Collection B {uuid.uuid4().hex[:8]}"
)
assert coll1 is not None and coll2 is not None
# Try to rename coll2 to coll1's name
response = authenticated_client.put(
f"/library/api/collections/{coll2['id']}",
json={"name": coll1["name"]},
content_type="application/json",
)
assert response.status_code == 400
data = response.get_json()
assert data["success"] is False
assert "exists" in data["error"].lower()
class TestDeleteCollection:
"""Tests for DELETE /library/api/collections/<id> endpoint."""
def test_delete_collection_unauthenticated(self, client):
"""Unauthenticated request is rejected."""
response = client.delete("/library/api/collections/some-id")
assert response.status_code in [302, 401]
def test_delete_collection_success(self, authenticated_client):
"""Successfully deletes a collection."""
# Create a collection to delete
unique_name = f"To Delete {uuid.uuid4().hex[:8]}"
create_response = authenticated_client.post(
"/library/api/collections",
json={"name": unique_name, "description": "Will be deleted"},
content_type="application/json",
)
assert create_response.status_code == 200
collection_id = create_response.get_json()["collection"]["id"]
# Delete it
response = authenticated_client.delete(
f"/library/api/collections/{collection_id}"
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
# Verify it's gone
list_response = authenticated_client.get("/library/api/collections")
collections = list_response.get_json()["collections"]
collection_ids = [c["id"] for c in collections]
assert collection_id not in collection_ids
def test_delete_collection_not_found(self, authenticated_client):
"""Returns 404 for non-existent collection."""
fake_id = str(uuid.uuid4())
response = authenticated_client.delete(
f"/library/api/collections/{fake_id}"
)
assert response.status_code == 404
data = response.get_json()
assert data["success"] is False
assert "not found" in data["error"].lower()
class TestCollectionIntegration:
"""Integration tests for collection workflow."""
def test_create_read_update_delete_flow(self, authenticated_client):
"""Full CRUD workflow."""
unique_name = f"CRUD Test {uuid.uuid4().hex[:8]}"
# Create
create_response = authenticated_client.post(
"/library/api/collections",
json={"name": unique_name, "description": "Initial"},
content_type="application/json",
)
assert create_response.status_code == 200
collection_id = create_response.get_json()["collection"]["id"]
try:
# Read
list_response = authenticated_client.get("/library/api/collections")
assert list_response.status_code == 200
collections = list_response.get_json()["collections"]
our_coll = next(
(c for c in collections if c["id"] == collection_id), None
)
assert our_coll is not None
assert our_coll["name"] == unique_name
# Update
updated_name = f"Updated {uuid.uuid4().hex[:8]}"
update_response = authenticated_client.put(
f"/library/api/collections/{collection_id}",
json={"name": updated_name, "description": "Updated desc"},
content_type="application/json",
)
assert update_response.status_code == 200
# Verify update
list_response2 = authenticated_client.get(
"/library/api/collections"
)
collections2 = list_response2.get_json()["collections"]
our_coll2 = next(
(c for c in collections2 if c["id"] == collection_id), None
)
assert our_coll2["name"] == updated_name
assert our_coll2["description"] == "Updated desc"
finally:
# Delete
delete_response = authenticated_client.delete(
f"/library/api/collections/{collection_id}"
)
assert delete_response.status_code == 200
+331
View File
@@ -0,0 +1,331 @@
"""
Comprehensive API test suite with proper authentication handling.
"""
import json
import time
from loguru import logger
class TestComprehensiveAPIs:
"""Comprehensive API test suite using Flask test client."""
def test_auth_apis(self, authenticated_client):
"""Test authentication APIs."""
logger.info("Testing Authentication APIs")
# Check auth status
response = authenticated_client.get("/auth/check")
assert response.status_code == 200
data = json.loads(response.data)
assert data["authenticated"] is True
# Test integrity check
response = authenticated_client.get("/auth/integrity-check")
assert response.status_code == 200
data = json.loads(response.data)
assert "integrity" in data
logger.info("✅ Auth APIs tested")
def test_settings_apis(self, authenticated_client):
"""Test settings APIs."""
logger.info("Testing Settings APIs")
# Get all settings
response = authenticated_client.get("/settings/api")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "success"
assert "settings" in data
# Get categories
response = authenticated_client.get("/settings/api/categories")
assert response.status_code == 200
data = json.loads(response.data)
assert "categories" in data
# Get types
response = authenticated_client.get("/settings/api/types")
assert response.status_code == 200
data = json.loads(response.data)
assert "types" in data
# Get available models
print("\n[DEBUG] Testing /settings/api/available-models")
response = authenticated_client.get("/settings/api/available-models")
print(f"[DEBUG] Response status: {response.status_code}")
assert response.status_code == 200
data = json.loads(response.data)
print(f"[DEBUG] Response keys: {list(data.keys())}")
# The endpoint returns provider_options and providers, not models
assert "provider_options" in data
assert "providers" in data
# Get available search engines
response = authenticated_client.get(
"/settings/api/available-search-engines"
)
assert response.status_code == 200
data = json.loads(response.data)
assert "engines" in data or "engine_options" in data
# Test setting CRUD (use an allow-listed prefix so the
# namespace gate permits creation of this throwaway key).
test_key = "llm.test_setting"
test_value = {"value": "test_value_123"}
# Create/Update setting
response = authenticated_client.put(
f"/settings/api/{test_key}",
json=test_value,
content_type="application/json",
)
assert response.status_code in [200, 201]
# Get the setting
response = authenticated_client.get(f"/settings/api/{test_key}")
# Setting might have been created or might not exist
assert response.status_code in [200, 404]
# Delete the setting
response = authenticated_client.delete(f"/settings/api/{test_key}")
assert response.status_code in [200, 204, 404]
logger.info("✅ Settings APIs tested")
def test_research_apis(self, authenticated_client):
"""Test research APIs."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
logger.info("Testing Research APIs")
# Get research history (endpoint is /api/history not /research/api/history)
response = authenticated_client.get("/api/history")
assert response.status_code == 200
data = json.loads(response.data)
assert "history" in data or "items" in data
# Start a test research
research_data = {
"query": "Test research from comprehensive API test",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama2",
"search_engine": "searxng",
"max_results": 5,
"iterations": 1,
"questions_per_iteration": 2,
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
if response.status_code == 200:
data = json.loads(response.data)
assert "research_id" in data
research_id = data["research_id"]
# Give it a moment to start
time.sleep(0.5) # allow: unmarked-sleep
# Check status
response = authenticated_client.get(
f"/api/research/{research_id}/status"
)
assert response.status_code == 200
# Terminate research
response = authenticated_client.post(
f"/api/terminate/{research_id}"
)
assert response.status_code in [200, 404]
logger.info("✅ Research APIs tested")
def test_metrics_apis(self, authenticated_client):
"""Test metrics APIs."""
logger.info("Testing Metrics APIs")
# Get metrics summary
response = authenticated_client.get("/metrics/api/metrics")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'metrics' not 'summary'
assert "metrics" in data
# Get enhanced metrics
response = authenticated_client.get("/metrics/api/metrics/enhanced")
assert response.status_code == 200
# Get pricing info
response = authenticated_client.get("/metrics/api/pricing")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'pricing' not 'models'
assert "pricing" in data
# Get cost analytics
response = authenticated_client.get("/metrics/api/cost-analytics")
assert response.status_code == 200
# Test cost calculation
cost_data = {
"model_name": "gpt-3.5-turbo",
"prompt_tokens": 1000,
"completion_tokens": 500,
}
response = authenticated_client.post(
"/metrics/api/cost-calculation",
json=cost_data,
content_type="application/json",
)
assert response.status_code == 200
logger.info("✅ Metrics APIs tested")
def test_benchmark_apis(self, authenticated_client):
"""Test benchmark APIs."""
logger.info("Testing Benchmark APIs")
# Get benchmark history
response = authenticated_client.get("/benchmark/api/history")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'runs' not 'history'
assert "runs" in data
# Get saved configs
response = authenticated_client.get("/benchmark/api/configs")
assert response.status_code == 200
data = json.loads(response.data)
assert "configs" in data
# Check running benchmark
response = authenticated_client.get("/benchmark/api/running")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint might return either is_running or success key
assert "is_running" in data or "success" in data
# Validate config
test_config = {
"name": "Test Config",
"queries": ["test query"],
"models": ["gpt-3.5-turbo"],
"search_engines": ["searxng"],
}
response = authenticated_client.post(
"/benchmark/api/validate-config",
json=test_config,
content_type="application/json",
)
assert response.status_code == 200
logger.info("✅ Benchmark APIs tested")
def test_history_apis(self, authenticated_client):
"""Test history APIs."""
logger.info("Testing History APIs")
response = authenticated_client.get("/history/api")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'items' not 'history'
assert "items" in data
assert isinstance(data["items"], list)
logger.info("✅ History APIs tested")
def test_config_apis(self, authenticated_client):
"""Test configuration APIs."""
logger.info("Testing Config APIs")
# Get current config (correct path is /research/api/settings/current-config)
response = authenticated_client.get(
"/research/api/settings/current-config"
)
assert response.status_code == 200
data = json.loads(response.data)
# The response has a 'config' key containing the actual config
assert "config" in data or ("llm" in data and "search" in data)
if "config" in data:
config = data["config"]
assert "provider" in config or "llm" in config
logger.info("✅ Config APIs tested")
def test_health_check_apis(self, authenticated_client):
"""Test health check APIs."""
logger.info("Testing Health Check APIs")
# Check Ollama status (correct path is /research/api/check/ollama_status)
response = authenticated_client.get("/research/api/check/ollama_status")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'running' not 'available'
assert "running" in data or "available" in data
# Check Ollama model (might return 400 if no model specified)
response = authenticated_client.get("/research/api/check/ollama_model")
assert response.status_code in [200, 400]
logger.info("✅ Health Check APIs tested")
def test_api_v1_endpoints(self, authenticated_client):
"""Test API v1 endpoints."""
logger.info("Testing API v1 endpoints")
# Health check
response = authenticated_client.get("/api/v1/health")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "ok"
# API documentation
response = authenticated_client.get("/api/v1/")
assert response.status_code == 200
data = json.loads(response.data)
assert data["api_version"] == "v1"
assert "endpoints" in data
logger.info("✅ API v1 endpoints tested")
def test_rate_limiting(self, authenticated_client):
"""Test rate limiting functionality."""
logger.info("Testing Rate Limiting")
# Get rate limiting status
response = authenticated_client.get(
"/settings/api/rate-limiting/status"
)
assert response.status_code == 200
# Get current rate limits
response = authenticated_client.get(
"/metrics/api/rate-limiting/current"
)
assert response.status_code == 200
# Test cleanup
response = authenticated_client.post(
"/settings/api/rate-limiting/cleanup"
)
assert response.status_code == 200
logger.info("✅ Rate limiting tested")
def test_data_location_api(self, authenticated_client):
"""Test data location API."""
logger.info("Testing Data Location API")
response = authenticated_client.get("/settings/api/data-location")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'data_directory' not 'data_dir'
assert "data_directory" in data
assert "is_custom" in data
logger.info("✅ Data location API tested")
+87
View File
@@ -0,0 +1,87 @@
"""Test CSRF with header."""
import time
class TestCSRFHeader:
"""Test CSRF protection with headers."""
def test_csrf_with_header(self, client, app):
"""Test CSRF protection using headers."""
# Since we disabled CSRF for testing, this test verifies the login flow
# In production, CSRF would be required
# Generate unique username to avoid conflicts
test_username = f"testuser_csrf_{int(time.time() * 1000)}"
# Get login page
response = client.get("/auth/login")
assert response.status_code == 200
# In test mode, CSRF is disabled, so we can login without token
login_data = {
"username": test_username,
"password": "TestPass123",
}
# First register the user
register_response = client.post(
"/auth/register",
data={
"username": test_username,
"password": "TestPass123",
"confirm_password": "TestPass123",
"acknowledge": "true",
},
)
assert register_response.status_code in [200, 302]
# Now test login
response = client.post(
"/auth/login",
data=login_data,
follow_redirects=False,
)
assert response.status_code in [200, 302]
if response.status_code == 302:
# Login successful, redirecting
assert "/" in response.location or "/research" in response.location
def test_api_csrf_exemption(self, authenticated_client):
"""Test that API routes are exempt from CSRF."""
# API routes should work without CSRF token
response = authenticated_client.get("/api/v1/health")
assert response.status_code == 200
# Research API should also work
response = authenticated_client.get("/research/api/history")
assert response.status_code in [200, 404] # 404 if no history yet
def test_csrf_blueprint_exemptions_configured(self, app):
"""Test that only api_v1 blueprint is CSRF-exempt.
Flask-WTF 1.2.2 requires Blueprint objects (not strings) in
_exempt_blueprints. Only api_v1 should be exempt — the browser-facing
blueprints (api, benchmark, research) should require CSRF tokens since
the frontend already sends them.
"""
csrf = app.extensions.get("csrf")
assert csrf is not None, "CSRFProtect extension not initialized"
exempt_names = {bp.name for bp in csrf._exempt_blueprints}
# api_v1 should be exempt (programmatic REST API)
assert "api_v1" in exempt_names, (
"Blueprint 'api_v1' missing from _exempt_blueprints. "
"Ensure csrf.exempt() receives the Blueprint object, not a string."
)
# Browser-facing blueprints should NOT be exempt
for should_not_be_exempt in ("api", "benchmark", "research"):
assert should_not_be_exempt not in exempt_names, (
f"Blueprint '{should_not_be_exempt}' should not be in "
f"_exempt_blueprints — it is browser-facing and the frontend "
f"already sends CSRF tokens."
)
+145
View File
@@ -0,0 +1,145 @@
"""Test with completely fresh session."""
import pytest
import re
import time
from loguru import logger
class TestFreshSession:
"""Test authentication with fresh session."""
def test_fresh_session_login(self, client):
"""Test login with completely fresh session."""
# Get login page
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
response = client.get("/auth/login")
assert response.status_code == 200
logger.info(f"GET /auth/login -> {response.status_code}")
# Extract CSRF token
csrf_match = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
csrf_token = csrf_match.group(1) if csrf_match else None
logger.info(f"CSRF token: {csrf_token}")
# Create unique test username to avoid conflicts
test_username = f"testuser_fresh_{int(time.time() * 1000)}"
test_password = "TestPass123"
# Register user first (in case it doesn't exist)
register_data = {
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
"csrf_token": csrf_token,
}
client.post("/auth/register", data=register_data)
# Get fresh login page for new CSRF token
response = client.get("/auth/login")
csrf_match = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
csrf_token = csrf_match.group(1) if csrf_match else None
# Login
login_data = {
"username": test_username,
"password": test_password,
"csrf_token": csrf_token,
}
logger.info("Posting login data...")
response = client.post(
"/auth/login", data=login_data, follow_redirects=False
)
logger.info(f"POST /auth/login -> {response.status_code}")
if response.status_code == 302:
logger.info("✅ Login successful! Got redirect")
# Follow redirect
location = response.headers.get("Location", "/")
response = client.get(location, follow_redirects=False)
logger.info(f"GET {location} -> {response.status_code}")
if response.status_code == 200:
logger.info("✅ Successfully accessed home page!")
elif response.status_code == 302:
logger.error(
f"Got redirected again to: {response.headers.get('Location')}"
)
# Test auth check
response = client.get("/auth/check")
logger.info(f"GET /auth/check -> {response.status_code}")
assert response.status_code == 200
data = response.get_json()
assert data.get("authenticated") is True
logger.info(f"✅ Authenticated as: {data.get('username')}")
else:
pytest.fail(
f"Login failed with status code: {response.status_code}"
)
def test_fresh_session_api_access(self, client):
"""Test API access with fresh session."""
# Try to access API without login
response = client.get("/settings/api")
assert response.status_code in [
401,
302,
] # Should be unauthorized or redirect
# Register and login
response = client.get("/auth/login")
csrf_match = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
csrf_token = csrf_match.group(1) if csrf_match else None
# Create unique test username to avoid conflicts
test_username = f"testuser_api_{int(time.time() * 1000)}"
test_password = "TestPass123"
register_data = {
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
"csrf_token": csrf_token,
}
client.post("/auth/register", data=register_data)
# Get fresh login page
response = client.get("/auth/login")
csrf_match = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
csrf_token = csrf_match.group(1) if csrf_match else None
login_data = {
"username": test_username,
"password": test_password,
"csrf_token": csrf_token,
}
response = client.post(
"/auth/login", data=login_data, follow_redirects=True
)
assert response.status_code == 200
# Now try API access
response = client.get("/settings/api")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "success"
assert "settings" in data
+156
View File
@@ -0,0 +1,156 @@
"""
Tests for HTTP API example scripts.
Validates that the example scripts under examples/api_usage/http/ exist,
are syntactically valid Python, and define the expected entry points.
The actual API functionality exercised by these examples is already covered
by test_rest_api.py and test_endpoints_health.py via the Flask test client.
"""
import ast
from pathlib import Path
import pytest
# Root of the repository
REPO_ROOT = Path(__file__).parent.parent.parent
EXAMPLES_DIR = REPO_ROOT / "examples" / "api_usage" / "http"
# All example scripts that should be tested — single source of truth
EXAMPLE_SCRIPTS = [
"simple_working_example.py",
"advanced/http_api_examples.py",
"advanced/simple_http_example.py",
]
def _parse_example(relative_path: str) -> tuple[Path, str, ast.Module]:
"""Read and parse an example file, failing if it doesn't exist.
Returns (path, source_text, ast_tree).
"""
path = EXAMPLES_DIR / relative_path
if not path.is_file():
pytest.fail(f"Example not found: {path}")
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
return path, source, tree
def _has_main_guard(tree: ast.Module) -> bool:
"""Check if an AST contains an ``if __name__ == '__main__':`` guard."""
for node in ast.walk(tree):
if not isinstance(node, ast.If):
continue
test = node.test
if not isinstance(test, ast.Compare):
continue
if len(test.ops) != 1 or not isinstance(test.ops[0], ast.Eq):
continue
# Match both orderings:
# __name__ == "__main__"
# "__main__" == __name__
left = test.left
comparators = test.comparators
if len(comparators) != 1:
continue
right = comparators[0]
if (
isinstance(left, ast.Name)
and left.id == "__name__"
and isinstance(right, ast.Constant)
and right.value == "__main__"
) or (
isinstance(right, ast.Name)
and right.id == "__name__"
and isinstance(left, ast.Constant)
and left.value == "__main__"
):
return True
return False
class TestHttpExamplesExist:
"""Verify that expected HTTP example files are present."""
def test_examples_directory_exists(self):
assert EXAMPLES_DIR.is_dir(), (
f"Examples directory not found: {EXAMPLES_DIR}"
)
@pytest.mark.parametrize("relative_path", EXAMPLE_SCRIPTS)
def test_example_file_exists(self, relative_path):
path = EXAMPLES_DIR / relative_path
assert path.is_file(), f"Missing example: {path}"
class TestHttpExamplesSyntax:
"""Verify that example scripts are syntactically valid Python."""
@pytest.mark.parametrize("relative_path", EXAMPLE_SCRIPTS)
def test_example_parses(self, relative_path):
"""Each example file must be valid Python (no syntax errors)."""
_path, _source, tree = _parse_example(relative_path)
assert tree is not None
@pytest.mark.parametrize("relative_path", EXAMPLE_SCRIPTS)
def test_example_has_main_function(self, relative_path):
"""Each example script should define a main() function."""
_path, _source, tree = _parse_example(relative_path)
function_names = [
node.name
for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef)
]
assert "main" in function_names, (
f"{relative_path} should define a main() function"
)
@pytest.mark.parametrize("relative_path", EXAMPLE_SCRIPTS)
def test_example_has_main_guard(self, relative_path):
"""Each example script should have an if __name__ == '__main__' guard."""
_path, _source, tree = _parse_example(relative_path)
assert _has_main_guard(tree), (
f"{relative_path} should have an if __name__ == '__main__' guard"
)
class TestAdvancedExampleStructure:
"""Verify the advanced example defines expected helper classes/functions."""
def test_ldr_client_class_defined(self):
"""The advanced example should define an LDRClient class."""
_path, _source, tree = _parse_example("advanced/http_api_examples.py")
class_names = [
node.name
for node in ast.walk(tree)
if isinstance(node, ast.ClassDef)
]
assert "LDRClient" in class_names, (
"Advanced example should define LDRClient class"
)
def test_example_functions_defined(self):
"""The advanced example should define the documented example functions."""
_path, _source, tree = _parse_example("advanced/http_api_examples.py")
function_names = {
node.name
for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef)
}
expected_examples = [
"example_quick_research",
"example_settings_management",
"example_research_history",
]
for fn in expected_examples:
assert fn in function_names, (
f"Advanced example should define {fn}()"
)
@@ -0,0 +1,95 @@
"""Test using manually created session from browser.
This test is skipped by default as it requires manual setup.
To run it, you need to provide a session cookie from the browser.
"""
import os
import pytest
from loguru import logger
@pytest.mark.skip(reason="Requires manual browser session setup")
class TestManualBrowserAuth:
"""Test authentication using browser session cookie."""
def test_manual_session_cookie(self, client):
"""Test using manually provided session cookie.
To use this test:
1. Login via browser at http://127.0.0.1:5000/auth/login
2. Open Developer Tools (F12)
3. Go to Application/Storage/Cookies
4. Copy the 'session' cookie value
5. Set MANUAL_SESSION_COOKIE environment variable
6. Run with: pytest -m "not skip" tests/api_tests/test_manual_browser_auth.py
"""
# Get session cookie from environment
session_cookie = os.environ.get("MANUAL_SESSION_COOKIE")
if not session_cookie:
pytest.skip("MANUAL_SESSION_COOKIE not set")
# Set the cookie in the test client
client.set_cookie(
domain="localhost", key="session", value=session_cookie
)
logger.info("Testing with manual session cookie...")
# Test auth check
response = client.get("/auth/check")
logger.info(f"GET /auth/check -> {response.status_code}")
if response.status_code == 200:
data = response.get_json()
logger.info(f"Auth data: {data}")
if data.get("authenticated"):
logger.info(f"✅ Authenticated as: {data.get('username')}")
# Test API endpoints
response = client.get("/settings/api")
logger.info(f"GET /settings/api -> {response.status_code}")
assert response.status_code == 200
response = client.get("/settings/api/available-search-engines")
logger.info(
f"GET /settings/api/available-search-engines -> {response.status_code}"
)
assert response.status_code == 200
data = response.get_json()
if "engine_options" in data:
logger.info(
f"✅ Found {len(data['engine_options'])} search engines"
)
assert len(data["engine_options"]) > 0
else:
pytest.fail("Not authenticated - session cookie may be invalid")
else:
pytest.fail(f"Auth check failed with status {response.status_code}")
def test_manual_session_api_operations(self, client):
"""Test API operations with manual session."""
# This test is also skipped by default
session_cookie = os.environ.get("MANUAL_SESSION_COOKIE")
if not session_cookie:
pytest.skip("MANUAL_SESSION_COOKIE not set")
client.set_cookie(
domain="localhost", key="session", value=session_cookie
)
# Test various API operations
endpoints = [
"/history/api",
"/metrics/api/cost-analytics",
"/settings/api/available-models",
]
for endpoint in endpoints:
response = client.get(endpoint)
logger.info(f"GET {endpoint} -> {response.status_code}")
assert response.status_code == 200
data = response.get_json()
assert data is not None
+148
View File
@@ -0,0 +1,148 @@
"""
Test metrics API endpoints specifically.
"""
import json
import pytest
from loguru import logger
class TestMetricsAPI:
"""Test metrics API endpoints."""
def test_metrics_summary(self, authenticated_client):
"""Test metrics summary endpoint."""
logger.info("Testing metrics summary...")
response = authenticated_client.get("/metrics/api/metrics")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'metrics' not 'summary'
assert "metrics" in data
logger.info("✅ Metrics summary passed")
def test_enhanced_metrics(self, authenticated_client):
"""Test enhanced metrics endpoint."""
logger.info("Testing enhanced metrics...")
response = authenticated_client.get("/metrics/api/metrics/enhanced")
assert response.status_code == 200
logger.info("✅ Enhanced metrics passed")
def test_pricing_info(self, authenticated_client):
"""Test pricing information endpoint."""
logger.info("Testing pricing info...")
response = authenticated_client.get("/metrics/api/pricing")
assert response.status_code == 200
data = json.loads(response.data)
# The endpoint returns 'pricing' not 'models'
assert "pricing" in data
logger.info("✅ Pricing info passed")
def test_cost_analytics(self, authenticated_client):
"""Test cost analytics endpoint."""
logger.info("Testing cost analytics...")
response = authenticated_client.get("/metrics/api/cost-analytics")
assert response.status_code == 200
logger.info("✅ Cost analytics passed")
def test_cost_calculation(self, authenticated_client):
"""Test cost calculation endpoint."""
logger.info("Testing cost calculation...")
cost_data = {
"model_name": "gpt-3.5-turbo",
"prompt_tokens": 1000,
"completion_tokens": 500,
}
response = authenticated_client.post(
"/metrics/api/cost-calculation",
json=cost_data,
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
assert "cost" in data or "total_cost" in data
logger.info("✅ Cost calculation passed")
def test_star_reviews(self, authenticated_client):
"""Test star reviews endpoint."""
logger.info("Testing star reviews...")
response = authenticated_client.get("/metrics/api/star-reviews")
assert response.status_code == 200
logger.info("✅ Star reviews passed")
def test_rate_limiting_metrics(self, authenticated_client):
"""Test rate limiting metrics endpoint."""
logger.info("Testing rate limiting metrics...")
response = authenticated_client.get("/metrics/api/rate-limiting")
assert response.status_code == 200
logger.info("✅ Rate limiting metrics passed")
def test_current_rate_limits(self, authenticated_client):
"""Test current rate limits endpoint."""
logger.info("Testing current rate limits...")
response = authenticated_client.get(
"/metrics/api/rate-limiting/current"
)
assert response.status_code == 200
logger.info("✅ Current rate limits passed")
def test_model_specific_pricing(self, authenticated_client):
"""Test model-specific pricing endpoint."""
logger.info("Testing model-specific pricing...")
# Test a specific model
response = authenticated_client.get(
"/metrics/api/pricing/gpt-3.5-turbo"
)
# This endpoint might not exist for all models
if response.status_code == 200:
data = json.loads(response.data)
logger.info(f"Model pricing data: {data}")
# Check for pricing fields based on actual response structure
assert (
"pricing" in data
or "input" in data
or "prompt" in data
or "price" in data
)
logger.info("✅ Model-specific pricing passed")
elif response.status_code == 404:
logger.info(
"⚠️ Model-specific pricing endpoint not found (expected)"
)
else:
pytest.fail(f"Unexpected status code: {response.status_code}")
def test_metrics_error_handling(self, authenticated_client):
"""Test metrics API error handling."""
logger.info("Testing metrics error handling...")
# Test cost calculation with missing data
response = authenticated_client.post(
"/metrics/api/cost-calculation",
json={},
content_type="application/json",
)
# The endpoint might return 200 with error in response body
assert response.status_code in [200, 400, 422]
# Test invalid model pricing
response = authenticated_client.get(
"/metrics/api/pricing/invalid-model-xyz"
)
# The endpoint returns 200 even for invalid models
assert response.status_code in [200, 404, 400]
logger.info("✅ Metrics error handling passed")
+188
View File
@@ -0,0 +1,188 @@
"""Test with proper CSRF handling."""
import re
import time
from loguru import logger
class TestProperCSRF:
"""Test authentication with proper CSRF token handling."""
def test_login_with_csrf(self, client):
"""Test login with proper CSRF token."""
# Step 1: Get the login page to establish session and get CSRF token
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
logger.info("Step 1: Getting login page...")
response = client.get("/auth/login")
assert response.status_code == 200
logger.info(f"GET /auth/login -> {response.status_code}")
# Extract CSRF token from the form
csrf_match = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
assert csrf_match is not None, "Could not find CSRF token in login page"
csrf_token = csrf_match.group(1)
logger.info(f"CSRF token extracted: {csrf_token[:20]}...")
# Create unique test username to avoid conflicts
test_username = f"testuser_csrf_{int(time.time() * 1000)}"
test_password = "TestPass123"
# Register user first
register_data = {
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
"csrf_token": csrf_token,
}
register_response = client.post(
"/auth/register", data=register_data, follow_redirects=False
)
logger.info(
f"Register response status: {register_response.status_code}"
)
# After registration, check if we were redirected and handle accordingly
if register_response.status_code == 302:
# We might be auto-logged in after registration, so go directly to auth check
response = client.get("/auth/check")
if response.status_code == 200:
data = response.get_json()
if data.get("authenticated") is True:
logger.info(
"✅ User auto-authenticated after registration!"
)
logger.info(f"Auth check: {data}")
return
# Get fresh CSRF token for login
response = client.get("/auth/login")
logger.info(
f"Login page status after registration: {response.status_code}"
)
# If we're redirected, we might already be logged in
if response.status_code == 302:
response = client.get("/auth/check")
if response.status_code == 200:
data = response.get_json()
if data.get("authenticated") is True:
logger.info("✅ User already authenticated!")
logger.info(f"Auth check: {data}")
return
csrf_match = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
assert csrf_match is not None, (
f"Could not find CSRF token in login page after registration. Status: {response.status_code}, Content preview: {response.data.decode()[:200]}"
)
csrf_token = csrf_match.group(1)
# Step 2: Submit login form with CSRF token
login_data = {
"username": test_username,
"password": test_password,
"csrf_token": csrf_token,
}
logger.info("Step 2: Submitting login form...")
response = client.post(
"/auth/login",
data=login_data,
headers={
"Referer": "http://localhost/auth/login"
}, # Some CSRF checks require referer
follow_redirects=False,
)
logger.info(f"POST /auth/login -> {response.status_code}")
assert response.status_code == 302, "Login should redirect on success"
logger.info("✅ Login successful!")
# Step 3: Follow redirect and test authenticated access
location = response.headers.get("Location", "/")
response = client.get(location)
logger.info(f"GET {location} -> {response.status_code}")
assert response.status_code == 200
# Test auth check
response = client.get("/auth/check")
logger.info(f"GET /auth/check -> {response.status_code}")
assert response.status_code == 200
data = response.get_json()
logger.info(f"Auth check: {data}")
assert data.get("authenticated") is True
logger.info("✅ Authentication test passed!")
def test_login_without_csrf(self, app):
"""Test that login fails without CSRF token."""
# Enable CSRF for this test
app.config["WTF_CSRF_ENABLED"] = True
client = app.test_client()
# Try to login without CSRF token
login_data = {
"username": "testuser",
"password": "TestPass123",
# No csrf_token
}
response = client.post(
"/auth/login", data=login_data, follow_redirects=False
)
# Should fail due to missing CSRF token
assert response.status_code != 302 # Should not redirect (success)
logger.info("✅ Login correctly rejected without CSRF token")
def test_login_with_invalid_csrf(self, app):
"""Test that login fails with invalid CSRF token."""
# Enable CSRF for this test
app.config["WTF_CSRF_ENABLED"] = True
client = app.test_client()
# Get login page
response = client.get("/auth/login")
assert response.status_code == 200
# Use an invalid CSRF token
login_data = {
"username": "testuser",
"password": "TestPass123",
"csrf_token": "invalid_token_12345",
}
response = client.post(
"/auth/login", data=login_data, follow_redirects=False
)
# Should fail due to invalid CSRF token
assert response.status_code != 302 # Should not redirect (success)
logger.info("✅ Login correctly rejected with invalid CSRF token")
def test_csrf_token_rotation(self, client):
"""Test that CSRF tokens are rotated between requests."""
# Get first CSRF token
response = client.get("/auth/login")
csrf_match1 = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
csrf_token1 = csrf_match1.group(1) if csrf_match1 else None
# Get second CSRF token
response = client.get("/auth/login")
csrf_match2 = re.search(
r'name="csrf_token" value="([^"]+)"', response.data.decode()
)
csrf_token2 = csrf_match2.group(1) if csrf_match2 else None
# Tokens should be different (rotation) or at least valid
assert csrf_token1 is not None
assert csrf_token2 is not None
logger.info("✅ CSRF tokens are properly generated")
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
Test research creation endpoint specifically
"""
import json
import time
import pytest
@pytest.mark.requires_llm
class TestResearchCreation:
"""Test research creation functionality.
All tests in this class POST to /api/start_research with real
Ollama / searxng / model dependencies (PUNCHLIST Tier 4: FLAKY_SLEEP).
The class-level @pytest.mark.requires_llm gate makes them auto-
skip in environments without a live LLM stack, so CI doesn't
flake on the timing-sensitive sleep(0.5) and model-availability
assertions. Manual runs can invoke them with -m requires_llm.
"""
def test_research_creation_endpoint(self, authenticated_client):
"""Test the research creation endpoint."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: FIX_ASSERTION).
print("\nTesting /api/start_research endpoint...")
research_data = {
"query": "Test research query from Python",
"mode": "quick",
"model": "gpt-3.5-turbo",
"search_engines": ["searxng"],
"local_context": 2000,
"web_context": 2000,
"temperature": 0.7,
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
print(f"Response status: {response.status_code}")
assert response.status_code == 200
data = json.loads(response.data)
if data.get("status") == "success":
assert "research_id" in data
print(f"\n✅ SUCCESS! Research ID: {data.get('research_id')}")
return data["research_id"]
# Alternative response format
assert "research_id" in data
print(f"\n✅ SUCCESS! Research ID: {data['research_id']}")
return data["research_id"]
def test_research_creation_with_minimal_params(self, authenticated_client):
"""Test research creation with minimal parameters."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
research_data = {
"query": "Minimal test query",
"mode": "quick",
"model": "llama2",
"model_provider": "OLLAMA",
}
print(f"\n[DEBUG] Sending minimal research request: {research_data}")
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
print(f"[DEBUG] Response status: {response.status_code}")
if response.status_code != 200:
print(f"[DEBUG] Response data: {response.data.decode()[:500]}")
assert response.status_code == 200
data = json.loads(response.data)
assert "research_id" in data or (
data.get("status") == "success" and "research_id" in data
)
def test_research_creation_validation(self, authenticated_client):
"""Test research creation validation."""
# Missing query
response = authenticated_client.post(
"/api/start_research",
json={"mode": "quick"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data or "message" in data
def test_research_status_check(self, authenticated_client):
"""Test checking research status."""
# First create a research
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
research_data = {
"query": "Status check test",
"mode": "quick",
"model": "llama2",
"model_provider": "OLLAMA",
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
# Extract research_id
if "research_id" in data:
research_id = data["research_id"]
else:
research_id = data.get("data", {}).get("research_id")
assert research_id is not None
# Give it a moment to start
time.sleep(0.5) # allow: unmarked-sleep
# Check status
response = authenticated_client.get(
f"/api/research/{research_id}/status"
)
assert response.status_code == 200
status_data = json.loads(response.data)
assert "status" in status_data or "research_status" in status_data
def test_research_termination(self, authenticated_client):
"""Test terminating a research."""
# First create a research
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
research_data = {
"query": "Termination test",
"mode": "quick",
"model": "llama2",
"model_provider": "OLLAMA",
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
research_id = data.get("research_id") or data.get("data", {}).get(
"research_id"
)
assert research_id is not None
# Give it a moment to start
time.sleep(0.5) # allow: unmarked-sleep
# Terminate it
response = authenticated_client.post(f"/api/terminate/{research_id}")
assert response.status_code in [200, 404] # 404 if already finished
def test_research_modes(self, authenticated_client):
"""Test different research modes."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: FIX_ASSERTION).
modes = ["quick", "normal", "comprehensive"]
for mode in modes:
research_data = {
"query": f"Test {mode} mode",
"mode": mode,
"model": "llama2",
"model_provider": "OLLAMA",
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
assert "research_id" in data or (
data.get("status") == "success" and "research_id" in data
)
print(f"{mode} mode research created successfully")
# Terminate to clean up
research_id = data.get("research_id") or data.get("data", {}).get(
"research_id"
)
if research_id:
authenticated_client.post(f"/api/terminate/{research_id}")
def test_research_with_custom_model(self, authenticated_client):
"""Test research with custom model settings."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
research_data = {
"query": "Custom model test",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "mistral",
"temperature": 0.5,
"max_tokens": 1000,
}
response = authenticated_client.post(
"/api/start_research",
json=research_data,
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
assert "research_id" in data or (
data.get("status") == "success" and "research_id" in data
)
+197
View File
@@ -0,0 +1,197 @@
"""
Test suite for REST API endpoints using minimal queries.
Tests programmatic access functionality with fast, simple requests.
"""
import json
import pytest
# Test timeout in seconds
TEST_TIMEOUT = 30
class TestRestAPI:
"""Test REST API endpoints with minimal queries."""
def test_health_check(self, client):
"""Test the health check endpoint."""
response = client.get("/api/v1/health")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "ok"
assert "timestamp" in data
print("✅ Health check passed")
def test_api_documentation(self, authenticated_client):
"""Test the API documentation endpoint (requires auth)."""
response = authenticated_client.get("/api/v1/")
assert response.status_code == 200
data = json.loads(response.data)
assert data["api_version"] == "v1"
assert "endpoints" in data
assert len(data["endpoints"]) >= 3 # Should have at least 3 endpoints
print("✅ API documentation passed")
@pytest.mark.requires_llm
def test_quick_summary_minimal(self, authenticated_client):
"""Test quick summary with minimal query."""
payload = {
"query": "Python",
"search_tool": "wikipedia",
"iterations": 1,
"temperature": 0.7,
}
response = authenticated_client.post(
"/api/v1/quick_summary",
json=payload,
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
# Verify response structure
assert "query" in data
assert "summary" in data
assert "findings" in data
assert data["query"] == "Python"
assert len(data["summary"]) > 10 # Should have actual content
assert isinstance(data["findings"], list)
print(
f"✅ Quick summary passed - got {len(data['summary'])} chars of summary"
)
def test_quick_summary_validation(self, authenticated_client):
"""Test quick summary endpoint validation."""
# Test missing query
response = authenticated_client.post(
"/api/v1/quick_summary",
json={},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
print("✅ Quick summary validation passed")
def test_analyze_documents_rejects_inline_documents(
self, authenticated_client
):
"""``/api/v1/analyze_documents`` searches a *named local collection*;
it does not accept inline ``documents``.
The endpoint validates request-body keys against the real
``analyze_documents`` signature (``web/api.py``) and rejects unknown
keys with a clear 400 *before* any LLM call, so this needs no real LLM.
Previously this test posted an unsupported ``documents`` key and
asserted a 200 with an ``analysis``/``processed_documents`` body that
the endpoint never returns (the real shape is
``{summary, documents, collection, document_count}``).
"""
payload = {
"documents": ["Python is a programming language."],
"query": "What is Python?",
"collection_name": "test_collection",
}
response = authenticated_client.post(
"/api/v1/analyze_documents",
json=payload,
content_type="application/json",
)
# Unsupported "documents" key -> clear 400 (not an opaque 500).
assert response.status_code == 400
data = json.loads(response.data)
# The rejected key is named *as the offender* (after the colon) — not
# merely a substring of "analyze_documents" in the message prefix.
offenders = data["error"].split(":", 1)[1]
assert "documents" in offenders
assert "documents" not in data["allowed_parameters"]
assert "max_results" in data["allowed_parameters"]
print("✅ Analyze documents rejects inline documents")
def test_analyze_documents_validation(self, authenticated_client):
"""Test analyze documents endpoint validation."""
# Test missing collection_name
response = authenticated_client.post(
"/api/v1/analyze_documents",
json={"query": "test"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
# Test missing query
response = authenticated_client.post(
"/api/v1/analyze_documents",
json={"collection_name": "test"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
print("✅ Analyze documents validation passed")
@pytest.mark.requires_llm
def test_generate_report_minimal(self, authenticated_client):
"""Test generate report with minimal input."""
payload = {
"query": "AI basics",
"research_type": "quick",
}
response = authenticated_client.post(
"/api/v1/generate_report",
json=payload,
content_type="application/json",
)
# This endpoint might not be fully implemented
assert response.status_code in [200, 404, 500]
if response.status_code == 200:
data = json.loads(response.data)
assert "report" in data or "research_id" in data
print("✅ Generate report passed")
else:
print("⚠️ Generate report endpoint not fully implemented")
def test_generate_report_validation(self, authenticated_client):
"""Test generate report endpoint validation."""
# Test with empty payload
response = authenticated_client.post(
"/api/v1/generate_report",
json={},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
print("✅ Generate report validation passed")
def test_error_handling(self, authenticated_client):
"""Test API error handling."""
# Test non-existent endpoint
response = authenticated_client.get("/api/v1/nonexistent")
assert response.status_code == 404
# Test invalid JSON
response = authenticated_client.post(
"/api/v1/quick_summary",
data="invalid json",
content_type="application/json",
)
assert response.status_code in [400, 500]
print("✅ Error handling passed")
+130
View File
@@ -0,0 +1,130 @@
"""
Simple REST API tests with ultra-minimal queries and longer timeouts.
Focus on basic functionality verification.
"""
import json
import pytest
# Extended timeout for research operations
RESEARCH_TIMEOUT = 120 # 2 minutes
class TestRestAPISimple:
"""Simple REST API tests."""
def test_health_and_docs(self, authenticated_client):
"""Test basic non-research endpoints."""
print("🔍 Testing health and documentation endpoints...")
# Health check
response = authenticated_client.get("/api/v1/health")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "ok"
print("✅ Health check passed")
# API documentation (requires auth)
response = authenticated_client.get("/api/v1/")
assert response.status_code == 200
data = json.loads(response.data)
assert data["api_version"] == "v1"
assert "endpoints" in data
print("✅ API documentation passed")
def test_error_handling(self, authenticated_client):
"""Test error handling for malformed requests."""
print("🔍 Testing error handling...")
# Missing query parameter
response = authenticated_client.post(
"/api/v1/quick_summary",
json={},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
print("✅ Error handling for missing query passed")
# Missing parameters for analyze_documents
response = authenticated_client.post(
"/api/v1/analyze_documents",
json={"query": "test"},
content_type="application/json",
)
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
print("✅ Error handling for missing collection_name passed")
@pytest.mark.requires_llm
def test_quick_summary_ultra_minimal(self, authenticated_client):
"""Test quick summary with the most minimal possible query."""
print("🔍 Testing quick summary with ultra-minimal query...")
payload = {
"query": "cat", # Single word, very common
"search_tool": "wikipedia",
"iterations": 1,
"temperature": 0.7,
}
print("Making request...")
response = authenticated_client.post(
"/api/v1/quick_summary",
json=payload,
content_type="application/json",
)
if response.status_code == 200:
data = json.loads(response.data)
# Basic structure validation
required_fields = ["query", "summary", "findings"]
for field in required_fields:
assert field in data, f"Missing field: {field}"
assert data["query"] == "cat"
assert len(data["summary"]) > 0, "Summary should not be empty"
assert isinstance(data["findings"], list), (
"Findings should be a list"
)
print(
f"✅ Quick summary passed - got {len(data['summary'])} chars of summary"
)
print(f" Found {len(data['findings'])} findings")
else:
pytest.fail(
f"Quick summary failed with status {response.status_code}"
)
print(f" Response: {response.data[:200]}")
@pytest.mark.requires_llm
def test_generate_report_minimal(self, authenticated_client):
"""Test generate report with minimal input."""
print("🔍 Testing generate report with minimal input...")
payload = {
"query": "Sun",
"research_type": "quick",
}
response = authenticated_client.post(
"/api/v1/generate_report",
json=payload,
content_type="application/json",
)
# This endpoint might not be fully implemented
if response.status_code == 200:
print("✅ Generate report passed")
elif response.status_code in [404, 500]:
print(
f"⚠️ Generate report endpoint not fully implemented ({response.status_code})"
)
else:
pytest.fail(
f"Generate report failed with unexpected status {response.status_code}"
)
+158
View File
@@ -0,0 +1,158 @@
"""
Focused test for the search engines API endpoint.
This test specifically debugs why the search engines dropdown is not loading.
"""
import json
import pytest
from loguru import logger
class TestSearchEnginesAPI:
"""Test the search engines API endpoint with detailed debugging."""
def test_search_engines_api(self, authenticated_client):
"""Test the search engines API endpoint."""
logger.info("Testing search engines API...")
response = authenticated_client.get(
"/settings/api/available-search-engines"
)
logger.info(f"Response status: {response.status_code}")
assert response.status_code == 200
data = json.loads(response.data)
logger.info(f"Response data: {json.dumps(data, indent=2)}")
# Check the structure
if "engines" in data:
logger.info(f"Found {len(data['engines'])} engines")
assert len(data["engines"]) > 0
for engine_name, engine_data in data["engines"].items():
logger.info(f" - {engine_name}: {engine_data}")
elif "engine_options" in data:
logger.info(f"Found {len(data['engine_options'])} engine options")
assert len(data["engine_options"]) > 0
for option in data["engine_options"]:
logger.info(f" - {option}")
else:
pytest.fail(
"Unexpected data structure - neither 'engines' nor 'engine_options' found"
)
@pytest.mark.skip(
reason="Search engine display_name settings are not loaded by default in test environment"
)
def test_related_search_settings(self, authenticated_client):
"""Check related search engine settings."""
logger.info("Checking related settings...")
# Check if search engine settings exist
endpoints = [
"/settings/api/search.engine.web.searxng.display_name",
"/settings/api/search.engine.web.duckduckgo.display_name",
"/settings/api/search.engine.web.google.display_name",
"/settings/api/search.engine.web.bing.display_name",
]
found_settings = 0
for endpoint in endpoints:
response = authenticated_client.get(endpoint)
if response.status_code == 200:
data = json.loads(response.data)
logger.info(f"{endpoint}: {data}")
found_settings += 1
else:
logger.warning(f"{endpoint}: {response.status_code}")
# At least some search engines should be configured
assert found_settings > 0, "No search engine settings found"
@pytest.mark.skip(
reason="Search settings are not loaded by default in test environment"
)
def test_all_search_settings(self, authenticated_client):
"""Check all settings to see what's available."""
logger.info("Checking all settings...")
response = authenticated_client.get("/settings/api")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "success"
assert "settings" in data
# Find search-related settings
search_settings = {
k: v for k, v in data["settings"].items() if "search" in k
}
logger.info(f"Found {len(search_settings)} search-related settings:")
for key, value in search_settings.items():
logger.info(f" {key}: {value}")
# Should have some search settings
assert len(search_settings) > 0, "No search settings found"
def test_search_engine_configuration(self, authenticated_client):
"""Test search engine configuration endpoints."""
# Test getting search engine configuration
response = authenticated_client.get(
"/settings/api/search.default_search_engine"
)
if response.status_code == 200:
data = json.loads(response.data)
logger.info(f"Default search engine: {data}")
assert "value" in data or "setting" in data
else:
logger.warning(
f"No default search engine configured: {response.status_code}"
)
# Test search engine specific settings
response = authenticated_client.get("/settings/api/search.max_results")
if response.status_code == 200:
data = json.loads(response.data)
logger.info(f"Max search results: {data}")
assert "value" in data or "setting" in data
def test_search_engines_dropdown_data(self, authenticated_client):
"""Test the exact data structure needed for dropdown."""
response = authenticated_client.get(
"/settings/api/available-search-engines"
)
assert response.status_code == 200
data = json.loads(response.data)
# The frontend expects either:
# 1. {"engines": {"name": {"display_name": "Name", "enabled": true}}}
# 2. {"engine_options": ["engine1", "engine2"]}
if "engines" in data:
# Validate engines structure
for engine_name, engine_data in data["engines"].items():
assert isinstance(engine_data, dict), (
f"Engine {engine_name} data should be dict"
)
# Check for expected fields
if "display_name" in engine_data:
assert isinstance(engine_data["display_name"], str)
if "enabled" in engine_data:
assert isinstance(engine_data["enabled"], bool)
elif "engine_options" in data:
# Validate engine_options structure
assert isinstance(data["engine_options"], list), (
"engine_options should be list"
)
assert len(data["engine_options"]) > 0, (
"engine_options should not be empty"
)
for option in data["engine_options"]:
assert isinstance(option, (str, dict)), (
"Each option should be string or dict"
)
+148
View File
@@ -0,0 +1,148 @@
"""Test session persistence after login."""
import json
import time
from loguru import logger
class TestSessionPersistence:
"""Test session persistence functionality."""
def test_session_after_login(self, client):
"""Test that session persists after login."""
# Create unique test username to avoid conflicts
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REFACTOR_SPLIT).
test_username = f"testuser_session_{int(time.time() * 1000)}"
test_password = "TestPass123"
# Get login page
response = client.get("/auth/login")
assert response.status_code == 200
logger.info(f"GET /auth/login -> {response.status_code}")
# Register/Login
response = client.post(
"/auth/register",
data={
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
},
follow_redirects=False,
)
# Login
response = client.post(
"/auth/login",
data={
"username": test_username,
"password": test_password,
},
follow_redirects=False,
)
logger.info(f"POST /auth/login -> {response.status_code}")
# Check if we're logged in by verifying successful redirect
assert (
response.status_code == 302
) # Should redirect after successful login
# Access home page
response = client.get("/", follow_redirects=False)
logger.info(f"GET / -> {response.status_code}")
# Should not redirect to login
if response.status_code == 302:
location = response.headers.get("Location", "")
assert "/auth/login" not in location, (
"Session was lost! Redirected back to login"
)
else:
assert response.status_code == 200
# Check if we're actually logged in
assert (
b"logout" in response.data.lower()
or b"dashboard" in response.data.lower()
)
logger.info("✅ Successfully logged in and session preserved!")
def test_auth_check_persistence(self, authenticated_client):
"""Test auth check endpoint with persistent session."""
response = authenticated_client.get("/auth/check")
assert response.status_code == 200
logger.info(f"GET /auth/check -> {response.status_code}")
data = json.loads(response.data)
logger.info(f"Auth check data: {data}")
assert data.get("authenticated") is True
assert "username" in data
logger.info(f"✅ Authenticated as: {data.get('username')}")
def test_api_access_persistence(self, authenticated_client):
"""Test API access with persistent session."""
response = authenticated_client.get("/settings/api")
assert response.status_code == 200
logger.info(f"GET /settings/api -> {response.status_code}")
logger.info("✅ API access works!")
data = json.loads(response.data)
assert data["status"] == "success"
assert "settings" in data
def test_multiple_requests_session(self, authenticated_client):
"""Test that session persists across multiple requests."""
endpoints = [
"/auth/check",
"/settings/api",
"/history/api",
"/metrics/api/metrics",
]
for endpoint in endpoints:
response = authenticated_client.get(endpoint)
assert response.status_code == 200
logger.info(f"{endpoint} -> {response.status_code}")
def test_session_after_page_navigation(self, authenticated_client):
"""Test session persists through page navigation."""
pages = ["/", "/history", "/settings", "/metrics/"]
for page in pages:
response = authenticated_client.get(page)
assert response.status_code == 200
# Should not redirect to login
assert b"login" not in response.request.url.encode()
logger.info(f"✅ Successfully accessed {page}")
def test_session_integrity_check(self, authenticated_client):
"""Test session integrity check endpoint."""
response = authenticated_client.get("/auth/integrity-check")
assert response.status_code == 200
data = json.loads(response.data)
assert "integrity" in data
assert "username" in data
logger.info(
f"✅ Session integrity verified for user: {data['username']}"
)
def test_logout_clears_session(self, authenticated_client):
"""Test that logout properly clears the session."""
# First verify we're logged in
response = authenticated_client.get("/auth/check")
data = json.loads(response.data)
assert data["authenticated"] is True
# Logout
response = authenticated_client.post(
"/auth/logout", follow_redirects=True
)
assert response.status_code == 200
# Try to access protected endpoint
response = authenticated_client.get("/settings/api")
# Should either get 401 or redirect to login
assert response.status_code in [401, 302]
logger.info("✅ Session properly cleared after logout")
+125
View File
@@ -0,0 +1,125 @@
"""Simple authentication test."""
import json
import time
from loguru import logger
class TestSimpleAuth:
"""Simple authentication tests."""
def test_login_flow(self, client):
"""Test the login flow."""
# Create unique test username to avoid conflicts
test_username = f"testuser_simple_{int(time.time() * 1000)}"
test_password = "TestPass123"
# 1. Get login page
response = client.get("/auth/login")
assert response.status_code == 200
logger.info(f"GET /auth/login -> {response.status_code}")
# 2. Register first (in case user doesn't exist)
response = client.post(
"/auth/register",
data={
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
},
follow_redirects=False,
)
# Either 302 (success) or error if already exists
logger.info(f"POST /auth/register -> {response.status_code}")
# 3. Login
response = client.post(
"/auth/login",
data={
"username": test_username,
"password": test_password,
},
follow_redirects=True,
)
logger.info(
f"POST /auth/login (with redirects) -> {response.status_code}"
)
logger.info(f"Final URL: {response.request.url}")
# Should redirect to home page after successful login
assert response.status_code == 200
def test_auth_check(self, authenticated_client):
"""Test auth check endpoint."""
response = authenticated_client.get("/auth/check")
assert response.status_code == 200
logger.info(f"GET /auth/check -> {response.status_code}")
data = json.loads(response.data)
logger.info(f"Auth check data: {data}")
assert data["authenticated"] is True
def test_access_home_page(self, authenticated_client):
"""Test accessing home page when authenticated."""
response = authenticated_client.get("/")
assert response.status_code == 200
logger.info(f"GET / -> {response.status_code}")
def test_api_access(self, authenticated_client):
"""Test API access when authenticated."""
response = authenticated_client.get(
"/settings/api/available-search-engines"
)
assert response.status_code == 200
logger.info(
f"GET /settings/api/available-search-engines -> {response.status_code}"
)
data = json.loads(response.data)
logger.info(f"Search engines response has keys: {list(data.keys())}")
if "engine_options" in data:
logger.info(
f"Found {len(data['engine_options'])} search engine options"
)
assert len(data["engine_options"]) > 0
elif "engines" in data:
logger.info(f"Found {len(data['engines'])} search engines")
assert len(data["engines"]) > 0
def test_unauthenticated_api_access(self, client):
"""Test API access without authentication."""
# Should get 401 or redirect to login
response = client.get("/settings/api/available-search-engines")
assert response.status_code in [401, 302]
logger.info(
f"GET /settings/api/available-search-engines (unauth) -> {response.status_code}"
)
def test_logout(self, authenticated_client):
"""Test logout functionality."""
# First verify we're authenticated
response = authenticated_client.get("/auth/check")
data = json.loads(response.data)
assert data["authenticated"] is True
# Logout
print("\n[DEBUG] Testing logout endpoint with POST method")
response = authenticated_client.post(
"/auth/logout", follow_redirects=True
)
print(f"[DEBUG] Logout response status: {response.status_code}")
print(f"[DEBUG] Logout response headers: {dict(response.headers)}")
if response.status_code != 200:
print(
f"[DEBUG] Logout response data: {response.data.decode()[:500]}"
)
assert response.status_code == 200
# Check we're no longer authenticated
response = authenticated_client.get("/auth/check")
assert response.status_code in [401, 200]
if response.status_code == 200:
data = json.loads(response.data)
assert data["authenticated"] is False
+109
View File
@@ -0,0 +1,109 @@
"""Test authentication without CSRF to verify CSRF protection."""
from loguru import logger
class TestWithoutCSRF:
"""Test that authentication properly requires CSRF tokens."""
def test_login_without_csrf_protection(self, app):
"""Test login behavior when CSRF protection is disabled."""
# Temporarily disable CSRF for this test
app.config["WTF_CSRF_ENABLED"] = False
client = app.test_client()
# Register user
register_data = {
"username": "testuser_nocsrf",
"password": "TestPass123",
"confirm_password": "TestPass123",
"acknowledge": "true",
}
client.post("/auth/register", data=register_data)
# Try login without CSRF token
login_data = {
"username": "testuser_nocsrf",
"password": "TestPass123",
}
# Direct POST without getting the login page first
response = client.post(
"/auth/login", data=login_data, follow_redirects=False
)
logger.info(
f"POST /auth/login (CSRF disabled) -> {response.status_code}"
)
# With CSRF disabled, login should work
assert response.status_code == 302
logger.info("✅ Login worked with CSRF disabled!")
# Re-enable CSRF
app.config["WTF_CSRF_ENABLED"] = True
def test_login_requires_csrf_by_default(self, app):
"""Test that login requires CSRF token when enabled."""
# Create a new client with CSRF enabled
app.config["WTF_CSRF_ENABLED"] = True
client = app.test_client()
# Try login without CSRF token (CSRF should be enabled)
login_data = {"username": "testuser", "password": "TestPass123"}
# Direct POST without CSRF token
response = client.post(
"/auth/login", data=login_data, follow_redirects=False
)
logger.info(
f"POST /auth/login (no CSRF token) -> {response.status_code}"
)
# Should fail because CSRF token is missing
assert response.status_code != 302 # Should not redirect on success
logger.info("✅ Login correctly requires CSRF token!")
def test_api_endpoints_without_csrf(self, authenticated_client):
"""Test that API endpoints work without CSRF for authenticated users."""
# API endpoints typically don't require CSRF for GET requests
endpoints = [
"/settings/api",
"/history/api",
"/metrics/api/cost-analytics",
]
for endpoint in endpoints:
response = authenticated_client.get(endpoint)
assert response.status_code == 200
logger.info(f"✅ GET {endpoint} works without CSRF token")
def test_post_api_without_csrf(self, authenticated_client):
"""Test that POST API endpoints properly handle CSRF."""
# Try to start research without CSRF token
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: FIX_ASSERTION).
research_data = {"query": "test query", "mode": "quick"}
# POST without CSRF should fail unless endpoint is exempt
response = authenticated_client.post(
"/api/start_research",
json=research_data, # JSON requests typically bypass CSRF
content_type="application/json",
)
# JSON API endpoints often bypass CSRF protection
logger.info(
f"POST /api/start_research (JSON) -> {response.status_code}"
)
# Form POST without CSRF should fail
response = authenticated_client.post(
"/api/start_research",
data=research_data, # Form data should require CSRF
follow_redirects=False,
)
logger.info(
f"POST /api/start_research (form data, no CSRF) -> {response.status_code}"
)