Skip to content

Commit

Permalink
feat: verify V2 (#376)
Browse files Browse the repository at this point in the history
* Add directory structure

* Add builder for start verification request

* Implement StartVerificationBuilder for SMS

* Add StartSmsVerificationRequestTest

* Implement serialization test for StartSmsVerificationRequest

* Implement flow StartSmsVerification

* Implement WhatsApp verification workflow

* Refactor VerificationRequest

* Implement Result.Merge

* WIP - grouping requests to common builder

* Implement generic builder, use PhoneNumber to enforce E.164 format validation

* Simplify workflow parsing

* Add WhatsAppInteractive workflow

* Simplify test names

* Implement Voice verification

* Implement Email verification

* Implement SilentAuth verification

* Move Client to internal visibility, removing the Client property from the VonageClient instance (consumers can't use VerifyV2Client.cs yet)

* Implement VerifyCode

* Fixes from test run

* Remove verify client

* Unify all verification workflows

* Handling fallback workflows

* Remove test app

* Code cleanup

* Remove console app from file system

* Fix merge conflicts

* Fix issues in project file
  • Loading branch information
Tr00d authored Apr 14, 2023
1 parent 5e4d741 commit 5a3828a
Show file tree
Hide file tree
Showing 49 changed files with 2,347 additions and 17 deletions.
21 changes: 21 additions & 0 deletions Vonage.Common.Test/MailAddressTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Vonage.Common.Failures;
using Vonage.Common.Test.Extensions;

namespace Vonage.Common.Test;

public class MailAddressTest
{
[Theory]
[InlineData("dummy-vonage.com")]
[InlineData("123456789")]
[InlineData("test@test@test.com")]
public void Parse_ShouldReturnFailure_GivenAddressIsInvalid(string email) =>
MailAddress.Parse(email).Map(value => value.Address).Should()
.BeFailure(ResultFailure.FromErrorMessage("Email is invalid."));

[Theory]
[InlineData("dummy@vonage.com")]
[InlineData("dum@von.co.uk")]
public void Parse_ShouldReturnSuccess_GivenAddressIsValid(string email) =>
MailAddress.Parse(email).Map(value => value.Address).Should().BeSuccess(email);
}
25 changes: 25 additions & 0 deletions Vonage.Common.Test/Monads/ResultTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,31 @@ public void Match_ShouldReturnSuccessOperation_GivenValueIsSuccess() =>
.Should()
.Be(6);

[Fact]
public void Merge_ShouldReturnFailure_GivenFirstMonadIsFailure() =>
CreateFailure()
.Merge(CreateSuccess(5), (first, second) => new {First = first, Second = second})
.Should()
.BeFailure(CreateResultFailure());

[Fact]
public void Merge_ShouldReturnFailure_GivenSecondMonadIsFailure() =>
CreateSuccess(5)
.Merge(CreateFailure(), (first, second) => new {First = first, Second = second})
.Should()
.BeFailure(CreateResultFailure());

[Fact]
public void Merge_ShouldReturnSuccess_GivenBothResultsAreSuccess() =>
CreateSuccess(5)
.Merge(CreateSuccess(10), (first, second) => new {First = first, Second = second})
.Should()
.BeSuccess(success =>
{
success.First.Should().Be(5);
success.Second.Should().Be(10);
});

private static Result<int> CreateFailure() =>
Result<int>.FromFailure(CreateResultFailure());

Expand Down
52 changes: 52 additions & 0 deletions Vonage.Common.Test/PhoneNumberTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Vonage.Common.Failures;
using Vonage.Common.Test.Extensions;

namespace Vonage.Common.Test;

public class PhoneNumberTest
{
[Fact]
public void NumberWithInternationalIndicator_ShouldReturnNumberWithPlusIndicator() =>
PhoneNumber.Parse("123456789").Map(number => number.NumberWithInternationalIndicator).Should()
.BeSuccess("+123456789");

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

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

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

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

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

[Fact]
public void ToString_ShouldReturnNumber() =>
PhoneNumber.Parse("123456789").Map(number => number.ToString()).Should().BeSuccess("123456789");
}
5 changes: 5 additions & 0 deletions Vonage.Common/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public JsonSerializer()
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
this.settings.Converters.Add(new ColorJsonConverter());
this.settings.Converters.Add(new PhoneNumberJsonConverter());
this.settings.Converters.Add(new EmailJsonConverter());
}

/// <summary>
Expand All @@ -39,6 +41,9 @@ public JsonSerializer()
public JsonSerializer(JsonNamingPolicy namingPolicy) : this() =>
this.settings.PropertyNamingPolicy = namingPolicy;

public JsonSerializer(JsonSerializerOptions options) : this() =>
this.settings = options;

/// <summary>
/// </summary>
/// <param name="converters"></param>
Expand Down
38 changes: 38 additions & 0 deletions Vonage.Common/MailAddress.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using Vonage.Common.Failures;
using Vonage.Common.Monads;

namespace Vonage.Common;

/// <summary>
/// Represents an email address.
/// </summary>
public readonly struct MailAddress
{
private const string InvalidEmail = "Email is invalid.";

private MailAddress(string email) => this.Address = email;

/// <summary>
/// The mail address.
/// </summary>
public string Address { get; }

/// <summary>
/// Parses an email.
/// </summary>
/// <param name="email">The email.</param>
/// <returns>Success or failure.</returns>
public static Result<MailAddress> Parse(string email)
{
try
{
_ = new System.Net.Mail.MailAddress(email);
return Result<MailAddress>.FromSuccess(new MailAddress(email));
}
catch (Exception)
{
return Result<MailAddress>.FromFailure(ResultFailure.FromErrorMessage(InvalidEmail));
}
}
}
12 changes: 12 additions & 0 deletions Vonage.Common/Monads/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@ public async Task<Result<TB>> MapAsync<TB>(Func<T, Task<TB>> map) =>
public TB Match<TB>(Func<T, TB> successOperation, Func<IResultFailure, TB> failureOperation) =>
this.IsFailure ? failureOperation(this.failure) : successOperation(this.success);

/// <summary>
/// Merge two results together. The merge operation will be used if they're both in a Success state.
/// </summary>
/// <param name="other">The other result.</param>
/// <param name="merge">The operation used if they're both in a Success state.</param>
/// <typeparam name="TB">The return type.</typeparam>
/// <returns>A result.</returns>
public Result<TB> Merge<TB>(Result<T> other, Func<T, T, TB> merge) =>
this.IsSuccess && other.IsSuccess
? Result<TB>.FromSuccess(merge(this.success, other.success))
: Result<TB>.FromFailure(this.IsFailure ? this.failure : other.failure);

/// <summary>
/// Implicit operator from TA to Result of TA.
/// </summary>
Expand Down
72 changes: 72 additions & 0 deletions Vonage.Common/PhoneNumber.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Linq;
using Vonage.Common.Failures;
using Vonage.Common.Monads;
using Vonage.Common.Validation;

namespace Vonage.Common;

/// <summary>
/// Represents a E.164 phone number.
/// </summary>
/// <remarks>
/// See https://en.wikipedia.org/wiki/E.164.
/// </remarks>
public readonly struct PhoneNumber
{
private const int MaximumLength = 15;
private const int MinimumLength = 7;
private const string InternationalIndicator = "+";
private const string MustContainDigits = "Number can only contain digits.";
private const string NumberLengthIdentifier = "Number length";

private PhoneNumber(string number) => this.Number = number;

/// <summary>
/// Gets the phone number without international indicator.
/// </summary>
public string Number { get; }

/// <summary>
/// Gets the phone number with international indicator.
/// </summary>
public string NumberWithInternationalIndicator => string.Concat("+", this.Number);

/// <summary>
/// Parses the input into a PhoneNumber following E.164 specifications.
/// </summary>
/// <param name="number">The number.</param>
/// <returns>Success if the input matches all requirements. Failure otherwise.</returns>
public static Result<PhoneNumber> Parse(string number) =>
Result<PhoneNumber>.FromSuccess(new PhoneNumber(number))
.Bind(VerifyNumberNotEmpty)
.Map(RemoveInternationalIndicator)
.Bind(VerifyLengthHigherThanMinimum)
.Bind(VerifyLengthLowerThanMaximum)
.Bind(VerifyDigitsOnly);

/// <inheritdoc />
public override string ToString() => this.Number;

private static PhoneNumber RemoveInternationalIndicator(PhoneNumber value) =>
new(value.Number.Replace(InternationalIndicator, string.Empty));

private static Result<PhoneNumber> VerifyDigitsOnly(
PhoneNumber request) =>
request.Number.Select(char.IsDigit).All(_ => _)
? request
: Result<PhoneNumber>.FromFailure(ResultFailure.FromErrorMessage(MustContainDigits));

private static Result<PhoneNumber> VerifyLengthHigherThanMinimum(
PhoneNumber request) =>
InputValidation
.VerifyHigherOrEqualThan(request, request.Number.Length, MinimumLength, NumberLengthIdentifier);

private static Result<PhoneNumber> VerifyLengthLowerThanMaximum(
PhoneNumber request) =>
InputValidation
.VerifyLowerOrEqualThan(request, request.Number.Length, MaximumLength, NumberLengthIdentifier);

private static Result<PhoneNumber> VerifyNumberNotEmpty(PhoneNumber number) =>
InputValidation
.VerifyNotEmpty(number, number.Number, nameof(number.Number));
}
19 changes: 19 additions & 0 deletions Vonage.Common/Serialization/EmailJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Vonage.Common.Serialization;

/// <summary>
/// Represents a custom converter from Email to Json.
/// </summary>
public class EmailJsonConverter : JsonConverter<MailAddress>
{
/// <inheritdoc />
public override MailAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
MailAddress.Parse(reader.GetString()).IfFailure(default(MailAddress));

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, MailAddress value, JsonSerializerOptions options) =>
writer.WriteStringValue(value.Address);
}
19 changes: 19 additions & 0 deletions Vonage.Common/Serialization/PhoneNumberJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Vonage.Common.Serialization;

/// <summary>
/// Represents a custom converter from PhoneNumber to Json.
/// </summary>
public class PhoneNumberJsonConverter : JsonConverter<PhoneNumber>
{
/// <inheritdoc />
public override PhoneNumber Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
PhoneNumber.Parse(reader.GetString()).IfFailure(default(PhoneNumber));

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, PhoneNumber value, JsonSerializerOptions options) =>
writer.WriteStringValue(value.Number);
}
17 changes: 17 additions & 0 deletions Vonage.Common/Validation/InputValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static class InputValidation
private const string IntCannotBeLowerThan = "cannot be lower than {value}.";
private const string IntCannotBeNegative = "cannot be negative.";
private const string StringCannotBeNullOrWhitespace = "cannot be null or whitespace.";
private const string UnexpectedLength = "length should be {value}.";

/// <summary>
/// Verifies if higher or equal than specified threshold.
Expand All @@ -33,6 +34,22 @@ public static Result<T> VerifyHigherOrEqualThan<T>(T request, int value, int min
$"{name} {IntCannotBeLowerThan.Replace("{value}", minValue.ToString())}"))
: request;

/// <summary>
/// Verifies string length.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="value">The string.</param>
/// <param name="expectedLength">The expected length.</param>
/// <param name="name">The display name.</param>
/// <typeparam name="T">The request type.</typeparam>
/// <returns>Success or Failure.</returns>
public static Result<T> VerifyLength<T>(T request, string value, int expectedLength, string name) =>
value.Length != expectedLength
? Result<T>.FromFailure(
ResultFailure.FromErrorMessage(
$"{name} {UnexpectedLength.Replace("{value}", expectedLength.ToString())}"))
: request;

/// <summary>
/// Verifies if lower or equal than specified threshold.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"request_id": "c11236f4-00bf-4b89-84ba-88b25df97315"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"locale": "es-es",
"channel_timeout": 300,
"client_ref": "my-personal-reference",
"code_length": 4,
"brand": "ACME, Inc",
"workflow": [
{
"channel": "email",
"to": "alice@company.com"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"locale": "en-us",
"channel_timeout": 300,
"code_length": 4,
"brand": "ACME, Inc",
"workflow": [
{
"channel": "email",
"to": "alice@company.com"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"locale": "en-us",
"channel_timeout": 300,
"code_length": 4,
"brand": "ACME, Inc",
"workflow": [
{
"channel": "whatsapp_interactive",
"to": "447700900000"
},
{
"channel": "whatsapp",
"to": "447700900000"
},
{
"channel": "voice",
"to": "447700900000"
}
]
}
Loading

0 comments on commit 5a3828a

Please sign in to comment.