Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
143
tests/legacy/test_access_fix.py
Normal file
143
tests/legacy/test_access_fix.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test API Key Access Control Fix
|
||||
|
||||
Validates that:
|
||||
1. Site-level keys get proper error message for system tools (not validation failure)
|
||||
2. Global keys work for all tools
|
||||
3. Per-project keys work for their own unified tools
|
||||
"""
|
||||
|
||||
|
||||
def test_system_tool_detection():
|
||||
"""Test that system tools are detected correctly."""
|
||||
SYSTEM_TOOLS = [
|
||||
"list_projects",
|
||||
"get_project_info",
|
||||
"check_all_projects_health",
|
||||
"get_project_health",
|
||||
"get_system_metrics",
|
||||
"get_system_uptime",
|
||||
"get_rate_limit_stats",
|
||||
"export_health_metrics",
|
||||
"manage_api_keys_list",
|
||||
"manage_api_keys_get_info",
|
||||
]
|
||||
|
||||
# Test with and without MCP prefix
|
||||
test_cases = [
|
||||
("list_projects", True),
|
||||
("mcp__coolify-projects__list_projects", True),
|
||||
("get_project_info", True),
|
||||
("wordpress_list_posts", False),
|
||||
("woocommerce_list_orders", False),
|
||||
("unknown", False),
|
||||
]
|
||||
|
||||
for tool_name, expected in test_cases:
|
||||
is_system_tool = any(tool_name.endswith(st) for st in SYSTEM_TOOLS)
|
||||
assert (
|
||||
is_system_tool == expected
|
||||
), f"Failed for {tool_name}: expected {expected}, got {is_system_tool}"
|
||||
print(f"✓ {tool_name}: is_system_tool={is_system_tool}")
|
||||
|
||||
|
||||
def test_unified_tool_detection():
|
||||
"""Test that unified tools are detected correctly."""
|
||||
test_cases = [
|
||||
("wordpress_list_posts", True),
|
||||
("woocommerce_list_orders", True),
|
||||
("mcp__coolify-projects__wordpress_list_posts", True),
|
||||
("mcp__coolify-projects__woocommerce_list_orders", True),
|
||||
("list_projects", False),
|
||||
("get_project_info", False),
|
||||
("unknown", True), # Falls to fallback
|
||||
]
|
||||
|
||||
for tool_name, expected in test_cases:
|
||||
if tool_name == "unknown":
|
||||
is_unified_tool = True # Fallback
|
||||
else:
|
||||
is_unified_tool = (
|
||||
tool_name.startswith("wordpress_")
|
||||
or tool_name.startswith("woocommerce_")
|
||||
or tool_name.startswith("mcp__coolify-projects__wordpress_")
|
||||
or tool_name.startswith("mcp__coolify-projects__woocommerce_")
|
||||
)
|
||||
assert (
|
||||
is_unified_tool == expected
|
||||
), f"Failed for {tool_name}: expected {expected}, got {is_unified_tool}"
|
||||
print(f"✓ {tool_name}: is_unified_tool={is_unified_tool}")
|
||||
|
||||
|
||||
def test_skip_project_check_logic():
|
||||
"""Test that skip_project_check is set correctly."""
|
||||
SYSTEM_TOOLS = [
|
||||
"list_projects",
|
||||
"get_project_info",
|
||||
"check_all_projects_health",
|
||||
"get_project_health",
|
||||
"get_system_metrics",
|
||||
"get_system_uptime",
|
||||
"get_rate_limit_stats",
|
||||
"export_health_metrics",
|
||||
"manage_api_keys_list",
|
||||
"manage_api_keys_get_info",
|
||||
]
|
||||
|
||||
test_cases = [
|
||||
# (tool_name, expected_skip)
|
||||
("list_projects", True), # System tool
|
||||
("wordpress_list_posts", True), # Unified tool
|
||||
("woocommerce_list_orders", True), # Unified tool
|
||||
("mcp__coolify-projects__list_projects", True), # System tool with prefix
|
||||
("mcp__coolify-projects__wordpress_list_posts", True), # Unified tool with prefix
|
||||
("unknown", True), # Fallback to unified
|
||||
]
|
||||
|
||||
for tool_name, expected_skip in test_cases:
|
||||
# Determine is_system_tool
|
||||
is_system_tool = any(tool_name.endswith(st) for st in SYSTEM_TOOLS)
|
||||
|
||||
# Determine is_unified_tool
|
||||
if tool_name == "unknown":
|
||||
is_unified_tool = True
|
||||
else:
|
||||
is_unified_tool = (
|
||||
tool_name.startswith("wordpress_")
|
||||
or tool_name.startswith("woocommerce_")
|
||||
or tool_name.startswith("mcp__coolify-projects__wordpress_")
|
||||
or tool_name.startswith("mcp__coolify-projects__woocommerce_")
|
||||
)
|
||||
|
||||
# Calculate skip_project_check
|
||||
skip_project_check = is_unified_tool or is_system_tool
|
||||
|
||||
assert skip_project_check == expected_skip, (
|
||||
f"Failed for {tool_name}: expected skip={expected_skip}, got {skip_project_check} "
|
||||
f"(unified={is_unified_tool}, system={is_system_tool})"
|
||||
)
|
||||
print(
|
||||
f"✓ {tool_name}: skip_project_check={skip_project_check} (unified={is_unified_tool}, system={is_system_tool})"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n=== Testing System Tool Detection ===")
|
||||
test_system_tool_detection()
|
||||
|
||||
print("\n=== Testing Unified Tool Detection ===")
|
||||
test_unified_tool_detection()
|
||||
|
||||
print("\n=== Testing skip_project_check Logic ===")
|
||||
test_skip_project_check_logic()
|
||||
|
||||
print("\n✅ All tests passed!")
|
||||
print("\nExpected behavior after fix:")
|
||||
print("1. Site-level key + list_projects → Validation passes, then gets proper error:")
|
||||
print(" 'System tools require global API key (project_id=\"*\")'")
|
||||
print(
|
||||
"2. Site-level key + wordpress_list_posts(site=other) → Validation passes, handler blocks with:"
|
||||
)
|
||||
print(" 'Access denied. This API key is restricted to project X'")
|
||||
print("3. Global key + any tool → Works")
|
||||
161
tests/legacy/test_api_key_isolation.py
Normal file
161
tests/legacy/test_api_key_isolation.py
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick test to verify per-project API key isolation.
|
||||
|
||||
This test checks that:
|
||||
1. Per-project API key can access its own project
|
||||
2. Per-project API key CANNOT access other projects
|
||||
3. Global API key can access all projects
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
# Mock setup
|
||||
os.environ["MASTER_API_KEY"] = "test_master_key_123"
|
||||
os.environ["WORDPRESS_SITE1_URL"] = "https://site1.example.com"
|
||||
os.environ["WORDPRESS_SITE1_USERNAME"] = "admin"
|
||||
os.environ["WORDPRESS_SITE1_APP_PASSWORD"] = "password1"
|
||||
os.environ["WORDPRESS_SITE4_URL"] = "https://site4.example.com"
|
||||
os.environ["WORDPRESS_SITE4_USERNAME"] = "admin"
|
||||
os.environ["WORDPRESS_SITE4_APP_PASSWORD"] = "password4"
|
||||
|
||||
# Import after env setup
|
||||
from core.api_keys import get_api_key_manager
|
||||
from core.project_manager import get_project_manager
|
||||
from core.site_registry import get_site_registry
|
||||
from core.unified_tools import UnifiedToolGenerator
|
||||
|
||||
print("=" * 60)
|
||||
print("Testing Per-Project API Key Isolation")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize
|
||||
api_key_manager = get_api_key_manager()
|
||||
project_manager = get_project_manager()
|
||||
site_registry = get_site_registry()
|
||||
|
||||
# Discover sites
|
||||
from plugins import registry as plugin_registry
|
||||
|
||||
plugin_types = plugin_registry.get_registered_types()
|
||||
site_registry.discover_sites(plugin_types)
|
||||
|
||||
print(f"\nDiscovered sites: {list(site_registry.sites.keys())}")
|
||||
|
||||
# Create API keys
|
||||
print("\n1. Creating API keys...")
|
||||
|
||||
# Global key
|
||||
global_key = api_key_manager.create_key(
|
||||
project_id="*", scope="admin", description="Global test key"
|
||||
)
|
||||
print(f" ✓ Global key created: {global_key}")
|
||||
|
||||
# Per-project key for wordpress_site4
|
||||
site4_key = api_key_manager.create_key(
|
||||
project_id="wordpress_site4", scope="admin", description="Site4 only key"
|
||||
)
|
||||
print(f" ✓ Site4 key created: {site4_key}")
|
||||
|
||||
# Create unified tool generator
|
||||
unified_gen = UnifiedToolGenerator(project_manager)
|
||||
unified_tools = unified_gen.generate_all_unified_tools()
|
||||
print(f"\n2. Generated {len(unified_tools)} unified tools")
|
||||
|
||||
# Find wordpress_list_posts tool
|
||||
list_posts_tool = None
|
||||
for tool in unified_tools:
|
||||
if tool["name"] == "wordpress_list_posts":
|
||||
list_posts_tool = tool
|
||||
break
|
||||
|
||||
if not list_posts_tool:
|
||||
print(" ✗ wordpress_list_posts tool not found!")
|
||||
exit(1)
|
||||
|
||||
print(" ✓ Found wordpress_list_posts tool")
|
||||
|
||||
|
||||
async def test_access(key_token, site_id, expected_result):
|
||||
"""Test if a key can access a site"""
|
||||
from server import _api_key_context
|
||||
|
||||
# Validate key and set context (simulating middleware)
|
||||
key_id = api_key_manager.validate_key(
|
||||
key_token, project_id="*", required_scope="read", skip_project_check=True
|
||||
)
|
||||
|
||||
if key_id:
|
||||
key = api_key_manager.keys.get(key_id)
|
||||
_api_key_context.set(
|
||||
{
|
||||
"key_id": key_id,
|
||||
"project_id": key.project_id,
|
||||
"scope": key.scope,
|
||||
"is_global": key.project_id == "*",
|
||||
}
|
||||
)
|
||||
|
||||
# Try to call the handler
|
||||
handler = list_posts_tool["handler"]
|
||||
result = await handler(site=site_id, per_page=1)
|
||||
|
||||
# Check result
|
||||
is_error = isinstance(result, str) and result.startswith("Error: Access denied")
|
||||
|
||||
if expected_result == "allowed":
|
||||
if not is_error:
|
||||
print(" ✓ Access allowed as expected")
|
||||
return True
|
||||
else:
|
||||
print(" ✗ FAIL: Access denied but should be allowed!")
|
||||
print(f" Result: {result[:100]}")
|
||||
return False
|
||||
else: # expected_result == "denied"
|
||||
if is_error:
|
||||
print(" ✓ Access denied as expected")
|
||||
return True
|
||||
else:
|
||||
print(" ✗ FAIL: Access allowed but should be denied!")
|
||||
print(f" Result: {result[:100] if isinstance(result, str) else str(result)[:100]}")
|
||||
return False
|
||||
|
||||
|
||||
async def run_tests():
|
||||
"""Run all test cases"""
|
||||
print("\n3. Testing access control...")
|
||||
|
||||
all_pass = True
|
||||
|
||||
# Test 1: Per-project key accessing its own project
|
||||
print("\n Test 1: Per-project key (site4) → site4")
|
||||
all_pass &= await test_access(site4_key, "site4", "allowed")
|
||||
|
||||
# Test 2: Per-project key accessing different project
|
||||
print("\n Test 2: Per-project key (site4) → site1")
|
||||
all_pass &= await test_access(site4_key, "site1", "denied")
|
||||
|
||||
# Test 3: Global key accessing any project
|
||||
print("\n Test 3: Global key → site1")
|
||||
all_pass &= await test_access(global_key, "site1", "allowed")
|
||||
|
||||
print("\n Test 4: Global key → site4")
|
||||
all_pass &= await test_access(global_key, "site4", "allowed")
|
||||
|
||||
return all_pass
|
||||
|
||||
|
||||
# Run tests
|
||||
result = asyncio.run(run_tests())
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if result:
|
||||
print("✅ All tests PASSED!")
|
||||
print("Per-project API key isolation is working correctly!")
|
||||
else:
|
||||
print("❌ Some tests FAILED!")
|
||||
print("Per-project API key isolation has issues!")
|
||||
print("=" * 60)
|
||||
|
||||
exit(0 if result else 1)
|
||||
60
tests/legacy/test_context_fix.py
Normal file
60
tests/legacy/test_context_fix.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test context-based API key isolation after fixing circular import.
|
||||
"""
|
||||
|
||||
from core.context import clear_api_key_context, get_api_key_context, set_api_key_context
|
||||
|
||||
print("=" * 60)
|
||||
print("Testing Context API")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: Set and get context
|
||||
print("\nTest 1: Set and get context")
|
||||
set_api_key_context(
|
||||
key_id="key_test123", project_id="wordpress_site4", scope="admin", is_global=False
|
||||
)
|
||||
ctx = get_api_key_context()
|
||||
assert ctx["key_id"] == "key_test123"
|
||||
assert ctx["project_id"] == "wordpress_site4"
|
||||
assert not ctx["is_global"]
|
||||
print(" ✓ PASS: Context set and retrieved correctly")
|
||||
|
||||
# Test 2: Check access logic
|
||||
print("\nTest 2: Access logic for per-project key")
|
||||
allowed_project = ctx["project_id"]
|
||||
target_project = "wordpress_site4"
|
||||
if allowed_project == target_project:
|
||||
print(" ✓ PASS: Access allowed (same project)")
|
||||
else:
|
||||
print(" ✗ FAIL: Should allow access")
|
||||
|
||||
# Test 3: Different project access
|
||||
print("\nTest 3: Access logic for different project")
|
||||
target_project = "wordpress_site1"
|
||||
if allowed_project != target_project:
|
||||
print(" ✓ PASS: Access denied (different project)")
|
||||
else:
|
||||
print(" ✗ FAIL: Should deny access")
|
||||
|
||||
# Test 4: Global key
|
||||
print("\nTest 4: Global key access")
|
||||
set_api_key_context(key_id="key_global", project_id="*", scope="admin", is_global=True)
|
||||
ctx = get_api_key_context()
|
||||
if ctx["is_global"]:
|
||||
print(" ✓ PASS: Global key bypasses project check")
|
||||
else:
|
||||
print(" ✗ FAIL: Should be global")
|
||||
|
||||
# Test 5: Clear context
|
||||
print("\nTest 5: Clear context")
|
||||
clear_api_key_context()
|
||||
ctx = get_api_key_context()
|
||||
if ctx is None:
|
||||
print(" ✓ PASS: Context cleared")
|
||||
else:
|
||||
print(" ✗ FAIL: Context should be None")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ All context tests passed!")
|
||||
print("=" * 60)
|
||||
200
tests/legacy/test_health_monitor.py
Normal file
200
tests/legacy/test_health_monitor.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Phase 7.2 Enhanced Health Monitoring
|
||||
|
||||
Tests:
|
||||
1. Health monitor initialization
|
||||
2. Metrics recording
|
||||
3. Health checks
|
||||
4. System metrics
|
||||
5. Alert thresholds
|
||||
6. Metrics export
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Fix Windows console encoding
|
||||
if sys.platform == "win32":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from core import AuditLogger, ProjectManager, initialize_health_monitor
|
||||
|
||||
|
||||
async def test_health_monitor():
|
||||
"""Test health monitoring system."""
|
||||
print("=" * 60)
|
||||
print("Phase 7.2 - Enhanced Health Monitoring Tests")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize components
|
||||
print("\n1. Initializing components...")
|
||||
project_manager = ProjectManager()
|
||||
audit_logger = AuditLogger()
|
||||
|
||||
health_monitor = initialize_health_monitor(
|
||||
project_manager=project_manager,
|
||||
audit_logger=audit_logger,
|
||||
metrics_retention_hours=24,
|
||||
max_metrics_per_project=1000,
|
||||
)
|
||||
|
||||
print("✅ Health monitor initialized")
|
||||
print(" Retention: 24 hours")
|
||||
print(" Max metrics per project: 1000")
|
||||
|
||||
# Test 2: Record some sample metrics
|
||||
print("\n2. Recording sample metrics...")
|
||||
|
||||
# Simulate successful requests
|
||||
for i in range(5):
|
||||
health_monitor.record_request(
|
||||
project_id="wordpress_site1", response_time_ms=100.0 + (i * 10), success=True
|
||||
)
|
||||
print("✅ Recorded 5 successful requests for wordpress_site1")
|
||||
|
||||
# Simulate failed requests
|
||||
for i in range(2):
|
||||
health_monitor.record_request(
|
||||
project_id="wordpress_site1",
|
||||
response_time_ms=500.0,
|
||||
success=False,
|
||||
error_message="Connection timeout",
|
||||
)
|
||||
print("✅ Recorded 2 failed requests for wordpress_site1")
|
||||
|
||||
# Record for another project
|
||||
health_monitor.record_request(project_id="wordpress_site2", response_time_ms=80.0, success=True)
|
||||
print("✅ Recorded 1 successful request for wordpress_site2")
|
||||
|
||||
# Test 3: Get project metrics
|
||||
print("\n3. Getting project metrics...")
|
||||
metrics = health_monitor.get_project_metrics("wordpress_site1", hours=1)
|
||||
print("✅ Metrics for wordpress_site1:")
|
||||
print(f" Total requests: {metrics['total_requests']}")
|
||||
print(f" Successful: {metrics['successful_requests']}")
|
||||
print(f" Failed: {metrics['failed_requests']}")
|
||||
print(f" Error rate: {metrics['error_rate_percent']}%")
|
||||
print(f" Avg response time: {metrics['response_time']['average_ms']}ms")
|
||||
|
||||
# Test 4: Get system metrics
|
||||
print("\n4. Getting system metrics...")
|
||||
system_metrics = health_monitor.get_system_metrics()
|
||||
print("✅ System metrics:")
|
||||
print(f" Uptime: {system_metrics.uptime_seconds:.2f}s")
|
||||
print(f" Total requests: {system_metrics.total_requests}")
|
||||
print(f" Successful: {system_metrics.successful_requests}")
|
||||
print(f" Failed: {system_metrics.failed_requests}")
|
||||
print(f" Error rate: {system_metrics.error_rate_percent}%")
|
||||
print(f" Avg response time: {system_metrics.average_response_time_ms}ms")
|
||||
|
||||
# Test 5: Get uptime
|
||||
print("\n5. Getting uptime...")
|
||||
uptime = health_monitor.get_uptime()
|
||||
print(f"✅ Uptime: {uptime['uptime_formatted']}")
|
||||
|
||||
# Test 6: Test alert thresholds
|
||||
print("\n6. Testing alert thresholds...")
|
||||
|
||||
# Record a slow request to trigger alert
|
||||
health_monitor.record_request(
|
||||
project_id="wordpress_site3", response_time_ms=6000.0, success=True # > 5000ms threshold
|
||||
)
|
||||
print("✅ Recorded slow request (6000ms)")
|
||||
|
||||
# Record many failures to trigger error rate alert
|
||||
for i in range(10):
|
||||
health_monitor.record_request(
|
||||
project_id="wordpress_site3",
|
||||
response_time_ms=200.0,
|
||||
success=False,
|
||||
error_message="API error",
|
||||
)
|
||||
print("✅ Recorded 10 failures to trigger error rate alert")
|
||||
|
||||
# Test 7: Check if project exists in manager
|
||||
print("\n7. Checking project health (simulation)...")
|
||||
|
||||
# Since we don't have actual WordPress sites running,
|
||||
# we'll just show the metrics we collected
|
||||
site3_metrics = health_monitor.get_project_metrics("wordpress_site3", hours=1)
|
||||
print("✅ wordpress_site3 metrics:")
|
||||
print(f" Total requests: {site3_metrics['total_requests']}")
|
||||
print(f" Error rate: {site3_metrics['error_rate_percent']}%")
|
||||
print(f" Max response time: {site3_metrics['response_time']['max_ms']}ms")
|
||||
|
||||
# Check for alerts
|
||||
alert_data = {
|
||||
"response_time_ms": site3_metrics["response_time"]["max_ms"],
|
||||
"error_rate_percent": site3_metrics["error_rate_percent"],
|
||||
}
|
||||
|
||||
alerts = health_monitor._check_alerts("wordpress_site3", alert_data)
|
||||
if alerts:
|
||||
print("⚠️ Alerts triggered:")
|
||||
for alert in alerts:
|
||||
print(f" {alert}")
|
||||
else:
|
||||
print("✅ No alerts")
|
||||
|
||||
# Test 8: Export metrics
|
||||
print("\n8. Exporting metrics...")
|
||||
export_path = "logs/test_metrics_export.json"
|
||||
exported_file = health_monitor.export_metrics(output_path=export_path)
|
||||
print(f"✅ Metrics exported to: {exported_file}")
|
||||
|
||||
# Verify export file
|
||||
if os.path.exists(export_path):
|
||||
with open(export_path, encoding="utf-8") as f:
|
||||
export_data = json.load(f)
|
||||
print(" Export contains:")
|
||||
print(" - System metrics: ✅")
|
||||
print(" - Uptime info: ✅")
|
||||
print(f" - {len(export_data['projects'])} projects")
|
||||
|
||||
# Test 9: Custom alert threshold
|
||||
print("\n9. Testing custom alert thresholds...")
|
||||
health_monitor.add_alert_threshold(
|
||||
project_id="wordpress_site1",
|
||||
name="Custom Response Time",
|
||||
metric="response_time_ms",
|
||||
threshold=150.0,
|
||||
comparison="gt",
|
||||
severity="warning",
|
||||
)
|
||||
print("✅ Added custom alert threshold for wordpress_site1")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 60)
|
||||
print("✅ Health monitor initialization - PASS")
|
||||
print("✅ Metrics recording - PASS")
|
||||
print("✅ Project metrics retrieval - PASS")
|
||||
print("✅ System metrics retrieval - PASS")
|
||||
print("✅ Uptime tracking - PASS")
|
||||
print("✅ Alert threshold checking - PASS")
|
||||
print("✅ Metrics export - PASS")
|
||||
print("✅ Custom alert thresholds - PASS")
|
||||
print("\n🎉 All tests passed!")
|
||||
print("=" * 60)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(test_health_monitor())
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed with error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
71
tests/legacy/test_oauth_metadata.py
Normal file
71
tests/legacy/test_oauth_metadata.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test OAuth metadata endpoints locally
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
print("Testing OAuth Metadata Endpoints")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: OAuth Authorization Server Metadata
|
||||
print("\n1. Testing /.well-known/oauth-authorization-server")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/.well-known/oauth-authorization-server")
|
||||
print(f" Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✓ Issuer: {data.get('issuer')}")
|
||||
print(f" ✓ Authorization Endpoint: {data.get('authorization_endpoint')}")
|
||||
print(f" ✓ Token Endpoint: {data.get('token_endpoint')}")
|
||||
print(f" ✓ Registration Endpoint: {data.get('registration_endpoint')}")
|
||||
|
||||
if data.get("registration_endpoint"):
|
||||
print(" ✅ RFC 7591 Dynamic Client Registration SUPPORTED")
|
||||
else:
|
||||
print(" ❌ RFC 7591 NOT FOUND in metadata")
|
||||
else:
|
||||
print(f" ❌ Failed: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
# Test 2: OAuth Protected Resource Metadata
|
||||
print("\n2. Testing /.well-known/oauth-protected-resource")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/.well-known/oauth-protected-resource")
|
||||
print(f" Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✓ Resource: {data.get('resource')}")
|
||||
print(f" ✓ Scopes: {data.get('scopes_supported')}")
|
||||
print(" ✅ Protected Resource metadata available")
|
||||
else:
|
||||
print(f" ❌ Failed: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
# Test 3: Registration Endpoint
|
||||
print("\n3. Testing POST /oauth/register")
|
||||
try:
|
||||
payload = {
|
||||
"client_name": "Test Client",
|
||||
"redirect_uris": ["http://localhost:3000/callback"],
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"scope": "read write",
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/oauth/register", json=payload)
|
||||
print(f" Status: {response.status_code}")
|
||||
if response.status_code == 201:
|
||||
data = response.json()
|
||||
print(f" ✓ Client ID: {data.get('client_id')}")
|
||||
print(f" ✓ Client Secret: {data.get('client_secret')[:20]}...")
|
||||
print(" ✅ Dynamic Client Registration WORKING")
|
||||
else:
|
||||
print(f" ❌ Failed: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test Complete!")
|
||||
124
tests/legacy/test_oauth_registration_security.py
Normal file
124
tests/legacy/test_oauth_registration_security.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test OAuth client registration endpoint security.
|
||||
|
||||
Verifies that:
|
||||
1. Registration endpoint requires Master API Key
|
||||
2. Unauthorized requests are rejected with 401
|
||||
3. Valid Master API Key allows registration
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
BASE_URL = os.getenv("BASE_URL", "http://localhost:8000")
|
||||
MASTER_API_KEY = os.getenv("MASTER_API_KEY", "your_master_key_here")
|
||||
|
||||
print("Testing OAuth Client Registration Security")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: Registration without Authorization header
|
||||
print("\n1. Testing registration WITHOUT Authorization header")
|
||||
try:
|
||||
payload = {
|
||||
"client_name": "Unauthorized Test Client",
|
||||
"redirect_uris": ["http://localhost:3000/callback"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"scope": "read",
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/oauth/register", json=payload)
|
||||
print(f" Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 401:
|
||||
data = response.json()
|
||||
print(f" ✅ Correctly rejected: {data.get('error')}")
|
||||
print(f" Message: {data.get('error_description')}")
|
||||
else:
|
||||
print(f" ❌ SECURITY ISSUE: Should return 401, got {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
# Test 2: Registration with invalid API key
|
||||
print("\n2. Testing registration WITH invalid API key")
|
||||
try:
|
||||
payload = {
|
||||
"client_name": "Unauthorized Test Client 2",
|
||||
"redirect_uris": ["http://localhost:3000/callback"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"scope": "read",
|
||||
}
|
||||
|
||||
headers = {"Authorization": "Bearer invalid_api_key_12345"}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/oauth/register", json=payload, headers=headers)
|
||||
print(f" Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 401:
|
||||
data = response.json()
|
||||
print(f" ✅ Correctly rejected: {data.get('error')}")
|
||||
print(f" Message: {data.get('error_description')}")
|
||||
else:
|
||||
print(f" ❌ SECURITY ISSUE: Should return 401, got {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
# Test 3: Registration with valid Master API Key
|
||||
print("\n3. Testing registration WITH valid Master API Key")
|
||||
try:
|
||||
payload = {
|
||||
"client_name": "Authorized Test Client",
|
||||
"redirect_uris": ["http://localhost:3000/callback"],
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"scope": "read write",
|
||||
}
|
||||
|
||||
headers = {"Authorization": f"Bearer {MASTER_API_KEY}"}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/oauth/register", json=payload, headers=headers)
|
||||
print(f" Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 201:
|
||||
data = response.json()
|
||||
print(" ✅ Successfully registered client")
|
||||
print(f" Client ID: {data.get('client_id')}")
|
||||
print(f" Client Secret: {data.get('client_secret', '')[:20]}...")
|
||||
print(f" Client Name: {data.get('client_name')}")
|
||||
else:
|
||||
print(f" ❌ Registration failed: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
# Test 4: Attempt registration with wrong Authorization format
|
||||
print("\n4. Testing registration with wrong Authorization format")
|
||||
try:
|
||||
payload = {
|
||||
"client_name": "Test Client Wrong Format",
|
||||
"redirect_uris": ["http://localhost:3000/callback"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"scope": "read",
|
||||
}
|
||||
|
||||
headers = {"Authorization": MASTER_API_KEY} # Missing "Bearer " prefix
|
||||
|
||||
response = requests.post(f"{BASE_URL}/oauth/register", json=payload, headers=headers)
|
||||
print(f" Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 401:
|
||||
data = response.json()
|
||||
print(f" ✅ Correctly rejected: {data.get('error')}")
|
||||
print(f" Message: {data.get('error_description')}")
|
||||
else:
|
||||
print(f" ❌ Should return 401, got {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Security Test Complete!")
|
||||
print("\nExpected Results:")
|
||||
print(" ✅ Test 1 & 2 & 4: Should return 401 Unauthorized")
|
||||
print(" ✅ Test 3: Should return 201 Created with client credentials")
|
||||
506
tests/legacy/test_rate_limiter.py
Normal file
506
tests/legacy/test_rate_limiter.py
Normal file
@@ -0,0 +1,506 @@
|
||||
"""
|
||||
Test suite for Rate Limiter (Phase 7.3)
|
||||
|
||||
Tests the Token Bucket algorithm, rate limiting logic,
|
||||
and middleware integration.
|
||||
|
||||
Run with: python test_rate_limiter.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from core.rate_limiter import (
|
||||
ClientRateLimitState,
|
||||
RateLimitConfig,
|
||||
RateLimiter,
|
||||
TokenBucket,
|
||||
get_rate_limiter,
|
||||
)
|
||||
|
||||
|
||||
class TestTokenBucket(unittest.TestCase):
|
||||
"""Test Token Bucket algorithm implementation."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test bucket initializes with full capacity."""
|
||||
bucket = TokenBucket(capacity=10, refill_rate=1.0)
|
||||
self.assertEqual(bucket.capacity, 10)
|
||||
self.assertEqual(bucket.refill_rate, 1.0)
|
||||
self.assertEqual(bucket.tokens, 10.0)
|
||||
|
||||
def test_consume_tokens(self):
|
||||
"""Test consuming tokens from bucket."""
|
||||
bucket = TokenBucket(capacity=10, refill_rate=1.0)
|
||||
|
||||
# Should successfully consume 5 tokens
|
||||
self.assertTrue(bucket.consume(5))
|
||||
self.assertEqual(bucket.get_available_tokens(), 5)
|
||||
|
||||
# Should successfully consume 5 more tokens
|
||||
self.assertTrue(bucket.consume(5))
|
||||
self.assertEqual(bucket.get_available_tokens(), 0)
|
||||
|
||||
# Should fail to consume when empty
|
||||
self.assertFalse(bucket.consume(1))
|
||||
|
||||
def test_token_refill(self):
|
||||
"""Test tokens refill over time."""
|
||||
bucket = TokenBucket(capacity=10, refill_rate=10.0) # 10 tokens/second
|
||||
|
||||
# Consume all tokens
|
||||
bucket.consume(10)
|
||||
self.assertEqual(bucket.get_available_tokens(), 0)
|
||||
|
||||
# Wait 0.5 seconds, should refill 5 tokens
|
||||
time.sleep(0.5)
|
||||
available = bucket.get_available_tokens()
|
||||
# Allow some tolerance for timing variations
|
||||
self.assertGreaterEqual(available, 4)
|
||||
self.assertLessEqual(available, 6)
|
||||
|
||||
def test_refill_cap(self):
|
||||
"""Test refill doesn't exceed capacity."""
|
||||
bucket = TokenBucket(capacity=10, refill_rate=10.0)
|
||||
|
||||
# Wait long enough to refill many times over
|
||||
time.sleep(2.0)
|
||||
|
||||
# Should be capped at capacity
|
||||
self.assertEqual(bucket.get_available_tokens(), 10)
|
||||
|
||||
def test_wait_time_calculation(self):
|
||||
"""Test wait time calculation when tokens unavailable."""
|
||||
bucket = TokenBucket(capacity=10, refill_rate=2.0) # 2 tokens/second
|
||||
|
||||
# Consume all tokens
|
||||
bucket.consume(10)
|
||||
|
||||
# Need 1 token, should take 0.5 seconds
|
||||
wait_time = bucket.get_wait_time(1)
|
||||
self.assertAlmostEqual(wait_time, 0.5, delta=0.1)
|
||||
|
||||
# Need 10 tokens, should take 5 seconds
|
||||
wait_time = bucket.get_wait_time(10)
|
||||
self.assertAlmostEqual(wait_time, 5.0, delta=0.1)
|
||||
|
||||
|
||||
class TestRateLimitConfig(unittest.TestCase):
|
||||
"""Test rate limit configuration."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default configuration values."""
|
||||
config = RateLimitConfig()
|
||||
self.assertEqual(config.per_minute, 60)
|
||||
self.assertEqual(config.per_hour, 1000)
|
||||
self.assertEqual(config.per_day, 10000)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test custom configuration values."""
|
||||
config = RateLimitConfig(per_minute=100, per_hour=2000, per_day=20000)
|
||||
self.assertEqual(config.per_minute, 100)
|
||||
self.assertEqual(config.per_hour, 2000)
|
||||
self.assertEqual(config.per_day, 20000)
|
||||
|
||||
def test_from_env(self):
|
||||
"""Test loading configuration from environment."""
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"RATE_LIMIT_PER_MINUTE": "120",
|
||||
"RATE_LIMIT_PER_HOUR": "2000",
|
||||
"RATE_LIMIT_PER_DAY": "15000",
|
||||
},
|
||||
):
|
||||
config = RateLimitConfig.from_env()
|
||||
self.assertEqual(config.per_minute, 120)
|
||||
self.assertEqual(config.per_hour, 2000)
|
||||
self.assertEqual(config.per_day, 15000)
|
||||
|
||||
def test_from_env_with_prefix(self):
|
||||
"""Test loading configuration with prefix."""
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"WORDPRESS_RATE_LIMIT_PER_MINUTE": "80",
|
||||
"WORDPRESS_RATE_LIMIT_PER_HOUR": "1500",
|
||||
"WORDPRESS_RATE_LIMIT_PER_DAY": "12000",
|
||||
},
|
||||
):
|
||||
config = RateLimitConfig.from_env(prefix="WORDPRESS")
|
||||
self.assertEqual(config.per_minute, 80)
|
||||
self.assertEqual(config.per_hour, 1500)
|
||||
self.assertEqual(config.per_day, 12000)
|
||||
|
||||
|
||||
class TestClientRateLimitState(unittest.TestCase):
|
||||
"""Test client rate limit state tracking."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test client state initialization."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(60, 1.0),
|
||||
hour_bucket=TokenBucket(1000, 1.0),
|
||||
day_bucket=TokenBucket(10000, 1.0),
|
||||
)
|
||||
self.assertEqual(state.client_id, "test_client")
|
||||
self.assertEqual(state.total_requests, 0)
|
||||
self.assertEqual(state.rejected_requests, 0)
|
||||
|
||||
def test_successful_request(self):
|
||||
"""Test successful request consumes tokens."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(60, 1.0),
|
||||
hour_bucket=TokenBucket(1000, 1.0),
|
||||
day_bucket=TokenBucket(10000, 1.0),
|
||||
)
|
||||
|
||||
allowed, message, retry_after = state.check_and_consume()
|
||||
self.assertTrue(allowed)
|
||||
self.assertEqual(message, "")
|
||||
self.assertEqual(retry_after, 0.0)
|
||||
self.assertEqual(state.total_requests, 1)
|
||||
self.assertEqual(state.rejected_requests, 0)
|
||||
|
||||
def test_minute_limit_exceeded(self):
|
||||
"""Test rejection when per-minute limit exceeded."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(2, 1.0), # Only 2 requests/minute
|
||||
hour_bucket=TokenBucket(1000, 1.0),
|
||||
day_bucket=TokenBucket(10000, 1.0),
|
||||
)
|
||||
|
||||
# First 2 requests should succeed
|
||||
state.check_and_consume()
|
||||
state.check_and_consume()
|
||||
|
||||
# Third request should fail
|
||||
allowed, message, retry_after = state.check_and_consume()
|
||||
self.assertFalse(allowed)
|
||||
self.assertIn("per minute", message.lower())
|
||||
self.assertGreater(retry_after, 0)
|
||||
self.assertEqual(state.rejected_requests, 1)
|
||||
|
||||
def test_hour_limit_exceeded(self):
|
||||
"""Test rejection when per-hour limit exceeded."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(100, 100.0), # High minute limit
|
||||
hour_bucket=TokenBucket(2, 1.0), # Only 2 requests/hour
|
||||
day_bucket=TokenBucket(10000, 1.0),
|
||||
)
|
||||
|
||||
# First 2 requests should succeed
|
||||
state.check_and_consume()
|
||||
state.check_and_consume()
|
||||
|
||||
# Third request should fail at hour limit
|
||||
allowed, message, retry_after = state.check_and_consume()
|
||||
self.assertFalse(allowed)
|
||||
self.assertIn("per hour", message.lower())
|
||||
|
||||
def test_day_limit_exceeded(self):
|
||||
"""Test rejection when daily limit exceeded."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(100, 100.0),
|
||||
hour_bucket=TokenBucket(100, 100.0),
|
||||
day_bucket=TokenBucket(2, 1.0), # Only 2 requests/day
|
||||
)
|
||||
|
||||
# First 2 requests should succeed
|
||||
state.check_and_consume()
|
||||
state.check_and_consume()
|
||||
|
||||
# Third request should fail at day limit
|
||||
allowed, message, retry_after = state.check_and_consume()
|
||||
self.assertFalse(allowed)
|
||||
self.assertIn("daily", message.lower())
|
||||
|
||||
def test_token_refund_on_rejection(self):
|
||||
"""Test tokens are refunded when request rejected at later stage."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(10, 1.0),
|
||||
hour_bucket=TokenBucket(2, 1.0), # Will fail here
|
||||
day_bucket=TokenBucket(10, 1.0),
|
||||
)
|
||||
|
||||
# Consume hour bucket
|
||||
state.check_and_consume()
|
||||
state.check_and_consume()
|
||||
|
||||
# Check minute bucket before rejection
|
||||
minute_before = state.minute_bucket.get_available_tokens()
|
||||
|
||||
# This should fail at hour limit
|
||||
allowed, message, retry_after = state.check_and_consume()
|
||||
self.assertFalse(allowed)
|
||||
|
||||
# Minute bucket should be refunded
|
||||
minute_after = state.minute_bucket.get_available_tokens()
|
||||
self.assertEqual(minute_before, minute_after)
|
||||
|
||||
def test_get_stats(self):
|
||||
"""Test statistics generation."""
|
||||
state = ClientRateLimitState(
|
||||
client_id="test_client",
|
||||
minute_bucket=TokenBucket(60, 1.0),
|
||||
hour_bucket=TokenBucket(1000, 1.0),
|
||||
day_bucket=TokenBucket(10000, 1.0),
|
||||
)
|
||||
|
||||
# Make some requests
|
||||
state.check_and_consume()
|
||||
state.check_and_consume()
|
||||
|
||||
stats = state.get_stats()
|
||||
self.assertEqual(stats["client_id"], "test_client")
|
||||
self.assertEqual(stats["total_requests"], 2)
|
||||
self.assertEqual(stats["rejected_requests"], 0)
|
||||
self.assertEqual(stats["success_rate"], 1.0)
|
||||
self.assertIn("available_tokens", stats)
|
||||
self.assertIn("limits", stats)
|
||||
|
||||
|
||||
class TestRateLimiter(unittest.TestCase):
|
||||
"""Test RateLimiter class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create a fresh rate limiter for each test."""
|
||||
# Reset singleton
|
||||
import core.rate_limiter
|
||||
|
||||
core.rate_limiter._rate_limiter = None
|
||||
self.limiter = RateLimiter()
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test rate limiter initialization."""
|
||||
self.assertIsNotNone(self.limiter.default_config)
|
||||
self.assertEqual(len(self.limiter.clients), 0)
|
||||
self.assertEqual(self.limiter.global_stats["total_requests"], 0)
|
||||
|
||||
def test_singleton_pattern(self):
|
||||
"""Test get_rate_limiter returns singleton."""
|
||||
limiter1 = get_rate_limiter()
|
||||
limiter2 = get_rate_limiter()
|
||||
self.assertIs(limiter1, limiter2)
|
||||
|
||||
def test_client_creation(self):
|
||||
"""Test automatic client state creation."""
|
||||
allowed, message, retry_after = self.limiter.check_rate_limit(
|
||||
client_id="client1", tool_name="test_tool"
|
||||
)
|
||||
self.assertTrue(allowed)
|
||||
self.assertIn("client1", self.limiter.clients)
|
||||
|
||||
def test_successful_rate_limit_check(self):
|
||||
"""Test successful rate limit check."""
|
||||
allowed, message, retry_after = self.limiter.check_rate_limit(
|
||||
client_id="client1", tool_name="test_tool"
|
||||
)
|
||||
self.assertTrue(allowed)
|
||||
self.assertEqual(message, "")
|
||||
self.assertEqual(retry_after, 0.0)
|
||||
self.assertEqual(self.limiter.global_stats["total_requests"], 1)
|
||||
self.assertEqual(self.limiter.global_stats["total_rejected"], 0)
|
||||
|
||||
def test_rate_limit_exceeded(self):
|
||||
"""Test rate limit exceeded scenario."""
|
||||
# Configure very low limits
|
||||
self.limiter.configure_limits("test_plugin", per_minute=2, per_hour=2, per_day=2)
|
||||
|
||||
# First 2 requests should succeed
|
||||
self.limiter.check_rate_limit("client1", "test", "test_plugin")
|
||||
self.limiter.check_rate_limit("client1", "test", "test_plugin")
|
||||
|
||||
# Third should fail
|
||||
allowed, message, retry_after = self.limiter.check_rate_limit(
|
||||
"client1", "test", "test_plugin"
|
||||
)
|
||||
self.assertFalse(allowed)
|
||||
self.assertGreater(retry_after, 0)
|
||||
self.assertEqual(self.limiter.global_stats["total_rejected"], 1)
|
||||
|
||||
def test_plugin_specific_limits(self):
|
||||
"""Test plugin-specific rate limits."""
|
||||
# Configure different limits for wordpress
|
||||
self.limiter.configure_limits("wordpress", per_minute=5, per_hour=5, per_day=5)
|
||||
|
||||
# Make 5 wordpress requests
|
||||
for _i in range(5):
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client1", "wordpress_tool", "wordpress")
|
||||
self.assertTrue(allowed)
|
||||
|
||||
# 6th should fail
|
||||
allowed, message, _ = self.limiter.check_rate_limit(
|
||||
"client1", "wordpress_tool", "wordpress"
|
||||
)
|
||||
self.assertFalse(allowed)
|
||||
|
||||
def test_multiple_clients(self):
|
||||
"""Test multiple clients tracked separately."""
|
||||
# Configure low limits
|
||||
self.limiter.configure_limits("test", per_minute=2, per_hour=2, per_day=2)
|
||||
|
||||
# Client 1 makes 2 requests
|
||||
self.limiter.check_rate_limit("client1", "test", "test")
|
||||
self.limiter.check_rate_limit("client1", "test", "test")
|
||||
|
||||
# Client 1's third request should fail
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client1", "test", "test")
|
||||
self.assertFalse(allowed)
|
||||
|
||||
# But client 2's first request should succeed
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client2", "test", "test")
|
||||
self.assertTrue(allowed)
|
||||
|
||||
def test_get_client_stats(self):
|
||||
"""Test getting client statistics."""
|
||||
# Make some requests
|
||||
self.limiter.check_rate_limit("client1", "test")
|
||||
self.limiter.check_rate_limit("client1", "test")
|
||||
|
||||
stats = self.limiter.get_client_stats("client1")
|
||||
self.assertIsNotNone(stats)
|
||||
self.assertEqual(stats["client_id"], "client1")
|
||||
self.assertEqual(stats["total_requests"], 2)
|
||||
|
||||
def test_get_client_stats_not_found(self):
|
||||
"""Test getting stats for non-existent client."""
|
||||
stats = self.limiter.get_client_stats("nonexistent")
|
||||
self.assertIsNone(stats)
|
||||
|
||||
def test_get_all_stats(self):
|
||||
"""Test getting all statistics."""
|
||||
# Make requests from multiple clients
|
||||
self.limiter.check_rate_limit("client1", "test")
|
||||
self.limiter.check_rate_limit("client2", "test")
|
||||
|
||||
stats = self.limiter.get_all_stats()
|
||||
self.assertIn("global", stats)
|
||||
self.assertIn("default_limits", stats)
|
||||
self.assertIn("plugin_limits", stats)
|
||||
self.assertIn("clients", stats)
|
||||
self.assertEqual(stats["global"]["total_requests"], 2)
|
||||
self.assertEqual(len(stats["clients"]), 2)
|
||||
|
||||
def test_reset_client(self):
|
||||
"""Test resetting specific client."""
|
||||
# Make some requests
|
||||
self.limiter.check_rate_limit("client1", "test")
|
||||
self.assertIn("client1", self.limiter.clients)
|
||||
|
||||
# Reset client
|
||||
success = self.limiter.reset_client("client1")
|
||||
self.assertTrue(success)
|
||||
self.assertNotIn("client1", self.limiter.clients)
|
||||
|
||||
def test_reset_client_not_found(self):
|
||||
"""Test resetting non-existent client."""
|
||||
success = self.limiter.reset_client("nonexistent")
|
||||
self.assertFalse(success)
|
||||
|
||||
def test_reset_all(self):
|
||||
"""Test resetting all clients."""
|
||||
# Make requests from multiple clients
|
||||
self.limiter.check_rate_limit("client1", "test")
|
||||
self.limiter.check_rate_limit("client2", "test")
|
||||
self.limiter.check_rate_limit("client3", "test")
|
||||
|
||||
# Reset all
|
||||
count = self.limiter.reset_all()
|
||||
self.assertEqual(count, 3)
|
||||
self.assertEqual(len(self.limiter.clients), 0)
|
||||
self.assertEqual(self.limiter.global_stats["total_requests"], 0)
|
||||
|
||||
def test_configure_limits(self):
|
||||
"""Test dynamic limit configuration."""
|
||||
self.limiter.configure_limits("custom_plugin", per_minute=100, per_hour=2000, per_day=20000)
|
||||
|
||||
config = self.limiter.plugin_configs["custom_plugin"]
|
||||
self.assertEqual(config.per_minute, 100)
|
||||
self.assertEqual(config.per_hour, 2000)
|
||||
self.assertEqual(config.per_day, 20000)
|
||||
|
||||
|
||||
class TestIntegration(unittest.TestCase):
|
||||
"""Integration tests simulating real-world usage."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create fresh rate limiter."""
|
||||
import core.rate_limiter
|
||||
|
||||
core.rate_limiter._rate_limiter = None
|
||||
self.limiter = RateLimiter()
|
||||
|
||||
def test_burst_traffic(self):
|
||||
"""Test handling of burst traffic within limits."""
|
||||
# Configure to allow 10/minute
|
||||
self.limiter.configure_limits("test", per_minute=10, per_hour=100, per_day=1000)
|
||||
|
||||
# Simulate burst of 10 requests
|
||||
success_count = 0
|
||||
for _i in range(10):
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client1", "test", "test")
|
||||
if allowed:
|
||||
success_count += 1
|
||||
|
||||
# All 10 should succeed (burst allowed)
|
||||
self.assertEqual(success_count, 10)
|
||||
|
||||
# 11th should fail
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client1", "test", "test")
|
||||
self.assertFalse(allowed)
|
||||
|
||||
def test_sustained_traffic(self):
|
||||
"""Test handling of sustained traffic with refill."""
|
||||
# Configure high refill rate for testing
|
||||
self.limiter.configure_limits("test", per_minute=5, per_hour=100, per_day=1000)
|
||||
|
||||
# Make 5 requests (consume all minute tokens)
|
||||
for _i in range(5):
|
||||
self.limiter.check_rate_limit("client1", "test", "test")
|
||||
|
||||
# Should be rate limited now
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client1", "test", "test")
|
||||
self.assertFalse(allowed)
|
||||
|
||||
# Wait for refill (should get ~2-3 tokens in 0.5 seconds at 5/min rate)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Should be able to make 1-2 more requests
|
||||
allowed, _, _ = self.limiter.check_rate_limit("client1", "test", "test")
|
||||
# May or may not succeed depending on exact timing
|
||||
# Just verify no crash and reasonable behavior
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""Run all tests."""
|
||||
# Create test suite
|
||||
loader = unittest.TestLoader()
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# Add all test classes
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestTokenBucket))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestRateLimitConfig))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestClientRateLimitState))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestRateLimiter))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestIntegration))
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
# Return exit code
|
||||
return 0 if result.wasSuccessful() else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(run_tests())
|
||||
133
tests/legacy/test_simple_isolation.py
Normal file
133
tests/legacy/test_simple_isolation.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple standalone test to verify the API key isolation logic.
|
||||
"""
|
||||
|
||||
# Simulate the context and validation logic
|
||||
from contextvars import ContextVar
|
||||
|
||||
# Create a mock context var
|
||||
_api_key_context = ContextVar("api_key_context", default=None)
|
||||
|
||||
|
||||
def test_case_1():
|
||||
"""Per-project key accessing its own project"""
|
||||
print("\nTest 1: Per-project key (site4) accessing site4")
|
||||
|
||||
# Set context for per-project key
|
||||
_api_key_context.set(
|
||||
{"key_id": "key_123", "project_id": "wordpress_site4", "scope": "admin", "is_global": False}
|
||||
)
|
||||
|
||||
# Check access
|
||||
api_key_info = _api_key_context.get()
|
||||
full_id = "wordpress_site4" # Target project
|
||||
|
||||
if api_key_info and not api_key_info.get("is_global"):
|
||||
allowed_project = api_key_info.get("project_id")
|
||||
if allowed_project != full_id:
|
||||
print(" ✗ FAIL: Access denied (should be allowed)")
|
||||
return False
|
||||
else:
|
||||
print(" ✓ PASS: Access allowed")
|
||||
return True
|
||||
else:
|
||||
print(" ✓ PASS: Access allowed (global key)")
|
||||
return True
|
||||
|
||||
|
||||
def test_case_2():
|
||||
"""Per-project key accessing different project"""
|
||||
print("\nTest 2: Per-project key (site4) accessing site1")
|
||||
|
||||
# Set context for per-project key
|
||||
_api_key_context.set(
|
||||
{"key_id": "key_123", "project_id": "wordpress_site4", "scope": "admin", "is_global": False}
|
||||
)
|
||||
|
||||
# Check access
|
||||
api_key_info = _api_key_context.get()
|
||||
full_id = "wordpress_site1" # Different project
|
||||
|
||||
if api_key_info and not api_key_info.get("is_global"):
|
||||
allowed_project = api_key_info.get("project_id")
|
||||
if allowed_project != full_id:
|
||||
print(" ✓ PASS: Access denied (as expected)")
|
||||
return True
|
||||
else:
|
||||
print(" ✗ FAIL: Access allowed (should be denied)")
|
||||
return False
|
||||
else:
|
||||
print(" ✗ FAIL: Access allowed (should be denied)")
|
||||
return False
|
||||
|
||||
|
||||
def test_case_3():
|
||||
"""Global key accessing any project"""
|
||||
print("\nTest 3: Global key accessing site1")
|
||||
|
||||
# Set context for global key
|
||||
_api_key_context.set(
|
||||
{"key_id": "key_456", "project_id": "*", "scope": "admin", "is_global": True}
|
||||
)
|
||||
|
||||
# Check access
|
||||
api_key_info = _api_key_context.get()
|
||||
full_id = "wordpress_site1"
|
||||
|
||||
if api_key_info and not api_key_info.get("is_global"):
|
||||
allowed_project = api_key_info.get("project_id")
|
||||
if allowed_project != full_id:
|
||||
print(" ✗ FAIL: Access denied (should be allowed)")
|
||||
return False
|
||||
else:
|
||||
print(" ✓ PASS: Access allowed")
|
||||
return True
|
||||
else:
|
||||
print(" ✓ PASS: Access allowed (global key)")
|
||||
return True
|
||||
|
||||
|
||||
def test_case_4():
|
||||
"""Global key accessing different project"""
|
||||
print("\nTest 4: Global key accessing site4")
|
||||
|
||||
# Set context for global key
|
||||
_api_key_context.set(
|
||||
{"key_id": "key_456", "project_id": "*", "scope": "admin", "is_global": True}
|
||||
)
|
||||
|
||||
# Check access
|
||||
api_key_info = _api_key_context.get()
|
||||
full_id = "wordpress_site4"
|
||||
|
||||
if api_key_info and not api_key_info.get("is_global"):
|
||||
allowed_project = api_key_info.get("project_id")
|
||||
if allowed_project != full_id:
|
||||
print(" ✗ FAIL: Access denied (should be allowed)")
|
||||
return False
|
||||
else:
|
||||
print(" ✓ PASS: Access allowed")
|
||||
return True
|
||||
else:
|
||||
print(" ✓ PASS: Access allowed (global key)")
|
||||
return True
|
||||
|
||||
|
||||
# Run all tests
|
||||
print("=" * 60)
|
||||
print("Testing API Key Isolation Logic")
|
||||
print("=" * 60)
|
||||
|
||||
results = [test_case_1(), test_case_2(), test_case_3(), test_case_4()]
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if all(results):
|
||||
print("✅ All tests PASSED!")
|
||||
print("The isolation logic is correct!")
|
||||
else:
|
||||
print("❌ Some tests FAILED!")
|
||||
print("The isolation logic has issues!")
|
||||
print("=" * 60)
|
||||
|
||||
exit(0 if all(results) else 1)
|
||||
Reference in New Issue
Block a user