diff --git a/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeAccessToken-response.json b/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeAccessToken-response.json new file mode 100644 index 00000000..73ab58da --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeAccessToken-response.json @@ -0,0 +1,5 @@ +{ + "access_token": "ABCDEFG", + "token_type": "Bearer", + "expires_in": 3600 +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeAuthorize-response.json b/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeAuthorize-response.json new file mode 100644 index 00000000..0d578eea --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeAuthorize-response.json @@ -0,0 +1,5 @@ +{ + "auth_req_id": "123456789", + "expires_in": 120, + "interval": 2 +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeVerify-response.json b/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeVerify-response.json new file mode 100644 index 00000000..efa5b08e --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/Data/ShouldDeserializeVerify-response.json @@ -0,0 +1,3 @@ +{ + "devicePhoneNumberVerified": true +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/Data/ShouldSerialize-request.json b/Vonage.Test/NumberVerification/Verify/Data/ShouldSerialize-request.json new file mode 100644 index 00000000..c67cde88 --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/Data/ShouldSerialize-request.json @@ -0,0 +1,3 @@ +{ + "phoneNumber": "346661113334" +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/Data/ShouldSerializeWithPeriod-request.json b/Vonage.Test/NumberVerification/Verify/Data/ShouldSerializeWithPeriod-request.json new file mode 100644 index 00000000..687bc593 --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/Data/ShouldSerializeWithPeriod-request.json @@ -0,0 +1,4 @@ +{ + "phoneNumber": "346661113334", + "maxAge": 15 +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/E2ETest.cs b/Vonage.Test/NumberVerification/Verify/E2ETest.cs new file mode 100644 index 00000000..7626d1da --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/E2ETest.cs @@ -0,0 +1,56 @@ +using System.Net; +using System.Threading.Tasks; +using Vonage.NumberVerification.Verify; +using Vonage.Test.Common.Extensions; +using WireMock.ResponseBuilders; +using Xunit; + +namespace Vonage.Test.NumberVerification.Verify; + +[Trait("Category", "E2E")] +public class E2ETest : SimSwap.E2EBase +{ + public E2ETest() : base(typeof(E2ETest).Namespace) + { + } + + [Fact] + public async Task CheckAsync() + { + this.SetupAuthorization(); + this.SetupToken(); + this.SetupCheck(nameof(SerializationTest.ShouldSerialize)); + await this.Helper.VonageClient.NumberVerificationClient + .VerifyAsync(VerifyRequest.Parse("346661113334")) + .Should() + .BeSuccessAsync(true); + } + + private void SetupCheck(string expectedOutput) => + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/camara/number-verification/v031/verify") + .WithHeader("Authorization", "Bearer ABCDEFG") + .WithBody(this.Serialization.GetRequestJson(expectedOutput)) + .UsingPost()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeVerify)))); + + private void SetupToken() => + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/oauth2/token") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .WithBody("auth_req_id=123456789&grant_type=urn:openid:params:grant-type:ciba") + .UsingPost()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeAccessToken)))); + + private void SetupAuthorization() => + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/oauth2/auth") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .WithBody( + "login_hint=tel:%2B346661113334&scope=openid+dpv%3AFraudPreventionAndDetection%23number-verification-verify-read") + .UsingPost()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeAuthorize)))); +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/RequestTest.cs b/Vonage.Test/NumberVerification/Verify/RequestTest.cs new file mode 100644 index 00000000..fc757f43 --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/RequestTest.cs @@ -0,0 +1,56 @@ +using Vonage.Common.Failures; +using Vonage.NumberVerification.Verify; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.NumberVerification.Verify; + +[Trait("Category", "Request")] +public class RequestTest +{ + [Fact] + public void GetEndpointPath_ShouldReturnApiEndpoint() => + VerifyRequest.Parse("123456789") + .Map(request => request.GetEndpointPath()) + .Should() + .BeSuccess("camara/number-verification/v031/verify"); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Build_ShouldReturnFailure_GivenNumberIsNullOrWhitespace(string value) => + VerifyRequest.Parse(value) + .Should() + .BeFailure(ResultFailure.FromErrorMessage("Number cannot be null or whitespace.")); + + [Fact] + public void Build_ShouldReturnFailure_GivenNumberContainsNonDigits() => + VerifyRequest.Parse("123456abc789") + .Should() + .BeFailure(ResultFailure.FromErrorMessage("Number can only contain digits.")); + + [Fact] + public void Build_ShouldReturnFailure_GivenNumberLengthIsHigherThan7() => + VerifyRequest.Parse("123456") + .Should() + .BeFailure(ResultFailure.FromErrorMessage("Number length cannot be lower than 7.")); + + [Fact] + public void Build_ShouldReturnFailure_GivenNumberLengthIsLowerThan15() => + VerifyRequest.Parse("1234567890123456") + .Should() + .BeFailure(ResultFailure.FromErrorMessage("Number length cannot be higher than 15.")); + + [Theory] + [InlineData("1234567", "1234567")] + [InlineData("123456789012345", "123456789012345")] + [InlineData("+1234567890", "1234567890")] + [InlineData("+123456789012345", "123456789012345")] + [InlineData("+++1234567890", "1234567890")] + public void Build_ShouldSetPhoneNumber_GivenNumberIsValid(string value, string expected) => + VerifyRequest.Parse(value) + .Map(number => number.PhoneNumber.Number) + .Should() + .BeSuccess(expected); +} \ No newline at end of file diff --git a/Vonage.Test/NumberVerification/Verify/SerializationTest.cs b/Vonage.Test/NumberVerification/Verify/SerializationTest.cs new file mode 100644 index 00000000..f78ee1cd --- /dev/null +++ b/Vonage.Test/NumberVerification/Verify/SerializationTest.cs @@ -0,0 +1,47 @@ +using Vonage.NumberVerification.Authenticate; +using Vonage.NumberVerification.Verify; +using Vonage.Serialization; +using Vonage.Test.Common; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.NumberVerification.Verify; + +[Trait("Category", "Serialization")] +public class SerializationTest +{ + private readonly SerializationTestHelper helper = new SerializationTestHelper( + typeof(SerializationTest).Namespace, + JsonSerializerBuilder.BuildWithSnakeCase()); + + [Fact] + public void ShouldDeserializeAuthorize() => this.helper.Serializer + .DeserializeObject(this.helper.GetResponseJson()) + .Should() + .BeSuccess(GetExpectedAuthorizeResponse()); + + [Fact] + public void ShouldDeserializeAccessToken() => this.helper.Serializer + .DeserializeObject(this.helper.GetResponseJson()) + .Should() + .BeSuccess(GetExpectedTokenResponse()); + + [Fact] + public void ShouldDeserializeVerify() => this.helper.Serializer + .DeserializeObject(this.helper.GetResponseJson()) + .Should() + .BeSuccess(GetExpectedResponse()); + + [Fact] + public void ShouldSerialize() => + VerifyRequest.Parse("346661113334") + .GetStringContent() + .Should() + .BeSuccess(this.helper.GetRequestJson()); + + private static AuthorizeResponse GetExpectedAuthorizeResponse() => new AuthorizeResponse("123456789", 120, 2); + + private static GetTokenResponse GetExpectedTokenResponse() => new GetTokenResponse("ABCDEFG", "Bearer", 3600); + + private static VerifyResponse GetExpectedResponse() => new VerifyResponse(true); +} \ No newline at end of file diff --git a/Vonage.Test/Vonage.Test.csproj b/Vonage.Test/Vonage.Test.csproj index 0d2b3faf..b11b5a15 100644 --- a/Vonage.Test/Vonage.Test.csproj +++ b/Vonage.Test/Vonage.Test.csproj @@ -1218,6 +1218,21 @@ Always + + Always + + + Always + + + Always + + + Always + + + Always + diff --git a/Vonage/NumberVerification/INumberVerificationClient.cs b/Vonage/NumberVerification/INumberVerificationClient.cs index 9c40a495..1a1ecb70 100644 --- a/Vonage/NumberVerification/INumberVerificationClient.cs +++ b/Vonage/NumberVerification/INumberVerificationClient.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Vonage.Common.Monads; using Vonage.NumberVerification.Authenticate; +using Vonage.NumberVerification.Verify; namespace Vonage.NumberVerification; @@ -15,4 +16,11 @@ public interface INumberVerificationClient /// The request. /// Success or Failure. Task> AuthenticateAsync(Result request); + + /// + /// Verifies if the specified phone number matches the one that the user is currently using. + /// + /// The request. + /// Success or Failure. + Task> VerifyAsync(Result request); } \ No newline at end of file diff --git a/Vonage/NumberVerification/NumberVerificationClient.cs b/Vonage/NumberVerification/NumberVerificationClient.cs index de06d79f..7c29dac5 100644 --- a/Vonage/NumberVerification/NumberVerificationClient.cs +++ b/Vonage/NumberVerification/NumberVerificationClient.cs @@ -1,7 +1,9 @@ -using System.Threading.Tasks; +using System.Net.Http.Headers; +using System.Threading.Tasks; using Vonage.Common.Client; using Vonage.Common.Monads; using Vonage.NumberVerification.Authenticate; +using Vonage.NumberVerification.Verify; using Vonage.Serialization; namespace Vonage.NumberVerification; @@ -21,6 +23,25 @@ public Task> AuthenticateAsync(Result + public async Task> VerifyAsync(Result request) => + await request + .Map(BuildAuthenticationRequest) + .BindAsync(this.AuthenticateAsync) + .Map(BuildAuthenticationHeader) + .Map(this.BuildClientWithAuthenticationHeader) + .BindAsync(client => client.SendWithResponseAsync(request)) + .Map(response => response.Verified); + + private VonageHttpClient BuildClientWithAuthenticationHeader(AuthenticationHeaderValue header) => + this.vonageClient.WithDifferentHeader(header); + + private static Result BuildAuthenticationRequest(VerifyRequest request) => + request.BuildAuthenticationRequest(); + + private static AuthenticationHeaderValue BuildAuthenticationHeader(AuthenticateResponse authentication) => + authentication.BuildAuthenticationHeader(); + private static AuthenticateResponse BuildAuthenticateResponse(GetTokenResponse response) => new AuthenticateResponse(response.AccessToken); diff --git a/Vonage/NumberVerification/Verify/VerifyRequest.cs b/Vonage/NumberVerification/Verify/VerifyRequest.cs new file mode 100644 index 00000000..d60b5a7f --- /dev/null +++ b/Vonage/NumberVerification/Verify/VerifyRequest.cs @@ -0,0 +1,54 @@ +using System.Net.Http; +using System.Text; +using System.Text.Json.Serialization; +using Vonage.Common; +using Vonage.Common.Client; +using Vonage.Common.Monads; +using Vonage.Common.Serialization; +using Vonage.NumberVerification.Authenticate; +using Vonage.Serialization; + +namespace Vonage.NumberVerification.Verify; + +/// +/// Represents a request to verify if the specified phone number matches the one that the user is currently using. +/// +public readonly struct VerifyRequest : IVonageRequest +{ + /// + public HttpRequestMessage BuildRequestMessage() => VonageRequestBuilder + .Initialize(HttpMethod.Post, this.GetEndpointPath()) + .WithContent(this.GetRequestContent()) + .Build(); + + private StringContent GetRequestContent() => + new StringContent(JsonSerializerBuilder.BuildWithSnakeCase().SerializeObject(this), Encoding.UTF8, + "application/json"); + + /// + public string GetEndpointPath() => "camara/number-verification/v031/verify"; + + /// + /// Subscriber number in E.164 format (starting with country code). Optionally prefixed with '+'. + /// + [JsonConverter(typeof(PhoneNumberJsonConverter))] + [JsonPropertyOrder(0)] + [JsonPropertyName("phoneNumber")] + public PhoneNumber PhoneNumber { get; internal init; } + + private static string Scope => "dpv:FraudPreventionAndDetection#number-verification-verify-read"; + + internal Result BuildAuthenticationRequest() => + AuthenticateRequest.Parse(this.PhoneNumber.NumberWithInternationalIndicator, Scope); + + /// + /// Parses the input into an VerifyRequest. + /// + /// The phone number. + /// Success if the input matches all requirements. Failure otherwise. + public static Result Parse(string number) => + PhoneNumber.Parse(number).Map(phoneNumber => new VerifyRequest + { + PhoneNumber = phoneNumber, + }); +} \ No newline at end of file diff --git a/Vonage/NumberVerification/Verify/VerifyResponse.cs b/Vonage/NumberVerification/Verify/VerifyResponse.cs new file mode 100644 index 00000000..2ffe7b42 --- /dev/null +++ b/Vonage/NumberVerification/Verify/VerifyResponse.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace Vonage.NumberVerification.Verify; + +internal record VerifyResponse( + [property: JsonPropertyName("devicePhoneNumberVerified")] + bool Verified); \ No newline at end of file diff --git a/Vonage/Vonage.csproj b/Vonage/Vonage.csproj index 64db36c6..158022de 100644 --- a/Vonage/Vonage.csproj +++ b/Vonage/Vonage.csproj @@ -80,9 +80,6 @@ - - -