from __future__ import annotations import unittest from datetime import datetime, timedelta, timezone from types import SimpleNamespace from unittest.mock import patch from sqlalchemy import create_engine from sqlalchemy.orm import Session from govoplan_access.backend.db.models import Account, User from govoplan_calendar.backend.db.models import CalendarCollection, CalendarEvent from govoplan_calendar.backend.schemas import CalendarCollectionDeleteRequest, CalendarEventResponse from govoplan_calendar.backend.service import ( calendar_is_visible_to_principal, delete_calendar, event_response, list_event_occurrences, list_events, ) from govoplan_core.db.base import Base class FakeSession: def __init__(self) -> None: self.flushed = False def flush(self) -> None: self.flushed = True class CalendarServiceResponseTests(unittest.TestCase): def test_event_response_marks_naive_database_datetimes_as_utc(self) -> None: event = SimpleNamespace( id="event-1", tenant_id="tenant-1", calendar_id="calendar-1", uid="event-1@example.test", recurrence_id=None, sequence=0, summary="Planning", description=None, location=None, status="CONFIRMED", transparency="OPAQUE", classification="PUBLIC", start_at=datetime(2026, 7, 7, 18, 0), end_at=datetime(2026, 7, 7, 19, 0), duration_seconds=None, all_day=False, timezone=None, organizer=None, attendees=[], categories=[], rrule=None, rdate=[], exdate=[], reminders=[], attachments=[], related_to=[], source_kind="local", source_href=None, etag=None, icalendar={}, created_at=datetime(2026, 7, 7, 17, 55), updated_at=datetime(2026, 7, 7, 17, 55), metadata_={}, ) payload = event_response(event) response = CalendarEventResponse.model_validate(payload) self.assertEqual(response.start_at.utcoffset(), timedelta(0)) self.assertEqual(response.end_at.utcoffset(), timedelta(0)) self.assertEqual(response.created_at.utcoffset(), timedelta(0)) self.assertEqual(response.updated_at.utcoffset(), timedelta(0)) self.assertEqual(response.start_at.tzinfo, timezone.utc) class CalendarVisibilityTests(unittest.TestCase): @staticmethod def calendar(**overrides): values = { "visibility": "private", "owner_type": "user", "owner_id": "owner-1", "created_by_user_id": "creator-1", } values.update(overrides) return SimpleNamespace(**values) def test_tenant_shared_and_public_calendars_are_visible_to_readers(self) -> None: for visibility in ("tenant", "shared", "public"): with self.subTest(visibility=visibility): self.assertTrue( calendar_is_visible_to_principal( self.calendar(visibility=visibility), user_id="reader-1", ) ) def test_private_calendar_is_visible_to_user_owner_creator_or_group_member(self) -> None: self.assertTrue( calendar_is_visible_to_principal( self.calendar(), user_id="owner-1", ) ) self.assertTrue( calendar_is_visible_to_principal( self.calendar(), user_id="creator-1", ) ) self.assertTrue( calendar_is_visible_to_principal( self.calendar(owner_type="group", owner_id="group-1"), user_id="member-1", group_ids=("group-1",), ) ) def test_event_queries_apply_visible_calendar_fence_and_limit(self) -> None: engine = create_engine("sqlite://") Base.metadata.create_all( engine, tables=( Account.__table__, User.__table__, CalendarCollection.__table__, CalendarEvent.__table__, ), ) session = Session(engine) start = datetime(2026, 8, 19, 9, tzinfo=timezone.utc) try: session.add_all( ( CalendarCollection( id="visible-calendar", tenant_id="tenant-1", slug="visible", name="Visible", visibility="tenant", ), CalendarCollection( id="private-calendar", tenant_id="tenant-1", slug="private", name="Private", visibility="private", owner_type="user", owner_id="other-user", ), CalendarEvent( id="visible-event", tenant_id="tenant-1", calendar_id="visible-calendar", uid="visible@example.test", summary="Visible event", start_at=start, end_at=start + timedelta(hours=1), ), CalendarEvent( id="private-event", tenant_id="tenant-1", calendar_id="private-calendar", uid="private@example.test", summary="Private event", start_at=start + timedelta(hours=2), end_at=start + timedelta(hours=3), ), ) ) session.commit() events = list_events( session, tenant_id="tenant-1", visible_calendar_ids=("visible-calendar",), limit=1, ) occurrences = list_event_occurrences( session, tenant_id="tenant-1", start_at=start - timedelta(hours=1), end_at=start + timedelta(days=1), visible_calendar_ids=("visible-calendar",), limit=1, ) self.assertEqual(["visible-event"], [event.id for event in events]) self.assertEqual(["visible-event"], [event["id"] for event in occurrences]) finally: session.close() engine.dispose() def test_private_calendar_is_hidden_from_other_readers_but_visible_to_admin(self) -> None: calendar = self.calendar() self.assertFalse( calendar_is_visible_to_principal( calendar, user_id="other-1", group_ids=("group-2",), ) ) self.assertTrue( calendar_is_visible_to_principal( calendar, user_id="other-1", can_admin=True, ) ) class CalendarServiceDeleteTests(unittest.TestCase): def test_delete_default_calendar_soft_deletes_calendar_and_events(self) -> None: session = FakeSession() event = SimpleNamespace(calendar_id="calendar-1", deleted_at=None) calendar = SimpleNamespace(id="calendar-1", is_default=True, deleted_at=None, events=[event]) with patch("govoplan_calendar.backend.service.get_calendar", return_value=calendar): delete_calendar( session, tenant_id="tenant-1", calendar_id="calendar-1", payload=CalendarCollectionDeleteRequest(event_action="delete"), ) self.assertTrue(session.flushed) self.assertFalse(calendar.is_default) self.assertIsNotNone(calendar.deleted_at) self.assertEqual(event.deleted_at, calendar.deleted_at) self.assertEqual(event.calendar_id, "calendar-1") def test_delete_calendar_can_move_events_to_another_calendar_and_make_it_default(self) -> None: session = FakeSession() event = SimpleNamespace(calendar_id="calendar-1", deleted_at=None) calendar = SimpleNamespace(id="calendar-1", is_default=True, deleted_at=None, events=[event]) target = SimpleNamespace(id="calendar-2", is_default=False, deleted_at=None, events=[]) with ( patch("govoplan_calendar.backend.service.get_calendar", side_effect=[calendar, target]), patch("govoplan_calendar.backend.service.clear_default_calendar") as clear_default_calendar, ): delete_calendar( session, tenant_id="tenant-1", calendar_id="calendar-1", payload=CalendarCollectionDeleteRequest(event_action="move", target_calendar_id="calendar-2", make_target_default=True), ) self.assertTrue(session.flushed) clear_default_calendar.assert_called_once_with(session, tenant_id="tenant-1") self.assertFalse(calendar.is_default) self.assertTrue(target.is_default) self.assertIsNotNone(calendar.deleted_at) self.assertIsNone(event.deleted_at) self.assertEqual(event.calendar_id, "calendar-2") if __name__ == "__main__": unittest.main()