Migrate Notifications interface patterns

This commit is contained in:
2026-08-03 15:33:49 +02:00
parent 3b1a87b3e2
commit ad6a31f68b
13 changed files with 825 additions and 104 deletions
@@ -0,0 +1,85 @@
from __future__ import annotations
from pathlib import Path
import unittest
from govoplan_notifications.backend.manifest import get_manifest
REPO_ROOT = Path(__file__).resolve().parents[1]
class NotificationsInterfaceDocumentationContractTests(unittest.TestCase):
def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None:
frontend = get_manifest().frontend
self.assertIsNotNone(frontend)
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
expected = {
"notifications.page.inbox",
"notifications.page.detail",
"notifications.page.delivery",
"notifications.action.mark-read",
"notifications.action.acknowledge",
"notifications.action.cancel",
"notifications.action.dispatch",
"notifications.settings.preferences",
"notifications.widget.summary",
}
self.assertEqual(expected, set(surfaces))
self.assertEqual(
"notifications.route.notifications",
frontend.routes[0].surface_id, # type: ignore[union-attr]
)
self.assertEqual(
"notifications.route.notifications",
surfaces["notifications.page.inbox"].parent_id,
)
self.assertEqual(
"notifications.route.notifications",
surfaces["notifications.page.detail"].parent_id,
)
self.assertEqual(
"notifications.page.detail",
surfaces["notifications.page.delivery"].parent_id,
)
self.assertEqual(
"notifications.page.delivery",
surfaces["notifications.action.dispatch"].parent_id,
)
def test_help_and_consequence_metadata_remain_published(self) -> None:
topics = {topic.id: topic for topic in get_manifest().documentation}
center = topics["notifications.center-and-preferences"]
delivery = topics["notifications.delivery-operations"]
self.assertIn("notifications.page.detail", center.metadata["help_contexts"])
self.assertIn("notifications.settings.preferences", center.metadata["help_contexts"])
self.assertIn("acknowledge", center.metadata["consequence_classes"])
self.assertIn("cancel", center.metadata["consequence_classes"])
self.assertIn("notifications.action.dispatch", delivery.metadata["help_contexts"])
self.assertIn("dispatch_pending", delivery.metadata["consequence_classes"])
def test_webui_uses_shared_consequence_and_draft_patterns(self) -> None:
center = (REPO_ROOT / "webui/src/features/notifications/NotificationCenterPage.tsx").read_text(encoding="utf-8")
settings = (REPO_ROOT / "webui/src/features/notifications/NotificationSettingsPanel.tsx").read_text(encoding="utf-8")
widget = (REPO_ROOT / "webui/src/features/notifications/NotificationSummaryWidget.tsx").read_text(encoding="utf-8")
for component in (
"ActionBlockerHint",
"ConfirmDialog",
"DocumentationHelpLink",
"SelectionList",
):
self.assertIn(component, center)
for component in (
"ActionBlockerHint",
"DocumentationHelpLink",
"ReferenceMultiSelect",
"useUnsavedDraftGuard",
):
self.assertIn(component, settings)
self.assertIn("DocumentationHelpLink", widget)
if __name__ == "__main__":
unittest.main()
+33
View File
@@ -21,12 +21,14 @@ from govoplan_notifications.backend.db.models import NotificationDeliveryAttempt
from govoplan_notifications.backend.router import api_get_notification, api_list_notifications, api_notification_summary, api_update_notification
from govoplan_notifications.backend.schemas import NotificationCreateRequest, NotificationPreferencesUpdateRequest, NotificationUpdateRequest
from govoplan_notifications.backend.service import (
NotificationError,
create_notification,
deliver_pending,
notification_preferences_response,
notification_response,
notification_summary,
update_notification_preferences,
update_notification,
)
@@ -222,6 +224,37 @@ class NotificationServiceTests(unittest.TestCase):
session.commit()
enqueue.assert_not_called()
def test_cancellation_is_limited_to_locally_controllable_delivery_states(self) -> None:
with self.Session() as session:
notification = create_notification(
session,
tenant_id="tenant-1",
payload=self._inbox_payload(recipient_id="user-1", subject="Cancellation boundary"),
)
notification.status = "sent"
session.flush()
with self.assertRaisesRegex(
NotificationError,
"cannot be cancelled from status sent",
):
update_notification(
session,
tenant_id="tenant-1",
notification_id=notification.id,
payload=NotificationUpdateRequest(status="cancelled"),
)
notification.status = "queued"
cancelled = update_notification(
session,
tenant_id="tenant-1",
notification_id=notification.id,
payload=NotificationUpdateRequest(status="cancelled"),
)
self.assertEqual("cancelled", cancelled.status)
self.assertIsNotNone(cancelled.cancelled_at)
def test_action_url_accepts_only_application_relative_paths(self) -> None:
payload = self._inbox_payload(recipient_id="user-1", subject="Safe action")
safe = payload.model_copy(update={"action_url": "/calendar?event=event-1#details"})