diff --git a/controllers/v2/unavailability/api.py b/controllers/v2/unavailability/api.py index 093186f7..0145cfa5 100644 --- a/controllers/v2/unavailability/api.py +++ b/controllers/v2/unavailability/api.py @@ -80,16 +80,15 @@ def get(self, user_id): def post(self, user_id): try: args = edit_parser.parse_args() - # Check if start time is earlier than end time. + # Check if start time is earlier than end time. redundancy may remove if args['start'] >= args['end']: return {"message": "Start time must be earlier than end time"}, 400 # HTTP 400 Bad Request - overlapping_events = self.event_repository.check_overlapping_events(user_id, args['start'], args['end'], - args['periodicity']) - if overlapping_events: - return {"message": "Time frames overlap with existing events", - "overlapping events": overlapping_events}, 400 - + duplicate_event = self.event_repository.check_duplicate_event(user_id, args['start'], args['end'], + args['periodicity']) + # Prevent duplicate events from being created. + if duplicate_event: + return {"message": "Event to be added already exist"}, 400 eventId = self.event_repository.create_event( user_id, diff --git a/repository/volunteer_unavailability_v2.py b/repository/volunteer_unavailability_v2.py index 1f011ed6..b9c80d85 100644 --- a/repository/volunteer_unavailability_v2.py +++ b/repository/volunteer_unavailability_v2.py @@ -121,3 +121,17 @@ def check_overlapping_events(self, userId, startTime, endTime, periodicity): }) return overlapping_details + + def check_duplicate_event(self, userId, startTime, endTime, periodicity): + with session_scope() as session: + # Query the database for events with the same start time, end time, and periodicity + duplicate_events_count = session.query(UnavailabilityTime).filter( + UnavailabilityTime.userId == userId, + UnavailabilityTime.start == startTime, + UnavailabilityTime.end == endTime, + UnavailabilityTime.periodicity == periodicity + ).count() + + # If the count of duplicate events is greater than 0, duplicates exist, return True + return duplicate_events_count > 0 + diff --git a/tests/functional/test_unavailability.py b/tests/functional/test_unavailability.py index f0e12833..e1cb6845 100644 --- a/tests/functional/test_unavailability.py +++ b/tests/functional/test_unavailability.py @@ -71,28 +71,28 @@ def test_create_unavailability_end_before_start(test_client, create_user): assert response.status_code == 400 -def test_create_unavailability_overlapped_time(test_client, create_user): - user_id = create_user - payload_1 = { - "title": "All Day Event", - "periodicity": 0, - "start": "2024-03-03T00:00:00Z", - "end": "2024-03-04T23:59:59Z" - } - payload_2 = { - "title": "All Day Event", - "periodicity": 0, - "start": "2024-03-01T00:00:00Z", - "end": "2024-03-05T23:59:59Z" - } - response_1 = test_client.post(f"/v2/volunteers/{user_id}/unavailability", - json=payload_1 - ) - response_2 = test_client.post(f"/v2/volunteers/{user_id}/unavailability", - json=payload_2 - ) - assert response_1.status_code == 200 - assert response_2.status_code == 400 +# def test_create_unavailability_overlapped_time(test_client, create_user): +# user_id = create_user +# payload_1 = { +# "title": "All Day Event", +# "periodicity": 0, +# "start": "2024-03-03T00:00:00Z", +# "end": "2024-03-04T23:59:59Z" +# } +# payload_2 = { +# "title": "All Day Event", +# "periodicity": 0, +# "start": "2024-03-01T00:00:00Z", +# "end": "2024-03-05T23:59:59Z" +# } +# response_1 = test_client.post(f"/v2/volunteers/{user_id}/unavailability", +# json=payload_1 +# ) +# response_2 = test_client.post(f"/v2/volunteers/{user_id}/unavailability", +# json=payload_2 +# ) +# assert response_1.status_code == 200 +# assert response_2.status_code == 400 # def test_merge_overlapping_unavailability_intervals(test_client, create_user): diff --git a/tests/functional/test_unavailability_delete.py b/tests/functional/test_unavailability_delete.py new file mode 100644 index 00000000..cb5177e5 --- /dev/null +++ b/tests/functional/test_unavailability_delete.py @@ -0,0 +1,138 @@ +from datetime import datetime, timezone +from unittest.mock import patch +from services.jwk import JWKService +from repository import volunteer_unavailability_v2 + +""" +Test Case 6: User with ROOT_ADMIN Role +Input: user_id of a user with ROOT_ADMIN role, event_id of an existing event. +Expected Output: Successful deletion of the event with HTTP status code 200. + +Test Case 7: User with other Roles +Input: user_id of a user with a role other than ROOT_ADMIN, event_id of an existing event. +Expected Output: HTTP status code 403 Forbidden with a message indicating that the user does not have permission to delete the event. + + + +Test Case 9: Attempt to Delete Event without Required Permissions +Input: user_id, event_id of an event that the user has permission to view but not delete. +Expected Output: HTTP status code 403 Forbidden with a message indicating the user does not have permission to delete the event. +""" + +payload = { + "title": "Test Event", + "periodicity": 0, + "start": "2025-05-02T00:00:00Z", + "end": "2025-05-02T23:59:59Z" +} + +"""Test Case 1: Successful Deletion +Input: user_id, event_id of an existing event that the user has permission to delete. +Expected Output: HTTP status code 200 with a success message.""" + + +def test_delete_volunteer_unavailability_success(test_client, create_user): + user_id = create_user + event_response = test_client.post(f"/v2/volunteers/{user_id}/unavailability", + json=payload + ) + # checks body was posted correctly + assert event_response.status_code == 200 + + # extract event_id + event_id = event_response.json["eventId"] + + # make delete request + response = test_client.delete(f"/v2/volunteers/{user_id}/unavailability/{event_id}") + + assert response.status_code == 200 + + +"""Test Case 2: Event Not Found +Input: user_id, event_id of a non-existing event. +Expected Output: HTTP status code 404 with a message indicating the event was not found.""" + + +def test_delete_event_not_found(test_client, create_user): + user_id = create_user + event_response = test_client.post(f"/v2/volunteers/{user_id}/unavailability", + json=payload + ) + + assert event_response.status_code == 200 + # extract event_id + event_id = event_response.json["eventId"] + + response = test_client.delete(f"/v2/volunteers/{user_id}/unavailability/{event_id + 1}") + + assert response.status_code == 404 + + +"""Test Case 3: Attempt to Delete Already Deleted Event +Input: user_id, event_id of an event that has already been deleted. +Expected Output: HTTP status code 404 with a message indicating the event was not found.""" + + +def test_delete_event_already_deleted(test_client, create_user): + user_id = create_user + event_response = test_client.post(f"/v2/volunteers/{user_id}/unavailability", json=payload) + assert event_response.status_code == 200 + event_id = event_response.json["eventId"] + response = test_client.delete(f"/v2/volunteers/{user_id}/unavailability/{event_id}") + assert response.status_code == 200 + response_repeat = test_client.delete(f"/v2/volunteers/{user_id}/unavailability/{event_id}") + assert response_repeat.status_code == 404 + + +# """Test Case 4: Unauthorized User Input: user_id of a non-authorized user, event_id of an existing event. +# Expected Output: HTTP status code 403 Forbidden or 404 Not Found with a message indicating the user is not authorized to +# delete the event.""" + + +# def test_delete_unauthorized(test_client, create_user1, create_user2): +# # Create the first user and post an unavailability event +# user_id1 = create_user1 +# event_response = test_client.post(f"/v2/volunteers/{user_id1}/unavailability", +# json=payload) +# assert event_response.status_code == 200 +# +# # Extract the event ID +# event_id = event_response.json()["eventId"] +# +# # Create the second user +# user_id2 = create_user2 +# +# # Generate a valid JWT token for user_id1 +# token = JWKService.generate(user_id1, "user1", "admin", +# datetime.now(), datetime.now()) +# +# # Attempt to delete the event created by user_id1 using the credentials of user_id2 +# response = test_client.delete(f"/v2/volunteers/{user_id2}/unavailability/{event_id}", +# headers={"Authorization": f"Bearer {token}"}) +# +# # Ensure that the unauthorized user receives a 401 Forbidden status code +# assert response.status_code == 401 + + +"""Test Case 4: Internal Server Error +Input: When an unexpected exception occurs during event deletion. +Expected Output: HTTP status code 500 Internal Server Error with a message indicating the server error.""" + + +def test_internal_server_error(test_client, create_user): + user_id = create_user + event_response = test_client.post(f"/v2/volunteers/{user_id}/unavailability", json=payload) + assert event_response.status_code == 200 + # extract event_id + event_id = event_response.json["eventId"] + + # Simulate an unexpected exception during event deletion + # Patch the remove_event method to raise an exception + with patch("repository.volunteer_unavailability_v2.EventRepository.remove_event") as mock_remove_event: + mock_remove_event.side_effect = Exception("Something went wrong") + + # Send a delete request + response = test_client.delete(f"/v2/volunteers/{user_id}/unavailability/{event_id}") + + # Ensure that the response status code is 500 Internal Server Error + assert response.status_code == 500 diff --git a/tests/unit/test_repository.py b/tests/unit/test_repository.py new file mode 100644 index 00000000..a42f3303 --- /dev/null +++ b/tests/unit/test_repository.py @@ -0,0 +1,71 @@ +import unittest +from unittest.mock import MagicMock, patch +from datetime import datetime, timedelta + +from domain import UnavailabilityTime +from repository import volunteer_unavailability_v2 + + +class TestEventRepository(unittest.TestCase): + def setUp(self): + self.repository = volunteer_unavailability_v2.EventRepository() + + def test_create_event(self): + with MagicMock() as session: + session.add.return_value = None + session.flush.return_value = None + result = session.repository.create_event(1, "Title", datetime.now(), datetime.now(), 1) + self.assertIsNotNone(result) + + # def test_edit_event(self): + # mock_event = MagicMock() + # with patch('repository.volunteer_unavailability_v2.session_scope') as mock_session_scope: + # with MagicMock() as session: + # session.add.return_value = None + # + # event_id = session.repository.create_event(1, "Title", datetime.now(), datetime.now(), 1) + # self.assertIsNotNone(event_id) + # result = session.repository.edit_event(1, event_id, "New Title", datetime.now(), datetime.now(), 1) + # # Assert that the result is True since an event exists + # session.flush() + # self.assertTrue(result.title == "New Title") + + def test_get_event_with_events(self): + event_id = self.repository.create_event(1, "Title", datetime.now(), datetime.now(), 1) + result = self.repository.get_event(event_id) + self.assertIsNotNone(result) + + def test_get_past_events(self): + past_datetime = (datetime.now() - timedelta(days=1)) + event_id = self.repository.create_event(1, "Title", past_datetime, datetime.now(), 1) + result = self.repository.get_event(event_id) + self.assertTrue([] == result) + + def test_remove_event(self): + with MagicMock() as session: + + # Create a mock event + event_id = session.repository.create_event(1, "Title", datetime.now(), datetime.now(), 1) + self.assertIsNotNone(event_id) + + # Remove the event + result = session.repository.remove_event(1, event_id) + + # Check if removal was successful + self.assertTrue(result) + + ## + def test_check_duplicate_events(self): + # Mock the session to simulate database interaction + with MagicMock() as session: + + # Call the method under test + session.repository.create_event(1, "Title", datetime.now(), datetime.now(), 1) + result = session.repository.check_duplicate_event(1, datetime.now(), datetime.now(), 1) + + # Check if the method correctly detects no duplicate events + self.assertTrue(result) + + +if __name__ == "__main__": + unittest.main()