640 lines
21 KiB
Python
640 lines
21 KiB
Python
"""
|
|
External Integration Test Plugin
|
|
|
|
Tests all ExternalAPI integration features:
|
|
- REST API server endpoints
|
|
- Incoming/outgoing webhooks
|
|
- API key authentication
|
|
- IPC communication
|
|
- Server-Sent Events (SSE)
|
|
|
|
This plugin verifies third-party integration capabilities.
|
|
"""
|
|
|
|
import time
|
|
import json
|
|
import threading
|
|
from datetime import datetime
|
|
from typing import Dict, List, Any
|
|
from dataclasses import dataclass
|
|
|
|
from core.base_plugin import BasePlugin
|
|
from core.api.external_api import get_external_api
|
|
from core.api.widget_api import get_widget_api, WidgetType
|
|
from core.api.plugin_api import get_api
|
|
|
|
|
|
@dataclass
|
|
class IntegrationTestResult:
|
|
"""Result of an integration test."""
|
|
category: str
|
|
test_name: str
|
|
passed: bool
|
|
duration_ms: float
|
|
details: Dict = None
|
|
error: str = None
|
|
|
|
|
|
class ExternalIntegrationTestPlugin(BasePlugin):
|
|
"""
|
|
Integration test suite for ExternalAPI.
|
|
|
|
Tests REST API, webhooks, authentication, IPC, and SSE functionality.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.external_api = None
|
|
self.widget_api = None
|
|
self.plugin_api = None
|
|
self.results: List[IntegrationTestResult] = []
|
|
self.widget = None
|
|
self.test_port = 9999
|
|
|
|
def initialize(self):
|
|
"""Initialize and run integration tests."""
|
|
self.external_api = get_external_api()
|
|
self.widget_api = get_widget_api()
|
|
self.plugin_api = get_api()
|
|
|
|
self._create_results_widget()
|
|
self._run_all_tests()
|
|
|
|
def _create_results_widget(self):
|
|
"""Create widget to display integration test results."""
|
|
self.widget = self.widget_api.create_widget(
|
|
name="external_integration_test",
|
|
title="🌐 External Integration Test",
|
|
size=(900, 650),
|
|
position=(150, 150),
|
|
widget_type=WidgetType.CUSTOM
|
|
)
|
|
self._update_widget_display()
|
|
self.widget.show()
|
|
|
|
def _update_widget_display(self):
|
|
"""Update widget content."""
|
|
try:
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
QPushButton, QTextBrowser, QTabWidget, QGroupBox
|
|
)
|
|
|
|
container = QWidget()
|
|
layout = QVBoxLayout(container)
|
|
|
|
# Header
|
|
header = QLabel("🌐 External Integration Test Suite")
|
|
header.setStyleSheet("font-size: 20px; font-weight: bold; color: #ff8c42;")
|
|
layout.addWidget(header)
|
|
|
|
# Summary
|
|
passed = sum(1 for r in self.results if r.passed)
|
|
total = len(self.results)
|
|
summary = QLabel(f"Results: {passed}/{total} passed")
|
|
summary.setStyleSheet(f"font-size: 14px; color: {'#4ecca3' if passed == total else '#ff6b6b'};")
|
|
layout.addWidget(summary)
|
|
|
|
# Results display
|
|
self.results_browser = QTextBrowser()
|
|
self.results_browser.setHtml(self._generate_results_html())
|
|
layout.addWidget(self.results_browser)
|
|
|
|
# Control buttons
|
|
btn_layout = QHBoxLayout()
|
|
|
|
btn_run = QPushButton("Run All Tests")
|
|
btn_run.clicked.connect(self._run_all_tests)
|
|
btn_layout.addWidget(btn_run)
|
|
|
|
btn_server = QPushButton("Test Server Start/Stop")
|
|
btn_server.clicked.connect(self._test_server_lifecycle)
|
|
btn_layout.addWidget(btn_server)
|
|
|
|
btn_webhook = QPushButton("Test Webhooks")
|
|
btn_webhook.clicked.connect(self._test_outgoing_webhook)
|
|
btn_layout.addWidget(btn_webhook)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
self.widget.set_content(container)
|
|
|
|
except ImportError:
|
|
pass
|
|
|
|
def _generate_results_html(self) -> str:
|
|
"""Generate results HTML."""
|
|
html = """
|
|
<style>
|
|
body { font-family: 'Segoe UI', monospace; background: #1a1a2e; color: #eee; padding: 15px; }
|
|
.category { color: #ff8c42; font-size: 14px; font-weight: bold; margin-top: 15px; }
|
|
.pass { color: #4ecca3; }
|
|
.fail { color: #ff6b6b; }
|
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }
|
|
th { background: #2d3748; padding: 10px; text-align: left; }
|
|
td { padding: 8px; border-bottom: 1px solid #444; }
|
|
tr:hover { background: #252540; }
|
|
.details { color: #888; font-size: 11px; }
|
|
</style>
|
|
<table>
|
|
<tr><th>Category</th><th>Test</th><th>Result</th><th>Duration</th><th>Details</th></tr>
|
|
"""
|
|
|
|
for result in self.results:
|
|
status_class = "pass" if result.passed else "fail"
|
|
status_icon = "✅" if result.passed else "❌"
|
|
details = json.dumps(result.details) if result.details else ""
|
|
if len(details) > 100:
|
|
details = details[:100] + "..."
|
|
|
|
html += f"""
|
|
<tr>
|
|
<td><strong>{result.category}</strong></td>
|
|
<td>{result.test_name}</td>
|
|
<td class="{status_class}">{status_icon}</td>
|
|
<td>{result.duration_ms:.2f}ms</td>
|
|
<td class="details">{details}</td>
|
|
</tr>
|
|
"""
|
|
|
|
html += "</table>"
|
|
return html
|
|
|
|
def _record_result(self, category: str, test_name: str, passed: bool,
|
|
duration_ms: float, details: Dict = None, error: str = None):
|
|
"""Record test result."""
|
|
result = IntegrationTestResult(
|
|
category=category,
|
|
test_name=test_name,
|
|
passed=passed,
|
|
duration_ms=duration_ms,
|
|
details=details,
|
|
error=error
|
|
)
|
|
self.results.append(result)
|
|
self._update_widget_display()
|
|
|
|
# Show notification for failures
|
|
if not passed:
|
|
self.plugin_api.show_notification(
|
|
"Integration Test Failed",
|
|
f"{category}/{test_name}: {error or 'Unknown error'}",
|
|
duration=3000
|
|
)
|
|
|
|
def _run_all_tests(self):
|
|
"""Execute all integration tests."""
|
|
self.results.clear()
|
|
|
|
# REST API Server Tests
|
|
self._test_server_lifecycle()
|
|
self._test_endpoint_registration()
|
|
self._test_endpoint_decorator()
|
|
self._test_cors_configuration()
|
|
|
|
# Webhook Tests
|
|
self._test_incoming_webhook()
|
|
self._test_outgoing_webhook()
|
|
self._test_webhook_hmac()
|
|
|
|
# Authentication Tests
|
|
self._test_api_key_creation()
|
|
self._test_api_key_revocation()
|
|
|
|
# IPC Tests
|
|
self._test_ipc_handler()
|
|
self._test_ipc_send()
|
|
|
|
# Utility Tests
|
|
self._test_status_endpoint()
|
|
self._test_url_generation()
|
|
self._test_webhook_history()
|
|
|
|
# =================================================================
|
|
# REST API Server Tests
|
|
# =================================================================
|
|
|
|
def _test_server_lifecycle(self):
|
|
"""Test server start and stop."""
|
|
start = time.time()
|
|
|
|
try:
|
|
# Start server
|
|
started = self.external_api.start_server(
|
|
port=self.test_port,
|
|
host="127.0.0.1",
|
|
cors_origins=["http://localhost:3000"]
|
|
)
|
|
|
|
if started:
|
|
time.sleep(0.5) # Let server initialize
|
|
|
|
# Verify it's running
|
|
status = self.external_api.get_status()
|
|
running = status.get('server_running', False)
|
|
|
|
# Stop server
|
|
stopped = self.external_api.stop_server()
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"REST Server",
|
|
"Server Lifecycle",
|
|
started and running and stopped,
|
|
duration,
|
|
{"started": started, "running": running, "stopped": stopped}
|
|
)
|
|
else:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"REST Server",
|
|
"Server Lifecycle",
|
|
False,
|
|
duration,
|
|
error="Server failed to start (may already be running)"
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("REST Server", "Server Lifecycle", False, duration, error=str(e))
|
|
|
|
def _test_endpoint_registration(self):
|
|
"""Test programmatic endpoint registration."""
|
|
start = time.time()
|
|
|
|
try:
|
|
def test_handler(params):
|
|
return {"test": "data", "params": params}
|
|
|
|
self.external_api.register_endpoint(
|
|
"test/registration",
|
|
test_handler,
|
|
methods=["GET", "POST"],
|
|
auth_required=False
|
|
)
|
|
|
|
endpoints = self.external_api.get_endpoints()
|
|
success = "test/registration" in endpoints
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"REST Server",
|
|
"Endpoint Registration",
|
|
success,
|
|
duration,
|
|
{"endpoints": endpoints}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("REST Server", "Endpoint Registration", False, duration, error=str(e))
|
|
|
|
def _test_endpoint_decorator(self):
|
|
"""Test decorator-based endpoint registration."""
|
|
start = time.time()
|
|
|
|
try:
|
|
@self.external_api.endpoint("test/decorator", methods=["GET"])
|
|
def decorated_endpoint():
|
|
return {"decorated": True}
|
|
|
|
endpoints = self.external_api.get_endpoints()
|
|
success = "test/decorator" in endpoints
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"REST Server",
|
|
"Endpoint Decorator",
|
|
success,
|
|
duration
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("REST Server", "Endpoint Decorator", False, duration, error=str(e))
|
|
|
|
def _test_cors_configuration(self):
|
|
"""Test CORS configuration."""
|
|
start = time.time()
|
|
|
|
try:
|
|
# CORS is configured during server start
|
|
status = self.external_api.get_status()
|
|
success = status.get('server_running') is not None
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"REST Server",
|
|
"CORS Configuration",
|
|
success,
|
|
duration,
|
|
{"cors_origins": ["http://localhost:3000"]}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("REST Server", "CORS Configuration", False, duration, error=str(e))
|
|
|
|
# =================================================================
|
|
# Webhook Tests
|
|
# =================================================================
|
|
|
|
def _test_incoming_webhook(self):
|
|
"""Test incoming webhook registration."""
|
|
start = time.time()
|
|
|
|
try:
|
|
def webhook_handler(payload):
|
|
return {"received": payload}
|
|
|
|
self.external_api.register_webhook(
|
|
"test_incoming",
|
|
webhook_handler,
|
|
methods=["POST"],
|
|
rate_limit=60
|
|
)
|
|
|
|
webhooks = self.external_api.get_webhooks()
|
|
success = "test_incoming" in webhooks
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Webhooks",
|
|
"Incoming Webhook Registration",
|
|
success,
|
|
duration,
|
|
{"webhooks": webhooks}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Webhooks", "Incoming Webhook", False, duration, error=str(e))
|
|
|
|
def _test_outgoing_webhook(self):
|
|
"""Test outgoing webhook POST."""
|
|
start = time.time()
|
|
|
|
try:
|
|
# Post to httpbin for testing
|
|
result = self.external_api.post_webhook(
|
|
"https://httpbin.org/post",
|
|
{"test": "data", "timestamp": time.time()},
|
|
headers={"X-Test-Header": "test"},
|
|
timeout=10
|
|
)
|
|
|
|
success = result.get('success', False)
|
|
status = result.get('status', 0)
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Webhooks",
|
|
"Outgoing Webhook POST",
|
|
success and 200 <= status < 300,
|
|
duration,
|
|
{"status": status}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Webhooks", "Outgoing Webhook", False, duration, error=str(e))
|
|
|
|
def _test_webhook_hmac(self):
|
|
"""Test webhook HMAC signature verification."""
|
|
start = time.time()
|
|
|
|
try:
|
|
import hmac
|
|
import hashlib
|
|
|
|
secret = "test_secret_key"
|
|
|
|
def secure_handler(payload):
|
|
return {"secure": True}
|
|
|
|
self.external_api.register_webhook(
|
|
"secure_webhook",
|
|
secure_handler,
|
|
secret=secret,
|
|
methods=["POST"]
|
|
)
|
|
|
|
# Generate test signature
|
|
payload = {"event": "test"}
|
|
payload_str = json.dumps(payload, sort_keys=True)
|
|
signature = hmac.new(
|
|
secret.encode(),
|
|
payload_str.encode(),
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
|
|
# Verify signature logic is in place
|
|
webhooks = self.external_api.get_webhooks()
|
|
success = "secure_webhook" in webhooks
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Webhooks",
|
|
"HMAC Signature",
|
|
success,
|
|
duration,
|
|
{"signature_generated": True}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Webhooks", "HMAC Signature", False, duration, error=str(e))
|
|
|
|
# =================================================================
|
|
# Authentication Tests
|
|
# =================================================================
|
|
|
|
def _test_api_key_creation(self):
|
|
"""Test API key creation."""
|
|
start = time.time()
|
|
|
|
try:
|
|
key = self.external_api.create_api_key(
|
|
name="test_integration_key",
|
|
permissions=["read", "write"]
|
|
)
|
|
|
|
success = len(key) > 0 and isinstance(key, str)
|
|
self._test_api_key = key # Save for revocation test
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Authentication",
|
|
"API Key Creation",
|
|
success,
|
|
duration,
|
|
{"key_length": len(key)}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Authentication", "API Key Creation", False, duration, error=str(e))
|
|
|
|
def _test_api_key_revocation(self):
|
|
"""Test API key revocation."""
|
|
start = time.time()
|
|
|
|
try:
|
|
# Create a key to revoke
|
|
key = self.external_api.create_api_key("temp_revoke_key")
|
|
|
|
# Revoke it
|
|
revoked = self.external_api.revoke_api_key(key)
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Authentication",
|
|
"API Key Revocation",
|
|
revoked,
|
|
duration
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Authentication", "API Key Revocation", False, duration, error=str(e))
|
|
|
|
# =================================================================
|
|
# IPC Tests
|
|
# =================================================================
|
|
|
|
def _test_ipc_handler(self):
|
|
"""Test IPC handler registration."""
|
|
start = time.time()
|
|
|
|
try:
|
|
self._ipc_received = False
|
|
|
|
def ipc_handler(data):
|
|
self._ipc_received = True
|
|
self._ipc_data = data
|
|
|
|
self.external_api.register_ipc_handler("test_channel", ipc_handler)
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"IPC",
|
|
"Handler Registration",
|
|
True,
|
|
duration
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("IPC", "Handler Registration", False, duration, error=str(e))
|
|
|
|
def _test_ipc_send(self):
|
|
"""Test IPC message sending."""
|
|
start = time.time()
|
|
|
|
try:
|
|
sent = self.external_api.send_ipc(
|
|
"test_channel",
|
|
{"message": "test", "timestamp": time.time()}
|
|
)
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"IPC",
|
|
"Send Message",
|
|
sent,
|
|
duration,
|
|
{"received": getattr(self, '_ipc_received', False)}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("IPC", "Send Message", False, duration, error=str(e))
|
|
|
|
# =================================================================
|
|
# Utility Tests
|
|
# =================================================================
|
|
|
|
def _test_status_endpoint(self):
|
|
"""Test /health endpoint availability."""
|
|
start = time.time()
|
|
|
|
try:
|
|
status = self.external_api.get_status()
|
|
|
|
success = (
|
|
'server_running' in status and
|
|
'endpoints' in status and
|
|
'webhooks' in status
|
|
)
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Utilities",
|
|
"Status Endpoint",
|
|
success,
|
|
duration,
|
|
status
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Utilities", "Status Endpoint", False, duration, error=str(e))
|
|
|
|
def _test_url_generation(self):
|
|
"""Test URL generation."""
|
|
start = time.time()
|
|
|
|
try:
|
|
url = self.external_api.get_url("api/v1/test")
|
|
|
|
success = url.startswith("http") and "api/v1/test" in url
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Utilities",
|
|
"URL Generation",
|
|
success,
|
|
duration,
|
|
{"url": url}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Utilities", "URL Generation", False, duration, error=str(e))
|
|
|
|
def _test_webhook_history(self):
|
|
"""Test webhook history tracking."""
|
|
start = time.time()
|
|
|
|
try:
|
|
history = self.external_api.get_webhook_history(limit=10)
|
|
|
|
success = isinstance(history, list)
|
|
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result(
|
|
"Utilities",
|
|
"Webhook History",
|
|
success,
|
|
duration,
|
|
{"history_entries": len(history)}
|
|
)
|
|
|
|
except Exception as e:
|
|
duration = (time.time() - start) * 1000
|
|
self._record_result("Utilities", "Webhook History", False, duration, error=str(e))
|
|
|
|
def shutdown(self):
|
|
"""Clean up resources."""
|
|
# Stop server if running
|
|
try:
|
|
self.external_api.stop_server()
|
|
except:
|
|
pass
|
|
|
|
if self.widget:
|
|
self.widget.close()
|
|
|
|
|
|
# Plugin entry point
|
|
plugin_class = ExternalIntegrationTestPlugin |