543 lines
19 KiB
Python
543 lines
19 KiB
Python
"""
|
|
EU-Utility Integration Test - Discord Webhook
|
|
==============================================
|
|
|
|
Tests Discord webhook integration for:
|
|
- Sending messages to Discord channels
|
|
- Webhook payload validation
|
|
- Error handling and retries
|
|
- Rate limiting compliance
|
|
- Embed formatting
|
|
|
|
Author: Integration Tester
|
|
Version: 1.0.0
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional, List
|
|
from dataclasses import dataclass, asdict
|
|
|
|
from plugins.base_plugin import BasePlugin
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
|
QTextEdit, QPushButton, QComboBox, QSpinBox, QCheckBox,
|
|
QGroupBox, QTabWidget, QProgressBar, QTableWidget,
|
|
QTableWidgetItem, QHeaderView
|
|
)
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
|
|
|
|
@dataclass
|
|
class WebhookTest:
|
|
"""Represents a single webhook test case."""
|
|
name: str
|
|
payload: Dict[str, Any]
|
|
expected_status: int
|
|
description: str
|
|
|
|
|
|
class DiscordWebhookTester(BasePlugin):
|
|
"""Plugin for testing Discord webhook integration."""
|
|
|
|
name = "Discord Webhook Tester"
|
|
version = "1.0.0"
|
|
author = "Integration Tester"
|
|
description = "Test Discord webhook integration and payload formats"
|
|
|
|
# Discord webhook URL pattern
|
|
WEBHOOK_PATTERN = "https://discord.com/api/webhooks/"
|
|
|
|
# Test cases
|
|
TEST_CASES = [
|
|
WebhookTest(
|
|
name="Simple Message",
|
|
payload={"content": "Hello from EU-Utility Test!"},
|
|
expected_status=204,
|
|
description="Basic text message"
|
|
),
|
|
WebhookTest(
|
|
name="Embed Message",
|
|
payload={
|
|
"embeds": [{
|
|
"title": "Loot Drop!",
|
|
"description": "You received **50 PED** worth of loot",
|
|
"color": 0x00ff00,
|
|
"fields": [
|
|
{"name": "Mob", "value": "Atrox", "inline": True},
|
|
{"name": "Damage", "value": "150", "inline": True},
|
|
{"name": "DPP", "value": "2.85", "inline": True}
|
|
],
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}]
|
|
},
|
|
expected_status=204,
|
|
description="Rich embed with fields"
|
|
),
|
|
WebhookTest(
|
|
name="Global Announcement",
|
|
payload={
|
|
"content": "🎉 **GLOBAL!** 🎉",
|
|
"embeds": [{
|
|
"title": "150 PED - Atrox Young",
|
|
"color": 0xffd700,
|
|
"fields": [
|
|
{"name": "Player", "value": "TestPlayer", "inline": True},
|
|
{"name": "Location", "value": "Calypso", "inline": True},
|
|
{"name": "Weapon", "value": "ArMatrix LP-35", "inline": True}
|
|
]
|
|
}]
|
|
},
|
|
expected_status=204,
|
|
description="Global/HOF announcement format"
|
|
),
|
|
WebhookTest(
|
|
name="Skill Gain",
|
|
payload={
|
|
"embeds": [{
|
|
"title": "🎯 Skill Gain",
|
|
"description": "Rifle +0.25",
|
|
"color": 0x3498db,
|
|
"footer": {"text": "Total: 4500.75"}
|
|
}]
|
|
},
|
|
expected_status=204,
|
|
description="Skill tracking notification"
|
|
),
|
|
WebhookTest(
|
|
name="Error Alert",
|
|
payload={
|
|
"content": "",
|
|
"embeds": [{
|
|
"title": "⚠️ Connection Error",
|
|
"description": "Failed to connect to Nexus API",
|
|
"color": 0xe74c3c,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}]
|
|
},
|
|
expected_status=204,
|
|
description="Error notification format"
|
|
),
|
|
WebhookTest(
|
|
name="Invalid Payload",
|
|
payload={"invalid_field": "test"},
|
|
expected_status=400,
|
|
description="Test error handling with invalid payload"
|
|
),
|
|
]
|
|
|
|
def initialize(self):
|
|
"""Initialize the tester."""
|
|
self.log_info("Discord Webhook Tester initialized")
|
|
self._webhook_url = ""
|
|
self._test_results: List[Dict] = []
|
|
|
|
def get_ui(self) -> QWidget:
|
|
"""Create the plugin UI."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Title
|
|
title = QLabel("Discord Webhook Integration Tester")
|
|
title.setStyleSheet("font-size: 16px; font-weight: bold;")
|
|
layout.addWidget(title)
|
|
|
|
# Description
|
|
desc = QLabel("Test Discord webhook integration for EU-Utility notifications")
|
|
desc.setWordWrap(True)
|
|
layout.addWidget(desc)
|
|
|
|
# Tabs
|
|
tabs = QTabWidget()
|
|
|
|
# Webhook URL tab
|
|
tabs.addTab(self._create_url_tab(), "Webhook URL")
|
|
|
|
# Test Cases tab
|
|
tabs.addTab(self._create_tests_tab(), "Test Cases")
|
|
|
|
# Results tab
|
|
tabs.addTab(self._create_results_tab(), "Results")
|
|
|
|
# Payload Builder tab
|
|
tabs.addTab(self._create_builder_tab(), "Payload Builder")
|
|
|
|
layout.addWidget(tabs)
|
|
|
|
return widget
|
|
|
|
def _create_url_tab(self) -> QWidget:
|
|
"""Create the webhook URL configuration tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# URL Input
|
|
url_group = QGroupBox("Webhook Configuration")
|
|
url_layout = QVBoxLayout(url_group)
|
|
|
|
url_row = QHBoxLayout()
|
|
url_row.addWidget(QLabel("Webhook URL:"))
|
|
self.url_input = QLineEdit()
|
|
self.url_input.setPlaceholderText("https://discord.com/api/webhooks/...")
|
|
self.url_input.textChanged.connect(self._on_url_changed)
|
|
url_row.addWidget(self.url_input)
|
|
url_layout.addLayout(url_row)
|
|
|
|
# URL validation status
|
|
self.url_status = QLabel("❌ No URL configured")
|
|
self.url_status.setStyleSheet("color: red;")
|
|
url_layout.addWidget(self.url_status)
|
|
|
|
# Instructions
|
|
instructions = QLabel(
|
|
"To get a webhook URL:\n"
|
|
"1. Open Discord → Server Settings → Integrations\n"
|
|
"2. Click 'Webhooks' → 'New Webhook'\n"
|
|
"3. Copy the webhook URL and paste above"
|
|
)
|
|
instructions.setStyleSheet("color: gray; padding: 10px;")
|
|
url_layout.addWidget(instructions)
|
|
|
|
layout.addWidget(url_group)
|
|
|
|
# Test connection
|
|
test_btn = QPushButton("Test Connection")
|
|
test_btn.clicked.connect(self._test_connection)
|
|
layout.addWidget(test_btn)
|
|
|
|
layout.addStretch()
|
|
return widget
|
|
|
|
def _create_tests_tab(self) -> QWidget:
|
|
"""Create the test cases tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Test list
|
|
layout.addWidget(QLabel("Available Test Cases:"))
|
|
|
|
self.test_table = QTableWidget()
|
|
self.test_table.setColumnCount(3)
|
|
self.test_table.setHorizontalHeaderLabels(["Test Name", "Description", "Status"])
|
|
self.test_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
|
|
# Populate tests
|
|
self.test_table.setRowCount(len(self.TEST_CASES))
|
|
for i, test in enumerate(self.TEST_CASES):
|
|
self.test_table.setItem(i, 0, QTableWidgetItem(test.name))
|
|
self.test_table.setItem(i, 1, QTableWidgetItem(test.description))
|
|
self.test_table.setItem(i, 2, QTableWidgetItem("⏳ Pending"))
|
|
|
|
layout.addWidget(self.test_table)
|
|
|
|
# Action buttons
|
|
btn_layout = QHBoxLayout()
|
|
|
|
run_all_btn = QPushButton("Run All Tests")
|
|
run_all_btn.clicked.connect(self._run_all_tests)
|
|
btn_layout.addWidget(run_all_btn)
|
|
|
|
run_sel_btn = QPushButton("Run Selected")
|
|
run_sel_btn.clicked.connect(self._run_selected_test)
|
|
btn_layout.addWidget(run_sel_btn)
|
|
|
|
clear_btn = QPushButton("Clear Results")
|
|
clear_btn.clicked.connect(self._clear_results)
|
|
btn_layout.addWidget(clear_btn)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
# Progress bar
|
|
self.progress_bar = QProgressBar()
|
|
layout.addWidget(self.progress_bar)
|
|
|
|
return widget
|
|
|
|
def _create_results_tab(self) -> QWidget:
|
|
"""Create the results tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Results summary
|
|
self.results_summary = QLabel("No tests run yet")
|
|
layout.addWidget(self.results_summary)
|
|
|
|
# Results text area
|
|
layout.addWidget(QLabel("Detailed Results:"))
|
|
self.results_text = QTextEdit()
|
|
self.results_text.setReadOnly(True)
|
|
layout.addWidget(self.results_text)
|
|
|
|
# Export button
|
|
export_btn = QPushButton("Export Results to JSON")
|
|
export_btn.clicked.connect(self._export_results)
|
|
layout.addWidget(export_btn)
|
|
|
|
return widget
|
|
|
|
def _create_builder_tab(self) -> QWidget:
|
|
"""Create the payload builder tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
layout.addWidget(QLabel("Custom Payload Builder"))
|
|
|
|
# Message type
|
|
type_layout = QHBoxLayout()
|
|
type_layout.addWidget(QLabel("Message Type:"))
|
|
self.msg_type = QComboBox()
|
|
self.msg_type.addItems(["Simple Text", "Embed", "Global Announcement", "Skill Gain"])
|
|
self.msg_type.currentTextChanged.connect(self._on_builder_type_changed)
|
|
type_layout.addWidget(self.msg_type)
|
|
layout.addLayout(type_layout)
|
|
|
|
# Content fields
|
|
self.builder_content = QTextEdit()
|
|
self.builder_content.setPlaceholderText("Enter message content...")
|
|
layout.addWidget(self.builder_content)
|
|
|
|
# Title (for embeds)
|
|
self.builder_title = QLineEdit()
|
|
self.builder_title.setPlaceholderText("Embed title (optional)")
|
|
layout.addWidget(self.builder_title)
|
|
|
|
# Color
|
|
color_layout = QHBoxLayout()
|
|
color_layout.addWidget(QLabel("Color:"))
|
|
self.builder_color = QComboBox()
|
|
self.builder_color.addItems(["Green", "Red", "Blue", "Gold", "Purple"])
|
|
color_layout.addWidget(self.builder_color)
|
|
layout.addLayout(color_layout)
|
|
|
|
# Preview
|
|
preview_btn = QPushButton("Preview Payload")
|
|
preview_btn.clicked.connect(self._preview_payload)
|
|
layout.addWidget(preview_btn)
|
|
|
|
# Send custom
|
|
send_btn = QPushButton("Send Custom Payload")
|
|
send_btn.clicked.connect(self._send_custom_payload)
|
|
layout.addWidget(send_btn)
|
|
|
|
# Payload preview
|
|
layout.addWidget(QLabel("Generated Payload:"))
|
|
self.payload_preview = QTextEdit()
|
|
self.payload_preview.setReadOnly(True)
|
|
self.payload_preview.setMaximumHeight(150)
|
|
layout.addWidget(self.payload_preview)
|
|
|
|
layout.addStretch()
|
|
return widget
|
|
|
|
def _on_url_changed(self, url: str):
|
|
"""Handle webhook URL changes."""
|
|
self._webhook_url = url.strip()
|
|
|
|
if not self._webhook_url:
|
|
self.url_status.setText("❌ No URL configured")
|
|
self.url_status.setStyleSheet("color: red;")
|
|
elif self._webhook_url.startswith(self.WEBHOOK_PATTERN):
|
|
self.url_status.setText("✅ Valid Discord webhook URL")
|
|
self.url_status.setStyleSheet("color: green;")
|
|
else:
|
|
self.url_status.setText("⚠️ URL doesn't match Discord webhook pattern")
|
|
self.url_status.setStyleSheet("color: orange;")
|
|
|
|
def _test_connection(self):
|
|
"""Test the webhook connection."""
|
|
if not self._webhook_url:
|
|
self.notify_error("No Webhook URL", "Please configure a webhook URL first")
|
|
return
|
|
|
|
# Send test message
|
|
payload = {"content": "🔌 EU-Utility Webhook Test - Connection successful!"}
|
|
self._send_webhook(payload, "Connection Test")
|
|
|
|
def _run_all_tests(self):
|
|
"""Run all test cases."""
|
|
if not self._webhook_url:
|
|
self.notify_error("No Webhook URL", "Please configure a webhook URL first")
|
|
return
|
|
|
|
self._test_results.clear()
|
|
self.progress_bar.setMaximum(len(self.TEST_CASES))
|
|
self.progress_bar.setValue(0)
|
|
|
|
for i, test in enumerate(self.TEST_CASES):
|
|
self._run_test(test, i)
|
|
self.progress_bar.setValue(i + 1)
|
|
|
|
self._update_results_display()
|
|
|
|
def _run_selected_test(self):
|
|
"""Run the selected test case."""
|
|
if not self._webhook_url:
|
|
self.notify_error("No Webhook URL", "Please configure a webhook URL first")
|
|
return
|
|
|
|
row = self.test_table.currentRow()
|
|
if row < 0:
|
|
self.notify_warning("No Test Selected", "Please select a test from the table")
|
|
return
|
|
|
|
self._run_test(self.TEST_CASES[row], row)
|
|
self._update_results_display()
|
|
|
|
def _run_test(self, test: WebhookTest, index: int):
|
|
"""Run a single test case."""
|
|
self.log_info(f"Running test: {test.name}")
|
|
|
|
start_time = time.time()
|
|
success, response = self._send_webhook_sync(test.payload)
|
|
elapsed = (time.time() - start_time) * 1000 # ms
|
|
|
|
result = {
|
|
"test_name": test.name,
|
|
"description": test.description,
|
|
"expected_status": test.expected_status,
|
|
"success": success,
|
|
"response": response,
|
|
"elapsed_ms": round(elapsed, 2),
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
self._test_results.append(result)
|
|
|
|
# Update table
|
|
status = "✅ PASS" if success else "❌ FAIL"
|
|
self.test_table.setItem(index, 2, QTableWidgetItem(status))
|
|
|
|
def _send_webhook_sync(self, payload: Dict) -> tuple[bool, Any]:
|
|
"""Send webhook synchronously and return result."""
|
|
try:
|
|
import requests
|
|
|
|
response = requests.post(
|
|
self._webhook_url,
|
|
json=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
timeout=10
|
|
)
|
|
|
|
success = 200 <= response.status_code < 300
|
|
return success, {
|
|
"status_code": response.status_code,
|
|
"response_text": response.text[:500]
|
|
}
|
|
|
|
except Exception as e:
|
|
return False, {"error": str(e)}
|
|
|
|
def _send_webhook(self, payload: Dict, test_name: str = "Manual"):
|
|
"""Send webhook and show notification."""
|
|
success, response = self._send_webhook_sync(payload)
|
|
|
|
if success:
|
|
self.notify_success(f"{test_name} Sent", f"Status: {response.get('status_code', 'OK')}")
|
|
else:
|
|
error = response.get('error', 'Unknown error')
|
|
self.notify_error(f"{test_name} Failed", error)
|
|
|
|
def _clear_results(self):
|
|
"""Clear all test results."""
|
|
self._test_results.clear()
|
|
for i in range(self.test_table.rowCount()):
|
|
self.test_table.setItem(i, 2, QTableWidgetItem("⏳ Pending"))
|
|
self.progress_bar.setValue(0)
|
|
self.results_text.clear()
|
|
self.results_summary.setText("No tests run yet")
|
|
|
|
def _update_results_display(self):
|
|
"""Update the results display."""
|
|
if not self._test_results:
|
|
return
|
|
|
|
passed = sum(1 for r in self._test_results if r["success"])
|
|
total = len(self._test_results)
|
|
|
|
self.results_summary.setText(f"Results: {passed}/{total} tests passed")
|
|
|
|
# Build detailed results
|
|
text = []
|
|
for result in self._test_results:
|
|
status = "✅ PASS" if result["success"] else "❌ FAIL"
|
|
text.append(f"[{status}] {result['test_name']}")
|
|
text.append(f" Description: {result['description']}")
|
|
text.append(f" Expected: {result['expected_status']}")
|
|
text.append(f" Elapsed: {result['elapsed_ms']}ms")
|
|
text.append(f" Response: {result['response']}")
|
|
text.append("")
|
|
|
|
self.results_text.setText("\n".join(text))
|
|
|
|
def _export_results(self):
|
|
"""Export results to JSON."""
|
|
if not self._test_results:
|
|
self.notify_warning("No Results", "No test results to export")
|
|
return
|
|
|
|
filename = f"discord_webhook_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
|
|
with open(filename, 'w') as f:
|
|
json.dump({
|
|
"webhook_url": self._webhook_url[:50] + "..." if self._webhook_url else None,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"results": self._test_results
|
|
}, f, indent=2)
|
|
|
|
self.notify_success("Exported", f"Results saved to {filename}")
|
|
|
|
def _on_builder_type_changed(self, msg_type: str):
|
|
"""Update builder based on message type."""
|
|
templates = {
|
|
"Simple Text": "Hello from EU-Utility!",
|
|
"Embed": "This is an embed message with rich formatting",
|
|
"Global Announcement": "🎉 GLOBAL! 150 PED loot drop!",
|
|
"Skill Gain": "Rifle skill increased by 0.25"
|
|
}
|
|
self.builder_content.setText(templates.get(msg_type, ""))
|
|
|
|
def _preview_payload(self):
|
|
"""Preview the generated payload."""
|
|
payload = self._build_custom_payload()
|
|
self.payload_preview.setText(json.dumps(payload, indent=2))
|
|
|
|
def _send_custom_payload(self):
|
|
"""Send the custom payload."""
|
|
if not self._webhook_url:
|
|
self.notify_error("No Webhook URL", "Please configure a webhook URL first")
|
|
return
|
|
|
|
payload = self._build_custom_payload()
|
|
self._send_webhook(payload, "Custom Payload")
|
|
|
|
def _build_custom_payload(self) -> Dict:
|
|
"""Build custom payload from builder inputs."""
|
|
msg_type = self.msg_type.currentText()
|
|
content = self.builder_content.toPlainText()
|
|
title = self.builder_title.text()
|
|
|
|
colors = {
|
|
"Green": 0x00ff00,
|
|
"Red": 0xe74c3c,
|
|
"Blue": 0x3498db,
|
|
"Gold": 0xffd700,
|
|
"Purple": 0x9b59b6
|
|
}
|
|
color = colors.get(self.builder_color.currentText(), 0x00ff00)
|
|
|
|
if msg_type == "Simple Text":
|
|
return {"content": content}
|
|
else:
|
|
embed = {
|
|
"title": title or msg_type,
|
|
"description": content,
|
|
"color": color,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
return {"embeds": [embed]}
|
|
|
|
|
|
# Plugin entry point
|
|
plugin_class = DiscordWebhookTester |