Skip to content

Commit 2e0d3fe

Browse files
committed
Check for subscriber calendar ownership when creating calendar records
1 parent 3203222 commit 2e0d3fe

File tree

2 files changed

+229
-2
lines changed

2 files changed

+229
-2
lines changed

backend/src/appointment/database/repo/calendar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def update_or_create(
134134
"""update or create a subscriber calendar"""
135135
subscriber_calendar = get_by_url(db, calendar_url)
136136

137-
if subscriber_calendar is None:
137+
if subscriber_calendar is None or subscriber_calendar.owner_id != subscriber_id:
138138
return create(db, calendar, subscriber_id, external_connection_id)
139139

140140
return update_by_calendar(db, calendar, subscriber_calendar)

backend/test/integration/test_calendar.py

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from appointment.database.models import CalendarProvider
66
from appointment.controller.calendar import CalDavConnector, GoogleConnector
7-
from appointment.database import schemas, models
7+
from appointment.database import schemas, models, repo
88
from appointment.defines import GOOGLE_CALDAV_DOMAINS
99

1010
from sqlalchemy import select
@@ -511,3 +511,230 @@ def mock_bust_cached_events(self, all_calendars=False):
511511
calendar_titles = [cal.title for cal in calendars]
512512
assert 'Test Calendar 1' in calendar_titles
513513
assert 'Test Calendar 2' in calendar_titles
514+
515+
516+
class TestCalendarUpdateOrCreate:
517+
"""Tests for the update_or_create function in the calendar repository.
518+
519+
These tests ensure that multiple subscribers can connect the same external calendar
520+
(e.g., the same Google calendar) and each gets their own calendar record.
521+
"""
522+
523+
def test_update_or_create_creates_new_calendar_when_none_exists(
524+
self, with_db, make_pro_subscriber, make_external_connections
525+
):
526+
"""Test that update_or_create creates a new calendar when no calendar exists with that URL."""
527+
subscriber = make_pro_subscriber()
528+
ec = make_external_connections(subscriber.id, type=models.ExternalConnectionType.google)
529+
530+
calendar_url = 'test-calendar@google.com'
531+
calendar_data = schemas.CalendarConnection(
532+
title='Test Calendar',
533+
color='#4285f4',
534+
provider=models.CalendarProvider.google,
535+
url=calendar_url,
536+
user=calendar_url,
537+
password='',
538+
)
539+
540+
with with_db() as db:
541+
result = repo.calendar.update_or_create(
542+
db=db,
543+
calendar=calendar_data,
544+
calendar_url=calendar_url,
545+
subscriber_id=subscriber.id,
546+
external_connection_id=ec.id,
547+
)
548+
549+
assert result is not None
550+
assert result.owner_id == subscriber.id
551+
assert result.url == calendar_url
552+
assert result.title == 'Test Calendar'
553+
assert result.external_connection_id == ec.id
554+
555+
def test_update_or_create_updates_existing_calendar_for_same_subscriber(
556+
self, with_db, make_pro_subscriber, make_external_connections
557+
):
558+
"""Test that update_or_create updates an existing calendar when the same subscriber calls it."""
559+
subscriber = make_pro_subscriber()
560+
ec = make_external_connections(subscriber.id, type=models.ExternalConnectionType.google)
561+
562+
calendar_url = 'test-calendar@google.com'
563+
original_calendar = schemas.CalendarConnection(
564+
title='Original Title',
565+
color='#4285f4',
566+
provider=models.CalendarProvider.google,
567+
url=calendar_url,
568+
user=calendar_url,
569+
password='',
570+
)
571+
572+
with with_db() as db:
573+
# Create the initial calendar
574+
first_result = repo.calendar.update_or_create(
575+
db=db,
576+
calendar=original_calendar,
577+
calendar_url=calendar_url,
578+
subscriber_id=subscriber.id,
579+
external_connection_id=ec.id,
580+
)
581+
first_calendar_id = first_result.id
582+
583+
# Now call update_or_create again with updated data
584+
updated_calendar = schemas.CalendarConnection(
585+
title='Updated Title',
586+
color='#ff0000',
587+
provider=models.CalendarProvider.google,
588+
url=calendar_url,
589+
user=calendar_url,
590+
password='',
591+
)
592+
593+
second_result = repo.calendar.update_or_create(
594+
db=db,
595+
calendar=updated_calendar,
596+
calendar_url=calendar_url,
597+
subscriber_id=subscriber.id,
598+
external_connection_id=ec.id,
599+
)
600+
601+
# Should update the existing calendar, not create a new one
602+
assert second_result.id == first_calendar_id
603+
assert second_result.title == 'Updated Title'
604+
assert second_result.color == '#ff0000'
605+
606+
# Verify only one calendar exists for this subscriber
607+
subscriber_calendars = repo.calendar.get_by_subscriber(db, subscriber.id)
608+
assert len(subscriber_calendars) == 1
609+
610+
def test_update_or_create_creates_separate_calendar_for_different_subscriber(
611+
self, with_db, make_pro_subscriber, make_external_connections
612+
):
613+
"""Test that update_or_create creates a new calendar when a different subscriber
614+
connects the same external calendar (e.g., same Google calendar URL) successfully.
615+
"""
616+
subscriber_a = make_pro_subscriber()
617+
subscriber_b = make_pro_subscriber()
618+
619+
ec_a = make_external_connections(subscriber_a.id, type=models.ExternalConnectionType.google)
620+
ec_b = make_external_connections(subscriber_b.id, type=models.ExternalConnectionType.google)
621+
622+
# Both subscribers are connecting the same Google calendar
623+
shared_calendar_url = 'shared-calendar@google.com'
624+
625+
calendar_data_a = schemas.CalendarConnection(
626+
title='Calendar for Subscriber A',
627+
color='#4285f4',
628+
provider=models.CalendarProvider.google,
629+
url=shared_calendar_url,
630+
user=shared_calendar_url,
631+
password='',
632+
)
633+
634+
calendar_data_b = schemas.CalendarConnection(
635+
title='Calendar for Subscriber B',
636+
color='#ff0000',
637+
provider=models.CalendarProvider.google,
638+
url=shared_calendar_url,
639+
user=shared_calendar_url,
640+
password='',
641+
)
642+
643+
with with_db() as db:
644+
# Subscriber A connects the calendar first
645+
calendar_a = repo.calendar.update_or_create(
646+
db=db,
647+
calendar=calendar_data_a,
648+
calendar_url=shared_calendar_url,
649+
subscriber_id=subscriber_a.id,
650+
external_connection_id=ec_a.id,
651+
)
652+
653+
assert calendar_a is not None
654+
assert calendar_a.owner_id == subscriber_a.id
655+
656+
# Subscriber B connects the same calendar
657+
calendar_b = repo.calendar.update_or_create(
658+
db=db,
659+
calendar=calendar_data_b,
660+
calendar_url=shared_calendar_url,
661+
subscriber_id=subscriber_b.id,
662+
external_connection_id=ec_b.id,
663+
)
664+
665+
# Subscriber B should get their own calendar record
666+
assert calendar_b is not None
667+
assert calendar_b.owner_id == subscriber_b.id
668+
assert calendar_b.id != calendar_a.id # Different calendar records
669+
670+
# Verify each subscriber has exactly one calendar
671+
calendars_a = repo.calendar.get_by_subscriber(db, subscriber_a.id)
672+
calendars_b = repo.calendar.get_by_subscriber(db, subscriber_b.id)
673+
674+
assert len(calendars_a) == 1
675+
assert len(calendars_b) == 1
676+
677+
# Verify the calendars belong to the correct subscribers
678+
assert calendars_a[0].owner_id == subscriber_a.id
679+
assert calendars_b[0].owner_id == subscriber_b.id
680+
681+
# Verify the external connections are correct
682+
assert calendars_a[0].external_connection_id == ec_a.id
683+
assert calendars_b[0].external_connection_id == ec_b.id
684+
685+
def test_update_or_create_does_not_modify_other_subscribers_calendar(
686+
self, with_db, make_pro_subscriber, make_external_connections
687+
):
688+
"""Test that when Subscriber B connects a calendar that Subscriber A already has,
689+
Subscriber A's calendar is not modified."""
690+
subscriber_a = make_pro_subscriber()
691+
subscriber_b = make_pro_subscriber()
692+
693+
ec_a = make_external_connections(subscriber_a.id, type=models.ExternalConnectionType.google)
694+
ec_b = make_external_connections(subscriber_b.id, type=models.ExternalConnectionType.google)
695+
696+
shared_calendar_url = 'shared-calendar@google.com'
697+
698+
calendar_data_a = schemas.CalendarConnection(
699+
title='Original Title A',
700+
color='#4285f4',
701+
provider=models.CalendarProvider.google,
702+
url=shared_calendar_url,
703+
user=shared_calendar_url,
704+
password='',
705+
)
706+
707+
calendar_data_b = schemas.CalendarConnection(
708+
title='Title B',
709+
color='#ff0000',
710+
provider=models.CalendarProvider.google,
711+
url=shared_calendar_url,
712+
user=shared_calendar_url,
713+
password='',
714+
)
715+
716+
with with_db() as db:
717+
# Subscriber A connects first
718+
repo.calendar.update_or_create(
719+
db=db,
720+
calendar=calendar_data_a,
721+
calendar_url=shared_calendar_url,
722+
subscriber_id=subscriber_a.id,
723+
external_connection_id=ec_a.id,
724+
)
725+
726+
# Subscriber B connects the same calendar
727+
repo.calendar.update_or_create(
728+
db=db,
729+
calendar=calendar_data_b,
730+
calendar_url=shared_calendar_url,
731+
subscriber_id=subscriber_b.id,
732+
external_connection_id=ec_b.id,
733+
)
734+
735+
# Verify Subscriber A's calendar was not modified
736+
calendars_a = repo.calendar.get_by_subscriber(db, subscriber_a.id)
737+
assert len(calendars_a) == 1
738+
assert calendars_a[0].title == 'Original Title A'
739+
assert calendars_a[0].color == '#4285f4'
740+
assert calendars_a[0].external_connection_id == ec_a.id

0 commit comments

Comments
 (0)