From f412d738f0a93534233e7492260a52db0f958d83 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 26 Nov 2021 12:46:17 -0500 Subject: [PATCH] [69624] Scheduler API Support (#173) This PR enables SDK support for the Scheduler API. --- CHANGELOG.md | 1 + nylas/client/client.py | 51 +++- nylas/client/restful_models.py | 43 +++ nylas/client/scheduler_models.py | 59 ++++ .../scheduler_restful_model_collection.py | 65 ++++ tests/conftest.py | 264 ++++++++++++++++ tests/test_scheduler.py | 287 ++++++++++++++++++ 7 files changed, 756 insertions(+), 14 deletions(-) create mode 100644 nylas/client/scheduler_models.py create mode 100644 nylas/client/scheduler_restful_model_collection.py create mode 100644 tests/test_scheduler.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e20b550d..e8c75fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ nylas-python Changelog Unreleased (dev) ---------------- +* Add support for Scheduler API * Add support for Event notifications * Add support for Component CRUD * Add metadata support for `Calendar`, `Message` and `Account` diff --git a/nylas/client/client.py b/nylas/client/client.py index d7d68065..8f970ff7 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -1,4 +1,5 @@ from __future__ import print_function + import sys from os import environ from base64 import b64encode @@ -31,6 +32,9 @@ Component, ) from nylas.client.neural_api_models import Neural +from nylas.client.scheduler_restful_model_collection import ( + SchedulerRestfulModelCollection, +) from nylas.utils import timestamp_from_dt, create_request_body DEBUG = environ.get("NYLAS_CLIENT_DEBUG") @@ -421,6 +425,10 @@ def room_resources(self): def calendars(self): return RestfulModelCollection(Calendar, self) + @property + def scheduler(self): + return SchedulerRestfulModelCollection(self) + @property def components(self): return RestfulModelCollection(Component, self) @@ -466,11 +474,13 @@ def _get_resources(self, cls, extra=None, **filters): return [cls.create(self, **x) for x in results if x is not None] def _get_resource_raw( - self, cls, id, extra=None, headers=None, stream=False, **filters + self, cls, id, extra=None, headers=None, stream=False, path=None, **filters ): """Get an individual REST resource""" + if path is None: + path = cls.collection_name postfix = "/{}".format(extra) if extra else "" - path = "/{}".format(cls.collection_name) if cls.collection_name else "" + path = "/{}".format(path) if path else "" id = "/{}".format(id) if id else "" if not cls.api_root: url = "{server}{path}{id}{postfix}".format( @@ -578,16 +588,20 @@ def _delete_resource(self, cls, id, data=None, **kwargs): else: _validate(session.delete(url)) - def _update_resource(self, cls, id, data, **kwargs): + def _put_resource(self, cls, id, data, extra=None, path=None, **kwargs): + if path is None: + path = cls.collection_name name = "{prefix}{path}".format( prefix="/{}/{}".format(cls.api_root, self.client_id) if cls.api_root else "", - path="/{}".format(cls.collection_name) if cls.collection_name else "", + path="/{}".format(path) if path else "", ) + + postfix = "/{}".format(extra) if extra else "" url = ( URLObject(self.api_server) - .with_path("{name}/{id}".format(name=name, id=id)) + .with_path("{name}/{id}{postfix}".format(name=name, id=id, postfix=postfix)) .set_query_params(**kwargs) ) @@ -596,17 +610,20 @@ def _update_resource(self, cls, id, data, **kwargs): converted_data = create_request_body(data, cls.datetime_attrs) response = session.put(url, json=converted_data) - result = _validate(response).json() - return cls.create(self, **result) + result = _validate(response) + return result.json() - def _call_resource_method(self, cls, id, method_name, data): - """POST a dictionary to an API method, - for example /a/.../accounts/id/upgrade""" + def _update_resource(self, cls, id, data, **kwargs): + result = self._put_resource(cls, id, data, kwargs) + return cls.create(self, **result) - path = "/{}".format(cls.collection_name) if cls.collection_name else "" + def _post_resource(self, cls, id, method_name, data, path=None): + if path is None: + path = cls.collection_name + path = "/{}".format(path) if path else "" if not cls.api_root: - url_path = "/{name}/{id}/{method}".format( - name=cls.collection_name, id=id, method=method_name + url_path = "{name}/{id}/{method}".format( + name=path, id=id, method=method_name ) else: # Management method. @@ -624,7 +641,13 @@ def _call_resource_method(self, cls, id, method_name, data): session = self._get_http_session(cls.api_root) response = session.post(url, json=converted_data) - result = _validate(response).json() + return _validate(response).json() + + def _call_resource_method(self, cls, id, method_name, data): + """POST a dictionary to an API method, + for example /a/.../accounts/id/upgrade""" + + result = self._post_resource(cls, id, method_name, data) return cls.create(self, **result) def _request_neural_resource(self, cls, data, path=None, method="PUT"): diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 1a60c4fd..13aaf454 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -715,6 +715,49 @@ def __init__(self, api): NylasAPIObject.__init__(self, RoomResource, api) +class Scheduler(NylasAPIObject): + attrs = [ + "id", + "access_tokens", + "app_client_id", + "app_organization_id", + "config", + "edit_token", + "name", + "slug", + ] + date_attrs = { + "created_at": "created_at", + "modified_at": "modified_at", + } + collection_name = "manage/pages" + + def __init__(self, api): + NylasAPIObject.__init__(self, Scheduler, api) + + def get_available_calendars(self): + if not self.id: + raise ValueError("Cannot get calendars for a page without an ID") + + response = self.api._get_resource_raw(Scheduler, self.id, extra="calendars") + response_body = response.json() + for body in response_body: + for i in range(len(body["calendars"])): + body["calendars"][i] = Calendar.create(self.api, **body["calendars"][i]) + + return response_body + + def upload_image(self, content_type, object_name): + if not self.id: + raise ValueError("Cannot upload an image to a page without an ID") + + data = {"contentType": content_type, "objectName": object_name} + response = self.api._put_resource( + Scheduler, self.id, data, extra="upload-image" + ) + return response + + class Component(NylasAPIObject): attrs = [ "id", diff --git a/nylas/client/scheduler_models.py b/nylas/client/scheduler_models.py new file mode 100644 index 00000000..14a47470 --- /dev/null +++ b/nylas/client/scheduler_models.py @@ -0,0 +1,59 @@ +from nylas.client.restful_models import RestfulModel + + +class SchedulerTimeSlot(RestfulModel): + attrs = ["account_id", "calendar_id", "host_name", "emails"] + datetime_attrs = {"start": "start", "end": "end"} + + def __init__(self, api): + RestfulModel.__init__(self, SchedulerTimeSlot, api) + + +class SchedulerBookingConfirmation(RestfulModel): + attrs = [ + "id", + "account_id", + "additional_field_values", + "calendar_event_id", + "calendar_id", + "edit_hash", + "is_confirmed", + "location", + "recipient_email", + "recipient_locale", + "recipient_name", + "recipient_tz", + "title", + ] + datetime_attrs = {"start_time": "start_time", "end_time": "end_time"} + + def __init__(self, api): + RestfulModel.__init__(self, SchedulerBookingConfirmation, api) + + +class SchedulerBookingRequest(RestfulModel): + attrs = [ + "additional_values", + "additional_emails", + "email", + "locale", + "name", + "page_hostname", + "replaces_booking_hash", + "timezone", + "slot", + ] + + def __init__(self, api): + RestfulModel.__init__(self, SchedulerBookingRequest, api) + + def as_json(self): + dct = RestfulModel.as_json(self) + if "additional_values" not in dct or dct["additional_values"] is None: + dct["additional_values"] = {} + if "additional_emails" not in dct or dct["additional_emails"] is None: + dct["additional_emails"] = [] + if "slot" in dct and isinstance(dct["slot"], SchedulerTimeSlot): + dct["slot"] = dct["slot"].as_json() + + return dct diff --git a/nylas/client/scheduler_restful_model_collection.py b/nylas/client/scheduler_restful_model_collection.py new file mode 100644 index 00000000..4e5071a6 --- /dev/null +++ b/nylas/client/scheduler_restful_model_collection.py @@ -0,0 +1,65 @@ +import copy + +from nylas.client.restful_model_collection import RestfulModelCollection +from nylas.client.restful_models import Scheduler +from nylas.client.scheduler_models import ( + SchedulerTimeSlot, + SchedulerBookingConfirmation, +) + + +class SchedulerRestfulModelCollection(RestfulModelCollection): + def __init__(self, api): + # Make a copy of the API as we need to change the base url for Scheduler calls + scheduler_api = copy.copy(api) + scheduler_api.api_server = "https://api.schedule.nylas.com" + RestfulModelCollection.__init__(self, Scheduler, scheduler_api) + + def get_google_availability(self): + return self._execute_provider_availability("google") + + def get_office_365_availability(self): + return self._execute_provider_availability("o365") + + def get_page_slug(self, slug): + page_response = self.api._get_resource_raw( + self.model_class, slug, extra="info", path="schedule" + ).json() + return Scheduler.create(self.api, **page_response) + + def get_available_time_slots(self, slug): + response = self.api._get_resource_raw( + self.model_class, slug, extra="timeslots", path="schedule" + ).json() + return [ + SchedulerTimeSlot.create(self.api, **x) for x in response if x is not None + ] + + def book_time_slot(self, slug, timeslot): + response = self.api._post_resource( + self.model_class, slug, "timeslots", timeslot.as_json(), path="schedule" + ) + return SchedulerBookingConfirmation.create(self.api, **response) + + def cancel_booking(self, slug, edit_hash, reason): + return self.api._post_resource( + self.model_class, + slug, + "{}/cancel".format(edit_hash), + {"reason": reason}, + path="schedule", + ) + + def confirm_booking(self, slug, edit_hash): + booking_response = self.api._post_resource( + self.model_class, slug, "{}/confirm".format(edit_hash), {}, path="schedule" + ) + return SchedulerBookingConfirmation.create(self.api, **booking_response) + + def _execute_provider_availability(self, provider): + return self.api._get_resource_raw( + self.model_class, + None, + extra="availability/{}".format(provider), + path="schedule", + ).json() diff --git a/tests/conftest.py b/tests/conftest.py index 3e9fb7ca..8191bfe0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -813,6 +813,28 @@ def callback(_request): ) +@pytest.fixture +def mock_scheduler_create_response(mocked_responses, api_url, message_body): + def callback(_request): + try: + payload = json.loads(_request.body) + except ValueError: + return 400, {}, "" + + payload["id"] = "cv4ei7syx10uvsxbs21ccsezf" + return 200, {}, json.dumps(payload) + + mocked_responses.add_callback( + responses.POST, "https://api.schedule.nylas.com/manage/pages", callback=callback + ) + + mocked_responses.add( + responses.PUT, + "https://api.schedule.nylas.com/manage/pages/cv4ei7syx10uvsxbs21ccsezf", + body=json.dumps(message_body), + ) + + @pytest.fixture def mock_event_create_response_with_limits(mocked_responses, api_url, message_body): def callback(request): @@ -1305,6 +1327,248 @@ def list_callback(request): ) +@pytest.fixture +def mock_schedulers(mocked_responses, api_url): + scheduler_list = [ + { + "app_client_id": "test-client-id", + "app_organization_id": 12345, + "config": { + "appearance": { + "color": "#0068D3", + "company_name": "", + "logo": "", + "show_autoschedule": "true", + "show_nylas_branding": "false", + "show_timezone_options": "true", + "show_week_view": "true", + "submit_text": "Submit", + }, + "locale": "en", + "reminders": [], + "timezone": "America/Los_Angeles", + }, + "created_at": "2021-10-22", + "edit_token": "test-edit-token-1", + "id": 90210, + "modified_at": "2021-10-22", + "name": "test-1", + "slug": "test1", + }, + { + "app_client_id": "test-client-id", + "app_organization_id": 12345, + "config": { + "calendar_ids": { + "test-calendar-id": { + "availability": ["availability-id"], + "booking": "booking-id", + } + }, + "event": { + "capacity": -1, + "duration": 45, + "location": "Location TBD", + "title": "test-event", + }, + "locale": "en", + "reminders": [], + "timezone": "America/Los_Angeles", + }, + "created_at": "2021-10-22", + "edit_token": "test-edit-token-2", + "id": 90211, + "modified_at": "2021-10-22", + "name": "test-2", + "slug": "test2", + }, + ] + + def list_callback(arg=None): + return 200, {}, json.dumps(scheduler_list) + + def return_one_callback(arg=None): + return 200, {}, json.dumps(scheduler_list[0]) + + info_endpoint = re.compile("https://api.schedule.nylas.com/schedule/.*/info") + + mocked_responses.add_callback( + responses.GET, + "https://api.schedule.nylas.com/manage/pages", + content_type="application/json", + callback=list_callback, + ) + + mocked_responses.add_callback( + responses.GET, + info_endpoint, + content_type="application/json", + callback=return_one_callback, + ) + + +@pytest.fixture +def mock_scheduler_get_available_calendars(mocked_responses, api_url): + calendars = [ + { + "calendars": [ + {"id": "calendar-id", "name": "Emailed events", "read_only": "true"}, + ], + "email": "swag@nylas.com", + "id": "scheduler-id", + "name": "Python Tester", + } + ] + + def list_callback(arg=None): + return 200, {}, json.dumps(calendars) + + calendars_url = "https://api.schedule.nylas.com/manage/pages/{id}/calendars".format( + id="cv4ei7syx10uvsxbs21ccsezf" + ) + + mocked_responses.add_callback( + responses.GET, + calendars_url, + content_type="application/json", + callback=list_callback, + ) + + +@pytest.fixture +def mock_scheduler_upload_image(mocked_responses, api_url): + upload = { + "filename": "test.png", + "originalFilename": "test.png", + "publicUrl": "https://public.nylas.com/test.png", + "signedUrl": "https://signed.nylas.com/test.png", + } + + def list_callback(arg=None): + return 200, {}, json.dumps(upload) + + calendars_url = ( + "https://api.schedule.nylas.com/manage/pages/{id}/upload-image".format( + id="cv4ei7syx10uvsxbs21ccsezf" + ) + ) + + mocked_responses.add_callback( + responses.PUT, + calendars_url, + content_type="application/json", + callback=list_callback, + ) + + +@pytest.fixture +def mock_scheduler_provider_availability(mocked_responses, api_url): + response = { + "busy": [ + { + "end": 1636731958, + "start": 1636728347, + }, + ], + "email": "test@example.com", + "name": "John Doe", + } + + def callback(arg=None): + return 200, {}, json.dumps(response) + + provider_url = re.compile( + "https://api.schedule.nylas.com/schedule/availability/(google|o365)" + ) + + mocked_responses.add_callback( + responses.GET, + provider_url, + callback=callback, + ) + + +@pytest.fixture +def mock_scheduler_timeslots(mocked_responses, api_url): + scheduler_time_slots = [ + { + "account_id": "test-account-id", + "calendar_id": "test-calendar-id", + "emails": ["test@example.com"], + "end": 1636731958, + "host_name": "www.hostname.com", + "start": 1636728347, + }, + ] + + booking_confirmation = { + "account_id": "test-account-id", + "additional_field_values": { + "test": "yes", + }, + "calendar_event_id": "test-event-id", + "calendar_id": "test-calendar-id", + "edit_hash": "test-edit-hash", + "end_time": 1636731958, + "id": 123, + "is_confirmed": False, + "location": "Earth", + "recipient_email": "recipient@example.com", + "recipient_locale": "en_US", + "recipient_name": "Recipient Doe", + "recipient_tz": "America/New_York", + "start_time": 1636728347, + "title": "Test Booking", + } + + cancel_payload = { + "success": True, + } + + def list_timeslots(arg=None): + return 200, {}, json.dumps(scheduler_time_slots) + + def book_timeslot(arg=None): + return 200, {}, json.dumps(booking_confirmation) + + def confirm_booking(arg=None): + booking_confirmation["is_confirmed"] = True + return 200, {}, json.dumps(booking_confirmation) + + def cancel_booking(arg=None): + return 200, {}, json.dumps(cancel_payload) + + timeslots_url = re.compile("https://api.schedule.nylas.com/schedule/.*/timeslots") + + confirm_url = re.compile("https://api.schedule.nylas.com/schedule/.*/.*/confirm") + + cancel_url = re.compile("https://api.schedule.nylas.com/schedule/.*/.*/cancel") + + mocked_responses.add_callback( + responses.GET, + timeslots_url, + callback=list_timeslots, + ) + + mocked_responses.add_callback( + responses.POST, + timeslots_url, + callback=book_timeslot, + ) + + mocked_responses.add_callback( + responses.POST, + confirm_url, + callback=confirm_booking, + ) + + mocked_responses.add_callback( + responses.POST, + cancel_url, + callback=cancel_booking, + ) + + @pytest.fixture def mock_components(mocked_responses, api_url): components = [ diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py new file mode 100644 index 00000000..77be03ff --- /dev/null +++ b/tests/test_scheduler.py @@ -0,0 +1,287 @@ +import json +from datetime import datetime +import pytest +import responses + +from nylas.client.restful_models import Scheduler, Calendar +from nylas.client.scheduler_models import SchedulerTimeSlot, SchedulerBookingRequest + + +def blank_scheduler_page(api_client): + scheduler = api_client.scheduler.create() + scheduler.access_tokens = ["test-access-token"] + scheduler.name = "Python SDK Example" + scheduler.slug = "py_example_1" + return scheduler + + +def test_scheduler_endpoint(api_client): + scheduler = api_client.scheduler + assert scheduler.api.api_server == "https://api.schedule.nylas.com" + + +@pytest.mark.usefixtures("mock_schedulers") +def test_scheduler(api_client): + scheduler = api_client.scheduler.first() + assert isinstance(scheduler, Scheduler) + assert scheduler.id == 90210 + assert scheduler.app_client_id == "test-client-id" + assert scheduler.app_organization_id == 12345 + assert len(scheduler.config) == 4 + assert isinstance(scheduler.config, dict) + assert scheduler.config["locale"] == "en" + assert len(scheduler.config["reminders"]) == 0 + assert scheduler.config["timezone"] == "America/Los_Angeles" + assert scheduler.edit_token == "test-edit-token-1" + assert scheduler.name == "test-1" + assert scheduler.slug == "test1" + assert scheduler.created_at == datetime.strptime("2021-10-22", "%Y-%m-%d").date() + assert scheduler.modified_at == datetime.strptime("2021-10-22", "%Y-%m-%d").date() + + +@pytest.mark.usefixtures("mock_scheduler_create_response") +def test_create_scheduler(api_client): + scheduler = blank_scheduler_page(api_client) + scheduler.save() + assert scheduler.id == "cv4ei7syx10uvsxbs21ccsezf" + + +@pytest.mark.usefixtures("mock_scheduler_create_response") +def test_modify_scheduler(api_client): + scheduler = blank_scheduler_page(api_client) + scheduler.id = "cv4ei7syx10uvsxbs21ccsezf" + scheduler.name = "Updated Name" + scheduler.save() + assert scheduler.name == "Updated Name" + + +@pytest.mark.usefixtures("mock_scheduler_get_available_calendars") +def test_scheduler_get_available_calendars(api_client): + scheduler = blank_scheduler_page(api_client) + scheduler.id = "cv4ei7syx10uvsxbs21ccsezf" + calendars = scheduler.get_available_calendars() + assert len(calendars) == 1 + calendar = calendars[0] + assert len(calendar["calendars"]) == 1 + assert isinstance(calendar["calendars"][0], Calendar) + assert calendar["calendars"][0].id == "calendar-id" + assert calendar["calendars"][0].name == "Emailed events" + assert calendar["calendars"][0].read_only + assert calendar["email"] == "swag@nylas.com" + assert calendar["id"] == "scheduler-id" + assert calendar["name"] == "Python Tester" + + +@pytest.mark.usefixtures("mock_scheduler_get_available_calendars") +def test_scheduler_get_available_calendars_no_id_throws_error(api_client): + scheduler = blank_scheduler_page(api_client) + with pytest.raises(ValueError): + scheduler.get_available_calendars() + + +@pytest.mark.usefixtures("mock_scheduler_upload_image") +def test_scheduler_upload_image(api_client): + scheduler = blank_scheduler_page(api_client) + scheduler.id = "cv4ei7syx10uvsxbs21ccsezf" + upload = scheduler.upload_image("image/png", "test.png") + assert upload["filename"] == "test.png" + assert upload["originalFilename"] == "test.png" + assert upload["publicUrl"] == "https://public.nylas.com/test.png" + assert upload["signedUrl"] == "https://signed.nylas.com/test.png" + + +@pytest.mark.usefixtures("mock_scheduler_get_available_calendars") +def test_scheduler_get_available_calendars_no_id_throws_error(api_client): + scheduler = blank_scheduler_page(api_client) + with pytest.raises(ValueError): + scheduler.upload_image("image/png", "test.png") + + +@pytest.mark.usefixtures("mock_scheduler_provider_availability") +def test_scheduler_get_google_availability(mocked_responses, api_client): + api_client.scheduler.get_google_availability() + request = mocked_responses.calls[0].request + assert request.url == "https://api.schedule.nylas.com/schedule/availability/google" + assert request.method == responses.GET + + +@pytest.mark.usefixtures("mock_scheduler_provider_availability") +def test_scheduler_get_o365_availability(mocked_responses, api_client): + api_client.scheduler.get_office_365_availability() + request = mocked_responses.calls[0].request + assert request.url == "https://api.schedule.nylas.com/schedule/availability/o365" + assert request.method == responses.GET + + +@pytest.mark.usefixtures("mock_schedulers") +def test_scheduler_get_page_slug(mocked_responses, api_client): + scheduler = api_client.scheduler.get_page_slug("test1") + request = mocked_responses.calls[0].request + assert request.url == "https://api.schedule.nylas.com/schedule/test1/info" + assert request.method == responses.GET + assert isinstance(scheduler, Scheduler) + assert scheduler.id == 90210 + assert scheduler.app_client_id == "test-client-id" + assert scheduler.app_organization_id == 12345 + assert len(scheduler.config) == 4 + assert isinstance(scheduler.config, dict) + assert scheduler.config["locale"] == "en" + assert len(scheduler.config["reminders"]) == 0 + assert scheduler.config["timezone"] == "America/Los_Angeles" + assert scheduler.edit_token == "test-edit-token-1" + assert scheduler.name == "test-1" + assert scheduler.slug == "test1" + assert scheduler.created_at == datetime.strptime("2021-10-22", "%Y-%m-%d").date() + assert scheduler.modified_at == datetime.strptime("2021-10-22", "%Y-%m-%d").date() + + +@pytest.mark.usefixtures("mock_scheduler_timeslots") +def test_scheduler_get_available_time_slots(mocked_responses, api_client): + scheduler = blank_scheduler_page(api_client) + timeslots = api_client.scheduler.get_available_time_slots(scheduler.slug) + request = mocked_responses.calls[0].request + assert ( + request.url == "https://api.schedule.nylas.com/schedule/py_example_1/timeslots" + ) + assert request.method == responses.GET + assert len(timeslots) == 1 + assert timeslots[0] + assert timeslots[0].account_id == "test-account-id" + assert timeslots[0].calendar_id == "test-calendar-id" + assert timeslots[0].emails[0] == "test@example.com" + assert timeslots[0].host_name == "www.hostname.com" + assert timeslots[0].end == datetime.utcfromtimestamp(1636731958) + assert timeslots[0].start == datetime.utcfromtimestamp(1636728347) + + +@pytest.mark.usefixtures("mock_scheduler_timeslots") +def test_scheduler_get_available_time_slots(mocked_responses, api_client): + scheduler = blank_scheduler_page(api_client) + timeslots = api_client.scheduler.get_available_time_slots(scheduler.slug) + request = mocked_responses.calls[0].request + assert ( + request.url == "https://api.schedule.nylas.com/schedule/py_example_1/timeslots" + ) + assert request.method == responses.GET + assert len(timeslots) == 1 + assert timeslots[0] + assert timeslots[0].account_id == "test-account-id" + assert timeslots[0].calendar_id == "test-calendar-id" + assert timeslots[0].emails[0] == "test@example.com" + assert timeslots[0].host_name == "www.hostname.com" + assert timeslots[0].end == datetime.utcfromtimestamp(1636731958) + assert timeslots[0].start == datetime.utcfromtimestamp(1636728347) + + +@pytest.mark.usefixtures("mock_scheduler_timeslots") +def test_scheduler_book_time_slot(mocked_responses, api_client): + scheduler = blank_scheduler_page(api_client) + slot = SchedulerTimeSlot.create(api_client) + slot.account_id = "test-account-id" + slot.calendar_id = "test-calendar-id" + slot.emails = ["recipient@example.com"] + slot.host_name = "www.nylas.com" + slot.start = datetime.utcfromtimestamp(1636728347) + slot.end = datetime.utcfromtimestamp(1636731958) + timeslot_to_book = SchedulerBookingRequest.create(api_client) + timeslot_to_book.additional_values = { + "test": "yes", + } + timeslot_to_book.email = "recipient@example.com" + timeslot_to_book.locale = "en_US" + timeslot_to_book.name = "Recipient Doe" + timeslot_to_book.timezone = "America/New_York" + timeslot_to_book.slot = slot + booking_response = api_client.scheduler.book_time_slot( + scheduler.slug, timeslot_to_book + ) + request = mocked_responses.calls[0].request + assert ( + request.url == "https://api.schedule.nylas.com/schedule/py_example_1/timeslots" + ) + assert request.method == responses.POST + assert json.loads(request.body) == { + "additional_emails": [], + "additional_values": { + "test": "yes", + }, + "email": "recipient@example.com", + "locale": "en_US", + "name": "Recipient Doe", + "timezone": "America/New_York", + "slot": { + "account_id": "test-account-id", + "calendar_id": "test-calendar-id", + "emails": ["recipient@example.com"], + "host_name": "www.nylas.com", + "start": 1636728347, + "end": 1636731958, + }, + } + assert booking_response.account_id == "test-account-id" + assert booking_response.calendar_id == "test-calendar-id" + assert booking_response.additional_field_values == { + "test": "yes", + } + assert booking_response.calendar_event_id == "test-event-id" + assert booking_response.calendar_id == "test-calendar-id" + assert booking_response.calendar_event_id == "test-event-id" + assert booking_response.edit_hash == "test-edit-hash" + assert booking_response.id == 123 + assert booking_response.is_confirmed is False + assert booking_response.location == "Earth" + assert booking_response.title == "Test Booking" + assert booking_response.recipient_email == "recipient@example.com" + assert booking_response.recipient_locale == "en_US" + assert booking_response.recipient_name == "Recipient Doe" + assert booking_response.recipient_tz == "America/New_York" + assert booking_response.end_time == datetime.utcfromtimestamp(1636731958) + assert booking_response.start_time == datetime.utcfromtimestamp(1636728347) + + +@pytest.mark.usefixtures("mock_scheduler_timeslots") +def test_scheduler_confirm_booking(mocked_responses, api_client): + scheduler = blank_scheduler_page(api_client) + booking_confirmation = api_client.scheduler.confirm_booking( + scheduler.slug, "test-edit-hash" + ) + request = mocked_responses.calls[0].request + assert ( + request.url + == "https://api.schedule.nylas.com/schedule/py_example_1/test-edit-hash/confirm" + ) + assert request.method == responses.POST + assert booking_confirmation.account_id == "test-account-id" + assert booking_confirmation.calendar_id == "test-calendar-id" + assert booking_confirmation.additional_field_values == { + "test": "yes", + } + assert booking_confirmation.calendar_event_id == "test-event-id" + assert booking_confirmation.calendar_id == "test-calendar-id" + assert booking_confirmation.calendar_event_id == "test-event-id" + assert booking_confirmation.edit_hash == "test-edit-hash" + assert booking_confirmation.id == 123 + assert booking_confirmation.is_confirmed is True + assert booking_confirmation.location == "Earth" + assert booking_confirmation.title == "Test Booking" + assert booking_confirmation.recipient_email == "recipient@example.com" + assert booking_confirmation.recipient_locale == "en_US" + assert booking_confirmation.recipient_name == "Recipient Doe" + assert booking_confirmation.recipient_tz == "America/New_York" + assert booking_confirmation.end_time == datetime.utcfromtimestamp(1636731958) + assert booking_confirmation.start_time == datetime.utcfromtimestamp(1636728347) + + +@pytest.mark.usefixtures("mock_scheduler_timeslots") +def test_scheduler_cancel_booking(mocked_responses, api_client): + scheduler = blank_scheduler_page(api_client) + timeslots = api_client.scheduler.cancel_booking( + scheduler.slug, "test-edit-hash", "It was a test." + ) + request = mocked_responses.calls[0].request + assert ( + request.url + == "https://api.schedule.nylas.com/schedule/py_example_1/test-edit-hash/cancel" + ) + assert request.method == responses.POST + assert json.loads(request.body) == {"reason": "It was a test."}