Skip to content

Commit

Permalink
feat: implement SimSwap authentication mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
Tr00d committed May 16, 2024
1 parent 5474055 commit fe131a2
Show file tree
Hide file tree
Showing 20 changed files with 439 additions and 42 deletions.
32 changes: 17 additions & 15 deletions Vonage.Test/NumberInsightsV2/FraudCheck/SerializationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,80 +14,82 @@ public class SerializationTest
private readonly SerializationTestHelper helper = new SerializationTestHelper(
typeof(SerializationTest).Namespace,
JsonSerializerBuilder.BuildWithSnakeCase());

[Fact]
public void ShouldDeserialize200() => this.helper.Serializer
.DeserializeObject<FraudCheckResponse>(this.helper.GetResponseJson())
.Should()
.BeSuccess(GetExpectedFraudCheckResponse());

[Fact]
public void ShouldDeserialize200WithoutFraudScore() => this.helper.Serializer
.DeserializeObject<FraudCheckResponse>(this.helper.GetResponseJson())
.Should()
.BeSuccess(GetExpectedFraudCheckResponseWithoutFraudScore());

[Fact]
public void ShouldDeserialize200WithoutSimSwap() => this.helper.Serializer
.DeserializeObject<FraudCheckResponse>(this.helper.GetResponseJson())
.Should()
.BeSuccess(GetExpectedFraudCheckResponseWithoutSimSwap());

[Fact]
public void ShouldSerializeWithFraudScore() => BuildRequestWithFraudScore()
.GetStringContent()
.Should()
.BeSuccess(this.helper.GetRequestJson());

[Fact]
public void ShouldSerializeWithFraudScoreAndSimSwap() => BuildRequestWithFraudScoreAndSimSwap()
.GetStringContent()
.Should()
.BeSuccess(this.helper.GetRequestJson());

[Fact]
public void ShouldSerializeWithSimSwap() => BuildRequestWithSimSwap()
.GetStringContent()
.Should()
.BeSuccess(this.helper.GetRequestJson());

private static FraudCheckResponse GetExpectedFraudCheckResponseWithoutFraudScore() => new FraudCheckResponse(
new Guid("6cb4c489-0fc8-4c40-8c3d-95e7e74f9450"),
"phone",
new PhoneData("16197363066", "Orange France", "MOBILE"),
Maybe<FraudScore>.None,
Maybe<SimSwap>.Some(new SimSwap(SimSwapStatus.Completed, true, "Mobile Network Operator Not Supported")));

Maybe<NumberInsightV2.FraudCheck.SimSwap>.Some(new NumberInsightV2.FraudCheck.SimSwap(SimSwapStatus.Completed,
true, "Mobile Network Operator Not Supported")));

private static FraudCheckResponse GetExpectedFraudCheckResponseWithoutSimSwap() => new FraudCheckResponse(
new Guid("6cb4c489-0fc8-4c40-8c3d-95e7e74f9450"),
"phone",
new PhoneData("16197363066", "Orange France", "MOBILE"),
Maybe<FraudScore>.Some(new FraudScore("54", RiskRecommendation.Block, FraudScoreLabel.Low, "completed")),
Maybe<SimSwap>.None);

Maybe<NumberInsightV2.FraudCheck.SimSwap>.None);
internal static Result<FraudCheckRequest> BuildRequestWithFraudScore() =>
FraudCheckRequest.Build()
.WithPhone("447009000000")
.WithFraudScore()
.Create();

internal static Result<FraudCheckRequest> BuildRequestWithFraudScoreAndSimSwap() =>
FraudCheckRequest.Build()
.WithPhone("447009000000")
.WithFraudScore()
.WithSimSwap()
.Create();

internal static Result<FraudCheckRequest> BuildRequestWithSimSwap() =>
FraudCheckRequest.Build()
.WithPhone("447009000000")
.WithSimSwap()
.Create();

internal static FraudCheckResponse GetExpectedFraudCheckResponse() => new FraudCheckResponse(
new Guid("6cb4c489-0fc8-4c40-8c3d-95e7e74f9450"),
"phone",
new PhoneData("16197363066", "Orange France", "MOBILE"),
Maybe<FraudScore>.Some(new FraudScore("54", RiskRecommendation.Flag, FraudScoreLabel.Medium, "completed")),
Maybe<SimSwap>.Some(new SimSwap(SimSwapStatus.Failed, true, "Mobile Network Operator Not Supported")));
Maybe<NumberInsightV2.FraudCheck.SimSwap>.Some(new NumberInsightV2.FraudCheck.SimSwap(SimSwapStatus.Failed,
true, "Mobile Network Operator Not Supported")));
}
68 changes: 68 additions & 0 deletions Vonage.Test/SimSwap/Authenticate/AuthenticateRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using FluentAssertions;
using Vonage.Common.Failures;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.SimSwap.Authenticate;

[Trait("Category", "Request")]
public class AuthenticateRequest
{
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Parse_ShouldReturnFailure_GivenNumberIsNullOrWhitespace(string value) =>
Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse(value).Should()
.BeFailure(ResultFailure.FromErrorMessage("Number cannot be null or whitespace."));

[Fact]
public void Parse_ShouldReturnNumberWithPlusIndicator() =>
Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse("123456789")
.Map(number => number.PhoneNumber.NumberWithInternationalIndicator)
.Should()
.BeSuccess("+123456789");

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

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

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

[Theory]
[InlineData("1234567", "1234567")]
[InlineData("123456789012345", "123456789012345")]
public void Parse_ShouldReturnSuccess_GivenNumberIsValid(string value, string expected) =>
Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse(value).Map(number => number.PhoneNumber.Number).Should()
.BeSuccess(expected);

[Theory]
[InlineData("+1234567890", "1234567890")]
[InlineData("+123456789012345", "123456789012345")]
[InlineData("+++1234567890", "1234567890")]
public void Parse_ShouldReturnSuccess_GivenNumberStartWithPlus(string value, string expected) =>
Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse(value).Map(number => number.PhoneNumber.Number).Should()
.BeSuccess(expected);

[Fact]
public void ToString_ShouldReturnNumber() =>
Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse("123456789").Map(number => number.PhoneNumber.ToString())
.Should()
.BeSuccess("123456789");

[Fact]
public void BuildAuthorizeRequest()
{
var request = Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse("123456789").GetSuccessUnsafe();
request.BuildAuthorizeRequest().Number.Should().Be(request.PhoneNumber);
}
}
15 changes: 15 additions & 0 deletions Vonage.Test/SimSwap/Authenticate/AuthorizeRequestTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.SimSwap.Authenticate;

[Trait("Category", "Request")]
public class AuthorizeRequestTest
{
[Fact]
public void GetEndpointPath_ShouldReturnApiEndpoint() =>
Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse("123456789")
.Map(request => request.BuildAuthorizeRequest())
.Map(r => r.GetEndpointPath())
.Should().BeSuccess("oauth2/bc-authorize");
}
15 changes: 15 additions & 0 deletions Vonage.Test/SimSwap/Authenticate/AuthorizeResponseTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using FluentAssertions;
using Vonage.SimSwap.Authenticate;
using Xunit;

namespace Vonage.Test.SimSwap.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"
}
40 changes: 40 additions & 0 deletions Vonage.Test/SimSwap/Authenticate/E2ETest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Net;
using System.Threading.Tasks;
using Vonage.SimSwap.Authenticate;
using Vonage.Test.Common.Extensions;
using WireMock.ResponseBuilders;
using Xunit;

namespace Vonage.Test.SimSwap.Authenticate;

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

[Fact]
public async Task Authenticate()
{
this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create()
.WithPath("/oauth2/bc-authorize")
.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.SimSwapClient
.AuthenticateAsync(Vonage.SimSwap.Authenticate.AuthenticateRequest.Parse("447700900000"))
.Should()
.BeSuccessAsync(new AuthenticateResponse("ABCDEFG"));
}
}
13 changes: 13 additions & 0 deletions Vonage.Test/SimSwap/Authenticate/GetTokenRequestTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using FluentAssertions;
using Vonage.SimSwap.Authenticate;
using Xunit;

namespace Vonage.Test.SimSwap.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/SimSwap/Authenticate/SerializationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Vonage.Serialization;
using Vonage.SimSwap.Authenticate;
using Vonage.Test.Common;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.SimSwap.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/SimSwap/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.SimSwap;

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");
}
7 changes: 7 additions & 0 deletions Vonage.Test/Vonage.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
</ItemGroup>

<ItemGroup>
<Folder Include="SimSwap\Check\"/>
<None Update="Data\NccoTests\TestRecord-request.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down Expand Up @@ -1101,6 +1102,12 @@
<None Update="Data\Webhooks\ShouldDeserializedAnswered_GivenStartTimeIsNull.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="SimSwap\Authenticate\Data\ShouldDeserializeAuthorize-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="SimSwap\Authenticate\Data\ShouldDeserializeAccessToken-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="node ../.scripts/init.js"/>
Expand Down
28 changes: 28 additions & 0 deletions Vonage/SimSwap/Authenticate/AuthenticateRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Vonage.Common;
using Vonage.Common.Monads;

namespace Vonage.SimSwap.Authenticate;

/// <summary>
/// Represents a request to authenticate towards SimSwap API.
/// </summary>
public readonly struct AuthenticateRequest
{
/// <summary>
/// Parses the input into an AuthenticateRequest.
/// </summary>
/// <param name="number">The phone number.</param>
/// <returns>Success if the input matches all requirements. Failure otherwise.</returns>
public static Result<AuthenticateRequest> Parse(string number) =>
PhoneNumber.Parse(number).Map(phoneNumber => new AuthenticateRequest
{
PhoneNumber = phoneNumber,
});

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

internal AuthorizeRequest BuildAuthorizeRequest() => new AuthorizeRequest(this.PhoneNumber);
}
7 changes: 7 additions & 0 deletions Vonage/SimSwap/Authenticate/AuthenticateResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Vonage.SimSwap.Authenticate;

/// <summary>
/// Represents an authentication response.
/// </summary>
/// <param name="AccessToken">The access token.</param>
public record AuthenticateResponse(string AccessToken);
Loading

0 comments on commit fe131a2

Please sign in to comment.