From e39b7f12c8dd02510364f3feb67380a30e705aef Mon Sep 17 00:00:00 2001 From: "seer-by-sentry[bot]" <157164994+seer-by-sentry[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:32:45 +0000 Subject: [PATCH 1/3] fix: handle 410 Gone statuses in API calls Handle 410 Gone status as ObjectDoesNotExistError in API calls. Refs: PT-1989 --- occurrences/event_api_services.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/occurrences/event_api_services.py b/occurrences/event_api_services.py index 7cbace7c..0d05728a 100644 --- a/occurrences/event_api_services.py +++ b/occurrences/event_api_services.py @@ -34,6 +34,11 @@ def fetch_place_as_json(place_id: str, **filter_params): if result.status_code == 404: raise ObjectDoesNotExistError("Could not find the place from the API.") + if result.status_code == 410: + raise ObjectDoesNotExistError( + "The place is no longer available in the API (gone)." + ) + result.raise_for_status() place_obj = json.loads(result.text) @@ -54,6 +59,11 @@ def fetch_event_as_json(linked_event_id: str, **filter_params): if result.status_code == 404: raise ObjectDoesNotExistError("Could not find the event from the API.") + if result.status_code == 410: + raise ObjectDoesNotExistError( + "The event is no longer available in the API (gone)." + ) + result.raise_for_status() event_obj = json.loads(result.text) @@ -75,6 +85,11 @@ def update_event_to_linkedevents_api(linked_event_id: str, event_obj) -> None: if result.status_code == 404: raise ObjectDoesNotExistError("Could not find the event from the API.") + if result.status_code == 410: + raise ObjectDoesNotExistError( + "The event is no longer available in the API (gone)." + ) + result.raise_for_status() @@ -193,6 +208,11 @@ def resolve_unit_name_with_unit_id(study_group: "StudyGroup"): if result.status_code == 404: raise ObjectDoesNotExistError("Could not find the place from the API.") + if result.status_code == 410: + raise ObjectDoesNotExistError( + "The place is no longer available in the API (gone)." + ) + result.raise_for_status() unit = json2obj(format_response(result)) From 09e9a12612dd94aa78ee7be9d0f7e97adbac1a07 Mon Sep 17 00:00:00 2001 From: Mika Hietanen Date: Wed, 14 Jan 2026 14:38:46 +0200 Subject: [PATCH 2/3] test: use sorting in person study groups test Use sorted, deterministic data in person study groups testing and snapshot. Future test runs will compare against this sorted snapshot. Refs: PT-1989 --- organisations/tests/__snapshots__/test_api.ambr | 13 +++++-------- organisations/tests/test_api.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/organisations/tests/__snapshots__/test_api.ambr b/organisations/tests/__snapshots__/test_api.ambr index 5347ee47..7d8a9203 100644 --- a/organisations/tests/__snapshots__/test_api.ambr +++ b/organisations/tests/__snapshots__/test_api.ambr @@ -421,30 +421,27 @@ 'edges': list([ dict({ 'node': dict({ - 'groupName': 'Hand human value base pattern democratic focus. Kind various laugh smile behavior.', + 'groupName': 'Group 00', }), }), dict({ 'node': dict({ - 'groupName': 'Close term where up notice environment father stay. Hold project month similar support line.', + 'groupName': 'Group 01', }), }), dict({ 'node': dict({ - 'groupName': 'Eat design give per kind history ahead. Herself consider fight us claim. Age feeling speech eye.', + 'groupName': 'Group 02', }), }), dict({ 'node': dict({ - 'groupName': ''' - Apply somebody especially far. Color price environmental. - Market him beyond. - ''', + 'groupName': 'Group 03', }), }), dict({ 'node': dict({ - 'groupName': 'Civil find learn follow. Tend practice other poor.', + 'groupName': 'Group 04', }), }), ]), diff --git a/organisations/tests/test_api.py b/organisations/tests/test_api.py index cc6623bb..d4247059 100644 --- a/organisations/tests/test_api.py +++ b/organisations/tests/test_api.py @@ -869,14 +869,22 @@ def test_person_study_groups( person = event_staff_api_client.user.person person.organisations.add(organisation) p_event = PalvelutarjotinEventFactory(organisation=organisation) - EnrolmentFactory.create_batch( - 5, study_group__person=person, person=person, occurrence__p_event=p_event - ) + for i in range(5): + EnrolmentFactory.create( + study_group__person=person, + study_group__group_name=f"Group {i:02d}", + person=person, + occurrence__p_event=p_event, + ) assert person.studygroup_set.count() == 5 executed = event_staff_api_client.execute( PERSON_STUDY_GROUPS_QUERY, variables={"id": to_global_id("PersonNode", person.id)}, ) + edges = executed["data"]["person"]["studygroupSet"]["edges"] + executed["data"]["person"]["studygroupSet"]["edges"] = sorted( + edges, key=lambda x: x["node"]["groupName"] + ) assert len(executed["data"]["person"]["studygroupSet"]["edges"]) == 5 snapshot.assert_match(executed) From 9cb8fa228cd0f6f6ed84ae288a13f8cf8b83eacc Mon Sep 17 00:00:00 2001 From: Mika Hietanen Date: Thu, 15 Jan 2026 08:16:35 +0200 Subject: [PATCH 3/3] test: add HTTP status code tests for API services Add comprehensive tests for 400, 404, and 410 status codes across API service functions: - fetch_place_as_json - fetch_event_as_json - update_event_to_linkedevents_api - resolve_unit_name_with_unit_id Organize tests into separate classes for better maintainability. Each test verifies correct exception handling (ApiBadRequestError for 400, ObjectDoesNotExistError for 404/410) with appropriate error messages distinguishing "not found" from "gone". Refs: PT-1989 --- occurrences/tests/test_event_api_services.py | 315 ++++++++++++------- 1 file changed, 207 insertions(+), 108 deletions(-) diff --git a/occurrences/tests/test_event_api_services.py b/occurrences/tests/test_event_api_services.py index b9c8c905..04f6c1c2 100644 --- a/occurrences/tests/test_event_api_services.py +++ b/occurrences/tests/test_event_api_services.py @@ -1,5 +1,5 @@ from datetime import timedelta -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from django.utils import timezone @@ -8,122 +8,221 @@ from common.utils import format_linked_event_datetime from occurrences.event_api_services import api_client as le_api_client from occurrences.event_api_services import ( + fetch_event_as_json, + fetch_place_as_json, get_enrollable_event_time_range_from_occurrences, resolve_unit_name_with_unit_id, send_event_republish, send_event_unpublish, + update_event_to_linkedevents_api, ) from occurrences.factories import OccurrenceFactory, PalvelutarjotinEventFactory from palvelutarjotin.exceptions import ApiBadRequestError, ObjectDoesNotExistError -@pytest.mark.django_db -@patch("occurrences.event_api_services.update_event_to_linkedevents_api") -def test_send_event_republish( - mock_update_event_to_linkedevents_api, - mock_get_event_data, - mock_update_event_data, - p_event, -): - OccurrenceFactory.create_batch(3, p_event=p_event, cancelled=False) - - first_occurrence = p_event.occurrences.earliest("start_time") - last_occurrence = p_event.occurrences.latest("end_time") - event_start = first_occurrence.start_time - event_end = last_occurrence.end_time - - # Set a logic enrolment_start - p_event.enrolment_start = first_occurrence.start_time - p_event.save() +class TestSendEventRepublish: + @pytest.mark.django_db + @patch("occurrences.event_api_services.update_event_to_linkedevents_api") + def test_republish_with_occurrences( + self, + mock_update_event_to_linkedevents_api, + mock_get_event_data, + mock_update_event_data, + p_event, + ): + OccurrenceFactory.create_batch(3, p_event=p_event, cancelled=False) + + first_occurrence = p_event.occurrences.earliest("start_time") + last_occurrence = p_event.occurrences.latest("end_time") + event_start = first_occurrence.start_time + event_end = last_occurrence.end_time + + p_event.enrolment_start = first_occurrence.start_time + p_event.save() + + enrolling_ends = last_occurrence.start_time - timedelta( + days=p_event.enrolment_end_days + ) + send_event_republish(p_event) + + called_linked_event_id = mock_update_event_to_linkedevents_api.call_args[0][0] + event_obj = mock_update_event_to_linkedevents_api.call_args[0][1] + assert called_linked_event_id == p_event.linked_event_id + assert event_obj["start_time"] == format_linked_event_datetime(event_start) + assert event_obj["end_time"] == format_linked_event_datetime(event_end) + assert event_obj["enrolment_start_time"] == format_linked_event_datetime( + p_event.enrolment_start + ) + assert event_obj["enrolment_end_time"] == format_linked_event_datetime( + enrolling_ends + ) + assert event_obj["enrolment_end_time"] is not None + + +class TestSendEventUnpublish: + @pytest.mark.django_db + @patch("occurrences.event_api_services.update_event_to_linkedevents_api") + def test_unpublish_sets_draft_status( + self, + mock_update_event_to_linkedevents_api, + mock_get_event_data, + mock_update_event_data, + p_event, + ): + send_event_unpublish(p_event) + + called_linked_event_id = mock_update_event_to_linkedevents_api.call_args[0][0] + event_obj = mock_update_event_to_linkedevents_api.call_args[0][1] + assert called_linked_event_id == p_event.linked_event_id + assert event_obj["publication_status"] == "draft" + + +class TestGetEnrollableEventTimeRangeFromOccurrences: + @pytest.mark.django_db + def test_no_active_occurrences(self, mock_get_event_data): + p_event = PalvelutarjotinEventFactory() + assert p_event.occurrences.count() == 0 + assert get_enrollable_event_time_range_from_occurrences(p_event) == (None, None) + OccurrenceFactory(p_event=p_event, cancelled=True) + assert get_enrollable_event_time_range_from_occurrences(p_event) == (None, None) + + @pytest.mark.django_db + def test_without_enrolment_start(self, mock_get_event_data): + p_event = PalvelutarjotinEventFactory(enrolment_start=None) + OccurrenceFactory(p_event=p_event, cancelled=False) + assert get_enrollable_event_time_range_from_occurrences(p_event) == (None, None) + + @pytest.mark.django_db + def test_with_active_occurrences(self, mock_get_event_data): + p_event = PalvelutarjotinEventFactory(enrolment_start=timezone.now()) + OccurrenceFactory(p_event=p_event, cancelled=False) + (start, _) = get_enrollable_event_time_range_from_occurrences(p_event) + assert start == p_event.enrolment_start + + +class TestFetchPlaceAsJson: + @patch("occurrences.event_api_services.api_client") + def test_400_raises_bad_request_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 400 + mock_api_client.retrieve.return_value = mock_response + + with pytest.raises(ApiBadRequestError): + fetch_place_as_json("place_123") + + @patch("occurrences.event_api_services.api_client") + def test_404_raises_not_found_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 404 + mock_api_client.retrieve.return_value = mock_response + + with pytest.raises(ObjectDoesNotExistError): + fetch_place_as_json("place_123") + + @patch("occurrences.event_api_services.api_client") + def test_410_raises_gone_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 410 + mock_api_client.retrieve.return_value = mock_response + + with pytest.raises(ObjectDoesNotExistError, match="no longer available"): + fetch_place_as_json("place_123") + + +class TestFetchEventAsJson: + @patch("occurrences.event_api_services.api_client") + def test_400_raises_bad_request_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 400 + mock_api_client.retrieve.return_value = mock_response + + with pytest.raises(ApiBadRequestError): + fetch_event_as_json("event_123") + + @patch("occurrences.event_api_services.api_client") + def test_404_raises_not_found_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 404 + mock_api_client.retrieve.return_value = mock_response + + with pytest.raises(ObjectDoesNotExistError): + fetch_event_as_json("event_123") + + @patch("occurrences.event_api_services.api_client") + def test_410_raises_gone_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 410 + mock_api_client.retrieve.return_value = mock_response + + with pytest.raises(ObjectDoesNotExistError, match="no longer available"): + fetch_event_as_json("event_123") + + +class TestUpdateEventToLinkedeventsApi: + @patch("occurrences.event_api_services.api_client") + def test_400_raises_bad_request_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 400 + mock_api_client.update.return_value = mock_response + + with pytest.raises(ApiBadRequestError): + update_event_to_linkedevents_api("event_123", {"name": "Test"}) + + @patch("occurrences.event_api_services.api_client") + def test_404_raises_not_found_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 404 + mock_api_client.update.return_value = mock_response + + with pytest.raises(ObjectDoesNotExistError): + update_event_to_linkedevents_api("event_123", {"name": "Test"}) + + @patch("occurrences.event_api_services.api_client") + def test_410_raises_gone_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 410 + mock_api_client.update.return_value = mock_response + + with pytest.raises(ObjectDoesNotExistError, match="no longer available"): + update_event_to_linkedevents_api("event_123", {"name": "Test"}) + + +class TestResolveUnitNameWithUnitId: + @pytest.mark.django_db + @patch("occurrences.event_api_services.update_event_to_linkedevents_api") + def test_raises_valueerror_without_unit_id( + self, mock_update_event_to_linkedevents_api, study_group + ): + study_group.unit_id = None + with pytest.raises(ValueError): + resolve_unit_name_with_unit_id(study_group) - enrolling_ends = last_occurrence.start_time - timedelta( - days=p_event.enrolment_end_days - ) - send_event_republish(p_event) - - called_linked_event_id = mock_update_event_to_linkedevents_api.call_args[0][0] - event_obj = mock_update_event_to_linkedevents_api.call_args[0][1] - assert called_linked_event_id == p_event.linked_event_id - assert event_obj["start_time"] == format_linked_event_datetime(event_start) - assert event_obj["end_time"] == format_linked_event_datetime(event_end) - assert event_obj["enrolment_start_time"] == format_linked_event_datetime( - p_event.enrolment_start - ) - assert event_obj["enrolment_end_time"] == format_linked_event_datetime( - enrolling_ends + @pytest.mark.django_db + @pytest.mark.parametrize( + "status_code,error_cls", + [(400, ApiBadRequestError), (404, ObjectDoesNotExistError)], ) - assert event_obj["enrolment_end_time"] is not None - - -@pytest.mark.django_db -@patch("occurrences.event_api_services.update_event_to_linkedevents_api") -def test_send_event_unpublish( - mock_update_event_to_linkedevents_api, - mock_get_event_data, - mock_update_event_data, - p_event, -): - send_event_unpublish(p_event) - - called_linked_event_id = mock_update_event_to_linkedevents_api.call_args[0][0] - event_obj = mock_update_event_to_linkedevents_api.call_args[0][1] - assert called_linked_event_id == p_event.linked_event_id - assert event_obj["publication_status"] == "draft" - - -@pytest.mark.django_db -def test_get_enrollable_event_time_range_from_occurrences_with_no_active_occurrences( - mock_get_event_data, -): - p_event = PalvelutarjotinEventFactory() - assert p_event.occurrences.count() == 0 - assert get_enrollable_event_time_range_from_occurrences(p_event) == (None, None) - OccurrenceFactory(p_event=p_event, cancelled=True) - assert get_enrollable_event_time_range_from_occurrences(p_event) == (None, None) - - -@pytest.mark.django_db -def test_get_enrollable_event_time_range_from_occurrences_without_enrolment_start( - mock_get_event_data, -): - p_event = PalvelutarjotinEventFactory(enrolment_start=None) - OccurrenceFactory(p_event=p_event, cancelled=False) - assert get_enrollable_event_time_range_from_occurrences(p_event) == (None, None) - - -@pytest.mark.django_db -def test_get_enrollable_event_time_range_from_occurrences_with_active_occurrences( - mock_get_event_data, -): - p_event = PalvelutarjotinEventFactory(enrolment_start=timezone.now()) - OccurrenceFactory(p_event=p_event, cancelled=False) - (start, _) = get_enrollable_event_time_range_from_occurrences(p_event) - assert start == p_event.enrolment_start - - -@pytest.mark.django_db -@patch("occurrences.event_api_services.update_event_to_linkedevents_api") -def test_resolve_unit_name_with_unit_id_raises_valueerror_without_unit_id( - mock_update_event_to_linkedevents_api, study_group -): - study_group.unit_id = None - with pytest.raises(ValueError): - resolve_unit_name_with_unit_id(study_group) - - -@pytest.mark.django_db -@pytest.mark.parametrize( - "status_code,error_cls", [(400, ApiBadRequestError), (404, ObjectDoesNotExistError)] -) -@patch("occurrences.event_api_services.update_event_to_linkedevents_api") -def test_resolve_unit_name_with_unit_id_raises_error_from_server_faults( - mock_update_event_to_linkedevents_api, status_code, error_cls, study_group -): - study_group.unit_id = "kultus:123" - with patch.object( - le_api_client, - "retrieve", - return_value=mocked_json_response(data=None, status_code=status_code), + @patch("occurrences.event_api_services.update_event_to_linkedevents_api") + def test_raises_error_from_server_faults( + self, mock_update_event_to_linkedevents_api, status_code, error_cls, study_group ): - with pytest.raises(error_cls): - resolve_unit_name_with_unit_id(study_group) + study_group.unit_id = "kultus:123" + with patch.object( + le_api_client, + "retrieve", + return_value=mocked_json_response(data=None, status_code=status_code), + ): + with pytest.raises(error_cls): + resolve_unit_name_with_unit_id(study_group) + + @patch("occurrences.event_api_services.api_client") + def test_410_raises_gone_error(self, mock_api_client): + mock_response = Mock() + mock_response.status_code = 410 + mock_api_client.retrieve.return_value = mock_response + + mock_study_group = Mock() + mock_study_group.unit_id = "unit_123" + + with pytest.raises(ObjectDoesNotExistError, match="no longer available"): + resolve_unit_name_with_unit_id(mock_study_group)