From 675ac9c32bcd9dd944b8f1f668ae3ae17752e0a0 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:12:30 -0500 Subject: [PATCH] fix: Make Sure Test Webhook Always Fires (#5816) Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> --- frontend/composables/use-group-webhooks.ts | 2 +- .../routes/households/controller_webhooks.py | 4 +- .../event_bus_service/event_bus_listeners.py | 28 +++++++---- .../services/scheduler/tasks/post_webhooks.py | 6 +-- .../test_group_webhooks.py | 47 +++++++++++++++++++ 5 files changed, 71 insertions(+), 16 deletions(-) diff --git a/frontend/composables/use-group-webhooks.ts b/frontend/composables/use-group-webhooks.ts index 60fdafd24..03ec40485 100644 --- a/frontend/composables/use-group-webhooks.ts +++ b/frontend/composables/use-group-webhooks.ts @@ -38,7 +38,7 @@ export const useGroupWebhooks = function () { loading.value = true; const payload = { - enabled: false, + enabled: true, name: "New Webhook", url: "", scheduledTime: "00:00", diff --git a/mealie/routes/households/controller_webhooks.py b/mealie/routes/households/controller_webhooks.py index 251bb3617..f76e33de9 100644 --- a/mealie/routes/households/controller_webhooks.py +++ b/mealie/routes/households/controller_webhooks.py @@ -10,7 +10,7 @@ from mealie.routes._base.mixins import HttpRepo from mealie.schema import mapper from mealie.schema.household.webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination from mealie.schema.response.pagination import PaginationQuery -from mealie.services.scheduler.tasks.post_webhooks import post_group_webhooks, post_single_webhook +from mealie.services.scheduler.tasks.post_webhooks import post_group_webhooks, post_test_webhook router = APIRouter(prefix="/households/webhooks", tags=["Households: Webhooks"]) @@ -55,7 +55,7 @@ class ReadWebhookController(BaseUserController): @router.post("/{item_id}/test") def test_one(self, item_id: UUID4, bg_tasks: BackgroundTasks): webhook = self.mixins.get_one(item_id) - bg_tasks.add_task(post_single_webhook, webhook, "Test Webhook") + bg_tasks.add_task(post_test_webhook, webhook, "Test Webhook") @router.put("/{item_id}", response_model=ReadWebhook) def update_one(self, item_id: UUID4, data: CreateWebhook): diff --git a/mealie/services/event_bus_service/event_bus_listeners.py b/mealie/services/event_bus_service/event_bus_listeners.py index 586876c63..0d1a84154 100644 --- a/mealie/services/event_bus_service/event_bus_listeners.py +++ b/mealie/services/event_bus_service/event_bus_listeners.py @@ -3,7 +3,6 @@ import json from abc import ABC, abstractmethod from collections.abc import Generator from datetime import UTC, datetime -from typing import cast from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit from fastapi.encoders import jsonable_encoder @@ -148,15 +147,24 @@ class WebhookEventListener(EventListenerBase): def publish_to_subscribers(self, event: Event, subscribers: list[ReadWebhook]) -> None: with self.ensure_repos(self.group_id, self.household_id) as repos: - if event.document_data.document_type == EventDocumentType.mealplan: - webhook_data = cast(EventWebhookData, event.document_data) - meal_repo = repos.meals - meal_data = meal_repo.get_meals_by_date_range( - webhook_data.webhook_start_dt, webhook_data.webhook_end_dt - ) - if meal_data: - webhook_data.webhook_body = meal_data - self.publisher.publish(event, [webhook.url for webhook in subscribers]) + if not isinstance(event.document_data, EventWebhookData): + return + + match event.document_data.document_type: + case EventDocumentType.mealplan: + meal_repo = repos.meals + meal_data = meal_repo.get_meals_by_date_range( + event.document_data.webhook_start_dt, event.document_data.webhook_end_dt + ) + event.document_data.webhook_body = meal_data or None + case _: + if event.event_type is EventTypes.test_message: + # make sure the webhook has a valid body so it gets sent + event.document_data.webhook_body = event.document_data.webhook_body or [] + + # Only publish to subscribers if we have a webhook body to send + if event.document_data.webhook_body is not None: + self.publisher.publish(event, [webhook.url for webhook in subscribers]) def get_scheduled_webhooks(self, start_dt: datetime, end_dt: datetime) -> list[ReadWebhook]: """Fetches all scheduled webhooks from the database""" diff --git a/mealie/services/scheduler/tasks/post_webhooks.py b/mealie/services/scheduler/tasks/post_webhooks.py index 5298fa2c8..87feef270 100644 --- a/mealie/services/scheduler/tasks/post_webhooks.py +++ b/mealie/services/scheduler/tasks/post_webhooks.py @@ -79,12 +79,12 @@ def post_group_webhooks( ) -def post_single_webhook(webhook: ReadWebhook, message: str = "") -> None: +def post_test_webhook(webhook: ReadWebhook, message: str = "") -> None: dt = datetime.min.replace(tzinfo=UTC) - event_type = EventTypes.webhook_task + event_type = EventTypes.test_message event_document_data = EventWebhookData( - document_type=EventDocumentType.mealplan, + document_type=EventDocumentType.generic, operation=EventOperation.info, webhook_start_dt=dt, webhook_end_dt=dt, diff --git a/tests/integration_tests/user_household_tests/test_group_webhooks.py b/tests/integration_tests/user_household_tests/test_group_webhooks.py index 3dd7d87b5..e91a25dd3 100644 --- a/tests/integration_tests/user_household_tests/test_group_webhooks.py +++ b/tests/integration_tests/user_household_tests/test_group_webhooks.py @@ -3,6 +3,8 @@ from datetime import UTC, datetime import pytest from fastapi.testclient import TestClient +from mealie.schema.household.webhook import ReadWebhook +from mealie.services.scheduler.tasks.post_webhooks import post_test_webhook from tests.utils import api_routes, assert_deserialize, jsonify from tests.utils.fixture_schemas import TestUser @@ -84,3 +86,48 @@ def test_delete_webhook(api_client: TestClient, webhook_data, unique_user: TestU response = api_client.get(api_routes.households_webhooks_item_id(item_id), headers=unique_user.token) assert response.status_code == 404 + + +def test_post_test_webhook( + monkeypatch: pytest.MonkeyPatch, api_client: TestClient, unique_user: TestUser, webhook_data +): + # Mock the requests.post to avoid actual HTTP calls + class MockResponse: + status_code = 200 + + mock_calls = [] + + def mock_post(*args, **kwargs): + mock_calls.append((args, kwargs)) + return MockResponse() + + monkeypatch.setattr("mealie.services.event_bus_service.publisher.requests.post", mock_post) + + # Create a webhook and post it + response = api_client.post( + api_routes.households_webhooks, + json=jsonify(webhook_data), + headers=unique_user.token, + ) + webhook_dict = assert_deserialize(response, 201) + + webhook = ReadWebhook( + id=webhook_dict["id"], + name=webhook_dict["name"], + url=webhook_dict["url"], + scheduled_time=webhook_dict["scheduledTime"], + enabled=webhook_dict["enabled"], + group_id=webhook_dict["groupId"], + household_id=webhook_dict["householdId"], + ) + + test_message = "This is a test webhook message" + post_test_webhook(webhook, test_message) + + # Verify that requests.post was called with the correct parameters + assert len(mock_calls) == 1 + args, kwargs = mock_calls[0] + + assert kwargs["json"]["message"]["body"] == test_message + assert kwargs["timeout"] == 15 + assert args[0] == webhook.url