diff --git a/Vonage.Test/VerifyV2/CreateTemplateFragment/Data/ShouldDeserialize200-response.json b/Vonage.Test/VerifyV2/CreateTemplateFragment/Data/ShouldDeserialize200-response.json new file mode 100644 index 00000000..5df76079 --- /dev/null +++ b/Vonage.Test/VerifyV2/CreateTemplateFragment/Data/ShouldDeserialize200-response.json @@ -0,0 +1,16 @@ +{ + "template_fragment_id": "c70f446e-997a-4313-a081-60a02a31dc19", + "channel": "sms", + "locale": "en-us", + "text": "Text content of the template. May contain 4 reserved variables: `${code}`, `${brand}`, `${time-limit}` and `${time-limit-unit}`", + "date_updated": "2023-08-30T15:20:15.178Z", + "date_created": "2021-08-30T20:12:15.178Z", + "_links": { + "self": { + "href": "https://api.nexmo.com/v2/verify/templates/8f35a1a7-eb2f-4552-8fdf-fffdaee41bc9/template_fragments/c70f446e-997a-4313-a081-60a02a31dc19" + }, + "template": { + "href": "https://api.nexmo.com/v2/verify/templates/8f35a1a7-eb2f-4552-8fdf-fffdaee41bc9" + } + } +} \ No newline at end of file diff --git a/Vonage.Test/VerifyV2/CreateTemplateFragment/Data/ShouldSerialize-request.json b/Vonage.Test/VerifyV2/CreateTemplateFragment/Data/ShouldSerialize-request.json new file mode 100644 index 00000000..3e53f1dd --- /dev/null +++ b/Vonage.Test/VerifyV2/CreateTemplateFragment/Data/ShouldSerialize-request.json @@ -0,0 +1,5 @@ +{ + "channel": "sms", + "locale": "en-us", + "text": "The authentication code for your ${brand} is: ${code}" +} \ No newline at end of file diff --git a/Vonage.Test/VerifyV2/CreateTemplateFragment/E2ETest.cs b/Vonage.Test/VerifyV2/CreateTemplateFragment/E2ETest.cs new file mode 100644 index 00000000..63e23cfd --- /dev/null +++ b/Vonage.Test/VerifyV2/CreateTemplateFragment/E2ETest.cs @@ -0,0 +1,33 @@ +#region +using System.Net; +using System.Threading.Tasks; +using Vonage.Test.Common.Extensions; +using WireMock.ResponseBuilders; +using Xunit; +#endregion + +namespace Vonage.Test.VerifyV2.CreateTemplateFragment; + +[Trait("Category", "E2E")] +public class E2ETest : E2EBase +{ + public E2ETest() : base(typeof(E2ETest).Namespace) + { + } + + [Fact] + public async Task CreateTemplateFragment() + { + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/v2/verify/templates/8f35a1a7-eb2f-4552-8fdf-fffdaee41bc9/template_fragments") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .WithBody(this.Serialization.GetRequestJson(nameof(SerializationTest.ShouldSerialize))) + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); + await this.Helper.VonageClient.VerifyV2Client.CreateTemplateFragmentAsync(SerializationTest.BuildRequest()) + .Should() + .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); + } +} \ No newline at end of file diff --git a/Vonage.Test/VerifyV2/CreateTemplateFragment/RequestBuilderTest.cs b/Vonage.Test/VerifyV2/CreateTemplateFragment/RequestBuilderTest.cs new file mode 100644 index 00000000..3ced2fc1 --- /dev/null +++ b/Vonage.Test/VerifyV2/CreateTemplateFragment/RequestBuilderTest.cs @@ -0,0 +1,107 @@ +#region +using System; +using Vonage.Test.Common.Extensions; +using Vonage.VerifyV2; +using Vonage.VerifyV2.CreateTemplateFragment; +using Vonage.VerifyV2.StartVerification; +using Xunit; +#endregion + +namespace Vonage.Test.VerifyV2.CreateTemplateFragment; + +[Trait("Category", "Request")] +public class RequestBuilderTest +{ + private const string ValidName = "my-fragment"; + private static readonly Guid ValidTemplateId = new Guid("f3a065af-ac5a-47a4-8dfe-819561a7a287"); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Create_ShouldReturnFailure_GivenTextIsNullOrWhitespace(string value) => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(ValidTemplateId) + .WithText(value) + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create() + .Should() + .BeParsingFailure("Text cannot be null or whitespace."); + + [Fact] + public void Create_ShouldReturnFailure_GivenTemplateIdIsEmpty() => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(Guid.Empty) + .WithText(ValidName) + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create() + .Should() + .BeParsingFailure("TemplateId cannot be empty."); + + [Theory] + [InlineData(VerificationChannel.SilentAuth)] + [InlineData(VerificationChannel.WhatsApp)] + [InlineData(VerificationChannel.WhatsAppInteractive)] + public void Create_ShouldReturnFailure_GivenChannelIsNotSupported(VerificationChannel channel) => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(ValidTemplateId) + .WithText(ValidName) + .WithLocale(Locale.EnUs) + .WithChannel(channel) + .Create() + .Should() + .BeParsingFailure("Channel must be one of Sms, Voice or Email."); + + [Fact] + public void Create_ShouldSetName() => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(ValidTemplateId) + .WithText("my-fragment") + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create() + .Map(request => request.Text) + .Should() + .BeSuccess("my-fragment"); + + [Fact] + public void Create_ShouldSetTemplateId() => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(ValidTemplateId) + .WithText("my-fragment") + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create() + .Map(request => request.TemplateId) + .Should() + .BeSuccess(ValidTemplateId); + + [Fact] + public void Create_ShouldSetLocale() => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(ValidTemplateId) + .WithText("my-fragment") + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create() + .Map(request => request.Locale) + .Should() + .BeSuccess(Locale.EnUs); + + [Theory] + [InlineData(VerificationChannel.Sms)] + [InlineData(VerificationChannel.Voice)] + [InlineData(VerificationChannel.Email)] + public void Create_ShouldSetChannel(VerificationChannel channel) => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(ValidTemplateId) + .WithText(ValidName) + .WithLocale(Locale.EnUs) + .WithChannel(channel) + .Create() + .Map(request => request.Channel) + .Should() + .BeSuccess(channel); +} \ No newline at end of file diff --git a/Vonage.Test/VerifyV2/CreateTemplateFragment/RequestTest.cs b/Vonage.Test/VerifyV2/CreateTemplateFragment/RequestTest.cs new file mode 100644 index 00000000..a75029e0 --- /dev/null +++ b/Vonage.Test/VerifyV2/CreateTemplateFragment/RequestTest.cs @@ -0,0 +1,26 @@ +#region +using System; +using Vonage.Test.Common.Extensions; +using Vonage.VerifyV2; +using Vonage.VerifyV2.CreateTemplateFragment; +using Vonage.VerifyV2.StartVerification; +using Xunit; +#endregion + +namespace Vonage.Test.VerifyV2.CreateTemplateFragment; + +[Trait("Category", "Request")] +public class RequestTest +{ + [Fact] + public void GetEndpointPath_ShouldReturnApiEndpoint() => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(new Guid("f3a065af-ac5a-47a4-8dfe-819561a7a287")) + .WithText("my-fragment") + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create() + .Map(request => request.GetEndpointPath()) + .Should() + .BeSuccess("/v2/verify/templates/f3a065af-ac5a-47a4-8dfe-819561a7a287/template_fragments"); +} \ No newline at end of file diff --git a/Vonage.Test/VerifyV2/CreateTemplateFragment/SerializationTest.cs b/Vonage.Test/VerifyV2/CreateTemplateFragment/SerializationTest.cs new file mode 100644 index 00000000..794fa172 --- /dev/null +++ b/Vonage.Test/VerifyV2/CreateTemplateFragment/SerializationTest.cs @@ -0,0 +1,52 @@ +#region +using System; +using FluentAssertions; +using Vonage.Common.Monads; +using Vonage.Serialization; +using Vonage.Test.Common; +using Vonage.Test.Common.Extensions; +using Vonage.VerifyV2; +using Vonage.VerifyV2.CreateTemplateFragment; +using Vonage.VerifyV2.StartVerification; +using Xunit; +#endregion + +namespace Vonage.Test.VerifyV2.CreateTemplateFragment; + +[Trait("Category", "Serialization")] +public class SerializationTest +{ + private readonly SerializationTestHelper helper = new SerializationTestHelper( + typeof(SerializationTest).Namespace, + JsonSerializerBuilder.BuildWithSnakeCase()); + + [Fact] + public void ShouldDeserialize200() => + this.helper.Serializer + .DeserializeObject(this.helper.GetResponseJson()) + .Should() + .BeSuccess(VerifyExpectedResponse); + + internal static void VerifyExpectedResponse(TemplateFragment response) => + response.Should().Be(new TemplateFragment( + new Guid("c70f446e-997a-4313-a081-60a02a31dc19"), + VerificationChannel.Sms, + Locale.EnUs, + "Text content of the template. May contain 4 reserved variables: `${code}`, `${brand}`, `${time-limit}` and `${time-limit-unit}`", + DateTimeOffset.Parse("2021-08-30T20:12:15.178Z"), + DateTimeOffset.Parse("2023-08-30T15:20:15.178Z"))); + + [Fact] + public void ShouldSerialize() => BuildRequest() + .GetStringContent() + .Should() + .BeSuccess(this.helper.GetRequestJson()); + + internal static Result BuildRequest() => + CreateTemplateFragmentRequest.Build() + .WithTemplateId(new Guid("8f35a1a7-eb2f-4552-8fdf-fffdaee41bc9")) + .WithText("The authentication code for your ${brand} is: ${code}") + .WithLocale(Locale.EnUs) + .WithChannel(VerificationChannel.Sms) + .Create(); +} \ No newline at end of file diff --git a/Vonage.Test/Vonage.Test.csproj b/Vonage.Test/Vonage.Test.csproj index ec948b20..d4bffe1c 100644 --- a/Vonage.Test/Vonage.Test.csproj +++ b/Vonage.Test/Vonage.Test.csproj @@ -1325,6 +1325,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/Vonage/VerifyV2/CreateTemplateFragment/CreateTemplateFragmentRequest.cs b/Vonage/VerifyV2/CreateTemplateFragment/CreateTemplateFragmentRequest.cs new file mode 100644 index 00000000..749d3eaa --- /dev/null +++ b/Vonage/VerifyV2/CreateTemplateFragment/CreateTemplateFragmentRequest.cs @@ -0,0 +1,61 @@ +#region +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json.Serialization; +using Vonage.Common.Client; +using Vonage.Common.Serialization; +using Vonage.Serialization; +using Vonage.VerifyV2.StartVerification; +#endregion + +namespace Vonage.VerifyV2.CreateTemplateFragment; + +/// +public readonly struct CreateTemplateFragmentRequest : IVonageRequest +{ + /// + /// The template text. There are 4 reserved variables available to use: ${code}, ${brand}, ${time-limit} and + /// ${time-limit-unit} + /// + [JsonPropertyOrder(2)] + public string Text { get; internal init; } + + /// + /// ID of the template. + /// + [JsonIgnore] + public Guid TemplateId { get; internal init; } + + /// + /// The locale code. + /// + [JsonPropertyOrder(1)] + public Locale Locale { get; internal init; } + + /// + /// The channel name. + /// + [JsonPropertyOrder(0)] + [JsonConverter(typeof(EnumDescriptionJsonConverter))] + public VerificationChannel Channel { get; internal init; } + + /// + public HttpRequestMessage BuildRequestMessage() => VonageRequestBuilder + .Initialize(HttpMethod.Post, this.GetEndpointPath()) + .WithContent(this.GetRequestContent()) + .Build(); + + /// + public string GetEndpointPath() => $"/v2/verify/templates/{this.TemplateId}/template_fragments"; + + private StringContent GetRequestContent() => + new StringContent(JsonSerializerBuilder.BuildWithSnakeCase().SerializeObject(this), Encoding.UTF8, + "application/json"); + + /// + /// Initializes a builder. + /// + /// + public static IBuilderForTemplateId Build() => new CreateTemplateFragmentRequestBuilder(); +} \ No newline at end of file diff --git a/Vonage/VerifyV2/CreateTemplateFragment/CreateTemplateFragmentRequestBuilder.cs b/Vonage/VerifyV2/CreateTemplateFragment/CreateTemplateFragmentRequestBuilder.cs new file mode 100644 index 00000000..4968a786 --- /dev/null +++ b/Vonage/VerifyV2/CreateTemplateFragment/CreateTemplateFragmentRequestBuilder.cs @@ -0,0 +1,110 @@ +#region +using System; +using System.Linq; +using Vonage.Common.Client; +using Vonage.Common.Failures; +using Vonage.Common.Monads; +using Vonage.Common.Validation; +using Vonage.VerifyV2.StartVerification; +#endregion + +namespace Vonage.VerifyV2.CreateTemplateFragment; + +internal struct CreateTemplateFragmentRequestBuilder + : IVonageRequestBuilder, IBuilderForText, IBuilderForChannel, IBuilderForLocale, + IBuilderForTemplateId +{ + private VerificationChannel channel; + private Locale locale; + private Guid templateId; + private string text; + + public IVonageRequestBuilder WithChannel(VerificationChannel value) => + this with {channel = value}; + + public IBuilderForChannel WithLocale(Locale value) => this with {locale = value}; + public IBuilderForText WithTemplateId(Guid value) => this with {templateId = value}; + public IBuilderForLocale WithText(string value) => this with {text = value}; + + public Result Create() => Result.FromSuccess( + new CreateTemplateFragmentRequest + { + Text = this.text, + Channel = this.channel, + Locale = this.locale, + TemplateId = this.templateId, + }) + .Map(InputEvaluation.Evaluate) + .Bind(evaluation => evaluation.WithRules(VerifyTemplate, VerifyName, VerifyChannel)); + + private static Result VerifyName( + CreateTemplateFragmentRequest request) => + InputValidation.VerifyNotEmpty(request, request.Text, nameof(request.Text)); + + private static Result VerifyTemplate( + CreateTemplateFragmentRequest request) => + InputValidation.VerifyNotEmpty(request, request.TemplateId, nameof(request.TemplateId)); + + private static Result VerifyChannel( + CreateTemplateFragmentRequest request) => + IsChannelSupported(request.Channel) + ? request + : Result.FromFailure( + ResultFailure.FromErrorMessage( + $"{nameof(request.Channel)} must be one of {VerificationChannel.Sms}, {VerificationChannel.Voice} or {VerificationChannel.Email}.")); + + private static bool IsChannelSupported(VerificationChannel channel) => + new[] {VerificationChannel.Sms, VerificationChannel.Voice, VerificationChannel.Email}.Contains(channel); +} + +/// +/// Represents a builder to set the TemplateId. +/// +public interface IBuilderForTemplateId +{ + /// + /// Sets the TemplateId. + /// + /// The templateId. + /// The builder. + IBuilderForText WithTemplateId(Guid value); +} + +/// +/// Represents a builder to set the Text. +/// +public interface IBuilderForText +{ + /// + /// Sets the Text. + /// + /// The text. + /// The builder. + IBuilderForLocale WithText(string value); +} + +/// +/// Represents a builder to set the Locale. +/// +public interface IBuilderForLocale +{ + /// + /// Sets the Locale. + /// + /// The Locale. + /// The builder. + IBuilderForChannel WithLocale(Locale value); +} + +/// +/// Represents a builder to set the Channel. +/// +public interface IBuilderForChannel +{ + /// + /// Sets the Channel. + /// + /// The channel. + /// The builder. + IVonageRequestBuilder WithChannel(VerificationChannel value); +} \ No newline at end of file diff --git a/Vonage/VerifyV2/IVerifyV2Client.cs b/Vonage/VerifyV2/IVerifyV2Client.cs index f4be4a49..43efd9c5 100644 --- a/Vonage/VerifyV2/IVerifyV2Client.cs +++ b/Vonage/VerifyV2/IVerifyV2Client.cs @@ -3,6 +3,7 @@ using Vonage.Common.Monads; using Vonage.VerifyV2.Cancel; using Vonage.VerifyV2.CreateTemplate; +using Vonage.VerifyV2.CreateTemplateFragment; using Vonage.VerifyV2.DeleteTemplate; using Vonage.VerifyV2.DeleteTemplateFragment; using Vonage.VerifyV2.GetTemplate; @@ -55,6 +56,13 @@ public interface IVerifyV2Client /// Success or Failure. Task> CreateTemplateAsync(Result request); + /// + /// Creates a new template fragment. + /// + /// The request. + /// Success or Failure. + Task> CreateTemplateFragmentAsync(Result request); + /// /// Deletes a template. /// diff --git a/Vonage/VerifyV2/TemplateFragment.cs b/Vonage/VerifyV2/TemplateFragment.cs new file mode 100644 index 00000000..7d632275 --- /dev/null +++ b/Vonage/VerifyV2/TemplateFragment.cs @@ -0,0 +1,21 @@ +#region +using System; +using System.Text.Json.Serialization; +using Vonage.Common.Serialization; +using Vonage.VerifyV2.StartVerification; +#endregion + +namespace Vonage.VerifyV2; + +public record TemplateFragment( + [property: JsonPropertyName("template_fragment_id")] + Guid TemplateFragmentId, + [property: JsonPropertyName("channel")] + [property: JsonConverter(typeof(EnumDescriptionJsonConverter))] + VerificationChannel Channel, + [property: JsonPropertyName("locale")] Locale Locale, + [property: JsonPropertyName("text")] string Text, + [property: JsonPropertyName("date_created")] + DateTimeOffset CreatedOn, + [property: JsonPropertyName("date_updated")] + DateTimeOffset UpdatedOn); \ No newline at end of file diff --git a/Vonage/VerifyV2/VerifyV2Client.cs b/Vonage/VerifyV2/VerifyV2Client.cs index 8a6e7bce..374e492c 100644 --- a/Vonage/VerifyV2/VerifyV2Client.cs +++ b/Vonage/VerifyV2/VerifyV2Client.cs @@ -5,6 +5,7 @@ using Vonage.Serialization; using Vonage.VerifyV2.Cancel; using Vonage.VerifyV2.CreateTemplate; +using Vonage.VerifyV2.CreateTemplateFragment; using Vonage.VerifyV2.DeleteTemplate; using Vonage.VerifyV2.DeleteTemplateFragment; using Vonage.VerifyV2.GetTemplate; @@ -48,6 +49,10 @@ public Task> VerifyCodeAsync(Result request) => public Task> CreateTemplateAsync(Result request) => this.vonageClient.SendWithResponseAsync(request); + /// + public Task> CreateTemplateFragmentAsync(Result request) => + this.vonageClient.SendWithResponseAsync(request); + /// public Task> DeleteTemplateAsync(Result request) => this.vonageClient.SendAsync(request);