diff --git a/CHANGES.md b/CHANGES.md index ee05138..d028781 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# 3.16.0 +- Add support for the [Vonage Number Verification API](https://developer.vonage.com/number-verification/overview) + # 3.15.0 - Add support for the [Vonage Sim Swap API](https://developer.vonage.com/en/sim-swap/overview) diff --git a/README.md b/README.md index 33ef277..01bea47 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup]. - [Application API](#application-api) - [Users API](#users-api) - [Sim Swap API](#sim-swap-api) +- [Number Verification API](#number-verification-api) - [Validating Webhook Signatures](#validate-webhook-signatures) - [JWT Parameters](#jwt-parameters) - [Overriding API Attributes](#overriding-api-attributes) @@ -1154,7 +1155,41 @@ client.sim_swap.check('447700900000', max_age=24) client.sim_swap.get_last_swap_date('447700900000') ``` -## Validate webhook signatures +## Number Verification API + +This can be used to verify a mobile device. You must register a business account with Vonage and create a network profile in order to use this API. [More information on authentication can be found in the Vonage Developer documentation]('https://developer.vonage.com/en/getting-started-network/authentication'). + +### Get an OIDC URL + +Get an OIDC URL for use in your front-end application. + +```python +url = client.number_verification.get_oidc_url( + redirect_uri='https://example.com/callback', + state='state_id', + login_hint='447700900000', +) +print(url) +``` + +### Create an Access Token from an Authentication Code + +To verify a number, you need a Camara access token. Your front-end application should have made an OIDC request that returned a `code`. Use this with your `redirect_uri` to generate an access token. + +```python +access_token = client.number_verification.create_camara_token('code', 'https://example.com/callback') +``` + +You can then use this access token when making a Number Verification request. + +### Make a Number Verification Request + +```python +response = client.number_verification.verify(access_token, phone_number='447700900000') +print(response) +``` + +## Validate Webhook Signatures ```python client = vonage.Client(signature_secret='secret') diff --git a/requirements.txt b/requirements.txt index 6b622a6..7da1716 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -e . -pytest==7.4.2 -responses==0.22.0 +pytest==8.2.2 +responses==0.25.0 +pydantic==2.7.3 coverage -pydantic==2.5.2 bump2version build diff --git a/src/vonage/camara_auth.py b/src/vonage/camara_auth.py index 6c6101a..c45179d 100644 --- a/src/vonage/camara_auth.py +++ b/src/vonage/camara_auth.py @@ -1,5 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +from urllib.parse import urlencode, urlunparse + if TYPE_CHECKING: from vonage import Client @@ -28,7 +30,41 @@ def make_oidc_request(self, number: str, scope: str): body_is_json=False, ) - def request_camara_token( + def get_oidc_url( + self, + redirect_uri: str, + state: str = None, + login_hint: str = None, + scope: str = 'openid+dpv:FraudPreventionAndDetection#number-verification-verify-read', + ): + """Get the URL to use for authentication in a front-end application. + + Args: + redirect_uri (str): The URI to redirect to after authentication. + scope (str): The scope of the request. + state (str): A unique identifier for the request. Can be any string. + login_hint (str): The phone number to use for the request. + + Returns: + The URL to use to make an OIDC request in a front-end application. + """ + base_url = 'https://oidc.idp.vonage.com/oauth2/auth' + + params = { + 'client_id': self._client.application_id, + 'redirect_uri': redirect_uri, + 'response_type': 'code', + 'scope': scope, + } + if state: + params['state'] = state + if login_hint: + params['login_hint'] = login_hint + + full_url = urlunparse(('', '', base_url, '', urlencode(params), '')) + return full_url + + def request_backend_camara_token( self, oidc_response: dict, grant_type: str = 'urn:openid:params:grant-type:ciba' ): """Request a Camara token using an authentication request ID given as a @@ -38,7 +74,18 @@ def request_camara_token( 'grant_type': grant_type, 'auth_req_id': oidc_response['auth_req_id'], } + return self._request_camara_token(params) + + def request_frontend_camara_token(self, code: str, redirect_uri: str): + """Request a Camara token using a code from an OIDC response.""" + params = { + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': redirect_uri, + } + return self._request_camara_token(params) + def _request_camara_token(self, params: dict): token_response = self._client.post( 'api-eu.vonage.com', '/oauth2/token', diff --git a/src/vonage/client.py b/src/vonage/client.py index 1ddc88e..07369a8 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 .number_verification import NumberVerification from .proactive_connect import ProactiveConnect from .redact import Redact from .sim_swap import SimSwap @@ -132,6 +133,7 @@ def __init__( self.messages = Messages(self) self.number_insight = NumberInsight(self) self.numbers = Numbers(self) + self.number_verification = NumberVerification(self) self.proactive_connect = ProactiveConnect(self) self.short_codes = ShortCodes(self) self.sim_swap = SimSwap(self) diff --git a/src/vonage/errors.py b/src/vonage/errors.py index 27e4f8d..91e4fa7 100644 --- a/src/vonage/errors.py +++ b/src/vonage/errors.py @@ -70,3 +70,7 @@ class TokenExpiryError(ClientError): class SipError(ClientError): """Error related to usage of SIP calls.""" + + +class NumberVerificationError(ClientError): + """An error relating to the Number Verification API.""" diff --git a/src/vonage/number_verification.py b/src/vonage/number_verification.py new file mode 100644 index 0000000..962cfca --- /dev/null +++ b/src/vonage/number_verification.py @@ -0,0 +1,67 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from vonage.errors import NumberVerificationError + +from .camara_auth import CamaraAuth + +if TYPE_CHECKING: + from vonage import Client + + +class NumberVerification: + """Class containing methods for working with the Vonage Number Verification API.""" + + def __init__(self, client: Client): + self._client = client + self._auth_type = 'oauth2' + self._camara_auth = CamaraAuth(client) + self._nvtoken = None + + def get_oidc_url( + self, + redirect_uri: str, + state: str = None, + login_hint: str = None, + scope: str = 'openid+dpv:FraudPreventionAndDetection#number-verification-verify-read', + ): + """Get the URL to use for authentication in a front-end application. + + Args: + redirect_uri (str): The URI to redirect to after authentication. + scope (str): The scope of the request. + state (str): A unique identifier for the request. Can be any string. + login_hint (str): The phone number to use for the request. + + Returns: + The URL to use to make an OIDC request in a front-end application. + """ + return self._camara_auth.get_oidc_url( + redirect_uri=redirect_uri, + scope=scope, + state=state, + login_hint=login_hint, + ) + + def exchange_code_for_token(self, code: str, redirect_uri: str) -> str: + return self._camara_auth.request_frontend_camara_token(code, redirect_uri) + + def verify(self, access_token: str, phone_number: str = None, hashed_phone_number: str = None): + """Verify a phone number using the Number Verification API.""" + + if phone_number and hashed_phone_number: + raise NumberVerificationError( + 'Only one of "phone_number" and "hashed_phone_number" can be provided.' + ) + if phone_number: + params = {'phoneNumber': phone_number} + elif hashed_phone_number: + params = {'hashedPhoneNumber': hashed_phone_number} + + return self._client.post( + 'api-eu.vonage.com', + '/camara/number-verification/v031/verify', + params=params, + auth_type=self._auth_type, + oauth_token=access_token, + ) diff --git a/src/vonage/sim_swap.py b/src/vonage/sim_swap.py index fe8fed2..95ab40d 100644 --- a/src/vonage/sim_swap.py +++ b/src/vonage/sim_swap.py @@ -28,7 +28,7 @@ def check(self, phone_number: str, max_age: int = None): oicd_response = self._camara_auth.make_oidc_request( number=phone_number, scope='dpv:FraudPreventionAndDetection#check-sim-swap' ) - token = self._camara_auth.request_camara_token(oicd_response) + token = self._camara_auth.request_backend_camara_token(oicd_response) params = {'phoneNumber': phone_number} if max_age: @@ -55,7 +55,7 @@ def get_last_swap_date(self, phone_number: str): number=phone_number, scope='dpv:FraudPreventionAndDetection#retrieve-sim-swap-date', ) - token = self._camara_auth.request_camara_token(oicd_response) + token = self._camara_auth.request_backend_camara_token(oicd_response) return self._client.post( 'api-eu.vonage.com', '/camara/sim-swap/v040/retrieve-date', diff --git a/tests/conftest.py b/tests/conftest.py index 4cbd032..a033071 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ def read_file(path): class DummyData(object): def __init__(self): - import vonage + from vonage import __version__ self.api_key = "nexmo-api-key" self.api_secret = "nexmo-api-secret" @@ -24,7 +24,7 @@ def __init__(self): self.application_id = "nexmo-application-id" self.private_key = read_file("data/private_key.txt") self.public_key = read_file("data/public_key.txt") - self.user_agent = f"vonage-python/{vonage.__version__} python/{platform.python_version()}" + self.user_agent = f"vonage-python/{__version__} python/{platform.python_version()}" self.host = "rest.nexmo.com" self.api_host = "api.nexmo.com" self.meetings_api_host = "api-eu.vonage.com/beta/meetings" @@ -37,9 +37,9 @@ def dummy_data(): @pytest.fixture def client(dummy_data): - import vonage + from vonage.client import Client - return vonage.Client( + return Client( key=dummy_data.api_key, secret=dummy_data.api_secret, application_id=dummy_data.application_id, @@ -50,95 +50,95 @@ def client(dummy_data): # Represents an instance of the Voice class for testing @pytest.fixture def voice(client): - import vonage + from vonage.voice import Voice - return vonage.Voice(client) + return Voice(client) # Represents an instance of the Sms class for testing @pytest.fixture def sms(client): - import vonage + from vonage.sms import Sms - return vonage.Sms(client) + return Sms(client) # Represents an instance of the Verify class for testing @pytest.fixture def verify(client): - import vonage + from vonage.verify import Verify - return vonage.Verify(client) + return Verify(client) @pytest.fixture def number_insight(client): - import vonage + from vonage.number_insight import NumberInsight - return vonage.NumberInsight(client) + return NumberInsight(client) @pytest.fixture def account(client): - import vonage + from vonage.account import Account - return vonage.Account(client) + return Account(client) @pytest.fixture def numbers(client): - import vonage + from vonage.number_management import Numbers - return vonage.Numbers(client) + return Numbers(client) @pytest.fixture def ussd(client): - import vonage + from vonage.ussd import Ussd - return vonage.Ussd(client) + return Ussd(client) @pytest.fixture def short_codes(client): - import vonage + from vonage.short_codes import ShortCodes - return vonage.ShortCodes(client) + return ShortCodes(client) @pytest.fixture def messages(client): - import vonage + from vonage.messages import Messages - return vonage.Messages(client) + return Messages(client) @pytest.fixture def redact(client): - import vonage + from vonage.redact import Redact - return vonage.Redact(client) + return Redact(client) @pytest.fixture def application_v2(client): - import vonage + from vonage.application import Application - return vonage.ApplicationV2(client) + return Application(client) @pytest.fixture def meetings(client): - import vonage + from vonage.meetings import Meetings - return vonage.Meetings(client) + return Meetings(client) @pytest.fixture def proc(client): - import vonage + from vonage.proactive_connect import ProactiveConnect - return vonage.ProactiveConnect(client) + return ProactiveConnect(client) @pytest.fixture @@ -150,6 +150,13 @@ def camara_auth(client): @pytest.fixture def sim_swap(client): - import vonage + from vonage.sim_swap import SimSwap - return vonage.SimSwap(client) + return SimSwap(client) + + +@pytest.fixture +def number_verification(client): + from vonage.number_verification import NumberVerification + + return NumberVerification(client) diff --git a/tests/data/number_verification/verify.json b/tests/data/number_verification/verify.json new file mode 100644 index 0000000..0eaf6b4 --- /dev/null +++ b/tests/data/number_verification/verify.json @@ -0,0 +1,3 @@ +{ + "devicePhoneNumberVerified": true +} \ No newline at end of file diff --git a/tests/test_camara_auth.py b/tests/test_camara_auth.py index fcbdf2a..1626fd2 100644 --- a/tests/test_camara_auth.py +++ b/tests/test_camara_auth.py @@ -23,7 +23,7 @@ def test_oidc_request(camara_auth: CamaraAuth): @responses.activate -def test_request_camara_access_token(camara_auth: CamaraAuth): +def test_request_backend_camara_access_token(camara_auth: CamaraAuth): stub( responses.POST, 'https://api-eu.vonage.com/oauth2/token', @@ -35,9 +35,38 @@ def test_request_camara_access_token(camara_auth: CamaraAuth): 'expires_in': 300, 'interval': 0, } - response = camara_auth.request_camara_token(oidc_response) + response = camara_auth.request_backend_camara_token(oidc_response) assert ( response == 'eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYW51YmlzLWNlcnRzLWMxLWV1dzEucHJvZC52MS52b25hZ2VuZXR3b3Jrcy5uZXQvandrcyIsImtpZCI6IkNOPVZvbmFnZSAxdmFwaWd3IEludGVybmFsIENBOjoxOTUxODQ2ODA3NDg1NTYwNjYzODY3MTM0NjE2MjU2MTU5MjU2NDkiLCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL2FudWJpcy1jZXJ0cy1jMS1ldXcxLnByb2QudjEudm9uYWdlbmV0d29ya3MubmV0L3YxL2NlcnRzLzA4NjliNDMyZTEzZmIyMzcwZTk2ZGI4YmUxMDc4MjJkIn0.eyJwcmluY2lwYWwiOnsiYXBpS2V5IjoiNGI1MmMwMGUiLCJhcHBsaWNhdGlvbklkIjoiMmJlZTViZWQtNmZlZS00ZjM2LTkxNmQtNWUzYjRjZDI1MjQzIiwibWFzdGVyQWNjb3VudElkIjoiNGI1MmMwMGUiLCJjYXBhYmlsaXRpZXMiOlsibmV0d29yay1hcGktZmVhdHVyZXMiXSwiZXh0cmFDb25maWciOnsiY2FtYXJhU3RhdGUiOiJmb0ZyQndnOFNmeGMydnd2S1o5Y3UrMlgrT0s1K2FvOWhJTTVGUGZMQ1dOeUlMTHR3WmY1dFRKbDdUc1p4QnY4QWx3aHM2bFNWcGVvVkhoWngvM3hUenFRWVkwcHpIZE5XL085ZEdRN1RKOE9sU1lDdTFYYXFEcnNFbEF4WEJVcUpGdnZTTkp5a1A5ZDBYWVN4ajZFd0F6UUFsNGluQjE1c3VMRFNsKy82U1FDa29Udnpld0tvcFRZb0F5MVg2dDJVWXdEVWFDNjZuOS9kVWxIemN3V0NGK3QwOGNReGxZVUxKZyt3T0hwV2xvWGx1MGc3REx0SCtHd0pvRGJoYnMyT2hVY3BobGZqajBpeHQ1OTRsSG5sQ1NYNkZrMmhvWEhKUW01S3JtOVBKSmttK0xTRjVsRTd3NUxtWTRvYTFXSGpkY0dwV1VsQlNQY000YnprOGU0bVE9PSJ9fSwiZmVkZXJhdGVkQXNzZXJ0aW9ucyI6e30sImF1ZCI6ImFwaS1ldS52b25hZ2UuY29tIiwiZXhwIjoxNzE3MDkyODY4LCJqdGkiOiJmNDZhYTViOC1hODA2LTRjMzctODQyMS02OGYwMzJjNDlhMWYiLCJpYXQiOjE3MTcwOTE5NzAsImlzcyI6IlZJQU0tSUFQIiwibmJmIjoxNzE3MDkxOTU1fQ.iLUbyDPR1HGLKh29fy6fqK65Q1O7mjWOletAEPJD4eu7gb0E85EL4M9R7ckJq5lIvgedQt3vBheTaON9_u-VYjMqo8ulPoEoGUDHbOzNbs4MmCW0_CRdDPGyxnUhvcbuJhPgnEHxmfHjJBljncUnk-Z7XCgyNajBNXeQQnHkRF_6NMngxJ-qjjhqbYL0VsF_JS7-TXxixNL0KAFl0SeN2DjkfwRBCclP-69CTExDjyOvouAcchqi-6ZYj_tXPCrTADuzUrQrW8C5nHp2-XjWJSFKzyvi48n8V1U6KseV-eYzBzvy7bJf0tRMX7G6gctTYq3DxdC_eXvXlnp1zx16mg' ) + + +@responses.activate +def test_request_frontend_camara_access_token(camara_auth: CamaraAuth): + stub( + responses.POST, + 'https://api-eu.vonage.com/oauth2/token', + fixture_path='camara_auth/token_request.json', + ) + + response = camara_auth.request_frontend_camara_token('code', 'https://example.com/callback') + + assert ( + response + == 'eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYW51YmlzLWNlcnRzLWMxLWV1dzEucHJvZC52MS52b25hZ2VuZXR3b3Jrcy5uZXQvandrcyIsImtpZCI6IkNOPVZvbmFnZSAxdmFwaWd3IEludGVybmFsIENBOjoxOTUxODQ2ODA3NDg1NTYwNjYzODY3MTM0NjE2MjU2MTU5MjU2NDkiLCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL2FudWJpcy1jZXJ0cy1jMS1ldXcxLnByb2QudjEudm9uYWdlbmV0d29ya3MubmV0L3YxL2NlcnRzLzA4NjliNDMyZTEzZmIyMzcwZTk2ZGI4YmUxMDc4MjJkIn0.eyJwcmluY2lwYWwiOnsiYXBpS2V5IjoiNGI1MmMwMGUiLCJhcHBsaWNhdGlvbklkIjoiMmJlZTViZWQtNmZlZS00ZjM2LTkxNmQtNWUzYjRjZDI1MjQzIiwibWFzdGVyQWNjb3VudElkIjoiNGI1MmMwMGUiLCJjYXBhYmlsaXRpZXMiOlsibmV0d29yay1hcGktZmVhdHVyZXMiXSwiZXh0cmFDb25maWciOnsiY2FtYXJhU3RhdGUiOiJmb0ZyQndnOFNmeGMydnd2S1o5Y3UrMlgrT0s1K2FvOWhJTTVGUGZMQ1dOeUlMTHR3WmY1dFRKbDdUc1p4QnY4QWx3aHM2bFNWcGVvVkhoWngvM3hUenFRWVkwcHpIZE5XL085ZEdRN1RKOE9sU1lDdTFYYXFEcnNFbEF4WEJVcUpGdnZTTkp5a1A5ZDBYWVN4ajZFd0F6UUFsNGluQjE1c3VMRFNsKy82U1FDa29Udnpld0tvcFRZb0F5MVg2dDJVWXdEVWFDNjZuOS9kVWxIemN3V0NGK3QwOGNReGxZVUxKZyt3T0hwV2xvWGx1MGc3REx0SCtHd0pvRGJoYnMyT2hVY3BobGZqajBpeHQ1OTRsSG5sQ1NYNkZrMmhvWEhKUW01S3JtOVBKSmttK0xTRjVsRTd3NUxtWTRvYTFXSGpkY0dwV1VsQlNQY000YnprOGU0bVE9PSJ9fSwiZmVkZXJhdGVkQXNzZXJ0aW9ucyI6e30sImF1ZCI6ImFwaS1ldS52b25hZ2UuY29tIiwiZXhwIjoxNzE3MDkyODY4LCJqdGkiOiJmNDZhYTViOC1hODA2LTRjMzctODQyMS02OGYwMzJjNDlhMWYiLCJpYXQiOjE3MTcwOTE5NzAsImlzcyI6IlZJQU0tSUFQIiwibmJmIjoxNzE3MDkxOTU1fQ.iLUbyDPR1HGLKh29fy6fqK65Q1O7mjWOletAEPJD4eu7gb0E85EL4M9R7ckJq5lIvgedQt3vBheTaON9_u-VYjMqo8ulPoEoGUDHbOzNbs4MmCW0_CRdDPGyxnUhvcbuJhPgnEHxmfHjJBljncUnk-Z7XCgyNajBNXeQQnHkRF_6NMngxJ-qjjhqbYL0VsF_JS7-TXxixNL0KAFl0SeN2DjkfwRBCclP-69CTExDjyOvouAcchqi-6ZYj_tXPCrTADuzUrQrW8C5nHp2-XjWJSFKzyvi48n8V1U6KseV-eYzBzvy7bJf0tRMX7G6gctTYq3DxdC_eXvXlnp1zx16mg' + ) + + +def test_get_oidc_url(camara_auth: CamaraAuth): + url = camara_auth.get_oidc_url( + redirect_uri='https://example.com/callback', + state='state_id', + login_hint='447700900000', + ) + + assert ( + url + == 'https://oidc.idp.vonage.com/oauth2/auth?client_id=nexmo-application-id&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=code&scope=openid%2Bdpv%3AFraudPreventionAndDetection%23number-verification-verify-read&state=state_id&login_hint=447700900000' + ) diff --git a/tests/test_number_verification.py b/tests/test_number_verification.py new file mode 100644 index 0000000..4567a2f --- /dev/null +++ b/tests/test_number_verification.py @@ -0,0 +1,62 @@ +from vonage.errors import NumberVerificationError +from vonage.number_verification import NumberVerification +from util import * + +import responses +from pytest import raises + + +def test_get_oidc_url(number_verification: NumberVerification): + url = number_verification.get_oidc_url( + redirect_uri='https://example.com/callback', + state='state_id', + login_hint='447700900000', + ) + + assert ( + url + == 'https://oidc.idp.vonage.com/oauth2/auth?client_id=nexmo-application-id&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=code&scope=openid%2Bdpv%3AFraudPreventionAndDetection%23number-verification-verify-read&state=state_id&login_hint=447700900000' + ) + + +@responses.activate +def test_create_camara_token_and_verify_numbers(number_verification: NumberVerification): + stub( + responses.POST, + 'https://api-eu.vonage.com/oauth2/token', + fixture_path='camara_auth/token_request.json', + ) + + access_token = number_verification.exchange_code_for_token( + 'code', 'https://example.com/callback' + ) + assert ( + access_token + == 'eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYW51YmlzLWNlcnRzLWMxLWV1dzEucHJvZC52MS52b25hZ2VuZXR3b3Jrcy5uZXQvandrcyIsImtpZCI6IkNOPVZvbmFnZSAxdmFwaWd3IEludGVybmFsIENBOjoxOTUxODQ2ODA3NDg1NTYwNjYzODY3MTM0NjE2MjU2MTU5MjU2NDkiLCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL2FudWJpcy1jZXJ0cy1jMS1ldXcxLnByb2QudjEudm9uYWdlbmV0d29ya3MubmV0L3YxL2NlcnRzLzA4NjliNDMyZTEzZmIyMzcwZTk2ZGI4YmUxMDc4MjJkIn0.eyJwcmluY2lwYWwiOnsiYXBpS2V5IjoiNGI1MmMwMGUiLCJhcHBsaWNhdGlvbklkIjoiMmJlZTViZWQtNmZlZS00ZjM2LTkxNmQtNWUzYjRjZDI1MjQzIiwibWFzdGVyQWNjb3VudElkIjoiNGI1MmMwMGUiLCJjYXBhYmlsaXRpZXMiOlsibmV0d29yay1hcGktZmVhdHVyZXMiXSwiZXh0cmFDb25maWciOnsiY2FtYXJhU3RhdGUiOiJmb0ZyQndnOFNmeGMydnd2S1o5Y3UrMlgrT0s1K2FvOWhJTTVGUGZMQ1dOeUlMTHR3WmY1dFRKbDdUc1p4QnY4QWx3aHM2bFNWcGVvVkhoWngvM3hUenFRWVkwcHpIZE5XL085ZEdRN1RKOE9sU1lDdTFYYXFEcnNFbEF4WEJVcUpGdnZTTkp5a1A5ZDBYWVN4ajZFd0F6UUFsNGluQjE1c3VMRFNsKy82U1FDa29Udnpld0tvcFRZb0F5MVg2dDJVWXdEVWFDNjZuOS9kVWxIemN3V0NGK3QwOGNReGxZVUxKZyt3T0hwV2xvWGx1MGc3REx0SCtHd0pvRGJoYnMyT2hVY3BobGZqajBpeHQ1OTRsSG5sQ1NYNkZrMmhvWEhKUW01S3JtOVBKSmttK0xTRjVsRTd3NUxtWTRvYTFXSGpkY0dwV1VsQlNQY000YnprOGU0bVE9PSJ9fSwiZmVkZXJhdGVkQXNzZXJ0aW9ucyI6e30sImF1ZCI6ImFwaS1ldS52b25hZ2UuY29tIiwiZXhwIjoxNzE3MDkyODY4LCJqdGkiOiJmNDZhYTViOC1hODA2LTRjMzctODQyMS02OGYwMzJjNDlhMWYiLCJpYXQiOjE3MTcwOTE5NzAsImlzcyI6IlZJQU0tSUFQIiwibmJmIjoxNzE3MDkxOTU1fQ.iLUbyDPR1HGLKh29fy6fqK65Q1O7mjWOletAEPJD4eu7gb0E85EL4M9R7ckJq5lIvgedQt3vBheTaON9_u-VYjMqo8ulPoEoGUDHbOzNbs4MmCW0_CRdDPGyxnUhvcbuJhPgnEHxmfHjJBljncUnk-Z7XCgyNajBNXeQQnHkRF_6NMngxJ-qjjhqbYL0VsF_JS7-TXxixNL0KAFl0SeN2DjkfwRBCclP-69CTExDjyOvouAcchqi-6ZYj_tXPCrTADuzUrQrW8C5nHp2-XjWJSFKzyvi48n8V1U6KseV-eYzBzvy7bJf0tRMX7G6gctTYq3DxdC_eXvXlnp1zx16mg' + ) + + stub( + responses.POST, + 'https://api-eu.vonage.com/camara/number-verification/v031/verify', + fixture_path='number_verification/verify.json', + ) + response = number_verification.verify(access_token, phone_number='447700900000') + assert response['devicePhoneNumberVerified'] == True + + new_access_token = number_verification.exchange_code_for_token( + 'code', 'https://example.com/callback' + ) + response = number_verification.verify(new_access_token, hashed_phone_number='hash') + assert response['devicePhoneNumberVerified'] == True + + +def test_verify_error_if_both_phone_number_and_hashed_phone_number_provided( + number_verification: NumberVerification, +): + with raises( + NumberVerificationError, + match='Only one of "phone_number" and "hashed_phone_number" can be provided.', + ): + number_verification.verify( + access_token='asdf', phone_number='447700900000', hashed_phone_number='hash' + )