diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1eb9d0d0..1d73a52d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.6.0 +current_version = 3.7.0 commit = True tag = False diff --git a/CHANGES.md b/CHANGES.md index a047b2d3..c8a6fce5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +# 3.7.0 +- Adding support for the [Vonage Meetings API](https://developer.vonage.com/en/meetings/overview) +- Adding partial support for the [Vonage Proactive Connect API](https://developer.vonage.com/en/proactive-connect/overview) - supporting API methods relating to `lists`, `items` and `events` +- Returning a more descriptive (non-internal) error message if invalid values are provided for `application_id` and/or `private_key` when instantiating a Vonage client object + # 3.6.0 - Adding support for the [Vonage Subaccounts API](https://developer.vonage.com/en/account/subaccounts/overview) diff --git a/README.md b/README.md index 39d4c442..38586894 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup]. - [Verify V1 API](#verify-v1-api) - [Meetings API](#meetings-api) - [Number Insight API](#number-insight-api) +- [Proactive Connect API](#proactive-connect-api) - [Account API](#account-api) - [Subaccounts API](#subaccounts-api) - [Number Management API](#number-management-api) @@ -744,6 +745,94 @@ client.number_insight.get_advanced_number_insight(number='447700900000') Docs: [https://developer.nexmo.com/api/number-insight#getNumberInsightAdvanced](https://developer.nexmo.com/api/number-insight?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library#getNumberInsightAdvanced) +## Proactive Connect API + +Full documentation for the [Proactive Connect API](https://developer.vonage.com/en/proactive-connect/overview) is available here. + +These methods help you manage lists of contacts when using the API: + +### Find all lists +```python +client.proactive_connect.list_all_lists() +``` + +### Create a list +Lists can be created manually or imported from Salesforce. + +```python +params = {'name': 'my list', 'description': 'my description', 'tags': ['vip']} +client.proactive_connect.create_list(params) +``` + +### Get a list +```python +client.proactive_connect.get_list(LIST_ID) +``` + +### Update a list +```python +params = {'name': 'my list', 'tags': ['sport', 'football']} +client.proactive_connect.update_list(LIST_ID, params) +``` + +### Delete a list +```python +client.proactive_connect.delete_list(LIST_ID) +``` + +### Sync a list from an external datasource +```python +params = {'name': 'my list', 'tags': ['sport', 'football']} +client.proactive_connect.sync_list_from_datasource(LIST_ID) +``` + +These methods help you work with individual items in a list: +### Find all items in a list +```python +client.proactive_connect.list_all_items(LIST_ID) +``` + +### Create a new list item +```python +data = {'firstName': 'John', 'lastName': 'Doe', 'phone': '123456789101'} +client.proactive_connect.create_item(LIST_ID, data) +``` + +### Get a list item +```python +client.proactive_connect.get_item(LIST_ID, ITEM_ID) +``` + +### Update a list item +```python +data = {'firstName': 'John', 'lastName': 'Doe', 'phone': '447007000000'} +client.proactive_connect.update_item(LIST_ID, ITEM_ID, data) +``` + +### Delete a list item +```python +client.proactive_connect.delete_item(LIST_ID, ITEM_ID) +``` + +### Download all items in a list as a .csv file +```python +FILE_PATH = 'path/to/the/downloaded/file/location' +client.proactive_connect.download_list_items(LIST_ID, FILE_PATH) +``` + +### Upload items from a .csv file into a list +```python +FILE_PATH = 'path/to/the/file/to/upload/location' +client.proactive_connect.upload_list_items(LIST_ID, FILE_PATH) +``` + +This method helps you work with events emitted by the Proactive Connect API when in use: + +### List all events +```python +client.proactive_connect.list_events() +``` + ## Account API ### Get your account balance diff --git a/setup.py b/setup.py index 151a3e59..338684d3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name="vonage", - version="3.6.0", + version="3.7.0", description="Vonage Server SDK for Python", long_description=long_description, long_description_content_type="text/markdown", diff --git a/src/vonage/__init__.py b/src/vonage/__init__.py index a5a5f623..dba80a04 100644 --- a/src/vonage/__init__.py +++ b/src/vonage/__init__.py @@ -1,4 +1,4 @@ from .client import * from .ncco_builder.ncco import * -__version__ = "3.6.0" +__version__ = "3.7.0" diff --git a/src/vonage/client.py b/src/vonage/client.py index b800c6b6..72f0d4aa 100644 --- a/src/vonage/client.py +++ b/src/vonage/client.py @@ -8,6 +8,7 @@ from .messages import Messages from .number_insight import NumberInsight from .number_management import Numbers +from .proactive_connect import ProactiveConnect from .redact import Redact from .short_codes import ShortCodes from .sms import Sms @@ -102,6 +103,7 @@ def __init__( self._host = "rest.nexmo.com" self._api_host = "api.nexmo.com" self._meetings_api_host = "api-eu.vonage.com/beta/meetings" + self._proactive_connect_host = "api-eu.vonage.com" user_agent = f"vonage-python/{vonage.__version__} python/{python_version()}" @@ -116,6 +118,7 @@ def __init__( self.messages = Messages(self) self.number_insight = NumberInsight(self) self.numbers = Numbers(self) + self.proactive_connect = ProactiveConnect(self) self.short_codes = ShortCodes(self) self.sms = Sms(self) self.subaccounts = Subaccounts(self) @@ -152,6 +155,12 @@ def meetings_api_host(self, value=None): else: self._meetings_api_host = value + def proactive_connect_host(self, value=None): + if value is None: + return self._proactive_connect_host + else: + self._proactive_connect_host = value + def auth(self, params=None, **kwargs): self._jwt_claims = params or kwargs @@ -198,12 +207,25 @@ def get(self, host, request_uri, params=None, auth_type=None): f'Invalid authentication type. Must be one of "jwt", "header" or "params".' ) - logger.debug(f"GET to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}") + logger.debug( + f"GET to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}" + ) return self.parse( - host, self.session.get(uri, params=params, headers=self._request_headers, timeout=self.timeout) + host, + self.session.get( + uri, params=params, headers=self._request_headers, timeout=self.timeout + ), ) - def post(self, host, request_uri, params, auth_type=None, body_is_json=True, supports_signature_auth=False): + def post( + self, + host, + request_uri, + params, + auth_type=None, + body_is_json=True, + supports_signature_auth=False, + ): """ Low-level method to make a post request to an API server. This method automatically adds authentication, picking the first applicable authentication method from the following: @@ -229,14 +251,22 @@ def post(self, host, request_uri, params, auth_type=None, body_is_json=True, sup f'Invalid authentication type. Must be one of "jwt", "header" or "params".' ) - logger.debug(f"POST to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}") + logger.debug( + f"POST to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}" + ) if body_is_json: return self.parse( - host, self.session.post(uri, json=params, headers=self._request_headers, timeout=self.timeout) + host, + self.session.post( + uri, json=params, headers=self._request_headers, timeout=self.timeout + ), ) else: return self.parse( - host, self.session.post(uri, data=params, headers=self._request_headers, timeout=self.timeout) + host, + self.session.post( + uri, data=params, headers=self._request_headers, timeout=self.timeout + ), ) def put(self, host, request_uri, params, auth_type=None): @@ -252,9 +282,14 @@ def put(self, host, request_uri, params, auth_type=None): f'Invalid authentication type. Must be one of "jwt", "header" or "params".' ) - logger.debug(f"PUT to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}") + logger.debug( + f"PUT to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}" + ) # All APIs that currently use put methods require a json-formatted body so don't need to check this - return self.parse(host, self.session.put(uri, json=params, headers=self._request_headers, timeout=self.timeout)) + return self.parse( + host, + self.session.put(uri, json=params, headers=self._request_headers, timeout=self.timeout), + ) def patch(self, host, request_uri, params, auth_type=None): uri = f"https://{host}{request_uri}" @@ -267,7 +302,9 @@ def patch(self, host, request_uri, params, auth_type=None): else: raise InvalidAuthenticationTypeError(f"""Invalid authentication type.""") - logger.debug(f"PATCH to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}") + logger.debug( + f"PATCH to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}" + ) # Only newer APIs (that expect json-bodies) currently use this method, so we will always send a json-formatted body return self.parse(host, self.session.patch(uri, json=params, headers=self._request_headers)) @@ -288,7 +325,10 @@ def delete(self, host, request_uri, params=None, auth_type=None): if params is not None: logger.debug(f"DELETE call has params {repr(params)}") return self.parse( - host, self.session.delete(uri, headers=self._request_headers, timeout=self.timeout, params=params) + host, + self.session.delete( + uri, headers=self._request_headers, timeout=self.timeout, params=params + ), ) def parse(self, host, response: Response): @@ -322,25 +362,33 @@ def parse(self, host, response: Response): title = error_data["title"] detail = error_data["detail"] type = error_data["type"] - message = f"{title}: {detail} ({type})" + message = f"{title}: {detail} ({type}){self._add_individual_errors(error_data)}" elif 'status' in error_data and 'message' in error_data and 'name' in error_data: - message = f'Status Code {error_data["status"]}: {error_data["name"]}: {error_data["message"]}' - if 'errors' in error_data: - for error in error_data['errors']: - message += f', error: {error}' + message = ( + f'Status Code {error_data["status"]}: {error_data["name"]}: {error_data["message"]}' + f'{self._add_individual_errors(error_data)}' + ) else: message = error_data except JSONDecodeError: pass raise ClientError(message) + elif 500 <= response.status_code < 600: logger.warning(f"Server error: {response.status_code} {repr(response.content)}") message = f"{response.status_code} response from {host}" raise ServerError(message) + def _add_individual_errors(self, error_data): + message = '' + if 'errors' in error_data: + for error in error_data["errors"]: + message += f"\nError: {error}" + return message + def _create_jwt_auth_string(self): return b"Bearer " + self._generate_application_jwt() - + def _generate_application_jwt(self): try: return self._jwt_client.generate_application_jwt(self._jwt_claims) diff --git a/src/vonage/errors.py b/src/vonage/errors.py index 1c3d11fb..f3f0f8da 100644 --- a/src/vonage/errors.py +++ b/src/vonage/errors.py @@ -46,3 +46,7 @@ class Verify2Error(ClientError): class SubaccountsError(ClientError): """An error relating to the Subaccounts API.""" + + +class ProactiveConnectError(ClientError): + """An error relating to the Proactive Connect API.""" diff --git a/src/vonage/proactive_connect.py b/src/vonage/proactive_connect.py new file mode 100644 index 00000000..a9197a17 --- /dev/null +++ b/src/vonage/proactive_connect.py @@ -0,0 +1,187 @@ +from .errors import ProactiveConnectError + +import requests +import logging +from typing import List + +logger = logging.getLogger("vonage") + + +class ProactiveConnect: + def __init__(self, client): + self._client = client + self._auth_type = 'jwt' + + def list_all_lists(self, page: int = None, page_size: int = None): + params = self._check_pagination_params(page, page_size) + return self._client.get( + self._client.proactive_connect_host(), + '/v0.1/bulk/lists', + params, + auth_type=self._auth_type, + ) + + def create_list(self, params: dict): + self._validate_list_params(params) + return self._client.post( + self._client.proactive_connect_host(), + '/v0.1/bulk/lists', + params, + auth_type=self._auth_type, + ) + + def get_list(self, list_id: str): + return self._client.get( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}', + auth_type=self._auth_type, + ) + + def update_list(self, list_id: str, params: dict): + self._validate_list_params(params) + return self._client.put( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}', + params, + auth_type=self._auth_type, + ) + + def delete_list(self, list_id: str): + return self._client.delete( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}', + auth_type=self._auth_type, + ) + + def clear_list(self, list_id: str): + return self._client.post( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/clear', + params=None, + auth_type=self._auth_type, + ) + + def sync_list_from_datasource(self, list_id: str): + return self._client.post( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/fetch', + params=None, + auth_type=self._auth_type, + ) + + def list_all_items(self, list_id: str, page: int = None, page_size: int = None): + params = self._check_pagination_params(page, page_size) + return self._client.get( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/items', + params, + auth_type=self._auth_type, + ) + + def create_item(self, list_id: str, data: dict): + params = {'data': data} + return self._client.post( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/items', + params, + auth_type=self._auth_type, + ) + + def get_item(self, list_id: str, item_id: str): + return self._client.get( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/items/{item_id}', + auth_type=self._auth_type, + ) + + def update_item(self, list_id: str, item_id: str, data: dict): + params = {'data': data} + return self._client.put( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/items/{item_id}', + params, + auth_type=self._auth_type, + ) + + def delete_item(self, list_id: str, item_id: str): + return self._client.delete( + self._client.proactive_connect_host(), + f'/v0.1/bulk/lists/{list_id}/items/{item_id}', + auth_type=self._auth_type, + ) + + def download_list_items(self, list_id: str, file_path: str) -> List[dict]: + uri = f'https://{self._client.proactive_connect_host()}/v0.1/bulk/lists/{list_id}/items/download' + logger.debug( + f'GET request with Proactive Connect to {repr(uri)}, downloading items from list {list_id} to file {file_path}' + ) + headers = {**self._client.headers, 'Authorization': self._client._create_jwt_auth_string()} + response = requests.get( + uri, + headers=headers, + ) + if 200 <= response.status_code < 300: + with open(file_path, 'wb') as file: + file.write(response.content) + else: + return self._client.parse(self._client.proactive_connect_host(), response) + + def upload_list_items(self, list_id: str, file_path: str): + uri = f'https://{self._client.proactive_connect_host()}/v0.1/bulk/lists/{list_id}/items/import' + with open(file_path, 'rb') as csv_file: + logger.debug( + f'POST request with Proactive Connect uploading {file_path} to {repr(uri)}' + ) + headers = { + **self._client.headers, + 'Authorization': self._client._create_jwt_auth_string(), + } + response = requests.post( + uri, + headers=headers, + files={'file': ('list_items.csv', csv_file, 'text/csv')}, + ) + return self._client.parse(self._client.proactive_connect_host(), response) + + def list_events(self, page: int = None, page_size: int = None): + params = self._check_pagination_params(page, page_size) + return self._client.get( + self._client.proactive_connect_host(), + '/v0.1/bulk/events', + params, + auth_type=self._auth_type, + ) + + def _check_pagination_params(self, page: int = None, page_size: int = None) -> dict: + params = {} + if page is not None: + if type(page) == int and page > 0: + params['page'] = page + elif page <= 0: + raise ProactiveConnectError('"page" must be an int > 0.') + if page_size is not None: + if type(page_size) == int and page_size > 0: + params['page_size'] = page_size + elif page_size and page_size <= 0: + raise ProactiveConnectError('"page_size" must be an int > 0.') + return params + + def _validate_list_params(self, params: dict): + if 'name' not in params: + raise ProactiveConnectError('You must supply a name for the new list.') + if ( + 'datasource' in params + and 'type' in params['datasource'] + and params['datasource']['type'] == 'salesforce' + ): + self._check_salesforce_params_correct(params['datasource']) + + def _check_salesforce_params_correct(self, datasource): + if 'integration_id' not in datasource or 'soql' not in datasource: + raise ProactiveConnectError( + 'You must supply a value for "integration_id" and "soql" when creating a list with Salesforce.' + ) + if type(datasource['integration_id']) is not str or type(datasource['soql']) is not str: + raise ProactiveConnectError( + 'You must supply values for "integration_id" and "soql" as strings.' + ) diff --git a/tests/conftest.py b/tests/conftest.py index 6e3a0790..0290bc72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -132,3 +132,10 @@ def meetings(client): import vonage return vonage.Meetings(client) + + +@pytest.fixture +def proc(client): + import vonage + + return vonage.ProactiveConnect(client) diff --git a/tests/data/null.json b/tests/data/null.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/proactive_connect/create_list_400.json b/tests/data/proactive_connect/create_list_400.json new file mode 100644 index 00000000..307817dd --- /dev/null +++ b/tests/data/proactive_connect/create_list_400.json @@ -0,0 +1,10 @@ +{ + "type": "https://developer.vonage.com/en/api-errors", + "title": "Request data did not validate", + "detail": "Bad Request", + "instance": "b6740287-41ad-41de-b950-f4e2d54cee86", + "errors": [ + "name must be longer than or equal to 1 and shorter than or equal to 255 characters", + "name must be a string" + ] +} \ No newline at end of file diff --git a/tests/data/proactive_connect/create_list_basic.json b/tests/data/proactive_connect/create_list_basic.json new file mode 100644 index 00000000..ea3e77c2 --- /dev/null +++ b/tests/data/proactive_connect/create_list_basic.json @@ -0,0 +1,16 @@ +{ + "items_count": 0, + "datasource": { + "type": "manual" + }, + "id": "6994fd17-7691-4463-be16-172ab1430d97", + "sync_status": { + "value": "configured", + "metadata_modified": false, + "data_modified": false, + "dirty": false + }, + "name": "my_list", + "created_at": "2023-04-28T13:42:49.031Z", + "updated_at": "2023-04-28T13:42:49.031Z" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/create_list_manual.json b/tests/data/proactive_connect/create_list_manual.json new file mode 100644 index 00000000..1241af8e --- /dev/null +++ b/tests/data/proactive_connect/create_list_manual.json @@ -0,0 +1,28 @@ +{ + "items_count": 0, + "datasource": { + "type": "manual" + }, + "id": "9508e7b8-fe99-4fdf-b022-65d7e461db2d", + "sync_status": { + "value": "configured", + "metadata_modified": false, + "data_modified": false, + "dirty": false + }, + "name": "my_list", + "description": "my description", + "tags": [ + "vip", + "sport" + ], + "attributes": [ + { + "key": false, + "name": "phone_number", + "alias": "phone" + } + ], + "created_at": "2023-04-28T13:56:12.920Z", + "updated_at": "2023-04-28T13:56:12.920Z" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/create_list_salesforce.json b/tests/data/proactive_connect/create_list_salesforce.json new file mode 100644 index 00000000..c7729f78 --- /dev/null +++ b/tests/data/proactive_connect/create_list_salesforce.json @@ -0,0 +1,30 @@ +{ + "items_count": 0, + "datasource": { + "type": "salesforce", + "integration_id": "salesforce_credentials", + "soql": "select Id, LastName, FirstName, Phone, Email FROM Contact" + }, + "id": "246d17c4-79e6-4a25-8b4e-b777a83f6c30", + "sync_status": { + "value": "configured", + "metadata_modified": true, + "data_modified": true, + "dirty": true + }, + "name": "my_salesforce_list", + "description": "my salesforce description", + "tags": [ + "vip", + "sport" + ], + "attributes": [ + { + "key": false, + "name": "phone_number", + "alias": "phone" + } + ], + "created_at": "2023-04-28T14:16:49.375Z", + "updated_at": "2023-04-28T14:16:49.375Z" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/csv_to_upload.csv b/tests/data/proactive_connect/csv_to_upload.csv new file mode 100644 index 00000000..06cbdfbb --- /dev/null +++ b/tests/data/proactive_connect/csv_to_upload.csv @@ -0,0 +1,4 @@ +user,phone +alice,1234 +bob,5678 +charlie,9012 diff --git a/tests/data/proactive_connect/fetch_list_400.json b/tests/data/proactive_connect/fetch_list_400.json new file mode 100644 index 00000000..7e68f7f6 --- /dev/null +++ b/tests/data/proactive_connect/fetch_list_400.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.vonage.com/en/api-errors", + "title": "Request data did not validate", + "detail": "Cannot Fetch a manual list", + "instance": "4c34affd-df25-4bdc-b7c0-30076d3df003" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/get_list.json b/tests/data/proactive_connect/get_list.json new file mode 100644 index 00000000..2920e6f9 --- /dev/null +++ b/tests/data/proactive_connect/get_list.json @@ -0,0 +1,28 @@ +{ + "items_count": 0, + "datasource": { + "type": "manual" + }, + "id": "9508e7b8-fe99-4fdf-b022-65d7e461db2d", + "created_at": "2023-04-28T13:56:12.920Z", + "updated_at": "2023-04-28T13:56:12.920Z", + "name": "my_list", + "description": "my description", + "tags": [ + "vip", + "sport" + ], + "attributes": [ + { + "key": false, + "name": "phone_number", + "alias": "phone" + } + ], + "sync_status": { + "value": "configured", + "metadata_modified": false, + "data_modified": false, + "dirty": false + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/item.json b/tests/data/proactive_connect/item.json new file mode 100644 index 00000000..5c5ecf96 --- /dev/null +++ b/tests/data/proactive_connect/item.json @@ -0,0 +1,11 @@ +{ + "id": "d91c39ed-7c34-4803-a139-34bb4b7c6d53", + "list_id": "246d17c4-79e6-4a25-8b4e-b777a83f6c30", + "data": { + "firstName": "John", + "lastName": "Doe", + "phone": "123456789101" + }, + "created_at": "2023-05-02T21:07:25.790Z", + "updated_at": "2023-05-02T21:07:25.790Z" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/item_400.json b/tests/data/proactive_connect/item_400.json new file mode 100644 index 00000000..778065b0 --- /dev/null +++ b/tests/data/proactive_connect/item_400.json @@ -0,0 +1,9 @@ +{ + "type": "https://developer.vonage.com/en/api-errors", + "title": "Request data did not validate", + "detail": "Bad Request", + "instance": "8e2dd3f1-1718-48fc-98de-53e1d289d0b4", + "errors": [ + "data must be an object" + ] +} \ No newline at end of file diff --git a/tests/data/proactive_connect/list_404.json b/tests/data/proactive_connect/list_404.json new file mode 100644 index 00000000..80ba7ad7 --- /dev/null +++ b/tests/data/proactive_connect/list_404.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.vonage.com/en/api-errors", + "title": "The requested resource does not exist", + "detail": "Not Found", + "instance": "3e661bd2-e429-4887-b0d4-8f37352ab1d3" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/list_all_items.json b/tests/data/proactive_connect/list_all_items.json new file mode 100644 index 00000000..28a6a795 --- /dev/null +++ b/tests/data/proactive_connect/list_all_items.json @@ -0,0 +1,40 @@ +{ + "total_items": 2, + "page": 1, + "page_size": 100, + "order": "asc", + "_embedded": { + "items": [ + { + "id": "04c7498c-bae9-40f9-bdcb-c4eabb0418fe", + "created_at": "2023-05-02T21:04:47.507Z", + "updated_at": "2023-05-02T21:04:47.507Z", + "list_id": "246d17c4-79e6-4a25-8b4e-b777a83f6c30", + "data": { + "test": 0, + "test2": 1 + } + }, + { + "id": "d91c39ed-7c34-4803-a139-34bb4b7c6d53", + "created_at": "2023-05-02T21:07:25.790Z", + "updated_at": "2023-05-02T21:07:25.790Z", + "list_id": "246d17c4-79e6-4a25-8b4e-b777a83f6c30", + "data": { + "phone": "123456789101", + "lastName": "Doe", + "firstName": "John" + } + } + ] + }, + "total_pages": 1, + "_links": { + "first": { + "href": "https://api-eu.vonage.com/v0.1/bulk/lists/246d17c4-79e6-4a25-8b4e-b777a83f6c30/items?page_size=100&order=asc&page=1" + }, + "self": { + "href": "https://api-eu.vonage.com/v0.1/bulk/lists/246d17c4-79e6-4a25-8b4e-b777a83f6c30/items?page_size=100&order=asc&page=1" + } + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/list_events.json b/tests/data/proactive_connect/list_events.json new file mode 100644 index 00000000..b00ef80c --- /dev/null +++ b/tests/data/proactive_connect/list_events.json @@ -0,0 +1,70 @@ +{ + "total_items": 1, + "page": 1, + "page_size": 100, + "total_pages": 1, + "_links": { + "self": { + "href": "https://api-eu.vonage.com/v0.1/bulk/events?page_size=100&page=1" + }, + "prev": { + "href": "https://api-eu.vonage.com/v0.1/bulk/events?page_size=100&page=1" + }, + "next": { + "href": "https://api-eu.vonage.com/v0.1/bulk/events?page_size=100&page=1" + }, + "first": { + "href": "https://api-eu.vonage.com/v0.1/bulk/events?page_size=100&page=1" + } + }, + "_embedded": { + "events": [ + { + "occurred_at": "2022-08-07T13:18:21.970Z", + "type": "action-call-succeeded", + "id": "e8e1eb4d-61e0-4099-8fa7-c96f1c0764ba", + "job_id": "c68e871a-c239-474d-a905-7b95f4563b7e", + "src_ctx": "et-e4ab4b75-9e7c-4f26-9328-394a5b842648", + "action_id": "26c5bbe2-113e-4201-bd93-f69e0a03d17f", + "data": { + "url": "https://postman-echo.com/post", + "args": {}, + "data": { + "from": "" + }, + "form": {}, + "json": { + "from": "" + }, + "files": {}, + "headers": { + "host": "postman-echo.com", + "user-agent": "got (https://github.com/sindresorhus/got)", + "content-type": "application/json", + "content-length": "11", + "accept-encoding": "gzip, deflate, br", + "x-amzn-trace-id": "Root=1-62efbb9e-53636b7b794accb87a3d662f", + "x-forwarded-port": "443", + "x-nexmo-trace-id": "8a6fed94-7296-4a39-9c52-348f12b4d61a", + "x-forwarded-proto": "https" + } + }, + "run_id": "7d0d4e5f-6453-4c63-87cf-f95b04377324", + "recipient_id": "14806904549" + }, + { + "occurred_at": "2022-08-07T13:18:20.289Z", + "type": "recipient-response", + "id": "8c8e9894-81be-4f6e-88d4-046b6c70ff8c", + "job_id": "c68e871a-c239-474d-a905-7b95f4563b7e", + "src_ctx": "et-e4ab4b75-9e7c-4f26-9328-394a5b842648", + "data": { + "from": "441632960411", + "text": "hello there" + }, + "run_id": "7d0d4e5f-6453-4c63-87cf-f95b04377324", + "recipient_id": "441632960758" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/list_items.csv b/tests/data/proactive_connect/list_items.csv new file mode 100644 index 00000000..0c167367 --- /dev/null +++ b/tests/data/proactive_connect/list_items.csv @@ -0,0 +1,4 @@ +"favourite_number","least_favourite_number" +0,1 +1,0 +0,0 diff --git a/tests/data/proactive_connect/list_lists.json b/tests/data/proactive_connect/list_lists.json new file mode 100644 index 00000000..c734e99e --- /dev/null +++ b/tests/data/proactive_connect/list_lists.json @@ -0,0 +1,84 @@ +{ + "page": 1, + "page_size": 100, + "total_items": 2, + "total_pages": 1, + "_links": { + "self": { + "href": "https://api-eu.vonage.com/v0.1/bulk/lists?page_size=100&page=1" + }, + "prev": { + "href": "https://api-eu.vonage.com/v0.1/bulk/lists?page_size=100&page=1" + }, + "next": { + "href": "https://api-eu.vonage.com/v0.1/bulk/lists?page_size=100&page=1" + }, + "first": { + "href": "https://api-eu.vonage.com/v0.1/bulk/lists?page_size=100&page=1" + } + }, + "_embedded": { + "lists": [ + { + "name": "Recipients for demo", + "description": "List of recipients for demo", + "tags": [ + "vip" + ], + "attributes": [ + { + "name": "firstName" + }, + { + "name": "lastName", + "key": false + }, + { + "name": "number", + "alias": "Phone", + "key": true + } + ], + "datasource": { + "type": "manual" + }, + "items_count": 1000, + "sync_status": { + "value": "configured", + "dirty": false, + "data_modified": false, + "metadata_modified": false + }, + "id": "af8a84b6-c712-4252-ac8d-6e28ac9317ce", + "created_at": "2022-06-23T13:13:16.491Z", + "updated_at": "2022-06-23T13:13:16.491Z" + }, + { + "name": "Salesforce contacts", + "description": "Salesforce contacts for campaign", + "tags": [ + "salesforce" + ], + "attributes": [ + { + "name": "Id", + "key": false + }, + { + "name": "Phone", + "key": true + }, + { + "name": "Email", + "key": false + } + ], + "datasource": { + "type": "salesforce", + "integration_id": "salesforce", + "soql": "SELECT Id, LastName, FirstName, Phone, Email, OtherCountry FROM Contact" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/not_found.json b/tests/data/proactive_connect/not_found.json new file mode 100644 index 00000000..02f8ec01 --- /dev/null +++ b/tests/data/proactive_connect/not_found.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.vonage.com/en/api-errors", + "title": "The requested resource does not exist", + "detail": "Not Found", + "instance": "04730b29-c292-4899-9419-f8cad88ec288" +} \ No newline at end of file diff --git a/tests/data/proactive_connect/update_item.json b/tests/data/proactive_connect/update_item.json new file mode 100644 index 00000000..7166b458 --- /dev/null +++ b/tests/data/proactive_connect/update_item.json @@ -0,0 +1,11 @@ +{ + "id": "d91c39ed-7c34-4803-a139-34bb4b7c6d53", + "created_at": "2023-05-02T21:07:25.790Z", + "updated_at": "2023-05-03T19:50:33.207Z", + "list_id": "246d17c4-79e6-4a25-8b4e-b777a83f6c30", + "data": { + "first_name": "John", + "last_name": "Doe", + "phone": "447007000000" + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/update_list.json b/tests/data/proactive_connect/update_list.json new file mode 100644 index 00000000..99df4c42 --- /dev/null +++ b/tests/data/proactive_connect/update_list.json @@ -0,0 +1,29 @@ +{ + "items_count": 0, + "datasource": { + "type": "manual" + }, + "id": "9508e7b8-fe99-4fdf-b022-65d7e461db2d", + "created_at": "2023-04-28T13:56:12.920Z", + "updated_at": "2023-04-28T21:39:17.825Z", + "name": "my_list", + "description": "my updated description", + "tags": [ + "vip", + "sport", + "football" + ], + "attributes": [ + { + "key": false, + "name": "phone_number", + "alias": "phone" + } + ], + "sync_status": { + "value": "configured", + "metadata_modified": false, + "data_modified": false, + "dirty": false + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/update_list_salesforce.json b/tests/data/proactive_connect/update_list_salesforce.json new file mode 100644 index 00000000..7e9eef48 --- /dev/null +++ b/tests/data/proactive_connect/update_list_salesforce.json @@ -0,0 +1,28 @@ +{ + "items_count": 0, + "datasource": { + "type": "manual" + }, + "id": "246d17c4-79e6-4a25-8b4e-b777a83f6c30", + "created_at": "2023-04-28T14:16:49.375Z", + "updated_at": "2023-04-28T22:23:37.054Z", + "name": "my_list", + "description": "my updated description", + "tags": [ + "music" + ], + "attributes": [ + { + "key": false, + "name": "phone_number", + "alias": "phone" + } + ], + "sync_status": { + "value": "configured", + "metadata_modified": false, + "data_modified": false, + "details": "failed to get secret: salesforce_credentials", + "dirty": false + } +} \ No newline at end of file diff --git a/tests/data/proactive_connect/upload_from_csv.json b/tests/data/proactive_connect/upload_from_csv.json new file mode 100644 index 00000000..bbdfa8fc --- /dev/null +++ b/tests/data/proactive_connect/upload_from_csv.json @@ -0,0 +1,3 @@ +{ + "inserted": 3 +} \ No newline at end of file diff --git a/tests/test_meetings.py b/tests/test_meetings.py index 7257c1f7..ced694e7 100644 --- a/tests/test_meetings.py +++ b/tests/test_meetings.py @@ -472,7 +472,7 @@ def test_delete_theme_in_use(meetings): meetings.delete_theme('90a21428-b74a-4221-adc3-783935d654db') assert ( str(err.value) - == 'Status Code 400: BadRequestError: could not delete theme, error: Theme 90a21428-b74a-4221-adc3-783935d654db is used by 1 room' + == 'Status Code 400: BadRequestError: could not delete theme\nError: Theme 90a21428-b74a-4221-adc3-783935d654db is used by 1 room' ) @@ -759,7 +759,7 @@ def test_add_logo_to_theme_key_error(meetings): ) assert ( str(err.value) - == "Status Code 400: BadRequestError: could not finalize logos, error: {'logoKey': 'not-a-key', 'code': 'key_not_found'}" + == "Status Code 400: BadRequestError: could not finalize logos\nError: {'logoKey': 'not-a-key', 'code': 'key_not_found'}" ) diff --git a/tests/test_proactive_connect.py b/tests/test_proactive_connect.py new file mode 100644 index 00000000..dfbf631e --- /dev/null +++ b/tests/test_proactive_connect.py @@ -0,0 +1,649 @@ +from vonage.errors import ProactiveConnectError, ClientError +from util import * + +import responses +from pytest import raises +import csv + + +@responses.activate +def test_list_all_lists(proc, dummy_data): + stub( + responses.GET, + 'https://api-eu.vonage.com/v0.1/bulk/lists', + fixture_path='proactive_connect/list_lists.json', + ) + + lists = proc.list_all_lists() + assert request_user_agent() == dummy_data.user_agent + assert lists['total_items'] == 2 + assert lists['_embedded']['lists'][0]['name'] == 'Recipients for demo' + assert lists['_embedded']['lists'][0]['id'] == 'af8a84b6-c712-4252-ac8d-6e28ac9317ce' + assert lists['_embedded']['lists'][1]['name'] == 'Salesforce contacts' + assert lists['_embedded']['lists'][1]['datasource']['type'] == 'salesforce' + + +@responses.activate +def test_list_all_lists_options(proc): + stub( + responses.GET, + 'https://api-eu.vonage.com/v0.1/bulk/lists', + fixture_path='proactive_connect/list_lists.json', + ) + + lists = proc.list_all_lists(page=1, page_size=5) + assert lists['total_items'] == 2 + assert lists['_embedded']['lists'][0]['name'] == 'Recipients for demo' + assert lists['_embedded']['lists'][0]['id'] == 'af8a84b6-c712-4252-ac8d-6e28ac9317ce' + assert lists['_embedded']['lists'][1]['name'] == 'Salesforce contacts' + + +def test_pagination_errors(proc): + with raises(ProactiveConnectError) as err: + proc.list_all_lists(page=-1) + assert str(err.value) == '"page" must be an int > 0.' + + with raises(ProactiveConnectError) as err: + proc.list_all_lists(page_size=-1) + assert str(err.value) == '"page_size" must be an int > 0.' + + +@responses.activate +def test_create_list_basic(proc): + stub( + responses.POST, + 'https://api-eu.vonage.com/v0.1/bulk/lists', + fixture_path='proactive_connect/create_list_basic.json', + status_code=201, + ) + + list = proc.create_list({'name': 'my_list'}) + assert list['id'] == '6994fd17-7691-4463-be16-172ab1430d97' + assert list['name'] == 'my_list' + + +@responses.activate +def test_create_list_manual(proc): + stub( + responses.POST, + 'https://api-eu.vonage.com/v0.1/bulk/lists', + fixture_path='proactive_connect/create_list_manual.json', + status_code=201, + ) + + params = { + "name": "my name", + "description": "my description", + "tags": ["vip", "sport"], + "attributes": [{"name": "phone_number", "alias": "phone"}], + "datasource": {"type": "manual"}, + } + + list = proc.create_list(params) + assert list['id'] == '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + assert list['name'] == 'my_list' + assert list['description'] == 'my description' + assert list['tags'] == ['vip', 'sport'] + assert list['attributes'][0]['name'] == 'phone_number' + + +@responses.activate +def test_create_list_salesforce(proc): + stub( + responses.POST, + 'https://api-eu.vonage.com/v0.1/bulk/lists', + fixture_path='proactive_connect/create_list_salesforce.json', + status_code=201, + ) + + params = { + "name": "my name", + "description": "my description", + "tags": ["vip", "sport"], + "attributes": [{"name": "phone_number", "alias": "phone"}], + "datasource": { + "type": "salesforce", + "integration_id": "salesforce_credentials", + "soql": "select Id, LastName, FirstName, Phone, Email FROM Contact", + }, + } + + list = proc.create_list(params) + assert list['id'] == '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + assert list['name'] == 'my_salesforce_list' + assert list['description'] == 'my salesforce description' + assert list['datasource']['type'] == 'salesforce' + assert list['datasource']['integration_id'] == 'salesforce_credentials' + assert list['datasource']['soql'] == 'select Id, LastName, FirstName, Phone, Email FROM Contact' + + +def test_create_list_errors(proc): + params = { + "name": "my name", + "datasource": { + "type": "salesforce", + "integration_id": 1234, + "soql": "select Id, LastName, FirstName, Phone, Email FROM Contact", + }, + } + + with raises(ProactiveConnectError) as err: + proc.create_list({}) + assert str(err.value) == 'You must supply a name for the new list.' + + with raises(ProactiveConnectError) as err: + proc.create_list(params) + assert str(err.value) == 'You must supply values for "integration_id" and "soql" as strings.' + + with raises(ProactiveConnectError) as err: + params['datasource'].pop('integration_id') + proc.create_list(params) + assert ( + str(err.value) + == 'You must supply a value for "integration_id" and "soql" when creating a list with Salesforce.' + ) + + +@responses.activate +def test_create_list_invalid_name_error(proc): + stub( + responses.POST, + 'https://api-eu.vonage.com/v0.1/bulk/lists', + fixture_path='proactive_connect/create_list_400.json', + status_code=400, + ) + + with raises(ClientError) as err: + proc.create_list({'name': 1234}) + assert ( + str(err.value) + == 'Request data did not validate: Bad Request (https://developer.vonage.com/en/api-errors)\nError: name must be longer than or equal to 1 and shorter than or equal to 255 characters\nError: name must be a string' + ) + + +@responses.activate +def test_get_list(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}', + fixture_path='proactive_connect/get_list.json', + ) + + list = proc.get_list(list_id) + assert list['id'] == '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + assert list['name'] == 'my_list' + assert list['tags'] == ['vip', 'sport'] + + +@responses.activate +def test_get_list_404(proc): + list_id = 'a508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.get_list(list_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_update_list(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.PUT, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}', + fixture_path='proactive_connect/update_list.json', + ) + + params = {'name': 'my_list', 'tags': ['vip', 'sport', 'football']} + list = proc.update_list(list_id, params) + assert list['id'] == '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + assert list['tags'] == ['vip', 'sport', 'football'] + assert list['description'] == 'my updated description' + assert list['updated_at'] == '2023-04-28T21:39:17.825Z' + + +@responses.activate +def test_update_list_salesforce(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.PUT, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}', + fixture_path='proactive_connect/update_list_salesforce.json', + ) + + params = {'name': 'my_list', 'tags': ['music']} + list = proc.update_list(list_id, params) + assert list['id'] == list_id + assert list['tags'] == ['music'] + assert list['updated_at'] == '2023-04-28T22:23:37.054Z' + + +def test_update_list_name_error(proc): + with raises(ProactiveConnectError) as err: + proc.update_list( + '9508e7b8-fe99-4fdf-b022-65d7e461db2d', {'description': 'my new description'} + ) + assert str(err.value) == 'You must supply a name for the new list.' + + +@responses.activate +def test_delete_list(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.DELETE, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}', + fixture_path='null.json', + status_code=204, + ) + + assert proc.delete_list(list_id) == None + + +@responses.activate +def test_delete_list_404(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.DELETE, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + with raises(ClientError) as err: + proc.delete_list(list_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_clear_list(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/clear', + fixture_path='null.json', + status_code=202, + ) + + assert proc.clear_list(list_id) == None + + +@responses.activate +def test_clear_list_404(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/clear', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + with raises(ClientError) as err: + proc.clear_list(list_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_sync_list_from_datasource(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/fetch', + fixture_path='null.json', + status_code=202, + ) + + assert proc.sync_list_from_datasource(list_id) == None + + +@responses.activate +def test_sync_list_manual_datasource_error(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/fetch', + fixture_path='proactive_connect/fetch_list_400.json', + status_code=400, + ) + + with raises(ClientError) as err: + proc.sync_list_from_datasource(list_id) == None + assert ( + str(err.value) + == 'Request data did not validate: Cannot Fetch a manual list (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_sync_list_from_datasource_404(proc): + list_id = '346d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/clear', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + with raises(ClientError) as err: + proc.clear_list(list_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_list_all_items(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items', + fixture_path='proactive_connect/list_all_items.json', + ) + + items = proc.list_all_items(list_id, page=1, page_size=10) + assert items['total_items'] == 2 + assert items['_embedded']['items'][0]['id'] == '04c7498c-bae9-40f9-bdcb-c4eabb0418fe' + assert items['_embedded']['items'][1]['id'] == 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + + +@responses.activate +def test_list_all_items_error_not_found(proc): + list_id = '9508e7b8-fe99-4fdf-b022-65d7e461db2d' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.list_all_items(list_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_create_item(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items', + fixture_path='proactive_connect/item.json', + status_code=201, + ) + + data = {'firstName': 'John', 'lastName': 'Doe', 'phone': '123456789101'} + item = proc.create_item(list_id, data) + + assert item['id'] == 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + assert item['data']['phone'] == '123456789101' + + +@responses.activate +def test_create_item_error_invalid_data(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items', + fixture_path='proactive_connect/item_400.json', + status_code=400, + ) + + with raises(ClientError) as err: + proc.create_item(list_id, {'data': 1234}) + assert ( + str(err.value) + == 'Request data did not validate: Bad Request (https://developer.vonage.com/en/api-errors)\nError: data must be an object' + ) + + +@responses.activate +def test_create_item_error_not_found(proc): + list_id = '346d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + data = {'firstName': 'John', 'lastName': 'Doe', 'phone': '123456789101'} + with raises(ClientError) as err: + proc.create_item(list_id, data) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_download_list_items(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/download', + fixture_path='proactive_connect/list_items.csv', + ) + + proc.download_list_items( + list_id, os.path.join(os.path.dirname(__file__), 'data/proactive_connect/list_items.csv') + ) + items = _read_csv_file( + os.path.join(os.path.dirname(__file__), 'data/proactive_connect/list_items.csv') + ) + assert items[0]['favourite_number'] == '0' + assert items[1]['least_favourite_number'] == '0' + + +@responses.activate +def test_download_list_items_error_not_found(proc): + list_id = '346d17c4-79e6-4a25-8b4e-b777a83f6c30' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/download', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.download_list_items(list_id, 'data/proactive_connect_list_items.csv') + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_get_item(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='proactive_connect/item.json', + ) + + item = proc.get_item(list_id, item_id) + assert item['id'] == 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + assert item['data']['phone'] == '123456789101' + + +@responses.activate +def test_get_item_404(proc): + list_id = '346d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + stub( + responses.GET, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.get_item(list_id, item_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_update_item(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + data = {'first_name': 'John', 'last_name': 'Doe', 'phone': '447007000000'} + stub( + responses.PUT, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='proactive_connect/update_item.json', + ) + + updated_item = proc.update_item(list_id, item_id, data) + + assert updated_item['id'] == item_id + assert updated_item['data'] == data + assert updated_item['updated_at'] == '2023-05-03T19:50:33.207Z' + + +@responses.activate +def test_update_item_error_invalid_data(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + data = 'asdf' + stub( + responses.PUT, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='proactive_connect/item_400.json', + status_code=400, + ) + + with raises(ClientError) as err: + proc.update_item(list_id, item_id, data) + assert ( + str(err.value) + == 'Request data did not validate: Bad Request (https://developer.vonage.com/en/api-errors)\nError: data must be an object' + ) + + +@responses.activate +def test_update_item_404(proc): + list_id = '346d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + data = {'first_name': 'John', 'last_name': 'Doe', 'phone': '447007000000'} + stub( + responses.PUT, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.update_item(list_id, item_id, data) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_delete_item(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'd91c39ed-7c34-4803-a139-34bb4b7c6d53' + stub( + responses.DELETE, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='null.json', + status_code=204, + ) + + response = proc.delete_item(list_id, item_id) + assert response is None + + +@responses.activate +def test_delete_item_404(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + item_id = 'e91c39ed-7c34-4803-a139-34bb4b7c6d53' + stub( + responses.DELETE, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/{item_id}', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.delete_item(list_id, item_id) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_upload_list_items_from_csv(proc): + list_id = '246d17c4-79e6-4a25-8b4e-b777a83f6c30' + file_path = os.path.join(os.path.dirname(__file__), 'data/proactive_connect/csv_to_upload.csv') + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/import', + fixture_path='proactive_connect/upload_from_csv.json', + ) + + response = proc.upload_list_items(list_id, file_path) + assert response['inserted'] == 3 + + +@responses.activate +def test_upload_list_items_from_csv_404(proc): + list_id = '346d17c4-79e6-4a25-8b4e-b777a83f6c30' + file_path = os.path.join(os.path.dirname(__file__), 'data/proactive_connect/csv_to_upload.csv') + stub( + responses.POST, + f'https://api-eu.vonage.com/v0.1/bulk/lists/{list_id}/items/import', + fixture_path='proactive_connect/not_found.json', + status_code=404, + ) + + with raises(ClientError) as err: + proc.upload_list_items(list_id, file_path) + assert ( + str(err.value) + == 'The requested resource does not exist: Not Found (https://developer.vonage.com/en/api-errors)' + ) + + +@responses.activate +def test_list_events(proc): + stub( + responses.GET, + 'https://api-eu.vonage.com/v0.1/bulk/events', + fixture_path='proactive_connect/list_events.json', + ) + + lists = proc.list_events() + assert lists['total_items'] == 1 + assert lists['_embedded']['events'][0]['occurred_at'] == '2022-08-07T13:18:21.970Z' + assert lists['_embedded']['events'][0]['type'] == 'action-call-succeeded' + assert lists['_embedded']['events'][0]['run_id'] == '7d0d4e5f-6453-4c63-87cf-f95b04377324' + + +def _read_csv_file(path): + with open(os.path.join(os.path.dirname(__file__), path)) as csv_file: + reader = csv.DictReader(csv_file) + dict_list = [row for row in reader] + return dict_list