mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Fix: Mattermost/Slack Webhook Support
Root Cause: The generic webhook sent a JSON payload with custom field names plus timestamp and source fields. Mattermost/Slack webhooks only accept {"text": "..."}
format and reject unknown fields with HTTP 400.
Users just need to select "Slack / Mattermost" from the Payload Format dropdown when configuring their Mattermost webhook.
Closes #133
This commit is contained in:
@@ -356,22 +356,33 @@ class NotificationService:
|
||||
return False, f"HTTP {response.status_code}: {response.text[:200]}"
|
||||
|
||||
async def _send_webhook(self, config: dict, title: str, message: str) -> tuple[bool, str]:
|
||||
"""Send notification via generic webhook (POST JSON)."""
|
||||
"""Send notification via generic webhook (POST JSON).
|
||||
|
||||
Supports two payload formats:
|
||||
- generic: Custom field names with timestamp/source metadata
|
||||
- slack: Slack/Mattermost compatible format (just {"text": "..."})
|
||||
"""
|
||||
webhook_url = config.get("webhook_url", "").strip()
|
||||
auth_header = config.get("auth_header", "").strip()
|
||||
custom_field_title = config.get("field_title", "title").strip() or "title"
|
||||
custom_field_message = config.get("field_message", "message").strip() or "message"
|
||||
payload_format = config.get("payload_format", "generic").strip()
|
||||
|
||||
if not webhook_url:
|
||||
return False, "Webhook URL is required"
|
||||
|
||||
# Build payload with custom field names
|
||||
data = {
|
||||
custom_field_title: title,
|
||||
custom_field_message: message,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"source": "Bambuddy",
|
||||
}
|
||||
# Build payload based on format
|
||||
if payload_format == "slack":
|
||||
# Slack/Mattermost format - just text field
|
||||
data = {"text": f"*{title}*\n{message}"}
|
||||
else:
|
||||
# Generic format with custom field names
|
||||
custom_field_title = config.get("field_title", "title").strip() or "title"
|
||||
custom_field_message = config.get("field_message", "message").strip() or "message"
|
||||
data = {
|
||||
custom_field_title: title,
|
||||
custom_field_message: message,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"source": "Bambuddy",
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if auth_header:
|
||||
|
||||
@@ -557,6 +557,38 @@ class TestNotificationProviderTypes:
|
||||
assert success is False
|
||||
assert "Connection failed" in message or "error" in message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_slack_format_sends_text_only(self, service):
|
||||
"""Verify Slack/Mattermost format sends only text field."""
|
||||
config = {
|
||||
"webhook_url": "http://mattermost.local/hooks/abc123",
|
||||
"payload_format": "slack",
|
||||
}
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
success, message = await service._send_webhook(config, "Test Title", "Test Message")
|
||||
|
||||
assert success is True
|
||||
mock_client.post.assert_called_once()
|
||||
|
||||
# Verify payload format is Slack-compatible
|
||||
call_args = mock_client.post.call_args
|
||||
payload = call_args.kwargs.get("json") or call_args[1].get("json")
|
||||
assert "text" in payload
|
||||
assert "*Test Title*" in payload["text"]
|
||||
assert "Test Message" in payload["text"]
|
||||
# Should NOT have generic fields
|
||||
assert "timestamp" not in payload
|
||||
assert "source" not in payload
|
||||
|
||||
|
||||
class TestNotificationVariableFallbacks:
|
||||
"""Tests for notification variable fallback values."""
|
||||
|
||||
Reference in New Issue
Block a user