From b43cf1fe3cae271c160bf2e80a95a7158cd7eefe Mon Sep 17 00:00:00 2001 From: tr00d Date: Wed, 12 Jun 2024 15:58:02 +0200 Subject: [PATCH] feat: implement GetEvents in Conversations --- .../Conversations/GetConversations/E2ETest.cs | 8 +- .../GetConversationsHalLinkTest.cs | 16 +- .../Data/ShouldDeserialize200-response.json | 49 +++++ .../Conversations/GetEvents/E2ETest.cs | 91 ++++++++ .../GetEvents/RequestBuilderTest.cs | 194 ++++++++++++++++++ .../Conversations/GetEvents/RequestTest.cs | 67 ++++++ .../GetEvents/SerializationTest.cs | 57 +++++ .../Conversations/GetMembers/E2ETest.cs | 6 +- Vonage.Test/Vonage.Test.csproj | 3 + Vonage/Conversations/ConversationsClient.cs | 5 + .../GetConversationsRequestBuilder.cs | 5 - .../GetConversationsResponse.cs | 14 +- .../GetEvents/GetEventRequest.cs | 83 ++++++++ .../GetEvents/GetEventsRequestBuilder.cs | 112 ++++++++++ .../GetEvents/GetEventsResponse.cs | 95 +++++++++ .../GetMembers/GetMembersResponse.cs | 12 +- .../GetUserConversationsResponse.cs | 14 +- Vonage/Conversations/IConversationsClient.cs | 8 + 18 files changed, 799 insertions(+), 40 deletions(-) create mode 100644 Vonage.Test/Conversations/GetEvents/Data/ShouldDeserialize200-response.json create mode 100644 Vonage.Test/Conversations/GetEvents/E2ETest.cs create mode 100644 Vonage.Test/Conversations/GetEvents/RequestBuilderTest.cs create mode 100644 Vonage.Test/Conversations/GetEvents/RequestTest.cs create mode 100644 Vonage.Test/Conversations/GetEvents/SerializationTest.cs create mode 100644 Vonage/Conversations/GetEvents/GetEventRequest.cs create mode 100644 Vonage/Conversations/GetEvents/GetEventsRequestBuilder.cs create mode 100644 Vonage/Conversations/GetEvents/GetEventsResponse.cs diff --git a/Vonage.Test/Conversations/GetConversations/E2ETest.cs b/Vonage.Test/Conversations/GetConversations/E2ETest.cs index d41a9449e..1bb9880f1 100644 --- a/Vonage.Test/Conversations/GetConversations/E2ETest.cs +++ b/Vonage.Test/Conversations/GetConversations/E2ETest.cs @@ -16,7 +16,7 @@ public class E2ETest : E2EBase public E2ETest() : base(typeof(E2ETest).Namespace) { } - + [Fact] public async Task GetConversations() { @@ -40,7 +40,7 @@ await this.Helper.VonageClient.ConversationsClient .Should() .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); } - + [Fact] public async Task GetConversationsFromHalLink() { @@ -56,13 +56,13 @@ public async Task GetConversationsFromHalLink() .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); await this.Helper.VonageClient.ConversationsClient - .GetConversationsAsync(new GetMembersHalLink(new Uri( + .GetConversationsAsync(new GetConversationsHalLink(new Uri( "https://api.nexmo.com/v1/conversations?order=desc&page_size=50&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D&date_start=2023-12-18T09%3A56%3A08Z&date_end=2023-12-18T10%3A56%3A08Z")) .BuildRequest()) .Should() .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); } - + [Fact] public async Task GetConversationsWithDefaultRequest() { diff --git a/Vonage.Test/Conversations/GetConversations/GetConversationsHalLinkTest.cs b/Vonage.Test/Conversations/GetConversations/GetConversationsHalLinkTest.cs index beacdefcc..de02ebe8d 100644 --- a/Vonage.Test/Conversations/GetConversations/GetConversationsHalLinkTest.cs +++ b/Vonage.Test/Conversations/GetConversations/GetConversationsHalLinkTest.cs @@ -9,11 +9,11 @@ namespace Vonage.Test.Conversations.GetConversations; [Trait("Category", "Request")] -public class GetMembersHalLinkTest +public class GetEventsHalLinkTest { [Fact] public void BuildRequestForPrevious_ShouldReturnSuccess() => - new GetMembersHalLink(new Uri("https://api.nexmo.com/v1/users?order=desc&page_size=10")) + new GetConversationsHalLink(new Uri("https://api.nexmo.com/v1/users?order=desc&page_size=10")) .BuildRequest() .Should() .BeSuccess(new GetConversationsRequest @@ -24,28 +24,28 @@ public void BuildRequestForPrevious_ShouldReturnSuccess() => PageSize = 10, Order = FetchOrder.Descending, }); - + [Fact] public void BuildRequestForPrevious_ShouldReturnSuccess_WithCursor() => - new GetMembersHalLink(new Uri( + new GetConversationsHalLink(new Uri( "https://api.nexmo.com/v1/users?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D")) .BuildRequest() .Map(request => request.Cursor) .Should() .BeSuccess("7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg="); - + [Fact] public void BuildRequestForPrevious_ShouldReturnSuccess_WithEndDate() => - new GetMembersHalLink(new Uri( + new GetConversationsHalLink(new Uri( "https://api.nexmo.com/v1/users?order=desc&page_size=10&date_end=2023-12-18T10%3A56%3A08Z")) .BuildRequest() .Map(request => request.EndDate) .Should() .BeSuccess(DateTimeOffset.Parse("2023-12-18T10:56:08Z", CultureInfo.InvariantCulture)); - + [Fact] public void BuildRequestForPrevious_ShouldReturnSuccess_WithStartDate() => - new GetMembersHalLink(new Uri( + new GetConversationsHalLink(new Uri( "https://api.nexmo.com/v1/users?order=desc&page_size=10&date_start=2023-12-18T09%3A56%3A08Z")) .BuildRequest() .Map(request => request.StartDate) diff --git a/Vonage.Test/Conversations/GetEvents/Data/ShouldDeserialize200-response.json b/Vonage.Test/Conversations/GetEvents/Data/ShouldDeserialize200-response.json new file mode 100644 index 000000000..9d0eb9c72 --- /dev/null +++ b/Vonage.Test/Conversations/GetEvents/Data/ShouldDeserialize200-response.json @@ -0,0 +1,49 @@ +{ + "page_size": 10, + "_links": { + "first": { + "href": "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false" + }, + "self": { + "href": "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false" + }, + "next": { + "href": "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false" + }, + "prev": { + "href": "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false" + } + }, + "_embedded": [ + { + "id": 100, + "type": "message", + "from": "string", + "body": { + "message_type": "text", + "text": "string" + }, + "timestamp": "2020-01-01T14:00:00.00Z", + "_embedded": { + "from_user": { + "id": "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + "name": "my_user_name", + "display_name": "My User Name", + "image_url": "https://example.com/image.png", + "custom_data": { + "field_1": "value_1", + "field_2": "value_2" + } + }, + "from_member": { + "id": "string" + } + }, + "_links": { + "self": { + "href": "https://api.nexmo.com/v1/conversations/CON-123/events/100" + } + } + } + ] +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/GetEvents/E2ETest.cs b/Vonage.Test/Conversations/GetEvents/E2ETest.cs new file mode 100644 index 000000000..bcbd0584e --- /dev/null +++ b/Vonage.Test/Conversations/GetEvents/E2ETest.cs @@ -0,0 +1,91 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Vonage.Conversations; +using Vonage.Conversations.GetEvents; +using Vonage.Test.Common.Extensions; +using WireMock.ResponseBuilders; +using Xunit; + +namespace Vonage.Test.Conversations.GetEvents; + +[Trait("Category", "E2E")] +public class E2ETest : E2EBase +{ + public E2ETest() : base(typeof(E2ETest).Namespace) + { + } + + [Fact] + public async Task GetEventsWithDefaultRequest() + { + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/v1/conversations/CON-123/events") + .WithParam("page_size", "10") + .WithParam("order", "asc") + .WithParam("exclude_deleted_events", "false") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .UsingGet()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); + await this.Helper.VonageClient.ConversationsClient + .GetEventsAsync(GetEventsRequest.Build() + .WithConversationId("CON-123") + .Create()) + .Should() + .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); + } + + [Fact] + public async Task GetEvents() + { + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/v1/conversations/CON-123/events") + .WithParam("page_size", "50") + .WithParam("order", "desc") + .WithParam("exclude_deleted_events", "true") + .WithParam("event_type", "submitted") + .WithParam("start_id", "123") + .WithParam("end_id", "456") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .UsingGet()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); + await this.Helper.VonageClient.ConversationsClient + .GetEventsAsync(GetEventsRequest.Build() + .WithConversationId("CON-123") + .WithPageSize(50) + .WithOrder(FetchOrder.Descending) + .WithEventType("submitted") + .WithStartId("123") + .WithEndId("456") + .ExcludeDeletedEvents() + .Create()) + .Should() + .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); + } + + [Fact] + public async Task GetEventsFromHalLink() + { + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/v1/conversations/CON-123/events") + .WithParam("page_size", "50") + .WithParam("order", "desc") + .WithParam("exclude_deleted_events", "true") + .WithParam("event_type", "submitted") + .WithParam("start_id", "123") + .WithParam("end_id", "456") + .WithParam("cursor", "7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .UsingGet()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); + await this.Helper.VonageClient.ConversationsClient + .GetEventsAsync(new GetEventsHalLink(new Uri( + "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=50&order=desc&exclude_deleted_events=true&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D&start_id=123&end_id=456&event_type=submitted")) + .BuildRequest()) + .Should() + .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); + } +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/GetEvents/RequestBuilderTest.cs b/Vonage.Test/Conversations/GetEvents/RequestBuilderTest.cs new file mode 100644 index 000000000..03d356525 --- /dev/null +++ b/Vonage.Test/Conversations/GetEvents/RequestBuilderTest.cs @@ -0,0 +1,194 @@ +using Vonage.Common.Monads; +using Vonage.Conversations; +using Vonage.Conversations.GetEvents; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.Conversations.GetEvents; + +[Trait("Category", "Request")] +public class RequestBuilderTest +{ + private const string ValidConversationId = "CON-123"; + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Parse_ShouldReturnFailure_GivenConversationIdIsEmpty(string invalidId) => + GetEventsRequest + .Build() + .WithConversationId(invalidId) + .Create() + .Should() + .BeParsingFailure("ConversationId cannot be null or whitespace."); + + [Fact] + public void Build_ShouldSetConversationId() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.ConversationId) + .Should() + .BeSuccess(ValidConversationId); + + [Fact] + public void Build_ShouldHaveDefaultOrder() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.Order) + .Should() + .BeSuccess(FetchOrder.Ascending); + + [Fact] + public void Build_ShouldHaveDefaultPageSize() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.PageSize) + .Should() + .BeSuccess(10); + + [Fact] + public void Build_ShouldHaveNoDefaultCursor() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.Cursor) + .Should() + .BeSuccess(Maybe.None); + + [Fact] + public void Build_ShouldHaveNoDefaultEventType() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.EventType) + .Should() + .BeSuccess(Maybe.None); + + [Fact] + public void Build_ShouldHaveNoDefaultEndId() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.EndId) + .Should() + .BeSuccess(Maybe.None); + + [Fact] + public void Build_ShouldHaveNoDefaultStartId() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.StartId) + .Should() + .BeSuccess(Maybe.None); + + [Fact] + public void Build_ShouldReturnFailure_GivenPageSizeIsHigherThanOneHundred() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithPageSize(101) + .Create() + .Should() + .BeParsingFailure("PageSize cannot be higher than 100."); + + [Fact] + public void Build_ShouldReturnFailure_GivenPageSizeIsLowerThanOne() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithPageSize(0) + .Create() + .Should() + .BeParsingFailure("PageSize cannot be lower than 1."); + + [Fact] + public void Build_ShouldSetEndId() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithEndId("123") + .Create() + .Map(request => request.EndId) + .Should() + .BeSuccess("123"); + + [Fact] + public void Build_ShouldSetOrder() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithOrder(FetchOrder.Descending) + .Create() + .Map(request => request.Order) + .Should() + .BeSuccess(FetchOrder.Descending); + + [Theory] + [InlineData(1)] + [InlineData(50)] + [InlineData(100)] + public void Build_ShouldSetPageSize(int pageSize) => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithPageSize(pageSize) + .Create() + .Map(request => request.PageSize) + .Should() + .BeSuccess(pageSize); + + [Fact] + public void Build_ShouldSetStartId() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithStartId("123") + .Create() + .Map(request => request.StartId) + .Should() + .BeSuccess("123"); + + [Fact] + public void Build_ShouldSetEventType() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .WithEventType("type") + .Create() + .Map(request => request.EventType) + .Should() + .BeSuccess("type"); + + [Fact] + public void Build_ShouldIncludeDeletedEventsGivenDefault() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .Create() + .Map(request => request.ExcludeDeletedEvents) + .Should() + .BeSuccess(false); + + [Fact] + public void Build_ShouldExcludeDeletedEvents() => + GetEventsRequest + .Build() + .WithConversationId(ValidConversationId) + .ExcludeDeletedEvents() + .Create() + .Map(request => request.ExcludeDeletedEvents) + .Should() + .BeSuccess(true); +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/GetEvents/RequestTest.cs b/Vonage.Test/Conversations/GetEvents/RequestTest.cs new file mode 100644 index 000000000..3e8f0317f --- /dev/null +++ b/Vonage.Test/Conversations/GetEvents/RequestTest.cs @@ -0,0 +1,67 @@ +using Vonage.Conversations; +using Vonage.Conversations.GetEvents; +using Vonage.Test.Common.Extensions; +using Xunit; + +namespace Vonage.Test.Conversations.GetEvents; + +[Trait("Category", "Request")] +public class RequestTest +{ + [Theory] + [InlineData(null, null, null, null, null, false, + "/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false")] + [InlineData(50, null, null, null, null, false, + "/v1/conversations/CON-123/events?page_size=50&order=asc&exclude_deleted_events=false")] + [InlineData(null, FetchOrder.Descending, null, null, null, false, + "/v1/conversations/CON-123/events?page_size=10&order=desc&exclude_deleted_events=false")] + [InlineData(null, null, "123", null, null, false, + "/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false&start_id=123")] + [InlineData(null, null, null, "123", null, false, + "/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false&end_id=123")] + [InlineData(null, null, null, null, "submitted", false, + "/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false&event_type=submitted")] + [InlineData(null, null, null, null, null, true, + "/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=true")] + [InlineData(50, FetchOrder.Descending, "123", "456", "submitted", true, + "/v1/conversations/CON-123/events?page_size=50&order=desc&exclude_deleted_events=true&start_id=123&end_id=456&event_type=submitted")] + public void GetEndpointPath_ShouldReturnApiEndpoint(int? pageSize, FetchOrder? order, string startId, + string endId, + string eventType, + bool excludeDeletedEvents, + string expectedEndpoint) + { + var builder = GetEventsRequest.Build().WithConversationId("CON-123"); + if (pageSize.HasValue) + { + builder = builder.WithPageSize(pageSize.Value); + } + + if (order.HasValue) + { + builder = builder.WithOrder(order.Value); + } + + if (!string.IsNullOrWhiteSpace(startId)) + { + builder = builder.WithStartId(startId); + } + + if (!string.IsNullOrWhiteSpace(endId)) + { + builder = builder.WithEndId(endId); + } + + if (!string.IsNullOrWhiteSpace(eventType)) + { + builder = builder.WithEventType(eventType); + } + + if (excludeDeletedEvents) + { + builder = builder.ExcludeDeletedEvents(); + } + + builder.Create().Map(request => request.GetEndpointPath()).Should().BeSuccess(expectedEndpoint); + } +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/GetEvents/SerializationTest.cs b/Vonage.Test/Conversations/GetEvents/SerializationTest.cs new file mode 100644 index 000000000..526d06cdd --- /dev/null +++ b/Vonage.Test/Conversations/GetEvents/SerializationTest.cs @@ -0,0 +1,57 @@ +using System; +using FluentAssertions; +using Vonage.Common; +using Vonage.Conversations; +using Vonage.Conversations.GetEvents; +using Vonage.Serialization; +using Vonage.Test.Common; +using Vonage.Test.Common.Extensions; +using Xunit; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Vonage.Test.Conversations.GetEvents; + +[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(GetEventsResponse response) + { + response.PageSize.Should().Be(10); + response.Embedded[0].Id.Should().Be(100); + response.Embedded[0].Type.Should().Be("message"); + response.Embedded[0].From.Should().Be("string"); + response.Embedded[0].Body.Should() + .Be(JsonSerializer.SerializeToElement(new {message_type = "text", text = "string"})); + response.Embedded[0].Embedded.Member.Should().Be(new EmbeddedEventMember("string")); + response.Embedded[0].Embedded.User.Id.Should().Be("USR-82e028d9-5201-4f1e-8188-604b2d3471ec"); + response.Embedded[0].Embedded.User.Name.Should().Be("my_user_name"); + response.Embedded[0].Embedded.User.DisplayName.Should().Be("My User Name"); + response.Embedded[0].Embedded.User.ImageUrl.Should().Be("https://example.com/image.png"); + response.Embedded[0].Embedded.User.CustomData.Should() + .Be(JsonSerializer.SerializeToElement(new {field_1 = "value_1", field_2 = "value_2"})); + response.Embedded[0].Links.Should() + .Be(new Links(new HalLink(new Uri("https://api.nexmo.com/v1/conversations/CON-123/events/100")))); + response.Links.First.Href.Should() + .Be(new Uri( + "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false")); + response.Links.Self.Href.Should() + .Be(new Uri( + "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false")); + response.Links.Next.Href.Should() + .Be(new Uri( + "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false")); + response.Links.Previous.Href.Should() + .Be(new Uri( + "https://api.nexmo.com/v1/conversations/CON-123/events?page_size=10&order=asc&exclude_deleted_events=false")); + } +} \ No newline at end of file diff --git a/Vonage.Test/Conversations/GetMembers/E2ETest.cs b/Vonage.Test/Conversations/GetMembers/E2ETest.cs index cf3412100..2ef79128e 100644 --- a/Vonage.Test/Conversations/GetMembers/E2ETest.cs +++ b/Vonage.Test/Conversations/GetMembers/E2ETest.cs @@ -15,7 +15,7 @@ public class E2ETest : E2EBase public E2ETest() : base(typeof(E2ETest).Namespace) { } - + [Fact] public async Task GetMembers() { @@ -36,7 +36,7 @@ await this.Helper.VonageClient.ConversationsClient .Should() .BeSuccessAsync(SerializationTest.VerifyExpectedResponse); } - + [Fact] public async Task GetMembersFromHalLink() { @@ -50,7 +50,7 @@ public async Task GetMembersFromHalLink() .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); await this.Helper.VonageClient.ConversationsClient - .GetMembersAsync(new GetMembersHalLink(new Uri( + .GetMembersAsync(new GetConversationsHalLink(new Uri( "https://api.nexmo.com/v1/conversations/CON-123/members?order=desc&page_size=50&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D")) .BuildRequest()) .Should() diff --git a/Vonage.Test/Vonage.Test.csproj b/Vonage.Test/Vonage.Test.csproj index 9743393b4..0787360cc 100644 --- a/Vonage.Test/Vonage.Test.csproj +++ b/Vonage.Test/Vonage.Test.csproj @@ -1200,6 +1200,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Vonage/Conversations/ConversationsClient.cs b/Vonage/Conversations/ConversationsClient.cs index f286472b2..17e75ba98 100644 --- a/Vonage/Conversations/ConversationsClient.cs +++ b/Vonage/Conversations/ConversationsClient.cs @@ -8,6 +8,7 @@ using Vonage.Conversations.GetConversation; using Vonage.Conversations.GetConversations; using Vonage.Conversations.GetEvent; +using Vonage.Conversations.GetEvents; using Vonage.Conversations.GetMember; using Vonage.Conversations.GetMembers; using Vonage.Conversations.GetUserConversations; @@ -49,6 +50,10 @@ public Task> GetConversationAsync(Result> GetEventAsync(Result request) => this.vonageClient.SendWithResponseAsync(request); + /// + public Task> GetEventsAsync(Result request) => + this.vonageClient.SendWithResponseAsync(request); + /// public Task> CreateMemberAsync(Result request) => this.vonageClient.SendWithResponseAsync(request); diff --git a/Vonage/Conversations/GetConversations/GetConversationsRequestBuilder.cs b/Vonage/Conversations/GetConversations/GetConversationsRequestBuilder.cs index 0de73d5d6..28ae2da51 100644 --- a/Vonage/Conversations/GetConversations/GetConversationsRequestBuilder.cs +++ b/Vonage/Conversations/GetConversations/GetConversationsRequestBuilder.cs @@ -17,7 +17,6 @@ internal struct GetConversationsRequestBuilder : IBuilderForOptional internal GetConversationsRequestBuilder(Maybe cursor) => this.cursor = cursor; - /// public Result Create() => Result.FromSuccess( new GetConversationsRequest { @@ -30,16 +29,12 @@ public Result Create() => Result.Evaluate) .Bind(evaluation => evaluation.WithRules(VerifyMinimumPageSize, VerifyMaximumPageSize)); - /// public IBuilderForOptional WithEndDate(DateTimeOffset value) => this with {endDate = value}; - /// public IBuilderForOptional WithOrder(FetchOrder value) => this with {fetchOrder = value}; - /// public IBuilderForOptional WithPageSize(int value) => this with {pageSize = value}; - /// public IBuilderForOptional WithStartDate(DateTimeOffset value) => this with {startDate = value}; private static Result VerifyMaximumPageSize(GetConversationsRequest request) => diff --git a/Vonage/Conversations/GetConversations/GetConversationsResponse.cs b/Vonage/Conversations/GetConversations/GetConversationsResponse.cs index 13e759e22..739845420 100644 --- a/Vonage/Conversations/GetConversations/GetConversationsResponse.cs +++ b/Vonage/Conversations/GetConversations/GetConversationsResponse.cs @@ -22,7 +22,7 @@ public record GetConversationsResponse( EmbeddedConversations Embedded, [property: JsonPropertyName("_links")] [property: JsonPropertyOrder(2)] - HalLinks Links); + HalLinks Links); /// /// Represents a list of conversations. @@ -34,10 +34,10 @@ public record EmbeddedConversations(Conversation[] Conversations); /// Represents a link to another resource. /// /// Hyperlink reference. -public record GetMembersHalLink(Uri Href) +public record GetConversationsHalLink(Uri Href) { /// - /// Transforms the link into a GetUsersRequest using the cursor pagination. + /// Transforms the link into a GetConversationsRequest using the cursor pagination. /// /// public Result BuildRequest() @@ -50,15 +50,15 @@ public Result BuildRequest() builder = ApplyOptionalEndDate(parameters, builder); return builder.Create(); } - + private static IBuilderForOptional ApplyOptionalStartDate(QueryParameters parameters, IBuilderForOptional builder) => parameters.StartDate.Match(builder.WithStartDate, () => builder); - + private static IBuilderForOptional ApplyOptionalEndDate(QueryParameters parameters, IBuilderForOptional builder) => parameters.EndDate.Match(builder.WithEndDate, () => builder); - + private static QueryParameters ExtractQueryParameters(Uri uri) { var queryParameters = HttpUtility.ParseQueryString(uri.Query); @@ -71,7 +71,7 @@ private static QueryParameters ExtractQueryParameters(Uri uri) startDate.Map(value => DateTimeOffset.Parse(value, CultureInfo.InvariantCulture)), endDate.Map(value => DateTimeOffset.Parse(value, CultureInfo.InvariantCulture))); } - + private record QueryParameters( Maybe Cursor, int PageSize, diff --git a/Vonage/Conversations/GetEvents/GetEventRequest.cs b/Vonage/Conversations/GetEvents/GetEventRequest.cs new file mode 100644 index 000000000..0e3aa3bfe --- /dev/null +++ b/Vonage/Conversations/GetEvents/GetEventRequest.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Net.Http; +using EnumsNET; +using Vonage.Common; +using Vonage.Common.Client; +using Vonage.Common.Monads; + +namespace Vonage.Conversations.GetEvents; + +/// +public readonly struct GetEventsRequest : IVonageRequest +{ + /// + /// The conversation Id. + /// + public string ConversationId { get; internal init; } + + /// + /// The cursor to start returning results from. You are not expected to provide this manually, but to follow the url + /// provided in _links.next.href or _links.prev.href in the response which contains a cursor value. + /// + public Maybe Cursor { get; internal init; } + + /// + /// The ID to end returning events at + /// + public Maybe EndId { get; internal init; } + + /// + /// Defines the data ordering. + /// + public FetchOrder Order { get; internal init; } + + /// + /// Number of results per page. + /// + public int PageSize { get; internal init; } + + /// + /// The ID to start returning events at + /// + public Maybe StartId { get; internal init; } + + /// + /// The type of event to search for. Does not currently support custom events + /// + public Maybe EventType { get; internal init; } + + /// + /// Exclude deleted events from the response + /// + public bool ExcludeDeletedEvents { get; internal init; } + + /// + public HttpRequestMessage BuildRequestMessage() => VonageRequestBuilder + .Initialize(HttpMethod.Get, this.GetEndpointPath()) + .Build(); + + /// + public string GetEndpointPath() => UriHelpers.BuildUri($"/v1/conversations/{this.ConversationId}/events", + this.GetQueryStringParameters()); + + private Dictionary GetQueryStringParameters() + { + var parameters = new Dictionary + { + {"page_size", this.PageSize.ToString()}, + {"order", this.Order.AsString(EnumFormat.Description)}, + {"exclude_deleted_events", this.ExcludeDeletedEvents.ToString().ToLowerInvariant()}, + }; + this.Cursor.IfSome(value => parameters.Add("cursor", value)); + this.StartId.IfSome(value => parameters.Add("start_id", value)); + this.EndId.IfSome(value => parameters.Add("end_id", value)); + this.EventType.IfSome(value => parameters.Add("event_type", value)); + return parameters; + } + + /// + /// Initializes a builder. + /// + /// The builder. + public static IBuilderForConversationId Build() => new GetEventsRequestBuilder(Maybe.None); +} \ No newline at end of file diff --git a/Vonage/Conversations/GetEvents/GetEventsRequestBuilder.cs b/Vonage/Conversations/GetEvents/GetEventsRequestBuilder.cs new file mode 100644 index 000000000..ccb3e0e78 --- /dev/null +++ b/Vonage/Conversations/GetEvents/GetEventsRequestBuilder.cs @@ -0,0 +1,112 @@ +using Vonage.Common.Client; +using Vonage.Common.Monads; +using Vonage.Common.Validation; + +namespace Vonage.Conversations.GetEvents; + +internal struct GetEventsRequestBuilder : IBuilderForConversationId, IBuilderForOptional +{ + private const int MaximumPageSize = 100; + private const int MinimumPageSize = 1; + private string conversationId; + private FetchOrder fetchOrder = FetchOrder.Ascending; + private int pageSize = 10; + private Maybe endId = Maybe.None; + private Maybe startId = Maybe.None; + private Maybe eventType = Maybe.None; + private bool excludeDeleted = false; + private readonly Maybe cursor; + + internal GetEventsRequestBuilder(Maybe cursor) => this.cursor = cursor; + + public Result Create() => Result.FromSuccess(new GetEventsRequest + { + ConversationId = this.conversationId, + ExcludeDeletedEvents = this.excludeDeleted, + Cursor = this.cursor, + Order = this.fetchOrder, + PageSize = this.pageSize, + EndId = this.endId, + EventType = this.eventType, + StartId = this.startId, + }) + .Map(InputEvaluation.Evaluate) + .Bind(evaluation => evaluation.WithRules(VerifyConversationId, VerifyMinimumPageSize, VerifyMaximumPageSize)); + + public IBuilderForOptional WithEndId(string value) => this with {endId = value}; + public IBuilderForOptional WithOrder(FetchOrder value) => this with {fetchOrder = value}; + public IBuilderForOptional WithPageSize(int value) => this with {pageSize = value}; + public IBuilderForOptional WithStartId(string value) => this with {startId = value}; + public IBuilderForOptional WithEventType(string value) => this with {eventType = value}; + public IBuilderForOptional ExcludeDeletedEvents() => this with {excludeDeleted = true}; + public IBuilderForOptional WithConversationId(string value) => this with {conversationId = value}; + + private static Result VerifyConversationId(GetEventsRequest request) => + InputValidation.VerifyNotEmpty(request, request.ConversationId, nameof(request.ConversationId)); + + private static Result VerifyMaximumPageSize(GetEventsRequest request) => + InputValidation.VerifyLowerOrEqualThan(request, request.PageSize, MaximumPageSize, nameof(request.PageSize)); + + private static Result VerifyMinimumPageSize(GetEventsRequest request) => + InputValidation.VerifyHigherOrEqualThan(request, request.PageSize, MinimumPageSize, nameof(request.PageSize)); +} + +/// +/// Represents a builder for ConversationId. +/// +public interface IBuilderForConversationId +{ + /// + /// Sets the ConversationId on the builder. + /// + /// The conversation Id. + /// The builder. + IBuilderForOptional WithConversationId(string value); +} + +/// +/// Represents a builder for optional values. +/// +public interface IBuilderForOptional : IVonageRequestBuilder +{ + /// + /// Sets the end id on the builder. + /// + /// The end id. + /// The builder. + IBuilderForOptional WithEndId(string value); + + /// + /// Sets the order on the builder. + /// + /// The order. + /// The builder. + IBuilderForOptional WithOrder(FetchOrder value); + + /// + /// Sets the page size on the builder. + /// + /// The page size. + /// The builder. + IBuilderForOptional WithPageSize(int value); + + /// + /// Sets the start id on the builder. + /// + /// The start id. + /// The builder. + IBuilderForOptional WithStartId(string value); + + /// + /// Sets the event type on the builder. + /// + /// The event type. + /// The builder. + IBuilderForOptional WithEventType(string value); + + /// + /// Sets builder to exclude deleted events. + /// + /// The builder. + IBuilderForOptional ExcludeDeletedEvents(); +} \ No newline at end of file diff --git a/Vonage/Conversations/GetEvents/GetEventsResponse.cs b/Vonage/Conversations/GetEvents/GetEventsResponse.cs new file mode 100644 index 000000000..c312b3470 --- /dev/null +++ b/Vonage/Conversations/GetEvents/GetEventsResponse.cs @@ -0,0 +1,95 @@ +using System; +using System.Text.Json.Serialization; +using System.Web; +using EnumsNET; +using Vonage.Common; +using Vonage.Common.Monads; + +namespace Vonage.Conversations.GetEvents; + +/// +/// +/// +/// +/// +public record GetEventsResponse( + [property: JsonPropertyName("page_size")] + [property: JsonPropertyOrder(0)] + int PageSize, + [property: JsonPropertyName("_embedded")] + [property: JsonPropertyOrder(1)] + Event[] Embedded, + [property: JsonPropertyName("_links")] + [property: JsonPropertyOrder(2)] + HalLinks Links); + +/// +/// Represents a link to another resource. +/// +/// Hyperlink reference. +public record GetEventsHalLink(Uri Href) +{ + /// + /// Transforms the link into a GetEventsRequest using the cursor pagination. + /// + /// + public Result BuildRequest() + { + var parameters = ExtractQueryParameters(this.Href); + var builder = new GetEventsRequestBuilder(parameters.Cursor) + .WithConversationId(parameters.ConversationId) + .WithPageSize(parameters.PageSize) + .WithOrder(parameters.Order); + builder = ApplyOptionalStartId(parameters, builder); + builder = ApplyOptionalEndDate(parameters, builder); + builder = ApplyOptionalEventType(parameters, builder); + builder = ExcludeDeletedEvents(parameters, builder); + return builder.Create(); + } + + private static IBuilderForOptional + ApplyOptionalStartId(QueryParameters parameters, IBuilderForOptional builder) => + parameters.StartId.Match(builder.WithStartId, () => builder); + + private static IBuilderForOptional + ApplyOptionalEndDate(QueryParameters parameters, IBuilderForOptional builder) => + parameters.EndId.Match(builder.WithEndId, () => builder); + + private static IBuilderForOptional + ApplyOptionalEventType(QueryParameters parameters, IBuilderForOptional builder) => + parameters.EventType.Match(builder.WithEventType, () => builder); + + private static IBuilderForOptional + ExcludeDeletedEvents(QueryParameters parameters, IBuilderForOptional builder) => + parameters.ExcludeDeletedEvents ? builder.ExcludeDeletedEvents() : builder; + + private static QueryParameters ExtractQueryParameters(Uri uri) + { + var queryParameters = HttpUtility.ParseQueryString(uri.Query); + var startDate = queryParameters["start_id"] ?? Maybe.None; + var endDate = queryParameters["end_id"] ?? Maybe.None; + var eventType = queryParameters["event_type"] ?? Maybe.None; + return new QueryParameters( + queryParameters["cursor"], + ExtractConversationId(uri), + int.Parse(queryParameters["page_size"]), + Enums.Parse(queryParameters["order"], false, EnumFormat.Description), + startDate, + endDate, + eventType, + bool.Parse(queryParameters["exclude_deleted_events"])); + } + + private static string ExtractConversationId(Uri uri) => uri.AbsolutePath.Replace("/v1/conversations/", string.Empty) + .Replace("/events", string.Empty); + + private record QueryParameters( + Maybe Cursor, + string ConversationId, + int PageSize, + FetchOrder Order, + Maybe StartId, + Maybe EndId, + Maybe EventType, + bool ExcludeDeletedEvents); +} \ No newline at end of file diff --git a/Vonage/Conversations/GetMembers/GetMembersResponse.cs b/Vonage/Conversations/GetMembers/GetMembersResponse.cs index 3e10b7c5e..f9242e1c4 100644 --- a/Vonage/Conversations/GetMembers/GetMembersResponse.cs +++ b/Vonage/Conversations/GetMembers/GetMembersResponse.cs @@ -16,7 +16,7 @@ public record GetMembersResponse( EmbeddedMembers Embedded, [property: JsonPropertyName("_links")] [property: JsonPropertyOrder(2)] - HalLinks Links); + HalLinks Links); /// /// Represents a list of conversations. @@ -28,10 +28,10 @@ public record EmbeddedMembers(Member[] Members); /// Represents a link to another resource. /// /// Hyperlink reference. -public record GetMembersHalLink(Uri Href) +public record GetConversationsHalLink(Uri Href) { /// - /// Transforms the link into a GetUsersRequest using the cursor pagination. + /// Transforms the link into a GetMembersRequest using the cursor pagination. /// /// public Result BuildRequest() @@ -42,7 +42,7 @@ public Result BuildRequest() .WithPageSize(parameters.PageSize) .WithOrder(parameters.Order).Create(); } - + private static QueryParameters ExtractQueryParameters(Uri uri) { var queryParameters = HttpUtility.ParseQueryString(uri.Query); @@ -52,10 +52,10 @@ private static QueryParameters ExtractQueryParameters(Uri uri) int.Parse(queryParameters["page_size"]), Enums.Parse(queryParameters["order"], false, EnumFormat.Description)); } - + private static string ExtractConversationId(Uri uri) => uri.AbsolutePath.Replace("/v1/conversations/", string.Empty) .Replace("/members", string.Empty); - + private record QueryParameters( Maybe Cursor, string ConversationId, diff --git a/Vonage/Conversations/GetUserConversations/GetUserConversationsResponse.cs b/Vonage/Conversations/GetUserConversations/GetUserConversationsResponse.cs index 6d55c5048..b609b727d 100644 --- a/Vonage/Conversations/GetUserConversations/GetUserConversationsResponse.cs +++ b/Vonage/Conversations/GetUserConversations/GetUserConversationsResponse.cs @@ -18,7 +18,7 @@ public record GetUserConversationsResponse( EmbeddedConversations Embedded, [property: JsonPropertyName("_links")] [property: JsonPropertyOrder(2)] - HalLinks Links); + HalLinks Links); /// /// Represents a list of conversations. @@ -49,21 +49,21 @@ public Result BuildRequest() builder = ApplyOptionalIncludeCustomData(parameters, builder); return builder.Create(); } - + private static IBuilderForOptional ApplyOptionalIncludeCustomData(QueryParameters parameters, IBuilderForOptional builder) => parameters.IncludeCustomData.IfNone(false) ? builder.IncludeCustomData() : builder; - + private static IBuilderForOptional ApplyOptionalState(QueryParameters parameters, IBuilderForOptional builder) => parameters.State.Match(builder.WithState, () => builder); - + private static IBuilderForOptional ApplyOptionalStartDate(QueryParameters parameters, IBuilderForOptional builder) => parameters.StartDate.Match(builder.WithStartDate, () => builder); - + private static IBuilderForOptional ApplyOptionalOrderBy(QueryParameters parameters, IBuilderForOptional builder) => parameters.OrderBy.Match(builder.WithOrderBy, () => builder); - + private static QueryParameters ExtractQueryParameters(Uri uri) { var queryParameters = HttpUtility.ParseQueryString(uri.Query); @@ -81,7 +81,7 @@ private static QueryParameters ExtractQueryParameters(Uri uri) includeCustomData.Match(bool.Parse, () => false), state.Map(value => Enums.Parse(value, false, EnumFormat.Description))); } - + private record QueryParameters( string UserId, Maybe Cursor, diff --git a/Vonage/Conversations/IConversationsClient.cs b/Vonage/Conversations/IConversationsClient.cs index 61b5fbc2f..69126f7fe 100644 --- a/Vonage/Conversations/IConversationsClient.cs +++ b/Vonage/Conversations/IConversationsClient.cs @@ -7,6 +7,7 @@ using Vonage.Conversations.GetConversation; using Vonage.Conversations.GetConversations; using Vonage.Conversations.GetEvent; +using Vonage.Conversations.GetEvents; using Vonage.Conversations.GetMember; using Vonage.Conversations.GetMembers; using Vonage.Conversations.GetUserConversations; @@ -55,6 +56,13 @@ public interface IConversationsClient /// Success or Failure. Task> GetEventAsync(Result request); + /// + /// Retrieves events. + /// + /// The request. + /// Success or Failure. + Task> GetEventsAsync(Result request); + /// /// Creates a member. ///