| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- """Notify configuration validation and lifecycle cleanup through the public API."""
- from unittest.mock import AsyncMock, patch
- import pytest
- from backend.app.models.notification_lock_screen_widget import NotificationLockScreenWidget
- from backend.app.services.notify_live_activities import _credential_key
- CONFIG = {
- "device_id": "IO12345678901234",
- "token": "example-token",
- "live_activities": True,
- "lock_screen_widgets": True,
- }
- async def create_provider(client):
- response = await client.post(
- "/api/v1/notifications/", json={"name": "Notify", "provider_type": "notify", "config": CONFIG}
- )
- assert response.status_code == 200
- return response.json()["id"]
- async def test_notify_settings_round_trip_and_push_flags_remain_independent(async_client):
- provider_id = await create_provider(async_client)
- response = await async_client.get(f"/api/v1/notifications/{provider_id}")
- assert response.json()["config"] == CONFIG
- assert response.json()["on_print_start"] is False
- assert response.json()["on_print_progress"] is False
- async def test_notify_crud_refreshes_worker_configuration_without_remote_work(async_client):
- with (
- patch("backend.app.api.routes.notifications.notify_live_activities.providers_changed") as live_changed,
- patch("backend.app.api.routes.notifications.notify_widgets.providers_changed") as widgets_changed,
- patch("backend.app.api.routes.notifications.notify_live_activities.schedule_cleanup", new_callable=AsyncMock),
- patch("backend.app.api.routes.notifications.notify_widgets.schedule_cleanup", new_callable=AsyncMock),
- patch(
- "backend.app.api.routes.notifications.notify_live_activities.cleanup_provider", new_callable=AsyncMock
- ) as live_http,
- patch(
- "backend.app.api.routes.notifications.notify_widgets.cleanup_provider", new_callable=AsyncMock
- ) as widget_http,
- ):
- provider_id = await create_provider(async_client)
- assert live_changed.call_count == widgets_changed.call_count == 1
- response = await async_client.patch(f"/api/v1/notifications/{provider_id}", json={"enabled": False})
- assert response.status_code == 200
- assert live_changed.call_count == widgets_changed.call_count == 2
- response = await async_client.delete(f"/api/v1/notifications/{provider_id}")
- assert response.status_code == 200
- assert live_changed.call_count == widgets_changed.call_count == 3
- live_http.assert_not_awaited()
- widget_http.assert_not_awaited()
- @pytest.mark.parametrize(
- "config",
- [
- {},
- {**CONFIG, "device_id": "GRP12345"},
- {**CONFIG, "device_id": "WB12345678901234"},
- {**CONFIG, "live_activities": "false"},
- {**CONFIG, "live_activities": False, "lock_screen_widgets": "false"},
- {**CONFIG, "device_id": "GRP12345", "live_activities": False},
- {**CONFIG, "device_id": "WB12345678901234", "live_activities": False},
- ],
- )
- async def test_invalid_notify_configuration_cannot_be_created(async_client, config):
- response = await async_client.post(
- "/api/v1/notifications/", json={"name": "Notify", "provider_type": "notify", "config": config}
- )
- assert response.status_code == 422
- async def test_empty_config_update_is_rejected_and_keeps_previous_credentials(async_client):
- provider_id = await create_provider(async_client)
- response = await async_client.patch(f"/api/v1/notifications/{provider_id}", json={"config": {}})
- assert response.status_code == 422
- saved = await async_client.get(f"/api/v1/notifications/{provider_id}")
- assert saved.json()["config"] == CONFIG
- async def test_credential_change_queues_old_device_cleanup_with_old_credentials(async_client):
- provider_id = await create_provider(async_client)
- with (
- patch(
- "backend.app.api.routes.notifications.notify_live_activities.schedule_cleanup", new_callable=AsyncMock
- ) as cleanup,
- patch(
- "backend.app.api.routes.notifications.notify_widgets.schedule_cleanup", new_callable=AsyncMock
- ) as widget_cleanup,
- ):
- response = await async_client.patch(
- f"/api/v1/notifications/{provider_id}", json={"config": {**CONFIG, "token": "replacement-token"}}
- )
- assert response.status_code == 200
- cleanup.assert_awaited_once_with(provider_id, CONFIG)
- widget_cleanup.assert_awaited_once_with(provider_id, CONFIG)
- assert response.json()["config"]["token"] == "replacement-token"
- async def test_disabling_or_changing_push_thread_leaves_cleanup_to_durable_worker(async_client):
- provider_id = await create_provider(async_client)
- with (
- patch(
- "backend.app.api.routes.notifications.notify_live_activities.schedule_cleanup", new_callable=AsyncMock
- ) as cleanup,
- patch(
- "backend.app.api.routes.notifications.notify_widgets.schedule_cleanup", new_callable=AsyncMock
- ) as widget_cleanup,
- ):
- response = await async_client.patch(
- f"/api/v1/notifications/{provider_id}",
- json={"enabled": False, "config": {**CONFIG, "group_type": "workshop"}},
- )
- assert response.status_code == 200
- cleanup.assert_not_awaited()
- widget_cleanup.assert_not_awaited()
- async def test_delete_queues_cleanup_before_forgetting_credentials(async_client):
- provider_id = await create_provider(async_client)
- with (
- patch(
- "backend.app.api.routes.notifications.notify_live_activities.schedule_cleanup", new_callable=AsyncMock
- ) as cleanup,
- patch(
- "backend.app.api.routes.notifications.notify_widgets.schedule_cleanup", new_callable=AsyncMock
- ) as widget_cleanup,
- ):
- response = await async_client.delete(f"/api/v1/notifications/{provider_id}")
- assert response.status_code == 200
- cleanup.assert_awaited_once_with(provider_id, CONFIG)
- widget_cleanup.assert_awaited_once_with(provider_id, CONFIG)
- assert (await async_client.get(f"/api/v1/notifications/{provider_id}")).status_code == 404
- async def test_lock_screen_widgets_can_be_enabled_without_live_activities_or_push_events(async_client):
- config = {**CONFIG, "live_activities": False}
- response = await async_client.post(
- "/api/v1/notifications/",
- json={
- "name": "Printer widget",
- "provider_type": "notify",
- "config": config,
- "on_print_start": False,
- "on_print_complete": False,
- "on_print_failed": False,
- },
- )
- assert response.status_code == 200
- assert response.json()["config"] == config
- assert response.json()["on_print_start"] is False
- assert response.json()["on_print_complete"] is False
- assert response.json()["on_print_failed"] is False
- async def test_switching_provider_type_cleans_up_both_owned_notify_resources(async_client):
- provider_id = await create_provider(async_client)
- with (
- patch(
- "backend.app.api.routes.notifications.notify_live_activities.schedule_cleanup", new_callable=AsyncMock
- ) as cleanup,
- patch(
- "backend.app.api.routes.notifications.notify_widgets.schedule_cleanup", new_callable=AsyncMock
- ) as widget_cleanup,
- ):
- response = await async_client.patch(
- f"/api/v1/notifications/{provider_id}",
- json={"provider_type": "ntfy", "config": {"topic": "prints"}},
- )
- assert response.status_code == 200
- cleanup.assert_awaited_once_with(provider_id, CONFIG)
- widget_cleanup.assert_awaited_once_with(provider_id, CONFIG)
- @pytest.mark.parametrize("disable", [{"config": {**CONFIG, "lock_screen_widgets": False}}, {"enabled": False}])
- async def test_quick_widget_disable_and_reenable_preserves_cleanup_intent(async_client, db_session, disable):
- provider_id = await create_provider(async_client)
- row = NotificationLockScreenWidget(
- provider_id=provider_id,
- printer_id=42,
- credential_key=_credential_key(CONFIG),
- widget_id="WG123456",
- state="active",
- content="{}",
- )
- db_session.add(row)
- await db_session.commit()
- response = await async_client.patch(f"/api/v1/notifications/{provider_id}", json=disable)
- assert response.status_code == 200
- await db_session.refresh(row)
- assert row.state == "deleting"
- response = await async_client.patch(
- f"/api/v1/notifications/{provider_id}", json={"config": CONFIG, "enabled": True}
- )
- assert response.status_code == 200
- await db_session.refresh(row)
- assert row.state == "deleting"
- assert row.widget_id == "WG123456"
|