From 271bfcc0d5622c1fea174fe147bed9baa2779e02 Mon Sep 17 00:00:00 2001 From: tr00d Date: Tue, 4 Jun 2024 14:51:53 +0200 Subject: [PATCH] feat: implement UpdateMember in Conversations --- .../Data/ShouldDeserialize200-response.json | 52 ++++++++ .../Data/ShouldSerializeWithFrom-request.json | 4 + ...houldSerializeWithJoinedState-request.json | 3 + .../ShouldSerializeWithLeftState-request.json | 7 ++ .../Conversations/UpdateMember/E2ETest.cs | 50 ++++++++ .../UpdateMember/RequestBuilderTest.cs | 65 ++++++++++ .../Conversations/UpdateMember/RequestTest.cs | 20 +++ .../UpdateMember/SerializationTest.cs | 118 ++++++++++++++++++ Vonage.Test/Vonage.Test.csproj | 12 ++ Vonage/Conversations/ConversationsClient.cs | 5 + Vonage/Conversations/IConversationsClient.cs | 8 ++ .../UpdateMember/UpdateMemberRequest.cs | 82 ++++++++++++ .../UpdateMemberRequestBuilder.cs | 93 ++++++++++++++ 13 files changed, 519 insertions(+) create mode 100644 Vonage.Test/Conversations/UpdateMember/Data/ShouldDeserialize200-response.json create mode 100644 Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithFrom-request.json create mode 100644 Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithJoinedState-request.json create mode 100644 Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithLeftState-request.json create mode 100644 Vonage.Test/Conversations/UpdateMember/E2ETest.cs create mode 100644 Vonage.Test/Conversations/UpdateMember/RequestBuilderTest.cs create mode 100644 Vonage.Test/Conversations/UpdateMember/RequestTest.cs create mode 100644 Vonage.Test/Conversations/UpdateMember/SerializationTest.cs create mode 100644 Vonage/Conversations/UpdateMember/UpdateMemberRequest.cs create mode 100644 Vonage/Conversations/UpdateMember/UpdateMemberRequestBuilder.cs diff --git a/Vonage.Test/Conversations/UpdateMember/Data/ShouldDeserialize200-response.json b/Vonage.Test/Conversations/UpdateMember/Data/ShouldDeserialize200-response.json new file mode 100644 index 00000000..9c70f91e --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/Data/ShouldDeserialize200-response.json @@ -0,0 +1,52 @@ +{ + "id": "MEM-63f61863-4a51-4f6b-86e1-46edebio0391", + "conversation_id": "CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a", + "_embedded": { + "user": { + "id": "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + "name": "my_user_name", + "display_name": "My User Name", + "_links": { + "self": { + "href": "https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471ec" + } + } + } + }, + "state": "JOINED", + "timestamp": { + "invited": "2020-01-01T14:00:00.00Z", + "joined": "2020-01-01T14:00:00.00Z", + "left": "2020-01-01T14:00:00.00Z" + }, + "initiator": { + "joined": { + "is_system": true, + "user_id": "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + "member_id": "MEM-63f61863-4a51-4f6b-86e1-46edebio0391" + } + }, + "channel": { + "type": "app", + "from": { + "type": "app" + }, + "to": { + "type": "app", + "user": "string" + } + }, + "media": { + "audio_settings": { + "enabled": true, + "earmuffed": true, + "muted": true + }, + "audio": true + }, + "knocking_id": "string", + "invited_by": "MEM-63f61863-4a51-4f6b-86e1-46edebio0378", + "_links": { + "href": "https://api.nexmo.com/v1/conversations/CON-63f61863-4a51-4f6b-86e1-46edebio0391/members/MEM-63f61863-4a51-4f6b-86e1-46edebio0391" + } +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithFrom-request.json b/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithFrom-request.json new file mode 100644 index 00000000..ae2b8323 --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithFrom-request.json @@ -0,0 +1,4 @@ +{ + "state": "joined", + "from": "123456789" +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithJoinedState-request.json b/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithJoinedState-request.json new file mode 100644 index 00000000..202a3bc1 --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithJoinedState-request.json @@ -0,0 +1,3 @@ +{ + "state": "joined" +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithLeftState-request.json b/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithLeftState-request.json new file mode 100644 index 00000000..da65b8a6 --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/Data/ShouldSerializeWithLeftState-request.json @@ -0,0 +1,7 @@ +{ + "state": "left", + "reason": { + "code": "123", + "text": "Some reason." + } +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/E2ETest.cs b/Vonage.Test/Conversations/UpdateMember/E2ETest.cs new file mode 100644 index 00000000..8b52390f --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/E2ETest.cs @@ -0,0 +1,50 @@ +using System.Net; +using System.Threading.Tasks; +using Vonage.Common.Monads; +using Vonage.Conversations.UpdateMember; +using Vonage.Test.Common.Extensions; +using WireMock.ResponseBuilders; +using Xunit; + +namespace Vonage.Test.Conversations.UpdateMember; + +[Trait("Category", "E2E")] +public class E2ETest : E2EBase +{ + public E2ETest() : base(typeof(E2ETest).Namespace) + { + } + + [Fact] + public Task UpdateMemberWithJoinedState() => + this.CreateConversationAsync( + this.Serialization.GetRequestJson(nameof(SerializationTest.ShouldSerializeWithJoinedState)), + SerializationTest.BuildRequestWithJoinedState()); + + [Fact] + public Task UpdateMemberWithLeftState() => + this.CreateConversationAsync( + this.Serialization.GetRequestJson(nameof(SerializationTest.ShouldSerializeWithLeftState)), + SerializationTest.BuildRequestWithLeftState()); + + [Fact] + public Task UpdateMemberWithFrom() => + this.CreateConversationAsync( + this.Serialization.GetRequestJson(nameof(SerializationTest.ShouldSerializeWithFrom)), + SerializationTest.BuildRequestWithFrom()); + + private async Task CreateConversationAsync(string jsonRequest, Result request) + { + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/v1/conversations/CON-123/members/MEM-123") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .WithBody(jsonRequest) + .UsingPatch()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); + await this.Helper.VonageClient.ConversationsClient + .UpdateMemberAsync(request) + .Should() + .BeSuccessAsync(SerializationTest.VerifyResponse); + } +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/RequestBuilderTest.cs b/Vonage.Test/Conversations/UpdateMember/RequestBuilderTest.cs new file mode 100644 index 00000000..8fe951d6 --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/RequestBuilderTest.cs @@ -0,0 +1,65 @@ +using Vonage.Common.Monads; +using Vonage.Conversations.UpdateMember; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.Conversations.UpdateMember; + +public class RequestBuilderTest +{ + [Fact] + public void Build_ShouldSetConversationId() => + SerializationTest.BuildRequestWithJoinedState() + .Map(request => request.ConversationId) + .Should() + .BeSuccess(SerializationTest.ValidConversationId); + + [Fact] + public void Build_ShouldSetMemberId() => + SerializationTest.BuildRequestWithJoinedState() + .Map(request => request.MemberId) + .Should() + .BeSuccess(SerializationTest.ValidMemberId); + + [Fact] + public void Build_ShouldSetJoinedState_GivenWithJoinedState() => + SerializationTest.BuildRequestWithJoinedState() + .Map(request => request.State) + .Should() + .BeSuccess(UpdateMemberRequest.AvailableStates.Joined); + + [Fact] + public void Build_ShouldSetLeftState_GivenWithLeftState() => + SerializationTest.BuildRequestWithLeftState() + .Map(request => request.State) + .Should() + .BeSuccess(UpdateMemberRequest.AvailableStates.Left); + + [Fact] + public void Build_ShouldHaveNoReason_GivenWithJoinedState() => + SerializationTest.BuildRequestWithJoinedState() + .Map(request => request.Reason) + .Should() + .BeSuccess(Maybe.None); + + [Fact] + public void Build_ShouldSetReason_GivenWithLeftState() => + SerializationTest.BuildRequestWithLeftState() + .Map(request => request.Reason) + .Should() + .BeSuccess(SerializationTest.ValidReason); + + [Fact] + public void Build_ShouldSetFrom() => + SerializationTest.BuildRequestWithFrom() + .Map(request => request.From) + .Should() + .BeSuccess(SerializationTest.ValidFrom); + + [Fact] + public void Build_ShouldHaveNoFrom_GivenDefault() => + SerializationTest.BuildRequestWithJoinedState() + .Map(request => request.From) + .Should() + .BeSuccess(Maybe.None); +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/RequestTest.cs b/Vonage.Test/Conversations/UpdateMember/RequestTest.cs new file mode 100644 index 00000000..13c5c2c0 --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/RequestTest.cs @@ -0,0 +1,20 @@ +using Vonage.Conversations.UpdateMember; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.Conversations.UpdateMember; + +[Trait("Category", "Request")] +public class RequestTest +{ + [Fact] + public void GetEndpointPath_ShouldReturnApiEndpoint() => + UpdateMemberRequest.Build() + .WithConversationId("CON-123") + .WithMemberId("MEM-123") + .WithJoinedState() + .Create() + .Map(request => request.GetEndpointPath()) + .Should() + .BeSuccess("/v1/conversations/CON-123/members/MEM-123"); +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/UpdateMember/SerializationTest.cs b/Vonage.Test/Conversations/UpdateMember/SerializationTest.cs new file mode 100644 index 00000000..58731baa --- /dev/null +++ b/Vonage.Test/Conversations/UpdateMember/SerializationTest.cs @@ -0,0 +1,118 @@ +using System; +using FluentAssertions; +using Vonage.Common; +using Vonage.Common.Monads; +using Vonage.Conversations; +using Vonage.Conversations.UpdateMember; +using Vonage.Serialization; +using Vonage.Test.Common; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.Conversations.UpdateMember; + +[Trait("Category", "Serialization")] +public class SerializationTest +{ + internal const string ValidConversationId = "CON-123"; + internal const string ValidMemberId = "MEM-123"; + internal const string ValidFrom = "123456789"; + + private readonly SerializationTestHelper helper = new SerializationTestHelper( + typeof(SerializationTest).Namespace, + JsonSerializerBuilder.BuildWithSnakeCase()); + + internal static Reason ValidReason => new Reason("123", "Some reason."); + + [Fact] + public void ShouldSerializeWithJoinedState() => + BuildRequestWithJoinedState() + .GetStringContent() + .Should() + .BeSuccess(this.helper.GetRequestJson()); + + internal static Result BuildRequestWithJoinedState() => + UpdateMemberRequest.Build() + .WithConversationId(ValidConversationId) + .WithMemberId(ValidMemberId) + .WithJoinedState() + .Create(); + + [Fact] + public void ShouldSerializeWithLeftState() => + BuildRequestWithLeftState() + .GetStringContent() + .Should() + .BeSuccess(this.helper.GetRequestJson()); + + internal static Result BuildRequestWithLeftState() => + UpdateMemberRequest.Build() + .WithConversationId(ValidConversationId) + .WithMemberId(ValidMemberId) + .WithLeftState(ValidReason) + .Create(); + + [Fact] + public void ShouldSerializeWithFrom() => + BuildRequestWithFrom() + .GetStringContent() + .Should() + .BeSuccess(this.helper.GetRequestJson()); + + internal static Result BuildRequestWithFrom() => + UpdateMemberRequest.Build() + .WithConversationId(ValidConversationId) + .WithMemberId(ValidMemberId) + .WithJoinedState() + .WithFrom(ValidFrom) + .Create(); + + [Fact] + public void ShouldDeserialize200() => this.helper.Serializer + .DeserializeObject(this.helper.GetResponseJson()) + .Should() + .BeSuccess(VerifyResponse); + + internal static void VerifyResponse(Member response) + { + response.Id.Should().Be("MEM-63f61863-4a51-4f6b-86e1-46edebio0391"); + response.ConversationId.Should().Be("CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a"); + response.State.Should().Be("JOINED"); + response.KnockingId.Should().Be("string"); + response.InvitedBy.Should().Be("MEM-63f61863-4a51-4f6b-86e1-46edebio0378"); + response.Timestamp.Should().Be(new MemberTimestamp( + DateTimeOffset.Parse("2020-01-01T14:00:00.00Z"), + DateTimeOffset.Parse("2020-01-01T14:00:00.00Z"), + DateTimeOffset.Parse("2020-01-01T14:00:00.00Z") + )); + response.Media.Should().Be(new MemberMedia( + new MemberMediaSettings(true, true, true), + true + )); + response.Links.Should() + .Be(new HalLink(new Uri( + "https://api.nexmo.com/v1/conversations/CON-63f61863-4a51-4f6b-86e1-46edebio0391/members/MEM-63f61863-4a51-4f6b-86e1-46edebio0391"))); + response.Embedded.Should().Be(new MemberEmbedded( + new MemberEmbeddedUser( + "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + "my_user_name", + "My User Name", + new HalLinks + { + Self = new HalLink( + new Uri("https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471ec")), + } + ) + )); + response.Initiator.Should().Be(new MemberInitiator( + new MemberInitiatorJoined(true, "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + "MEM-63f61863-4a51-4f6b-86e1-46edebio0391"), + null + )); + response.Channel.Should().Be(new MemberChannel( + ChannelType.App, + MemberChannelFrom.FromChannels(ChannelType.App), + new MemberChannelToV(ChannelType.App, "string", null, null) + )); + } +} \ No newline at end of file diff --git a/Vonage.Test/Vonage.Test.csproj b/Vonage.Test/Vonage.Test.csproj index 9738769e..da763261 100644 --- a/Vonage.Test/Vonage.Test.csproj +++ b/Vonage.Test/Vonage.Test.csproj @@ -1185,6 +1185,18 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/Vonage/Conversations/ConversationsClient.cs b/Vonage/Conversations/ConversationsClient.cs index 91fae230..47ff19a2 100644 --- a/Vonage/Conversations/ConversationsClient.cs +++ b/Vonage/Conversations/ConversationsClient.cs @@ -10,6 +10,7 @@ using Vonage.Conversations.GetMembers; using Vonage.Conversations.GetUserConversations; using Vonage.Conversations.UpdateConversation; +using Vonage.Conversations.UpdateMember; using Vonage.Serialization; namespace Vonage.Conversations; @@ -42,6 +43,10 @@ public Task> GetConversationAsync(Result> CreateMemberAsync(Result request) => this.vonageClient.SendWithResponseAsync(request); + /// + public Task> UpdateMemberAsync(Result request) => + this.vonageClient.SendWithResponseAsync(request); + /// public Task> GetMemberAsync(Result request) => this.vonageClient.SendWithResponseAsync(request); diff --git a/Vonage/Conversations/IConversationsClient.cs b/Vonage/Conversations/IConversationsClient.cs index ab4cb91d..90c2bb7d 100644 --- a/Vonage/Conversations/IConversationsClient.cs +++ b/Vonage/Conversations/IConversationsClient.cs @@ -9,6 +9,7 @@ using Vonage.Conversations.GetMembers; using Vonage.Conversations.GetUserConversations; using Vonage.Conversations.UpdateConversation; +using Vonage.Conversations.UpdateMember; namespace Vonage.Conversations; @@ -45,6 +46,13 @@ public interface IConversationsClient /// Success or Failure. Task> CreateMemberAsync(Result request); + /// + /// Updates a member. + /// + /// The request. + /// Success or Failure. + Task> UpdateMemberAsync(Result request); + /// /// Retrieves a member. /// diff --git a/Vonage/Conversations/UpdateMember/UpdateMemberRequest.cs b/Vonage/Conversations/UpdateMember/UpdateMemberRequest.cs new file mode 100644 index 00000000..17bf6d48 --- /dev/null +++ b/Vonage/Conversations/UpdateMember/UpdateMemberRequest.cs @@ -0,0 +1,82 @@ +using System.ComponentModel; +using System.Net.Http; +using System.Text; +using System.Text.Json.Serialization; +using Vonage.Common.Client; +using Vonage.Common.Monads; +using Vonage.Common.Serialization; +using Vonage.Serialization; + +namespace Vonage.Conversations.UpdateMember; + +/// +public readonly struct UpdateMemberRequest : IVonageRequest +{ + /// + public HttpRequestMessage BuildRequestMessage() => VonageRequestBuilder + .Initialize(new HttpMethod("PATCH"), this.GetEndpointPath()) + .WithContent(this.GetRequestContent()) + .Build(); + + /// + /// Initializes a builder for UpdateMemberRequest. + /// + /// The builder. + public static IBuilderForConversationId Build() => new UpdateMemberRequestBuilder(); + + /// + public string GetEndpointPath() => $"/v1/conversations/{this.ConversationId}/members/{this.MemberId}"; + + private StringContent GetRequestContent() => + new StringContent(JsonSerializerBuilder.BuildWithSnakeCase().SerializeObject(this), Encoding.UTF8, + "application/json"); + + /// + /// + [JsonIgnore] + public string ConversationId { get; internal init; } + + /// + /// + [JsonIgnore] + public string MemberId { get; internal init; } + + /// + /// Invite or join a member to a conversation + /// + [JsonConverter(typeof(EnumDescriptionJsonConverter))] + [JsonPropertyOrder(0)] + public AvailableStates State { get; internal init; } + + /// + /// + [JsonPropertyOrder(1)] + [JsonConverter(typeof(MaybeJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Maybe From { get; internal init; } + + /// + /// + [JsonConverter(typeof(MaybeJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Maybe Reason { get; internal init; } + + /// + /// + public enum AvailableStates + { + /// + /// + [Description("left")] Left, + + /// + /// + [Description("joined")] Joined, + } +} + +/// +/// +/// +/// +public record Reason(string Code, string Text); \ No newline at end of file diff --git a/Vonage/Conversations/UpdateMember/UpdateMemberRequestBuilder.cs b/Vonage/Conversations/UpdateMember/UpdateMemberRequestBuilder.cs new file mode 100644 index 00000000..29ca9377 --- /dev/null +++ b/Vonage/Conversations/UpdateMember/UpdateMemberRequestBuilder.cs @@ -0,0 +1,93 @@ +using Vonage.Common.Client; +using Vonage.Common.Monads; + +namespace Vonage.Conversations.UpdateMember; + +internal struct UpdateMemberRequestBuilder : IBuilderForConversationId, IBuilderForMemberId, IBuilderForState, + IBuilderForOptional +{ + private string conversationId; + private string memberId; + private UpdateMemberRequest.AvailableStates state; + private Maybe reason; + private Maybe from; + + IBuilderForMemberId IBuilderForConversationId.WithConversationId(string value) => + this with {conversationId = value}; + + IBuilderForState IBuilderForMemberId.WithMemberId(string value) => this with {memberId = value}; + + public Result Create() => Result.FromSuccess(new UpdateMemberRequest + { + ConversationId = this.conversationId, + MemberId = this.memberId, + State = this.state, + Reason = this.reason, + From = this.from, + }); + + public IBuilderForOptional WithFrom(string value) => this with {from = value}; + + public IBuilderForOptional WithJoinedState() => this with {state = UpdateMemberRequest.AvailableStates.Joined}; + + public IBuilderForOptional WithLeftState(Reason value) => + this with {state = UpdateMemberRequest.AvailableStates.Left, reason = value}; +} + +/// +/// Represents a builder for the ConversationId. +/// +public interface IBuilderForConversationId +{ + /// + /// Sets the Conversation Id. + /// + /// The conversation id. + /// The builder. + IBuilderForMemberId WithConversationId(string value); +} + +/// +/// Represents a builder for the MemberId. +/// +public interface IBuilderForMemberId +{ + /// + /// Sets the Member Id. + /// + /// The member id. + /// The builder. + IBuilderForState WithMemberId(string value); +} + +/// +/// Represents a builder for the state. +/// +public interface IBuilderForState +{ + /// + /// Sets the state to Joined. + /// + /// The builder. + IBuilderForOptional WithJoinedState(); + + /// + /// Sets the state to Left. + /// + /// The reason + /// The builder. + IBuilderForOptional WithLeftState(Reason value); +} + +/// +/// Represents a builder for optional values. +/// +public interface IBuilderForOptional : IVonageRequestBuilder +{ + /// + /// Sets the From. + /// + /// The From. + /// The builder. + IBuilderForOptional WithFrom(string value); +} \ No newline at end of file