Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional fixes for edge cases and for correct timezones #484

Merged
merged 2 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions app/integrations/google_workspace/google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 (
Expand All @@ -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
103 changes: 64 additions & 39 deletions app/tests/integrations/google_workspace/test_google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +39,12 @@ 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):
Expand Down Expand Up @@ -247,73 +253,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": {
Expand All @@ -330,23 +347,31 @@ 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
), "Expected to find an available slot after skipping the weekend"


# 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"},
]
}
}
Expand Down
Loading