From f98f1beba5286ea23da9d394b8a387c69ba1d00d Mon Sep 17 00:00:00 2001 From: Sylvia McLaughlin <85905333+sylviamclaughlin@users.noreply.github.com> Date: Fri, 26 Apr 2024 06:05:48 +0000 Subject: [PATCH 1/2] Additional fixes for edge cases and for correct timezones --- .../google_workspace/google_calendar.py | 27 ++--- .../google_workspace/test_google_calendar.py | 104 +++++++++++------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/app/integrations/google_workspace/google_calendar.py b/app/integrations/google_workspace/google_calendar.py index ce8232a4..505d468e 100644 --- a/app/integrations/google_workspace/google_calendar.py +++ b/app/integrations/google_workspace/google_calendar.py @@ -36,6 +36,7 @@ def get_freebusy(time_min, time_max, items, **kwargs): "items": items, } body.update({convert_string_to_camel_case(k): v for k, v in kwargs.items()}) + return execute_google_api_call( "calendar", "v3", @@ -102,33 +103,26 @@ def find_first_available_slot( busy_times = [] for calendar in freebusy_response["calendars"].values(): for busy_period in calendar["busy"]: - start = ( - datetime.strptime(busy_period["start"], "%Y-%m-%dT%H:%M:%SZ") - .replace(tzinfo=pytz.UTC) - .astimezone(est) - ) - end = ( - datetime.strptime(busy_period["end"], "%Y-%m-%dT%H:%M:%SZ") - .replace(tzinfo=pytz.UTC) - .astimezone(est) - ) + # convert from iso 8601 standard to datetime + start = datetime.fromisoformat(busy_period["start"][:-1]) + end = datetime.fromisoformat(busy_period["end"][:-1]) busy_times.append((start, end)) busy_times.sort(key=lambda x: x[0]) for day_offset in range(days_in_future, days_in_future + search_days_limit): # Calculate the start and end times of the search window for the current day - search_date = datetime.now(tz=est) + timedelta(days=day_offset) + search_date = datetime.utcnow() + timedelta(days=day_offset) # Check if the day is Saturday (5) or Sunday (6) and skip it if search_date.weekday() in [5, 6]: continue search_start = search_date.replace( - hour=13, minute=0, second=0, microsecond=0 - ) # 1 PM EST + hour=17, minute=0, second=0, microsecond=0 + ) # 1 PM EST, times are in UTC search_end = search_date.replace( - hour=15, minute=0, second=0, microsecond=0 - ) # 3 PM EST + hour=19, minute=0, second=0, microsecond=0 + ) # 3 PM EST, times are in UTC # Attempt to find an available slot within this day's search window for current_time in ( @@ -139,6 +133,7 @@ def find_first_available_slot( slot_end <= start or current_time >= end for start, end in busy_times ): if slot_end <= search_end: - return current_time, slot_end + # return the time and convert them to EST timezone + return current_time.astimezone(est), slot_end.astimezone(est) return None, None # No available slot found after searching the limit diff --git a/app/tests/integrations/google_workspace/test_google_calendar.py b/app/tests/integrations/google_workspace/test_google_calendar.py index 23168897..aaf43393 100644 --- a/app/tests/integrations/google_workspace/test_google_calendar.py +++ b/app/tests/integrations/google_workspace/test_google_calendar.py @@ -2,7 +2,7 @@ import json from unittest.mock import patch, MagicMock -from datetime import datetime +from datetime import datetime, timedelta import pytest import pytz from integrations.google_workspace import google_calendar @@ -18,7 +18,6 @@ def event_details(): } ) - # Fixture to mock the calendar service object @pytest.fixture def calendar_service_mock(): @@ -38,6 +37,10 @@ def calendar_service_mock(): def est_timezone(): return pytz.timezone("US/Eastern") +@pytest.fixture +def fixed_utc_now(): + # Return a fixed UTC datetime + return datetime(2023, 4, 10, 12, 0) # This is a Monday # Fixture to mock the datetime.now() function @pytest.fixture @@ -247,73 +250,84 @@ def test_insert_event_api_call_error( assert not mock_handle_errors.called -# Test out the find_first_available_slot function on the first available weekday -def test_available_slot_on_first_weekday(mock_datetime_now, est_timezone): +@patch('integrations.google_workspace.google_calendar.datetime') +def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timezone): + # Mock datetime to control the flow of time in the test + mock_datetime.utcnow.return_value = fixed_utc_now + mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1]) + mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + + # Simulate a freebusy response with no busy times on the first available day freebusy_response = { "calendars": { "primary": { "busy": [ - # Assuming the provided busy time is on April 10, 2023, from 1 PM to 1:30 PM UTC (9 AM to 9:30 AM EST) { - "start": "2023-04-10T17:00:00Z", - "end": "2023-04-10T17:30:00Z", - } # Busy at 1 PM to 1:30 PM EST on April 10 + "start": "2023-04-10T17:00:000Z", + "end": "2023-04-10T17:30:000Z", + } ] } } } - start, end = google_calendar.find_first_available_slot( - freebusy_response, duration_minutes=30, days_in_future=3, search_days_limit=60 + # Expected search date is three days in the future (which should be Thursday) + # Busy period is from 1 PM to 1:30 PM EST on the first day being checked (April 13th) + # The function should find an available slot after the busy period + expected_start_time = fixed_utc_now.replace( + day=fixed_utc_now.day + 3, hour=17, minute=0, second=0, microsecond=0 + ).astimezone(est_timezone) + expected_end_time = expected_start_time + timedelta(minutes=30) + + # Run the function under test + actual_start, actual_end = google_calendar.find_first_available_slot( + freebusy_response, days_in_future=3, duration_minutes=30, search_days_limit=60 ) - # Since April 10 is the "current" day and we start searching from 3 days in the future (April 13), - # we expect the function to find the first available slot on April 13 between 1 PM and 3 PM EST. - expected_start = datetime( - 2023, 4, 13, 13, 0, tzinfo=est_timezone - ) # Expected start time in EST - expected_end = datetime( - 2023, 4, 13, 13, 30, tzinfo=est_timezone - ) # Expected end time in EST - - assert ( - start == expected_start and end == expected_end - ), "The function should find the first available slot on April 13, 2023, from 1 PM to 1:30 PM EST." - + # Check if the times returned match the expected values + assert actual_start == expected_start_time + assert actual_end == expected_end_time + # Test out the find_first_available_slot function when multiple busy days -def test_opening_exists_after_busy_days(mock_datetime_now, est_timezone): +@patch('integrations.google_workspace.google_calendar.datetime') +def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezone): + # Mock datetime to control the flow of time in the test + mock_datetime.utcnow.return_value = fixed_utc_now + mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1]) + mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) freebusy_response = { "calendars": { "primary": { "busy": [ - {"start": "2023-04-13T17:00:00Z", "end": "2023-04-13T19:00:00Z"}, - {"start": "2023-04-14T17:00:00Z", "end": "2023-04-14T19:00:00Z"}, - {"start": "2023-04-17T17:00:00Z", "end": "2023-04-17T19:00:00Z"}, - {"start": "2023-04-18T17:00:00Z", "end": "2023-04-18T19:00:00Z"}, + {"start": "2023-04-13T17:00:000Z", "end": "2023-04-13T19:00:000Z"}, + {"start": "2023-04-14T17:00:000Z", "end": "2023-04-14T19:00:000Z"}, + {"start": "2023-04-17T17:00:000Z", "end": "2023-04-17T19:00:000Z"}, + {"start": "2023-04-18T17:00:000Z", "end": "2023-04-18T19:00:000Z"}, ] } } } start, end = google_calendar.find_first_available_slot( - freebusy_response, duration_minutes=30, days_in_future=3, search_days_limit=60 + freebusy_response, days_in_future=3, duration_minutes=30, search_days_limit=60 ) - - expected_start = datetime( - 2023, 4, 13, 14, 30, tzinfo=est_timezone - ) # Expected start time in EST - expected_end = datetime( - 2023, 4, 13, 15, 0, tzinfo=est_timezone - ) # Expected end time in EST - + expected_start = fixed_utc_now.replace( + day=fixed_utc_now.day + 9, hour=17, minute=0, second=0, microsecond=0 + ).astimezone(est_timezone) + expected_end = expected_start + timedelta(minutes=30) + assert ( start == expected_start and end == expected_end ), "Expected to find an available slot correctly." # Test that weekends are skipped when searching for available slots -def test_skipping_weekends(mock_datetime_now, est_timezone): +@patch('integrations.google_workspace.google_calendar.datetime') +def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone): + mock_datetime.utcnow.return_value = fixed_utc_now + mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1]) + mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) freebusy_response = { "calendars": { "primary": { @@ -330,8 +344,10 @@ def test_skipping_weekends(mock_datetime_now, est_timezone): ) # Adjust these expected values based on the specific 'now' being mocked - expected_start = datetime(2023, 4, 11, 13, 0, tzinfo=est_timezone) - expected_end = datetime(2023, 4, 11, 13, 30, tzinfo=est_timezone) + expected_start = fixed_utc_now.replace( + day=fixed_utc_now.day + 1, hour=17, minute=0, second=0, microsecond=0 + ).astimezone(est_timezone) + expected_end = expected_start + timedelta(minutes=30) assert ( start == expected_start and end == expected_end @@ -339,14 +355,18 @@ def test_skipping_weekends(mock_datetime_now, est_timezone): # Test that no available slots are found within the search limit -def test_no_available_slots_within_search_limit(mock_datetime_now): +@patch('integrations.google_workspace.google_calendar.datetime') +def test_no_available_slots_within_search_limit(mock_datetime, fixed_utc_now, est_timezone): + mock_datetime.utcnow.return_value = fixed_utc_now + mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1]) + mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) freebusy_response = { "calendars": { "primary": { "busy": [ # Simulate a scenario where every eligible day within the search window is fully booked # For simplicity, let's assume a pattern that covers the search hours for the next 60 days - {"start": "2023-04-10T17:00:00Z", "end": "2023-08-13T19:00:00Z"}, + {"start": "2023-04-10T17:00:000Z", "end": "2023-08-13T19:00:000Z"}, ] } } From 32014136862c958510e7d3e4c5c1ad6242b083ca Mon Sep 17 00:00:00 2001 From: Sylvia McLaughlin <85905333+sylviamclaughlin@users.noreply.github.com> Date: Fri, 26 Apr 2024 06:08:31 +0000 Subject: [PATCH 2/2] Formatting and linting --- .../google_workspace/google_calendar.py | 2 +- .../google_workspace/test_google_calendar.py | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/integrations/google_workspace/google_calendar.py b/app/integrations/google_workspace/google_calendar.py index 505d468e..c2444b92 100644 --- a/app/integrations/google_workspace/google_calendar.py +++ b/app/integrations/google_workspace/google_calendar.py @@ -36,7 +36,7 @@ def get_freebusy(time_min, time_max, items, **kwargs): "items": items, } body.update({convert_string_to_camel_case(k): v for k, v in kwargs.items()}) - + return execute_google_api_call( "calendar", "v3", diff --git a/app/tests/integrations/google_workspace/test_google_calendar.py b/app/tests/integrations/google_workspace/test_google_calendar.py index aaf43393..d6d039f6 100644 --- a/app/tests/integrations/google_workspace/test_google_calendar.py +++ b/app/tests/integrations/google_workspace/test_google_calendar.py @@ -18,6 +18,7 @@ def event_details(): } ) + # Fixture to mock the calendar service object @pytest.fixture def calendar_service_mock(): @@ -37,11 +38,13 @@ def calendar_service_mock(): def est_timezone(): return pytz.timezone("US/Eastern") + @pytest.fixture def fixed_utc_now(): # Return a fixed UTC datetime return datetime(2023, 4, 10, 12, 0) # This is a Monday + # Fixture to mock the datetime.now() function @pytest.fixture def mock_datetime_now(est_timezone): @@ -250,7 +253,7 @@ def test_insert_event_api_call_error( assert not mock_handle_errors.called -@patch('integrations.google_workspace.google_calendar.datetime') +@patch("integrations.google_workspace.google_calendar.datetime") def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timezone): # Mock datetime to control the flow of time in the test mock_datetime.utcnow.return_value = fixed_utc_now @@ -263,8 +266,8 @@ def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timez "primary": { "busy": [ { - "start": "2023-04-10T17:00:000Z", - "end": "2023-04-10T17:30:000Z", + "start": "2023-04-10T17:00:000Z", + "end": "2023-04-10T17:30:000Z", } ] } @@ -288,9 +291,9 @@ def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timez assert actual_start == expected_start_time assert actual_end == expected_end_time - + # Test out the find_first_available_slot function when multiple busy days -@patch('integrations.google_workspace.google_calendar.datetime') +@patch("integrations.google_workspace.google_calendar.datetime") def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezone): # Mock datetime to control the flow of time in the test mock_datetime.utcnow.return_value = fixed_utc_now @@ -316,14 +319,14 @@ def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezo day=fixed_utc_now.day + 9, hour=17, minute=0, second=0, microsecond=0 ).astimezone(est_timezone) expected_end = expected_start + timedelta(minutes=30) - + assert ( start == expected_start and end == expected_end ), "Expected to find an available slot correctly." # Test that weekends are skipped when searching for available slots -@patch('integrations.google_workspace.google_calendar.datetime') +@patch("integrations.google_workspace.google_calendar.datetime") def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone): mock_datetime.utcnow.return_value = fixed_utc_now mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1]) @@ -355,8 +358,10 @@ def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone): # Test that no available slots are found within the search limit -@patch('integrations.google_workspace.google_calendar.datetime') -def test_no_available_slots_within_search_limit(mock_datetime, fixed_utc_now, est_timezone): +@patch("integrations.google_workspace.google_calendar.datetime") +def test_no_available_slots_within_search_limit( + mock_datetime, fixed_utc_now, est_timezone +): mock_datetime.utcnow.return_value = fixed_utc_now mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1]) mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)