From 8a22b14f4e19807b39d462832b3d8b6c50d4ab68 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Thu, 4 Apr 2024 04:45:56 +0100 Subject: [PATCH] add verify controls --- http_client/src/vonage_http_client/auth.py | 1 - .../src/vonage_http_client/http_client.py | 2 +- .../tests/test_number_insight_v2.py | 1 - verify/src/vonage_verify/responses.py | 49 ++++++-- verify/src/vonage_verify/verify.py | 103 +++++++++++++-- verify/tests/data/cancel_verification.json | 4 + .../tests/data/cancel_verification_error.json | 4 + verify/tests/data/check_code.json | 8 ++ verify/tests/data/check_code_error.json | 5 + verify/tests/data/search_request.json | 32 +++++ verify/tests/data/search_request_error.json | 4 + verify/tests/data/search_request_list.json | 64 ++++++++++ verify/tests/data/trigger_next_event.json | 4 + .../tests/data/trigger_next_event_error.json | 4 + verify/tests/test_verify.py | 119 +++++++++++++++++- 15 files changed, 373 insertions(+), 31 deletions(-) create mode 100644 verify/tests/data/cancel_verification.json create mode 100644 verify/tests/data/cancel_verification_error.json create mode 100644 verify/tests/data/check_code_error.json create mode 100644 verify/tests/data/search_request.json create mode 100644 verify/tests/data/search_request_error.json create mode 100644 verify/tests/data/search_request_list.json create mode 100644 verify/tests/data/trigger_next_event.json create mode 100644 verify/tests/data/trigger_next_event_error.json diff --git a/http_client/src/vonage_http_client/auth.py b/http_client/src/vonage_http_client/auth.py index 43e453f3..f757378a 100644 --- a/http_client/src/vonage_http_client/auth.py +++ b/http_client/src/vonage_http_client/auth.py @@ -96,7 +96,6 @@ def sign_params(self, params: dict) -> str: if not params.get('timestamp'): params['timestamp'] = int(time()) - print(params['timestamp']) for key in sorted(params): value = params[key] diff --git a/http_client/src/vonage_http_client/http_client.py b/http_client/src/vonage_http_client/http_client.py index f6006496..6d8dd55c 100644 --- a/http_client/src/vonage_http_client/http_client.py +++ b/http_client/src/vonage_http_client/http_client.py @@ -221,7 +221,7 @@ def _parse_response(self, response: Response) -> Union[dict, None]: f'Response received from {response.url} with status code: {response.status_code}; headers: {response.headers}' ) self._last_response = response - + print(response.content) content_type = response.headers['Content-Type'].split(';', 1)[0] if 200 <= response.status_code < 300: if response.status_code == 204: diff --git a/number_insight_v2/tests/test_number_insight_v2.py b/number_insight_v2/tests/test_number_insight_v2.py index 26c63f16..c63982a0 100644 --- a/number_insight_v2/tests/test_number_insight_v2.py +++ b/number_insight_v2/tests/test_number_insight_v2.py @@ -72,7 +72,6 @@ def test_ni2_fraud_score_only(): assert response.sim_swap is None clear_response = asdict(response, dict_factory=remove_none_values) - print(clear_response) assert 'fraud_score' in clear_response assert 'sim_swap' not in clear_response diff --git a/verify/src/vonage_verify/responses.py b/verify/src/vonage_verify/responses.py index 56c16300..6aa16ef0 100644 --- a/verify/src/vonage_verify/responses.py +++ b/verify/src/vonage_verify/responses.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Literal, Optional from pydantic import BaseModel @@ -17,17 +17,40 @@ class CheckCodeResponse(BaseModel): estimated_price_messages_sent: Optional[str] = None -# class MessageResponse(BaseModel): -# to: str -# message_id: str = Field(..., validation_alias='message-id') -# status: str -# remaining_balance: str = Field(..., validation_alias='remaining-balance') -# message_price: str = Field(..., validation_alias='message-price') -# network: str -# client_ref: Optional[str] = Field(None, validation_alias='client-ref') -# account_ref: Optional[str] = Field(None, validation_alias='account-ref') +class Check(BaseModel): + date_received: Optional[str] = None + code: Optional[str] = None + status: Optional[str] = None + ip_address: Optional[str] = None + + +class Event(BaseModel): + type: Optional[str] = None + id: Optional[str] = None + + +class VerifyStatus(BaseModel): + request_id: Optional[str] = None + account_id: Optional[str] = None + status: Optional[str] = None + number: Optional[str] = None + price: Optional[str] = None + currency: Optional[str] = None + sender_id: Optional[str] = None + date_submitted: Optional[str] = None + date_finalized: Optional[str] = None + first_event_date: Optional[str] = None + last_event_date: Optional[str] = None + checks: Optional[List[Check]] = None + events: Optional[List[Event]] = None + estimated_price_messages_sent: Optional[str] = None + + +class VerifyControlStatus(BaseModel): + status: str + command: str -# class SmsResponse(BaseModel): -# message_count: str = Field(..., validation_alias='message-count') -# messages: List[MessageResponse] +class NetworkUnblockStatus(BaseModel): + network: str + unblocked_until: str diff --git a/verify/src/vonage_verify/verify.py b/verify/src/vonage_verify/verify.py index 0ea10b2a..40da4443 100644 --- a/verify/src/vonage_verify/verify.py +++ b/verify/src/vonage_verify/verify.py @@ -1,9 +1,17 @@ -from pydantic import validate_call +import re +from typing import List, Optional, Union +from pydantic import Field, validate_call from vonage_http_client.http_client import HttpClient from .errors import VerifyError from .requests import BaseVerifyRequest, Psd2Request, VerifyRequest -from .responses import CheckCodeResponse, StartVerificationResponse +from .responses import ( + CheckCodeResponse, + NetworkUnblockStatus, + StartVerificationResponse, + VerifyControlStatus, + VerifyStatus, +) class Verify: @@ -67,6 +75,85 @@ def check_code(self, request_id: str, code: str) -> CheckCodeResponse: self._check_for_error(response) return CheckCodeResponse(**response) + @validate_call + def search( + self, request: Union[str, List[str]] + ) -> Union[VerifyStatus, List[VerifyStatus]]: + """Search for past or current verification requests. + + Args: + request (str | list[str]): The request ID, or a list of request IDs. + + Returns: + Union[VerifyStatus, List[VerifyStatus]]: Either the response object + containing the verification result, or a list of response objects. + """ + params = {} + if type(request) == str: + params['request_id'] = request + elif type(request) == list: + params['request_ids'] = request + + response = self._http_client.get( + self._http_client.api_host, '/verify/search/json', params, self._auth_type + ) + + if 'verification_requests' in response: + parsed_response = [] + for verification_request in response['verification_requests']: + parsed_response.append(VerifyStatus(**verification_request)) + return parsed_response + elif 'error_text' in response: + error_message = f'Error with the following details: {response}' + raise VerifyError(error_message) + else: + parsed_response = VerifyStatus(**response) + return parsed_response + + @validate_call + def cancel_verification(self, request_id: str) -> VerifyControlStatus: + """Cancel a verification request. + + Args: + request_id (str): The request ID. + + Returns: + VerifyControlStatus: The response object containing details of the submitted + verification control. + """ + response = self._http_client.post( + self._http_client.api_host, + '/verify/control/json', + {'request_id': request_id, 'cmd': 'cancel'}, + self._auth_type, + self._sent_data_type, + ) + self._check_for_error(response) + + return VerifyControlStatus(**response) + + @validate_call + def trigger_next_event(self, request_id: str) -> VerifyControlStatus: + """Trigger the next event in the verification process. + + Args: + request_id (str): The request ID. + + Returns: + VerifyControlStatus: The response object containing details of the submitted + verification control. + """ + response = self._http_client.post( + self._http_client.api_host, + '/verify/control/json', + {'request_id': request_id, 'cmd': 'trigger_next_event'}, + self._auth_type, + self._sent_data_type, + ) + self._check_for_error(response) + + return VerifyControlStatus(**response) + def _make_verify_request( self, verify_request: BaseVerifyRequest ) -> StartVerificationResponse: @@ -84,6 +171,7 @@ def _make_verify_request( request_path = '/verify/json' elif type(verify_request) == Psd2Request: request_path = '/verify/psd2/json' + response = self._http_client.post( self._http_client.api_host, request_path, @@ -92,14 +180,14 @@ def _make_verify_request( self._sent_data_type, ) self._check_for_error(response) - parsed_response = StartVerificationResponse(**response) - return parsed_response + return StartVerificationResponse(**response) def _check_for_error(self, response: dict) -> None: """Check for error in the response. - This method checks if the response contains an error and raises a VerifyError if an error is found. + This method checks if the response contains a non-zero status code + and raises a VerifyError if this is found. Args: response (dict): The response object. @@ -107,9 +195,6 @@ def _check_for_error(self, response: dict) -> None: Raises: VerifyError: If an error is found in the response. """ - print(self._http_client.last_request.body) if int(response['status']) != 0: - error_message = f'Error with Vonage status code {response["status"]}: {response["error_text"]}.' - if 'network' in response: - error_message += f' Network ID: {response["network"]}' + error_message = f'Error with the following details: {response}' raise VerifyError(error_message) diff --git a/verify/tests/data/cancel_verification.json b/verify/tests/data/cancel_verification.json new file mode 100644 index 00000000..8bfcf7bf --- /dev/null +++ b/verify/tests/data/cancel_verification.json @@ -0,0 +1,4 @@ +{ + "status": "0", + "command": "cancel" +} \ No newline at end of file diff --git a/verify/tests/data/cancel_verification_error.json b/verify/tests/data/cancel_verification_error.json new file mode 100644 index 00000000..e74e6622 --- /dev/null +++ b/verify/tests/data/cancel_verification_error.json @@ -0,0 +1,4 @@ +{ + "status": "6", + "error_text": "The requestId 'cc121958d8fb4368aa3bb762bb9a0f75' does not exist or its no longer active." +} \ No newline at end of file diff --git a/verify/tests/data/check_code.json b/verify/tests/data/check_code.json index e69de29b..37267b48 100644 --- a/verify/tests/data/check_code.json +++ b/verify/tests/data/check_code.json @@ -0,0 +1,8 @@ +{ + "request_id": "c5037cb8b47449158ed6611afde58990", + "status": "0", + "event_id": "390f7296-aeff-45ba-8931-84a13f3f76d7", + "price": "0.05000000", + "currency": "EUR", + "estimated_price_messages_sent": "0.04675" +} \ No newline at end of file diff --git a/verify/tests/data/check_code_error.json b/verify/tests/data/check_code_error.json new file mode 100644 index 00000000..29ac3017 --- /dev/null +++ b/verify/tests/data/check_code_error.json @@ -0,0 +1,5 @@ +{ + "request_id": "cc121958d8fb4368aa3bb762bb9a0f74", + "status": "16", + "error_text": "The code provided does not match the expected value" +} \ No newline at end of file diff --git a/verify/tests/data/search_request.json b/verify/tests/data/search_request.json new file mode 100644 index 00000000..4330554c --- /dev/null +++ b/verify/tests/data/search_request.json @@ -0,0 +1,32 @@ +{ + "request_id": "cc121958d8fb4368aa3bb762bb9a0f74", + "account_id": "abcdef01", + "status": "EXPIRED", + "number": "1234567890", + "price": "0", + "currency": "EUR", + "sender_id": "Acme Inc.", + "date_submitted": "2024-04-03 02:22:37", + "date_finalized": "2024-04-03 02:27:38", + "first_event_date": "2024-04-03 02:22:37", + "last_event_date": "2024-04-03 02:24:38", + "checks": [ + { + "date_received": "2024-04-03 02:23:04", + "code": "1234", + "status": "INVALID", + "ip_address": "" + } + ], + "events": [ + { + "type": "sms", + "id": "23f3a13d-6d03-4262-8f4d-67f12a56e1c8" + }, + { + "type": "sms", + "id": "09ef3984-3f62-453d-8f9c-1a161b373dba" + } + ], + "estimated_price_messages_sent": "0.09350" +} \ No newline at end of file diff --git a/verify/tests/data/search_request_error.json b/verify/tests/data/search_request_error.json new file mode 100644 index 00000000..a0d020f6 --- /dev/null +++ b/verify/tests/data/search_request_error.json @@ -0,0 +1,4 @@ +{ + "status": "101", + "error_text": "No response found" +} \ No newline at end of file diff --git a/verify/tests/data/search_request_list.json b/verify/tests/data/search_request_list.json new file mode 100644 index 00000000..939e9ed2 --- /dev/null +++ b/verify/tests/data/search_request_list.json @@ -0,0 +1,64 @@ +{ + "verification_requests": [ + { + "request_id": "cc121958d8fb4368aa3bb762bb9a0f74", + "account_id": "abcdef01", + "number": "1234567890", + "sender_id": "verify", + "date_submitted": "2024-04-03 02:22:37", + "date_finalized": "2024-04-03 02:27:38", + "checks": [ + { + "date_received": "2024-04-03 02:23:04", + "code": "1234", + "status": "INVALID", + "ip_address": "" + } + ], + "first_event_date": "2024-04-03 02:22:37", + "last_event_date": "2024-04-03 02:24:38", + "price": "0", + "currency": "EUR", + "status": "EXPIRED", + "estimated_price_messages_sent": "0.09350", + "events": [ + { + "id": "23f3a13d-6d03-4262-8f4d-67f12a56e1c8", + "type": "sms" + }, + { + "id": "09ef3984-3f62-453d-8f9c-1a161b373dba", + "type": "sms" + } + ] + }, + { + "request_id": "c5037cb8b47449158ed6611afde58990", + "account_id": "abcdef01", + "number": "1234567890", + "sender_id": "verify", + "date_submitted": "2024-04-03 02:09:22", + "date_finalized": "2024-04-03 02:09:59", + "checks": [ + { + "date_received": "2024-04-03 02:09:59", + "code": "5700", + "status": "VALID", + "ip_address": "" + } + ], + "first_event_date": "2024-04-03 02:09:23", + "last_event_date": "2024-04-03 02:09:23", + "price": "0.05000000", + "currency": "EUR", + "status": "SUCCESS", + "estimated_price_messages_sent": "0.04675", + "events": [ + { + "id": "390f7296-aeff-45ba-8931-84a13f3f76d7", + "type": "sms" + } + ] + } + ] +} \ No newline at end of file diff --git a/verify/tests/data/trigger_next_event.json b/verify/tests/data/trigger_next_event.json new file mode 100644 index 00000000..7939ad17 --- /dev/null +++ b/verify/tests/data/trigger_next_event.json @@ -0,0 +1,4 @@ +{ + "status": "0", + "command": "trigger_next_event" +} \ No newline at end of file diff --git a/verify/tests/data/trigger_next_event_error.json b/verify/tests/data/trigger_next_event_error.json new file mode 100644 index 00000000..1a52f3a4 --- /dev/null +++ b/verify/tests/data/trigger_next_event_error.json @@ -0,0 +1,4 @@ +{ + "status": "19", + "error_text": "No more events are left to execute for the request ['2c021d25cf2e47a9b277a996f4325b81']" +} \ No newline at end of file diff --git a/verify/tests/test_verify.py b/verify/tests/test_verify.py index 2bcecbc4..451a8feb 100644 --- a/verify/tests/test_verify.py +++ b/verify/tests/test_verify.py @@ -6,6 +6,7 @@ from vonage_verify.errors import VerifyError from vonage_verify.language_codes import LanguageCode, Psd2LanguageCode from vonage_verify.requests import Psd2Request, VerifyRequest +from vonage_verify.responses import VerifyControlStatus from vonage_verify.verify import Verify from testutils import build_response, get_mock_api_key_auth @@ -86,7 +87,7 @@ def test_verify_request_error(): verify.start_verification(request) assert e.match( - 'Error with Vonage status code 10: Concurrent verifications to the same number are not allowed' + "'error_text': 'Concurrent verifications to the same number are not allowed'" ) @@ -104,7 +105,7 @@ def test_verify_request_error_with_network(): with raises(VerifyError) as e: verify.start_verification(request) - assert e.match('Network ID: 244523') + assert e.match("'network': '244523'") @responses.activate @@ -113,10 +114,116 @@ def test_check_code(): path, 'POST', 'https://api.nexmo.com/verify/check/json', 'check_code.json' ) response = verify.check_code( - request_id='abcdef0123456789abcdef0123456789', code='1234' + request_id='c5037cb8b47449158ed6611afde58990', code='1234' ) - assert response.request_id == 'abcdef0123456789abcdef0123456789' + assert response.request_id == 'c5037cb8b47449158ed6611afde58990' assert response.status == '0' - assert response.event_id == 'abcdef0123456789abcdef0123456789' - assert response.price == '0.10000000' + assert response.event_id == '390f7296-aeff-45ba-8931-84a13f3f76d7' + assert response.price == '0.05000000' + assert response.currency == 'EUR' + assert response.estimated_price_messages_sent == '0.04675' + + +@responses.activate +def test_check_code_error(): + build_response( + path, 'POST', 'https://api.nexmo.com/verify/check/json', 'check_code_error.json' + ) + + with raises(VerifyError) as e: + verify.check_code(request_id='c5037cb8b47449158ed6611afde58990', code='1234') + + assert e.match( + "'status': '16', 'error_text': 'The code provided does not match the expected value'" + ) + + +@responses.activate +def test_search(): + build_response( + path, 'GET', 'https://api.nexmo.com/verify/search/json', 'search_request.json' + ) + response = verify.search('c5037cb8b47449158ed6611afde58990') + + assert response.request_id == 'cc121958d8fb4368aa3bb762bb9a0f74' + assert response.account_id == 'abcdef01' + assert response.status == 'EXPIRED' + assert response.number == '1234567890' + assert response.price == '0' assert response.currency == 'EUR' + assert response.sender_id == 'Acme Inc.' + assert response.date_submitted == '2024-04-03 02:22:37' + assert response.date_finalized == '2024-04-03 02:27:38' + assert response.first_event_date == '2024-04-03 02:22:37' + assert response.last_event_date == '2024-04-03 02:24:38' + assert response.estimated_price_messages_sent == '0.09350' + assert response.checks[0].date_received == '2024-04-03 02:23:04' + assert response.checks[0].code == '1234' + assert response.checks[0].status == 'INVALID' + assert response.checks[0].ip_address == '' + assert response.events[0].type == 'sms' + assert response.events[0].id == '23f3a13d-6d03-4262-8f4d-67f12a56e1c8' + + +@responses.activate +def test_search_list_of_ids(): + build_response( + path, + 'GET', + 'https://api.nexmo.com/verify/search/json', + 'search_request_list.json', + ) + response0, response1 = verify.search( + ['cc121958d8fb4368aa3bb762bb9a0f75', 'c5037cb8b47449158ed6611afde58990'] + ) + assert response0.request_id == 'cc121958d8fb4368aa3bb762bb9a0f74' + assert response1.request_id == 'c5037cb8b47449158ed6611afde58990' + assert response1.status == 'SUCCESS' + assert response1.checks[0].status == 'VALID' + + +@responses.activate +def test_search_error(): + build_response( + path, + 'GET', + 'https://api.nexmo.com/verify/search/json', + 'search_request_error.json', + ) + + with raises(VerifyError) as e: + verify.search('c5037cb8b47449158ed6611afde58990') + + assert e.match("{'status': '101', 'error_text': 'No response found'}") + + +@responses.activate +def test_cancel_verification(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/verify/control/json', + 'cancel_verification.json', + ) + response = verify.cancel_verification('c5037cb8b47449158ed6611afde58990') + + assert type(response) == VerifyControlStatus + assert response.status == '0' + assert response.command == 'cancel' + + +@responses.activate +def test_cancel_verification_error(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/verify/control/json', + 'cancel_verification_error.json', + ) + + with raises(VerifyError) as e: + verify.cancel_verification('c5037cb8b47449158ed6611afde58990') + + assert e.match( + "The requestId 'cc121958d8fb4368aa3bb762bb9a0f75' does not exist or its no longer active." + )