1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 21:15:22 +02:00

fix: Make Sure Test Webhook Always Fires (#5816)

Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Michael Genson 2025-07-28 03:12:30 -05:00 committed by GitHub
parent d7191983bd
commit 675ac9c32b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 71 additions and 16 deletions

View file

@ -38,7 +38,7 @@ export const useGroupWebhooks = function () {
loading.value = true; loading.value = true;
const payload = { const payload = {
enabled: false, enabled: true,
name: "New Webhook", name: "New Webhook",
url: "", url: "",
scheduledTime: "00:00", scheduledTime: "00:00",

View file

@ -10,7 +10,7 @@ from mealie.routes._base.mixins import HttpRepo
from mealie.schema import mapper from mealie.schema import mapper
from mealie.schema.household.webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination from mealie.schema.household.webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination
from mealie.schema.response.pagination import PaginationQuery 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"]) router = APIRouter(prefix="/households/webhooks", tags=["Households: Webhooks"])
@ -55,7 +55,7 @@ class ReadWebhookController(BaseUserController):
@router.post("/{item_id}/test") @router.post("/{item_id}/test")
def test_one(self, item_id: UUID4, bg_tasks: BackgroundTasks): def test_one(self, item_id: UUID4, bg_tasks: BackgroundTasks):
webhook = self.mixins.get_one(item_id) 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) @router.put("/{item_id}", response_model=ReadWebhook)
def update_one(self, item_id: UUID4, data: CreateWebhook): def update_one(self, item_id: UUID4, data: CreateWebhook):

View file

@ -3,7 +3,6 @@ import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Generator from collections.abc import Generator
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import cast
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
@ -148,14 +147,23 @@ class WebhookEventListener(EventListenerBase):
def publish_to_subscribers(self, event: Event, subscribers: list[ReadWebhook]) -> None: def publish_to_subscribers(self, event: Event, subscribers: list[ReadWebhook]) -> None:
with self.ensure_repos(self.group_id, self.household_id) as repos: with self.ensure_repos(self.group_id, self.household_id) as repos:
if event.document_data.document_type == EventDocumentType.mealplan: if not isinstance(event.document_data, EventWebhookData):
webhook_data = cast(EventWebhookData, event.document_data) return
match event.document_data.document_type:
case EventDocumentType.mealplan:
meal_repo = repos.meals meal_repo = repos.meals
meal_data = meal_repo.get_meals_by_date_range( meal_data = meal_repo.get_meals_by_date_range(
webhook_data.webhook_start_dt, webhook_data.webhook_end_dt event.document_data.webhook_start_dt, event.document_data.webhook_end_dt
) )
if meal_data: event.document_data.webhook_body = meal_data or None
webhook_data.webhook_body = meal_data 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]) self.publisher.publish(event, [webhook.url for webhook in subscribers])
def get_scheduled_webhooks(self, start_dt: datetime, end_dt: datetime) -> list[ReadWebhook]: def get_scheduled_webhooks(self, start_dt: datetime, end_dt: datetime) -> list[ReadWebhook]:

View file

@ -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) dt = datetime.min.replace(tzinfo=UTC)
event_type = EventTypes.webhook_task event_type = EventTypes.test_message
event_document_data = EventWebhookData( event_document_data = EventWebhookData(
document_type=EventDocumentType.mealplan, document_type=EventDocumentType.generic,
operation=EventOperation.info, operation=EventOperation.info,
webhook_start_dt=dt, webhook_start_dt=dt,
webhook_end_dt=dt, webhook_end_dt=dt,

View file

@ -3,6 +3,8 @@ from datetime import UTC, datetime
import pytest import pytest
from fastapi.testclient import TestClient 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 import api_routes, assert_deserialize, jsonify
from tests.utils.fixture_schemas import TestUser 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) response = api_client.get(api_routes.households_webhooks_item_id(item_id), headers=unique_user.token)
assert response.status_code == 404 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