From 6d6f1bf4f11d27eeefd4d3bc385ade1a8fd09a5b Mon Sep 17 00:00:00 2001 From: tr00d Date: Mon, 24 Jun 2024 15:57:53 +0200 Subject: [PATCH] feat: implement request validation for Start in ExperienceComposer --- .../Start/RequestBuilderTest.cs | 236 ++++++++++++++++++ Vonage.Test/Vonage.Test.csproj | 1 + .../ExperienceComposerClient.cs | 12 + .../ExperienceComposer/Start/StartRequest.cs | 55 ++++ .../Start/StartRequestBuilder.cs | 171 +++++++++++++ 5 files changed, 475 insertions(+) create mode 100644 Vonage.Test/Video/ExperienceComposer/Start/RequestBuilderTest.cs create mode 100644 Vonage/Video/ExperienceComposer/Start/StartRequest.cs create mode 100644 Vonage/Video/ExperienceComposer/Start/StartRequestBuilder.cs diff --git a/Vonage.Test/Video/ExperienceComposer/Start/RequestBuilderTest.cs b/Vonage.Test/Video/ExperienceComposer/Start/RequestBuilderTest.cs new file mode 100644 index 00000000..78db7468 --- /dev/null +++ b/Vonage.Test/Video/ExperienceComposer/Start/RequestBuilderTest.cs @@ -0,0 +1,236 @@ +using System; +using Vonage.Server; +using Vonage.Test.Common.Extensions; +using Vonage.Video.ExperienceComposer.Start; +using Xunit; + +namespace Vonage.Test.Video.ExperienceComposer.Start; + +[Trait("Category", "Request")] +public class RequestBuilderTest +{ + private const string ValidSessionId = "session-id"; + private const string ValidToken = "token"; + private const RenderResolution ValidResolution = RenderResolution.FullHighDefinitionPortrait; + private const string ValidName = "name"; + private readonly Guid validApplicationId = Guid.NewGuid(); + private readonly Uri validUri = new Uri("https://example.com"); + + [Fact] + public void Build_ShouldSetApplicationId() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.ApplicationId) + .Should() + .BeSuccess(this.validApplicationId); + + [Fact] + public void Build_ShouldSetSessionId() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.SessionId) + .Should() + .BeSuccess(ValidSessionId); + + [Fact] + public void Build_ShouldSetToken() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.Token) + .Should() + .BeSuccess(ValidToken); + + [Fact] + public void Build_ShouldSetUrl() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.Url) + .Should() + .BeSuccess(this.validUri); + + [Fact] + public void Build_ShouldSetResolution() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.Resolution) + .Should() + .BeSuccess(ValidResolution); + + [Fact] + public void Build_ShouldSetName() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.Properties) + .Should() + .BeSuccess(new StartProperties(ValidName)); + + [Theory] + [InlineData(60)] + [InlineData(36000)] + public void Build_ShouldSetMaxDuration(int validDuration) => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .WithMaxDuration(validDuration) + .Create() + .Map(request => request.MaxDuration) + .Should() + .BeSuccess(validDuration); + + [Fact] + public void Build_ShouldHaveDefaultMaxDuration() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Map(request => request.MaxDuration) + .Should() + .BeSuccess(7200); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Parse_ShouldReturnFailure_GivenSessionIdIsEmpty(string invalidId) => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(invalidId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Should() + .BeParsingFailure("SessionId cannot be null or whitespace."); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Parse_ShouldReturnFailure_GivenTokenIsEmpty(string invalidToken) => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(invalidToken) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Should() + .BeParsingFailure("SessionId cannot be null or whitespace."); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Parse_ShouldReturnFailure_GivenNameIsEmpty(string invalidName) => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(invalidName) + .Create() + .Should() + .BeParsingFailure("Name cannot be null or whitespace."); + + [Fact] + public void Parse_ShouldReturnFailure_GivenApplicationIdIsEmpty() => + StartRequest + .Build() + .WithApplicationId(Guid.Empty) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .Create() + .Should() + .BeParsingFailure("ApplicationId cannot be empty."); + + [Fact] + public void Build_ShouldReturnFailure_GivenMaxDurationIsLowerThanSixty() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .WithMaxDuration(59) + .Create() + .Should() + .BeParsingFailure("MaxDuration cannot be lower than 60."); + + [Fact] + public void Build_ShouldReturnFailure_GivenMaxDurationIsHigherThanThirtySixThousand() => + StartRequest + .Build() + .WithApplicationId(this.validApplicationId) + .WithSessionId(ValidSessionId) + .WithToken(ValidToken) + .WithUrl(this.validUri) + .WithResolution(ValidResolution) + .WithName(ValidName) + .WithMaxDuration(36001) + .Create() + .Should() + .BeParsingFailure("MaxDuration cannot be higher than 36000."); +} \ No newline at end of file diff --git a/Vonage.Test/Vonage.Test.csproj b/Vonage.Test/Vonage.Test.csproj index 07caeeb3..0f72b760 100644 --- a/Vonage.Test/Vonage.Test.csproj +++ b/Vonage.Test/Vonage.Test.csproj @@ -47,6 +47,7 @@ + PreserveNewest diff --git a/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs b/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs index 040d7a0f..e9f713d7 100644 --- a/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs +++ b/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs @@ -4,6 +4,7 @@ using Vonage.Serialization; using Vonage.Video.ExperienceComposer.GetSession; using Vonage.Video.ExperienceComposer.GetSessions; +using Vonage.Video.ExperienceComposer.Start; using Vonage.Video.ExperienceComposer.Stop; namespace Vonage.Video.ExperienceComposer; @@ -40,6 +41,17 @@ public Task> GetSessionAsync(Result request) public Task> StopAsync(Result request) => this.vonageClient.SendAsync(request); + /// + /// Starts an Experience Composer session. + /// + /// The request. + /// + /// A success state with the archive if the operation succeeded. A failure state with the error message if it + /// failed. + /// + public Task> StartAsync(Result request) => + this.vonageClient.SendWithResponseAsync(request); + /// /// Retrieves all experience composer sessions in an application. /// diff --git a/Vonage/Video/ExperienceComposer/Start/StartRequest.cs b/Vonage/Video/ExperienceComposer/Start/StartRequest.cs new file mode 100644 index 00000000..8e20a17c --- /dev/null +++ b/Vonage/Video/ExperienceComposer/Start/StartRequest.cs @@ -0,0 +1,55 @@ +using System; +using System.Net.Http; +using Vonage.Common.Client; +using Vonage.Server; + +namespace Vonage.Video.ExperienceComposer.Start; + +/// +public struct StartRequest : IVonageRequest +{ + /// + /// + public Guid ApplicationId { get; internal init; } + + /// + /// + public string SessionId { get; internal init; } + + /// + /// + public string Token { get; internal init; } + + /// + /// + public Uri Url { get; internal init; } + + /// + /// + public int MaxDuration { get; internal init; } + + /// + /// + public RenderResolution Resolution { get; internal init; } + + /// + /// + public StartProperties Properties { get; internal init; } + + /// + public HttpRequestMessage BuildRequestMessage() => throw new NotImplementedException(); + + /// + public string GetEndpointPath() => throw new NotImplementedException(); + + /// + /// Initializes a builder. + /// + /// The builder. + public static IBuilderForApplicationId Build() => new StartRequestBuilder(); +} + +/// +/// +/// +public record StartProperties(string Name); \ No newline at end of file diff --git a/Vonage/Video/ExperienceComposer/Start/StartRequestBuilder.cs b/Vonage/Video/ExperienceComposer/Start/StartRequestBuilder.cs new file mode 100644 index 00000000..11a66635 --- /dev/null +++ b/Vonage/Video/ExperienceComposer/Start/StartRequestBuilder.cs @@ -0,0 +1,171 @@ +using System; +using Vonage.Common.Client; +using Vonage.Common.Monads; +using Vonage.Common.Validation; +using Vonage.Server; + +namespace Vonage.Video.ExperienceComposer.Start; + +internal struct StartRequestBuilder : + IBuilderForApplicationId, + IBuilderForName, + IBuilderForResolution, + IBuilderForToken, + IBuilderForUrl, + IBuilderForSessionId, + IBuilderForOptional +{ + private Guid applicationId = Guid.Empty; + private string sessionId = string.Empty; + private string token = string.Empty; + private int maxDuration = 7200; + private string name = string.Empty; + private RenderResolution resolution = RenderResolution.StandardDefinitionLandscape; + private Uri url; + + public StartRequestBuilder() + { + } + + public IBuilderForSessionId WithApplicationId(Guid value) => this with {applicationId = value}; + + public Result Create() => Result.FromSuccess(new StartRequest + { + ApplicationId = this.applicationId, + SessionId = this.sessionId, + Token = this.token, + Resolution = this.resolution, + Url = this.url, + MaxDuration = this.maxDuration, + Properties = new StartProperties(this.name), + }) + .Map(InputEvaluation.Evaluate) + .Bind(evaluation => evaluation.WithRules( + VerifyName, + VerifyMinimumDuration, + VerifyMaximumDuration, + VerifyToken, + VerifyApplicationId, + VerifySessionId)); + + public IBuilderForOptional WithMaxDuration(int value) => this with {maxDuration = value}; + + public IBuilderForOptional WithName(string value) => this with {name = value}; + + public IBuilderForName WithResolution(RenderResolution value) => this with {resolution = value}; + + public IBuilderForUrl WithToken(string value) => this with {token = value}; + + public IBuilderForResolution WithUrl(Uri value) => this with {url = value}; + + public IBuilderForToken WithSessionId(string value) => this with {sessionId = value}; + + private static Result VerifyApplicationId(StartRequest request) => + InputValidation.VerifyNotEmpty(request, request.ApplicationId, nameof(request.ApplicationId)); + + private static Result VerifySessionId(StartRequest request) => + InputValidation.VerifyNotEmpty(request, request.SessionId, nameof(request.SessionId)); + + private static Result VerifyToken(StartRequest request) => + InputValidation.VerifyNotEmpty(request, request.Token, nameof(request.Token)); + + private static Result VerifyName(StartRequest request) => + InputValidation.VerifyNotEmpty(request, request.Properties.Name, nameof(request.Properties.Name)); + + private static Result VerifyMinimumDuration(StartRequest request) => + InputValidation.VerifyHigherOrEqualThan(request, request.MaxDuration, 60, nameof(request.MaxDuration)); + + private static Result VerifyMaximumDuration(StartRequest request) => + InputValidation.VerifyLowerOrEqualThan(request, request.MaxDuration, 36000, nameof(request.MaxDuration)); +} + +/// +/// Represents a builder for application Id. +/// +public interface IBuilderForApplicationId +{ + /// + /// Sets the application Id on the builder. + /// + /// The application Id. + /// The builder. + IBuilderForSessionId WithApplicationId(Guid value); +} + +/// +/// Represents a builder for session Id. +/// +public interface IBuilderForSessionId +{ + /// + /// Sets the session Id on the builder. + /// + /// The session Id. + /// The builder. + IBuilderForToken WithSessionId(string value); +} + +/// +/// Represents a builder for token. +/// +public interface IBuilderForToken +{ + /// + /// Sets the token on the builder. + /// + /// The token. + /// The builder. + IBuilderForUrl WithToken(string value); +} + +/// +/// Represents a builder for Url. +/// +public interface IBuilderForUrl +{ + /// + /// Sets the Url on the builder. + /// + /// The Url. + /// The builder. + IBuilderForResolution WithUrl(Uri value); +} + +/// +/// Represents a builder for resolution. +/// +public interface IBuilderForResolution +{ + /// + /// Sets the resolution on the builder. + /// + /// The resolution. + /// The builder. + IBuilderForName WithResolution(RenderResolution value); +} + +/// +/// Represents a builder for name. +/// +public interface IBuilderForName +{ + /// + /// Sets the name on the builder. + /// + /// The name. + /// The builder. + IBuilderForOptional WithName(string value); +} + +/// +/// Represents a builder for optional values. +/// +public interface IBuilderForOptional : IVonageRequestBuilder +{ + /// + /// Sets the maximum duration on the builder. + /// + /// The maximum duration. + /// The builder. + IBuilderForOptional WithMaxDuration(int value); +} \ No newline at end of file