Skip to content

Commit

Permalink
feat: implement SimSwap date retrieval
Browse files Browse the repository at this point in the history
  • Loading branch information
Tr00d committed May 22, 2024
1 parent ccef03b commit a0d8d27
Show file tree
Hide file tree
Showing 16 changed files with 280 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Vonage.Test/SimSwap/Authenticate/E2ETest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task Authenticate()
.WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeAccessToken))));
await this.Helper.VonageClient.SimSwapClient
.AuthenticateAsync(AuthenticateRequest.Parse("447700900000",
"scope=openid dpv:FraudPreventionAndDetection#check-sim-swap"))
"dpv:FraudPreventionAndDetection#check-sim-swap"))
.Should()
.BeSuccessAsync(new AuthenticateResponse("ABCDEFG"));
}
Expand Down
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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"latestSimChange": "2019-08-24T14:15:22Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"phoneNumber": "346661113334"
}
57 changes: 57 additions & 0 deletions Vonage.Test/SimSwap/GetSwapDate/E2ETest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Vonage.SimSwap.GetSwapDate;
using Vonage.Test.Common.Extensions;
using WireMock.ResponseBuilders;
using Xunit;

namespace Vonage.Test.SimSwap.GetSwapDate;

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

[Fact]
public async Task GetSwapDateAsync()
{
this.SetupAuthorization();
this.SetupToken();
this.SetupSimSwap(nameof(SerializationTest.ShouldSerialize));
await this.Helper.VonageClient.SimSwapClient
.GetSwapDateAsync(GetSwapDateRequest.Parse("346661113334"))
.Should()
.BeSuccessAsync(DateTimeOffset.Parse("2019-08-24T14:15:22Z"));
}

private void SetupSimSwap(string expectedOutput) =>
this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create()
.WithPath("/camara/sim-swap/v040/retrieve-date")
.WithHeader("Authorization", "Bearer ABCDEFG")
.WithBody(this.Serialization.GetRequestJson(expectedOutput))
.UsingPost())
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK)
.WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeGetSwapDate))));

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/bc-authorize")
.WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue)
.WithBody(
"login_hint=tel:%2B346661113334&scope=openid+dpv%3AFraudPreventionAndDetection%23retrieve-sim-swap-date")
.UsingPost())
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK)
.WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserializeAuthorize))));
}
52 changes: 52 additions & 0 deletions Vonage.Test/SimSwap/GetSwapDate/RequestTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Vonage.Common.Failures;
using Vonage.SimSwap.GetSwapDate;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.SimSwap.GetSwapDate;

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

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

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

[Fact]
public void Parse_ShouldReturnFailure_GivenNumberLengthIsLowerThan15() =>
GetSwapDateRequest.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 Parse_ShouldReturnSuccess(string value, string expected) =>
GetSwapDateRequest.Parse(value)
.Map(request => request.PhoneNumber.Number)
.Should()
.BeSuccess(expected);

[Fact]
public void GetEndpointPath_ShouldReturnApiEndpoint() =>
GetSwapDateRequest.Parse("123456789")
.Map(request => request.GetEndpointPath())
.Should()
.BeSuccess("camara/sim-swap/v040/retrieve-date");
}
49 changes: 49 additions & 0 deletions Vonage.Test/SimSwap/GetSwapDate/SerializationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using Vonage.Serialization;
using Vonage.SimSwap.Authenticate;
using Vonage.SimSwap.GetSwapDate;
using Vonage.Test.Common;
using Vonage.Test.Common.Extensions;
using Xunit;

namespace Vonage.Test.SimSwap.GetSwapDate;

[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());

[Fact]
public void ShouldDeserializeGetSwapDate() => this.helper.Serializer
.DeserializeObject<GetSwapDateResponse>(this.helper.GetResponseJson())
.Should()
.BeSuccess(GetExpectedResponse());

[Fact]
public void ShouldSerialize() =>
GetSwapDateRequest.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 GetSwapDateResponse GetExpectedResponse() =>
new GetSwapDateResponse(DateTimeOffset.Parse("2019-08-24T14:15:22Z"));
}
12 changes: 12 additions & 0 deletions Vonage.Test/Vonage.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,18 @@
<None Update="SimSwap\Check\Data\ShouldSerializeWithPeriod-request.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="SimSwap\GetSwapDate\Data\ShouldDeserializeAccessToken-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="SimSwap\GetSwapDate\Data\ShouldDeserializeAuthorize-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="SimSwap\GetSwapDate\Data\ShouldDeserializeGetSwapDate-response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="SimSwap\GetSwapDate\Data\ShouldSerialize-request.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="node ../.scripts/init.js"/>
Expand Down
2 changes: 1 addition & 1 deletion Vonage/SimSwap/Authenticate/AuthenticateRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ namespace Vonage.SimSwap.Authenticate;
private static Result<AuthenticateRequest> VerifyScope(AuthenticateRequest request) =>
InputValidation.VerifyNotEmpty(request, request.Scope, nameof(request.Scope));

internal AuthorizeRequest BuildAuthorizeRequest() => new AuthorizeRequest(this.PhoneNumber);
internal AuthorizeRequest BuildAuthorizeRequest() => new AuthorizeRequest(this.PhoneNumber, this.Scope);
}
6 changes: 3 additions & 3 deletions Vonage/SimSwap/Authenticate/AuthorizeRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Vonage.SimSwap.Authenticate;

internal record AuthorizeRequest(PhoneNumber Number) : IVonageRequest
internal record AuthorizeRequest(PhoneNumber Number, string Scope) : IVonageRequest
{
public string GetEndpointPath() => "oauth2/bc-authorize";

Expand All @@ -21,8 +21,8 @@ private string GetUrlEncoded()
var builder = new StringBuilder();
builder.Append("login_hint=tel:");
builder.Append(WebUtility.UrlEncode(this.Number.NumberWithInternationalIndicator));
builder.Append("&scope=");
builder.Append(WebUtility.UrlEncode("openid dpv:FraudPreventionAndDetection#check-sim-swap"));
builder.Append("&scope=openid");
builder.Append(WebUtility.UrlEncode($" {this.Scope}"));
return builder.ToString();
}

Expand Down
8 changes: 2 additions & 6 deletions Vonage/SimSwap/Check/CheckRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,10 @@ namespace Vonage.SimSwap.Check;
[JsonPropertyName("maxAge")]
public int Period { get; internal init; }

/// <summary>
/// The authorization scope for the token.
/// </summary>
[JsonIgnore]
public string Scope => "scope=openid dpv:FraudPreventionAndDetection#check-sim-swap";
private static string Scope => "dpv:FraudPreventionAndDetection#check-sim-swap";

internal Result<AuthenticateRequest> BuildAuthenticationRequest() =>
AuthenticateRequest.Parse(this.PhoneNumber.NumberWithInternationalIndicator, this.Scope);
AuthenticateRequest.Parse(this.PhoneNumber.NumberWithInternationalIndicator, Scope);

/// <summary>
/// Initializes a builder.
Expand Down
53 changes: 53 additions & 0 deletions Vonage/SimSwap/GetSwapDate/GetSwapDateRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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.Serialization;
using Vonage.SimSwap.Authenticate;

namespace Vonage.SimSwap.GetSwapDate;

/// <summary>
/// Represents a request to retrieve a SIM swap date.
/// </summary>
public readonly struct GetSwapDateRequest : IVonageRequest
{
/// <inheritdoc />
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");

/// <inheritdoc />
public string GetEndpointPath() => "camara/sim-swap/v040/retrieve-date";

/// <summary>
/// Subscriber number in E.164 format (starting with country code). Optionally prefixed with '+'.
/// </summary>
[JsonConverter(typeof(PhoneNumberJsonConverter))]
[JsonPropertyName("phoneNumber")]
public PhoneNumber PhoneNumber { get; internal init; }

private static string Scope => "dpv:FraudPreventionAndDetection#retrieve-sim-swap-date";

internal Result<AuthenticateRequest> BuildAuthenticationRequest() =>
AuthenticateRequest.Parse(this.PhoneNumber.NumberWithInternationalIndicator, Scope);

/// <summary>
/// Parses the input into an GetSwapDateRequest.
/// </summary>
/// <param name="number">The phone number.</param>
/// <returns>Success if the input matches all requirements. Failure otherwise.</returns>
public static Result<GetSwapDateRequest> Parse(string number) =>
PhoneNumber.Parse(number).Map(phoneNumber => new GetSwapDateRequest
{
PhoneNumber = phoneNumber,
});
}
8 changes: 8 additions & 0 deletions Vonage/SimSwap/GetSwapDate/GetSwapDateResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;
using System.Text.Json.Serialization;

namespace Vonage.SimSwap.GetSwapDate;

internal record GetSwapDateResponse(
[property: JsonPropertyName("latestSimChange")]
DateTimeOffset LatestSimChange);
11 changes: 10 additions & 1 deletion Vonage/SimSwap/ISimSwapClient.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Vonage.Common.Monads;
using Vonage.SimSwap.Authenticate;
using Vonage.SimSwap.Check;
using Vonage.SimSwap.GetSwapDate;

namespace Vonage.SimSwap;

Expand All @@ -23,4 +25,11 @@ public interface ISimSwapClient
/// <param name="request">The request.</param>
/// <returns>Success or Failure.</returns>
Task<Result<bool>> CheckAsync(Result<CheckRequest> request);

/// <summary>
/// Get timestamp of last MSISDN/IMSI pairing change for a mobile user account provided with MSIDN.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Success or Failure.</returns>
Task<Result<DateTimeOffset>> GetSwapDateAsync(Result<GetSwapDateRequest> request);
}
17 changes: 16 additions & 1 deletion Vonage/SimSwap/SimSwapClient.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Net.Http.Headers;
using System;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Vonage.Common.Client;
using Vonage.Common.Monads;
using Vonage.Serialization;
using Vonage.SimSwap.Authenticate;
using Vonage.SimSwap.Check;
using Vonage.SimSwap.GetSwapDate;

namespace Vonage.SimSwap;

Expand All @@ -31,6 +33,16 @@ await request.BindAsync(this.AuthenticateCheckRequest)
.BindAsync(client => client.SendWithResponseAsync<CheckRequest, CheckResponse>(request))
.Map(response => response.Swapped);

/// <inheritdoc />
public async Task<Result<DateTimeOffset>> GetSwapDateAsync(Result<GetSwapDateRequest> request)
{
return await request.BindAsync(this.AuthenticateGetSwapDateRequest)
.Map(BuildAuthenticationHeader)
.Map(this.BuildClientWithAuthenticationHeader)
.BindAsync(client => client.SendWithResponseAsync<GetSwapDateRequest, GetSwapDateResponse>(request))
.Map(response => response.LatestSimChange);
}

private VonageHttpClient BuildClientWithAuthenticationHeader(AuthenticationHeaderValue header) =>
this.vonageClient.WithDifferentHeader(header);

Expand All @@ -40,6 +52,9 @@ await request.BindAsync(this.AuthenticateCheckRequest)
private Task<Result<AuthenticateResponse>> AuthenticateCheckRequest(CheckRequest request) =>
this.AuthenticateAsync(request.BuildAuthenticationRequest());

private Task<Result<AuthenticateResponse>> AuthenticateGetSwapDateRequest(GetSwapDateRequest request) =>
this.AuthenticateAsync(request.BuildAuthenticationRequest());

private static AuthenticateResponse BuildAuthenticateResponse(GetTokenResponse response) =>
new AuthenticateResponse(response.AccessToken);

Expand Down

0 comments on commit a0d8d27

Please sign in to comment.