Skip to content

Commit

Permalink
feat: implement authentication mechanism for NumberVerification API
Browse files Browse the repository at this point in the history
  • Loading branch information
Tr00d committed Jun 19, 2024
1 parent c3615eb commit dfa1c39
Show file tree
Hide file tree
Showing 22 changed files with 477 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using FluentAssertions;
using Vonage.Common.Failures;
using Vonage.NumberVerification.Authenticate;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[Trait("Category", "Request")]
public class AuthenticateRequestTest
{
private const string ValidScope = "scope=test";

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void ParseFromPhoneNumber_ShouldReturnFailure_GivenNumberIsNullOrWhitespace(string value) =>
AuthenticateRequest.Parse(value, ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number cannot be null or whitespace."));

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void ParseFromPhoneNumber_ShouldReturnFailure_GivenScopeIsNullOrWhitespace(string value) =>
AuthenticateRequest.Parse("1234567", value).Should()
.BeParsingFailure("Scope cannot be null or whitespace.");

[Fact]
public void ParseFromPhoneNumber_ShouldReturnFailure_GivenNumberContainsNonDigits() =>
AuthenticateRequest.Parse("1234567abc123", ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number can only contain digits."));

[Fact]
public void ParseFromPhoneNumber_ShouldReturnFailure_GivenNumberLengthIsLowerThan7() =>
AuthenticateRequest.Parse("123456", ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number length cannot be lower than 7."));

[Fact]
public void ParseFromPhoneNumber_ShouldReturnFailure_GivenNumberLengthIsHigherThan15() =>
AuthenticateRequest.Parse("1234567890123456", ValidScope).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 ParseFromPhoneNumber_ShouldSetNumber(string value, string expected) =>
AuthenticateRequest.Parse(value, ValidScope)
.Map(request => request.PhoneNumber.Number)
.Should()
.BeSuccess(expected);

[Fact]
public void ParseFromPhoneNumber_ShouldSetScope() =>
AuthenticateRequest.Parse("1234567", ValidScope)
.Map(request => request.Scope)
.Should()
.BeSuccess(ValidScope);

[Fact]
public void BuildAuthorizeRequest()
{
var request = AuthenticateRequest.Parse("123456789", ValidScope).GetSuccessUnsafe();
request.BuildAuthorizeRequest().Number.Should().Be(request.PhoneNumber);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Net.Http.Headers;
using FluentAssertions;
using Vonage.NumberVerification.Authenticate;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[Trait("Category", "Request")]
public class AuthenticateResponseTest
{
[Fact]
public void BuildAuthenticationHeader_ShouldReturnBearerAuth() =>
new AuthenticateResponse("123456789")
.BuildAuthenticationHeader()
.Should()
.Be(new AuthenticationHeaderValue("Bearer", "123456789"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Vonage.NumberVerification.Authenticate;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[Trait("Category", "Request")]
public class AuthorizeRequestTest
{
[Fact]
public void GetEndpointPath_ShouldReturnApiEndpoint() =>
AuthenticateRequest.Parse("123456789", "scope")
.Map(request => request.BuildAuthorizeRequest())
.Map(r => r.GetEndpointPath())
.Should().BeSuccess("oauth2/auth");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using FluentAssertions;
using Vonage.NumberVerification.Authenticate;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[Trait("Category", "Request")]
public class AuthorizeResponseTest
{
[Fact]
public void BuildGetTokenRequest() =>
new AuthorizeResponse("123456", 0, 0)
.BuildGetTokenRequest()
.Should().Be(new GetTokenRequest("123456"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"access_token": "ABCDEFG",
"token_type": "Bearer",
"expires_in": 3600
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"auth_req_id": "123456789",
"expires_in": 120,
"interval": 2
}
41 changes: 41 additions & 0 deletions Vonage.Test/NumberVerification/Authenticate/E2ETest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Net;
using System.Threading.Tasks;
using Vonage.NumberVerification.Authenticate;
using Vonage.Test.Common.Extensions;
using WireMock.ResponseBuilders;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[Trait("Category", "E2E")]
public class E2ETest : SimSwap.E2EBase
{
public E2ETest() : base(typeof(E2ETest).Namespace)
{
}

[Fact]
public async Task Authenticate()
{
this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create()
.WithPath("/oauth2/auth")
.WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue)
.WithBody(
"login_hint=tel:%2B447700900000&scope=openid+dpv%3AFraudPreventionAndDetection%23check-sim-swap")
.UsingPost())
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK)
.WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeAuthorize))));
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))));
await this.Helper.VonageClient.NumberVerificationClient
.AuthenticateAsync(AuthenticateRequest.Parse("447700900000",
"dpv:FraudPreventionAndDetection#check-sim-swap"))
.Should()
.BeSuccessAsync(new AuthenticateResponse("ABCDEFG"));
}
}
13 changes: 13 additions & 0 deletions Vonage.Test/NumberVerification/Authenticate/GetTokenRequestTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using FluentAssertions;
using Vonage.NumberVerification.Authenticate;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[Trait("Category", "Request")]
public class GetTokenRequestTest
{
[Fact]
public void GetEndpointPath_ShouldReturnApiEndpoint() =>
new GetTokenRequest("123456").GetEndpointPath().Should().Be("oauth2/token");
}
31 changes: 31 additions & 0 deletions Vonage.Test/NumberVerification/Authenticate/SerializationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Vonage.NumberVerification.Authenticate;
using Vonage.Serialization;
using Vonage.Test.Common;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.NumberVerification.Authenticate;

[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<AuthorizeResponse>(this.helper.GetResponseJson())
.Should()
.BeSuccess(GetExpectedAuthorizeResponse());

[Fact]
public void ShouldDeserializeAccessToken() => this.helper.Serializer
.DeserializeObject<GetTokenResponse>(this.helper.GetResponseJson())
.Should()
.BeSuccess(GetExpectedTokenResponse());

internal static AuthorizeResponse GetExpectedAuthorizeResponse() => new AuthorizeResponse("123456789", 120, 2);

internal static GetTokenResponse GetExpectedTokenResponse() => new GetTokenResponse("ABCDEFG", "Bearer", 3600);
}
16 changes: 16 additions & 0 deletions Vonage.Test/NumberVerification/E2EBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Vonage.Serialization;
using Vonage.Test.Common;
using Vonage.Test.TestHelpers;

namespace Vonage.Test.NumberVerification;

public class E2EBase
{
internal readonly TestingContext Helper;
internal readonly SerializationTestHelper Serialization;

protected E2EBase(string serializationNamespace) : this() => this.Serialization =
new SerializationTestHelper(serializationNamespace, JsonSerializerBuilder.BuildWithSnakeCase());

protected E2EBase() => this.Helper = TestingContext.WithBearerCredentials("Url.Api.EMEA");
}
20 changes: 10 additions & 10 deletions Vonage.Test/SimSwap/Authenticate/AuthenticateRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,38 @@ namespace Vonage.Test.SimSwap.Authenticate;
public class AuthenticateRequestTest
{
private const string ValidScope = "scope=test";

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Parse_ShouldReturnFailure_GivenNumberIsNullOrWhitespace(string value) =>
AuthenticateRequest.Parse(value, ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number cannot be null or whitespace."));

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Parse_ShouldReturnFailure_GivenScopeIsNullOrWhitespace(string value) =>
AuthenticateRequest.Parse("1234567", value).Should()
.BeParsingFailure("Scope cannot be null or whitespace.");

[Fact]
public void Parse_ShouldReturnFailure_GivenNumberContainsNonDigits() =>
AuthenticateRequest.Parse("1234567abc123", ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number can only contain digits."));

[Fact]
public void Parse_ShouldReturnFailure_GivenNumberLengthIsHigherThan7() =>
public void Parse_ShouldReturnFailure_GivenNumberLengthIsLowerThan7() =>
AuthenticateRequest.Parse("123456", ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number length cannot be lower than 7."));

[Fact]
public void Parse_ShouldReturnFailure_GivenNumberLengthIsLowerThan15() =>
public void Parse_ShouldReturnFailure_GivenNumberLengthIsHigherThan15() =>
AuthenticateRequest.Parse("1234567890123456", ValidScope).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number length cannot be higher than 15."));

[Theory]
[InlineData("1234567", "1234567")]
[InlineData("123456789012345", "123456789012345")]
Expand All @@ -53,14 +53,14 @@ public class AuthenticateRequestTest
.Map(request => request.PhoneNumber.Number)
.Should()
.BeSuccess(expected);

[Fact]
public void Parse_ShouldSetScope() =>
AuthenticateRequest.Parse("1234567", ValidScope)
.Map(request => request.Scope)
.Should()
.BeSuccess(ValidScope);

[Fact]
public void BuildAuthorizeRequest()
{
Expand Down
6 changes: 6 additions & 0 deletions Vonage.Test/Vonage.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,12 @@
<None Update="Conversations\CreateEvent\Data\ShouldDeserialize200-response.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="NumberVerification\Authenticate\Data\ShouldDeserializeAccessToken-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="NumberVerification\Authenticate\Data\ShouldDeserializeAuthorize-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="node ../.scripts/init.js"/>
Expand Down
41 changes: 41 additions & 0 deletions Vonage/NumberVerification/Authenticate/AuthenticateRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Vonage.Common;
using Vonage.Common.Monads;
using Vonage.Common.Validation;

namespace Vonage.NumberVerification.Authenticate;

/// <summary>
/// Represents a request to authenticate towards NumberVerification API.
/// </summary>
public readonly struct AuthenticateRequest
{
/// <summary>
/// Parses the input into an AuthenticateRequest.
/// </summary>
/// <param name="number">The phone number.</param>
/// <param name="tokenScope">The authorization scope for the token.</param>
/// <returns>Success if the input matches all requirements. Failure otherwise.</returns>
public static Result<AuthenticateRequest> Parse(string number, string tokenScope) =>
PhoneNumber.Parse(number).Map(phoneNumber => new AuthenticateRequest
{
PhoneNumber = phoneNumber,
Scope = tokenScope,
})
.Map(InputEvaluation<AuthenticateRequest>.Evaluate)
.Bind(evaluation => evaluation.WithRules(VerifyScope));

/// <summary>
/// Subscriber number in E.164 format (starting with country code). Optionally prefixed with '+'.
/// </summary>
public PhoneNumber PhoneNumber { get; private init; }

/// <summary>
/// The authorization scope for the token.
/// </summary>
public string Scope { get; private init; }

private static Result<AuthenticateRequest> VerifyScope(AuthenticateRequest request) =>
InputValidation.VerifyNotEmpty(request, request.Scope, nameof(request.Scope));

internal AuthorizeRequest BuildAuthorizeRequest() => new AuthorizeRequest(this.PhoneNumber, this.Scope);
}
16 changes: 16 additions & 0 deletions Vonage/NumberVerification/Authenticate/AuthenticateResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Net.Http.Headers;

namespace Vonage.NumberVerification.Authenticate;

/// <summary>
/// Represents an authentication response.
/// </summary>
/// <param name="AccessToken">The access token.</param>
public record AuthenticateResponse(string AccessToken)
{
/// <summary>
/// </summary>
/// <returns></returns>
public AuthenticationHeaderValue BuildAuthenticationHeader() =>
new AuthenticationHeaderValue("Bearer", this.AccessToken);
}
Loading

0 comments on commit dfa1c39

Please sign in to comment.