Migrate Notifications interface patterns
This commit is contained in:
@@ -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()
|
||||
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user