Files
govoplan-calendar/tests/test_service.py
T

264 lines
9.4 KiB
Python

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()