diff --git a/examples/WebApi/Controllers/ReceiveConversationCallbackController.cs b/examples/WebApi/Controllers/ReceiveConversationCallbackController.cs index eab43ef..22bd362 100644 --- a/examples/WebApi/Controllers/ReceiveConversationCallbackController.cs +++ b/examples/WebApi/Controllers/ReceiveConversationCallbackController.cs @@ -30,8 +30,11 @@ public ActionResult Handle([FromBody] JsonObject json) _logger.LogError("Failed to authorize received callback."); return Unauthorized(); } - + // you can use System.Text.Json Deserialize method + // ReSharper disable once RedundantAssignment var callbackEvent = json.Deserialize(); + // or use Deserialize provided by SDK + callbackEvent = _sinch.Conversation.Webhooks.DeserializeCallbackEvent(json); // do something with specific event switch (callbackEvent) { diff --git a/src/Sinch/Conversation/Common/ChannelIdentity.cs b/src/Sinch/Conversation/Common/ChannelIdentity.cs index b80bd74..9974775 100644 --- a/src/Sinch/Conversation/Common/ChannelIdentity.cs +++ b/src/Sinch/Conversation/Common/ChannelIdentity.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Sinch.Conversation.Common { public class ChannelIdentity @@ -8,18 +10,21 @@ public class ChannelIdentity /// different channel identities on different Conversation API apps. These can be thought of as virtual identities that /// are app-specific and, therefore, the app_id must be included in the API call. /// + [JsonPropertyName("app_id")] public string? AppId { get; set; } /// /// The channel identity. This will differ from channel to channel. For example, a phone number for SMS, WhatsApp, and /// Viber Business. /// + [JsonPropertyName("identity")] public string? Identity { get; set; } /// /// The identifier of the channel you want to include. Must be one of the enum values. See /// fields. /// + [JsonPropertyName("channel")] public ConversationChannel? Channel { get; set; } } } diff --git a/src/Sinch/Conversation/Contacts/Contact.cs b/src/Sinch/Conversation/Contacts/Contact.cs index d66aa8d..06f3e7d 100644 --- a/src/Sinch/Conversation/Contacts/Contact.cs +++ b/src/Sinch/Conversation/Contacts/Contact.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text; +using System.Text.Json.Serialization; using Sinch.Conversation.Common; using Sinch.Core; @@ -19,6 +20,7 @@ public sealed class Contact : PropertyMaskQuery /// /// List of channel identities. /// + [JsonPropertyName("channel_identities")] public List? ChannelIdentities { get => _channelIdentities; @@ -33,6 +35,7 @@ public List? ChannelIdentities /// /// List of channels defining the channel priority. /// + [JsonPropertyName("channel_priority")] public List? ChannelPriority { get => _channelPriority; @@ -47,6 +50,7 @@ public List? ChannelPriority /// /// The display name. A default 'Unknown' will be assigned if left empty. /// + [JsonPropertyName("display_name")] public string? DisplayName { get => _displayName; @@ -61,6 +65,7 @@ public string? DisplayName /// /// Email of the contact. /// + [JsonPropertyName("email")] public string? Email { get => _email; @@ -75,6 +80,7 @@ public string? Email /// /// Contact identifier in an external system. /// + [JsonPropertyName("external_id")] public string? ExternalId { get => _externalId; @@ -89,6 +95,7 @@ public string? ExternalId /// /// The ID of the contact. /// + [JsonPropertyName("id")] public string? Id { get => _id; @@ -103,6 +110,7 @@ public string? Id /// /// Gets or Sets Language /// + [JsonPropertyName("language")] public ConversationLanguage? Language { get => _language; @@ -117,6 +125,7 @@ public ConversationLanguage? Language /// /// Metadata associated with the contact. Up to 1024 characters long. /// + [JsonPropertyName("metadata")] public string? Metadata { get => _metadata; diff --git a/src/Sinch/Conversation/Conversations/Conversation.cs b/src/Sinch/Conversation/Conversations/Conversation.cs index 03a3c56..b598cb4 100644 --- a/src/Sinch/Conversation/Conversations/Conversation.cs +++ b/src/Sinch/Conversation/Conversations/Conversation.cs @@ -24,6 +24,7 @@ public sealed class Conversation : PropertyMaskQuery /// /// Gets or Sets ActiveChannel /// + [JsonPropertyName("active_channel")] public ConversationChannel? ActiveChannel { get => _activeChannel; @@ -38,6 +39,7 @@ public ConversationChannel? ActiveChannel /// /// Flag for whether this conversation is active. /// + [JsonPropertyName("active")] public bool? Active { get => _active; @@ -51,6 +53,7 @@ public bool? Active /// /// The ID of the participating app. /// + [JsonPropertyName("app_id")] public string? AppId { get => _appId; @@ -64,6 +67,7 @@ public string? AppId /// /// The ID of the participating contact. /// + [JsonPropertyName("contact_id")] public string? ContactId { get => _contactId; @@ -77,17 +81,20 @@ public string? ContactId /// /// The ID of the conversation. /// + [JsonPropertyName("id")] public string? Id { get; set; } /// /// The timestamp of the latest message in the conversation. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("last_received")] public DateTime LastReceived { get; set; } /// /// Arbitrary data set by the Conversation API clients. Up to 1024 characters long. /// + [JsonPropertyName("metadata")] public string? Metadata { get => _metadata; @@ -102,6 +109,7 @@ public string? Metadata /// Arbitrary data set by the Conversation API clients and/or provided in the conversation_metadata field of a /// SendMessageRequest. A valid JSON object. /// + [JsonPropertyName("metadata_json")] public JsonObject? MetadataJson { get => _metadataJson; @@ -112,6 +120,7 @@ public JsonObject? MetadataJson } } + [JsonPropertyName("correlation_id")] public string? CorrelationId { get => _correlationId; diff --git a/src/Sinch/Conversation/Hooks/CallbackEventBase.cs b/src/Sinch/Conversation/Hooks/CallbackEventBase.cs index f377810..614394f 100644 --- a/src/Sinch/Conversation/Hooks/CallbackEventBase.cs +++ b/src/Sinch/Conversation/Hooks/CallbackEventBase.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace Sinch.Conversation.Hooks @@ -35,7 +36,7 @@ public abstract class CallbackEventBase : ICallbackEvent /// Context-dependent metadata. Refer to specific callback's documentation for exact information provided. /// [JsonPropertyName("message_metadata")] - public string? MessageMetadata { get; set; } + public JsonNode? MessageMetadata { get; set; } /// diff --git a/src/Sinch/Conversation/Messages/Message/ContactMessage.cs b/src/Sinch/Conversation/Messages/Message/ContactMessage.cs index 520afb7..8fce07d 100644 --- a/src/Sinch/Conversation/Messages/Message/ContactMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/ContactMessage.cs @@ -53,6 +53,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("choice_response_message")] public ChoiceResponseMessage? ChoiceResponseMessage { get; private set; } @@ -61,6 +62,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fallback_message")] public FallbackMessage? FallbackMessage { get; private set; } @@ -69,6 +71,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("location_message")] public LocationMessage? LocationMessage { get; private set; } @@ -77,6 +80,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("media_card_message")] public MediaCarouselMessage? MediaCardMessage { get; private set; } @@ -85,6 +89,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("media_message")] public MediaMessage? MediaMessage { get; private set; } @@ -93,6 +98,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reply_to")] public ReplyTo? ReplyTo { get; private set; } @@ -101,6 +107,7 @@ public ContactMessage(TextMessage textMessage) /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("text_message")] public TextMessage? TextMessage { get; private set; } diff --git a/src/Sinch/Conversation/Messages/Message/FallbackMessage.cs b/src/Sinch/Conversation/Messages/Message/FallbackMessage.cs index 9b0fc27..c8fc2ab 100644 --- a/src/Sinch/Conversation/Messages/Message/FallbackMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/FallbackMessage.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json.Serialization; namespace Sinch.Conversation.Messages.Message { @@ -42,18 +43,21 @@ public sealed class Reason /// /// Gets or Sets Code /// + [JsonPropertyName("code")] public string? Code { get; set; } /// /// A textual description of the reason. /// + [JsonPropertyName("description")] public string? Description { get; set; } /// /// Gets or Sets SubCode /// + [JsonPropertyName("sub_code")] public string? SubCode { get; set; } diff --git a/src/Sinch/Conversation/Messages/Message/MediaMessage.cs b/src/Sinch/Conversation/Messages/Message/MediaMessage.cs index 3846215..cc7936b 100644 --- a/src/Sinch/Conversation/Messages/Message/MediaMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/MediaMessage.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Text.Json.Serialization; namespace Sinch.Conversation.Messages.Message { @@ -11,21 +12,24 @@ public sealed class MediaMessage : IOmniMessageOverride /// /// An optional parameter. Will be used where it is natively supported. /// - public Uri? ThumbnailUrl { get; set; } + [JsonPropertyName("thumbnail_url")] + public string? ThumbnailUrl { get; set; } /// /// Url to the media file. /// + [JsonPropertyName("url")] #if NET7_0_OR_GREATER - public required Uri Url { get; init; } + public required string Url { get; init; } #else - public Uri Url { get; set; } = null!; + public string Url { get; set; } = null!; #endif /// /// Overrides the media file name. /// + [JsonPropertyName("filename_override")] public string? FilenameOverride { get; set; } /// diff --git a/src/Sinch/Conversation/Messages/Message/TextMessage.cs b/src/Sinch/Conversation/Messages/Message/TextMessage.cs index f54ec0e..fd1190d 100644 --- a/src/Sinch/Conversation/Messages/Message/TextMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/TextMessage.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json.Serialization; namespace Sinch.Conversation.Messages.Message { @@ -30,6 +31,8 @@ public override string ToString() } /// Text + [JsonPropertyName("text")] + [JsonInclude] public string Text { get; init; } } } diff --git a/src/Sinch/Conversation/Webhooks/Webhooks.cs b/src/Sinch/Conversation/Webhooks/Webhooks.cs index a44f96c..0068211 100644 --- a/src/Sinch/Conversation/Webhooks/Webhooks.cs +++ b/src/Sinch/Conversation/Webhooks/Webhooks.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using System.Web; using Microsoft.Extensions.Primitives; +using Sinch.Conversation.Hooks; using Sinch.Core; using Sinch.Logger; @@ -70,6 +73,12 @@ public interface ISinchConversationWebhooks /// /// True, if produced signature match with that of a header. bool ValidateAuthenticationHeader(Dictionary headers, JsonObject body, string secret); + + ICallbackEvent DeserializeCallbackEvent(string json); + + ICallbackEvent DeserializeCallbackEvent(JsonNode json); + + Task DeserializeCallbackEventAsync(Stream json, CancellationToken cancellationToken = default); } /// @@ -163,7 +172,8 @@ public Task Delete(string webhookId, CancellationToken cancellationToken = defau cancellationToken); } - public bool ValidateAuthenticationHeader(Dictionary headers, JsonObject body, string secret) + public bool ValidateAuthenticationHeader(Dictionary headers, JsonObject body, + string secret) { var headersCaseInsensitive = new Dictionary(headers, StringComparer.InvariantCultureIgnoreCase); @@ -201,6 +211,42 @@ public bool ValidateAuthenticationHeader(Dictionary header _logger?.LogInformation("The signature was validated with {success}", isValidSignature); return isValidSignature; } + + public ICallbackEvent DeserializeCallbackEvent(string json) + { + var jsonResult = JsonSerializer.Deserialize(json, _http.JsonSerializerOptions); + if (jsonResult == null) + { + throw new InvalidOperationException("Deserialization of callback event failed"); + } + + return jsonResult; + } + + public ICallbackEvent DeserializeCallbackEvent(JsonNode json) + { + var jsonResult = json.Deserialize(_http.JsonSerializerOptions); + if (jsonResult == null) + { + throw new InvalidOperationException("Deserialization of callback event failed"); + } + + return jsonResult; + } + + public async Task DeserializeCallbackEventAsync(Stream jsonStream, + CancellationToken cancellationToken = default) + { + var jsonResult = + await JsonSerializer.DeserializeAsync(jsonStream, _http.JsonSerializerOptions, + cancellationToken); + if (jsonResult == null) + { + throw new InvalidOperationException("Deserialization of callback event failed"); + } + + return jsonResult; + } } internal class ListWebhooksResponse diff --git a/src/Sinch/Core/Http.cs b/src/Sinch/Core/Http.cs index b157a07..ec21d70 100644 --- a/src/Sinch/Core/Http.cs +++ b/src/Sinch/Core/Http.cs @@ -48,6 +48,8 @@ Task Send(Uri uri, HttpMethod httpMethod, /// Task Send(Uri uri, HttpMethod httpMethod, TRequest httpContent, CancellationToken cancellationToken = default, Dictionary>? headers = null); + + JsonSerializerOptions JsonSerializerOptions { get; } } /// @@ -148,7 +150,7 @@ public async Task Send(Uri uri, HttpMethod httpM ?? throw new InvalidOperationException( $"{typeof(TResponse).Name} is null"); - // if empty response is expected, any non related response is dropped + // if empty response is expected, any non-related response is dropped if (typeof(TResponse) == typeof(EmptyResponse)) { // if not empty content, check what is there for debug purposes. @@ -171,6 +173,8 @@ public async Task Send(Uri uri, HttpMethod httpM } } + public JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions; + private static void AddOrOverrideHeaders(HttpRequestMessage msg, Dictionary> headers) { diff --git a/tests/Sinch.Tests/Conversation/MessagesTests.cs b/tests/Sinch.Tests/Conversation/MessagesTests.cs index dd5e33e..622efba 100644 --- a/tests/Sinch.Tests/Conversation/MessagesTests.cs +++ b/tests/Sinch.Tests/Conversation/MessagesTests.cs @@ -52,7 +52,7 @@ public async Task GetMessage() Description = "desc", Media = new MediaMessage() { - Url = new Uri("http://localhost") + Url = "http://localhost" }, PostbackData = "postback" } @@ -408,7 +408,7 @@ public class OmniMessageTestData : IEnumerable new object[] { Text, new TextMessage("hello") }, new object[] { Media, new MediaMessage() { - Url = new Uri("https://hello.net") + Url = "https://hello.net" }}, new object[] { Template, new TemplateReference() { diff --git a/tests/Sinch.Tests/Conversation/SendMessageTests.cs b/tests/Sinch.Tests/Conversation/SendMessageTests.cs index b5f2496..892c06f 100644 --- a/tests/Sinch.Tests/Conversation/SendMessageTests.cs +++ b/tests/Sinch.Tests/Conversation/SendMessageTests.cs @@ -244,8 +244,8 @@ public async Task SendMedia() }; _baseRequest.Message = new AppMessage(new MediaMessage { - Url = new Uri("http://yup/ls"), - ThumbnailUrl = new Uri("https://img.c") + Url = "http://yup/ls", + ThumbnailUrl = "https://img.c" }); HttpMessageHandlerMock .When(HttpMethod.Post, @@ -392,8 +392,8 @@ public async Task SendList() Description = "desc", Media = new MediaMessage() { - Url = new Uri("https://nolocalhost"), - ThumbnailUrl = new Uri("https://knowyourmeme.com/photos/377946") + Url = "https://nolocalhost", + ThumbnailUrl = "https://knowyourmeme.com/photos/377946" } }, new ProductItem diff --git a/tests/Sinch.Tests/e2e/Conversation/HooksTests.cs b/tests/Sinch.Tests/e2e/Conversation/HooksTests.cs new file mode 100644 index 0000000..9701e0a --- /dev/null +++ b/tests/Sinch.Tests/e2e/Conversation/HooksTests.cs @@ -0,0 +1,951 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using FluentAssertions; +using Sinch.Conversation; +using Sinch.Conversation.Common; +using Sinch.Conversation.Contacts; +using Sinch.Conversation.Events.EventTypes; +using Sinch.Conversation.Hooks; +using Sinch.Conversation.Hooks.Models; +using Sinch.Conversation.Messages.Message; +using Sinch.Conversation.Webhooks; +using Sinch.Numbers.Hooks; +using Xunit; + +namespace Sinch.Tests.e2e.Conversation +{ + // utilizes mocks initialized from sinch-mock-server json files + // the models are from a custom endpoint, meaning no sinch client endpoint matches them + // json is fetched with http requests, testing parsing here + // for two types: plain deser with JsonSerializer, and provided Deserialize from ISinchConversationWebhooks + public class HooksTests : TestBase + { + private readonly HttpClient _httpClient; + private readonly ISinchConversationWebhooks _sinchConversationWebhooks; + + public HooksTests() + { + _httpClient = new HttpClient() + { + BaseAddress = new Uri(WebhooksEventsBaseAddress) + }; + _sinchConversationWebhooks = SinchClientMockServer.Conversation.Webhooks; + } + + [Fact] + public async Task MessageInbound() + { + var json = await _httpClient.GetStringAsync("message-inbound"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new MessageInboundEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.151Z").ToUniversalTime(), + EventTime = DateTime.Parse("2024-06-06T14:42:42.1492634Z").ToUniversalTime(), + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "correlatorId", + Message = new MessageInboundEventItem() + { + Id = "01W4FFL35P4NC4K35MESSAGE01", + Direction = ConversationDirection.ToApp, + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Metadata = "", + AcceptTime = DateTime.Parse("2024-06-06T14:42:42.151Z").ToUniversalTime(), + SenderId = "", + ProcessingMode = ProcessingMode.Conversation, + Injected = false, + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Rcs, + Identity = "12015555555", + AppId = "" + }, + ContactMessage = new ContactMessage(new TextMessage("Hello")), + }, + }, x => x.Excluding(m => m.MessageMetadata)); + callbackEvent.As().MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task ContactDelete() + { + var json = await _httpClient.GetStringAsync("contact-delete"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new ContactDeleteEvent() + { + AppId = "", + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.726873733Z").ToUniversalTime(), + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + ContactDeleteNotification = new ContactNotification() + { + Contact = new Contact() + { + Id = "01W4FFL35P4NC4K35CONTACT01", + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Sms, + Identity = "12015555555", + AppId = "" + } + }, + DisplayName = "Source Contact", + Email = "source@mail.com", + ExternalId = "", + Metadata = "Some metadata belonging to the source contact", + Language = ConversationLanguage.French, + } + } + }, x => x.Excluding(m => m.MessageMetadata)); + callbackEvent.As().MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task ContactUpdate() + { + var json = await _httpClient.GetStringAsync("contact-update"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new ContactUpdateEvent() + { + AppId = "", + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.1646148Z").ToUniversalTime(), + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + ContactUpdateNotification = new ContactNotification() + { + Contact = new Contact() + { + Id = "01W4FFL35P4NC4K35CONTACT01", + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Rcs, + Identity = "12015556666", + AppId = "" + } + }, + ChannelPriority = new List() + { + ConversationChannel.Rcs + }, + DisplayName = "Updated name with the Sinch SDK", + Email = "", + ExternalId = "", + Metadata = "", + Language = ConversationLanguage.French, + } + } + }, x => x.Excluding(m => m.MessageMetadata)); + callbackEvent.As().MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task ContactMerge() + { + var json = await _httpClient.GetStringAsync("contact-merge"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new ContactMergeEvent() + { + AppId = "", + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.722089838Z").ToUniversalTime(), + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + ContactMergeNotification = new ContactMergeNotification() + { + PreservedContact = new Contact() + { + Id = "01W4FFL35P4NC4K35CONTACT02", + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Mms, + Identity = "12016666666", + AppId = "" + } + }, + ChannelPriority = new List() + { + ConversationChannel.Mms + }, + DisplayName = "Destination Contact", + Email = "", + ExternalId = "", + Metadata = "", + Language = ConversationLanguage.EnglishUS, + }, + DeletedContact = new Contact() + { + Id = "01W4FFL35P4NC4K35CONTACT01", + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Sms, + Identity = "12015555555", + AppId = "" + } + }, + DisplayName = "Source Contact", + Email = "source@mail.com", + ExternalId = "", + Metadata = "Some metadata belonging to the source contact", + Language = ConversationLanguage.French, + } + } + }, x => x.Excluding(m => m.MessageMetadata)); + callbackEvent.As().MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task ConversationDelete() + { + var json = await _httpClient.GetStringAsync("conversation-delete"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new ConversationDeleteEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + ConversationDeleteNotification = new ConversationNotification() + { + Conversation = new Sinch.Conversation.Conversations.Conversation() + { + Id = "01W4FFL35P4NC4K35CONVERS01", + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + LastReceived = DateTime.Parse("2024-06-06T14:42:42Z").ToUniversalTime(), + ActiveChannel = ConversationChannel.Rcs, + Active = false, + Metadata = "", + CorrelationId = "correlatorId" + } + } + }, + x => x.Excluding(m => + m.MessageMetadata).Excluding(m => m.ConversationDeleteNotification.Conversation.MetadataJson)); + var deleteEvent = callbackEvent.As(); + deleteEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + deleteEvent.ConversationDeleteNotification!.Conversation!.MetadataJson!.ToJsonString().Should() + .Be("{}"); + } + } + + [Fact] + public async Task ConversationStart() + { + var json = await _httpClient.GetStringAsync("conversation-start"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new ConversationStartEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + ConversationStartNotification = new ConversationNotification() + { + Conversation = new Sinch.Conversation.Conversations.Conversation() + { + Id = "01W4FFL35P4NC4K35CONVERS01", + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + LastReceived = new DateTime(), + ActiveChannel = ConversationChannel.Unspecified, + Active = true, + Metadata = "", + CorrelationId = "correlatorId", + } + } + }, + x => x.Excluding(m => + m.MessageMetadata).Excluding(m => m.ConversationStartNotification.Conversation.MetadataJson)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + convEvent.ConversationStartNotification!.Conversation!.MetadataJson!.ToJsonString().Should() + .Be("{}"); + } + } + + [Fact] + public async Task ConversationStop() + { + var json = await _httpClient.GetStringAsync("conversation-stop"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new ConversationStopEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + ConversationStopNotification = new ConversationNotification() + { + Conversation = new Sinch.Conversation.Conversations.Conversation() + { + Id = "01W4FFL35P4NC4K35CONVERS01", + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + LastReceived = DateTime.Parse("2024-06-06T14:42:42Z").ToUniversalTime(), + ActiveChannel = ConversationChannel.Rcs, + Active = false, + Metadata = "", + CorrelationId = "correlatorId", + } + } + }, + x => x.Excluding(m => + m.MessageMetadata).Excluding(m => m.ConversationStopNotification.Conversation.MetadataJson)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + convEvent.ConversationStopNotification!.Conversation!.MetadataJson!.ToJsonString().Should() + .Be("{}"); + } + } + + [Fact] + public async Task EventDeliveryReportFailed() + { + var json = await _httpClient.GetStringAsync("event-delivery-report/failed"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new DeliveryEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.208Z").ToUniversalTime(), + EventTime = DateTime.Parse("2024-06-06T14:42:42.251277147Z").ToUniversalTime(), + EventDeliveryReport = new EventDeliveryAllOfEventDeliveryReport() + { + EventId = "01W4FFL35P4NC4K35EVENT0003", + Status = DeliveryStatus.Failed, + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Messenger, + Identity = "7968425018576406", + AppId = "01W4FFL35P4NC4K35CONVAPP01" + }, + ContactId = "", + Reason = new Reason() + { + Code = "BAD_REQUEST", + Description = + "The underlying channel reported: Message type [MESSAGE_NOT_SET] not supported on Messenger", + SubCode = "UNSPECIFIED_SUB_CODE" + }, + Metadata = "", + ProcessingMode = ProcessingMode.Conversation + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task EventDeliveryReportSucceeded() + { + var json = await _httpClient.GetStringAsync("event-delivery-report/succeeded"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new DeliveryEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + CorrelationId = "", + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.132Z").ToUniversalTime(), + EventTime = DateTime.Parse("2024-06-06T14:42:42.891Z").ToUniversalTime(), + EventDeliveryReport = new EventDeliveryAllOfEventDeliveryReport() + { + EventId = "01W4FFL35P4NC4K35EVENT0002", + Status = DeliveryStatus.Delivered, + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Messenger, + Identity = "7968425018576406", + AppId = "01W4FFL35P4NC4K35CONVAPP01" + }, + ContactId = "", + Metadata = "", + ProcessingMode = ProcessingMode.Conversation + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task EventInbound() + { + var json = await _httpClient.GetStringAsync("event-inbound"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new InboundEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:42.379863404Z").ToUniversalTime(), + CorrelationId = "", + Event = new EventInboundAllOfEvent() + { + Direction = ConversationDirection.ToApp, + ContactEvent = new ContactEvent() + { + ComposingEvent = new object() + }, + Id = "01W4FFL35P4NC4K35EVENT0001", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Rcs, + Identity = "12015555555", + AppId = "" + }, + AcceptTime = DateTime.Parse("2024-06-06T14:42:42.429455346Z").ToUniversalTime(), + ProcessingMode = ProcessingMode.Conversation, + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task MessageDeliveryReportFailed() + { + var json = await _httpClient.GetStringAsync("message-delivery-report/failed"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new MessageDeliveryReceiptEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:43Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.721Z").ToUniversalTime(), + CorrelationId = "correlatorId", + MessageDeliveryReport = new MessageDeliveryReport() + { + MessageId = "01W4FFL35P4NC4K35MESSAGE05", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + Status = DeliveryStatus.Failed, + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Rcs, + Identity = "12016666666", + AppId = "", + }, + ContactId = "01W4FFL35P4NC4K35CONTACT02", + Reason = new Reason() + { + Code = "RECIPIENT_NOT_REACHABLE", + Description = + "The underlying channel reported: Unable to find rcs support for the given recipient", + SubCode = "UNSPECIFIED_SUB_CODE", + }, + Metadata = "", + ProcessingMode = ProcessingMode.Conversation + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task MessageDeliveryReportQueued() + { + var json = await _httpClient.GetStringAsync("message-delivery-report/succeeded"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new MessageDeliveryReceiptEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:43.0093518Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.721Z").ToUniversalTime(), + CorrelationId = "correlatorId", + MessageDeliveryReport = new MessageDeliveryReport() + { + MessageId = "01W4FFL35P4NC4K35MESSAGE01", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + Status = DeliveryStatus.QueuedOnChannel, + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Rcs, + Identity = "12015555555", + AppId = "", + }, + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Metadata = "", + ProcessingMode = ProcessingMode.Conversation + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task MessageInboundSmartConversationRedaction() + { + var json = await _httpClient.GetStringAsync("message-inbound/smart-conversation-redaction"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new MessageInboundSmartConversationRedactionEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:41.293Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.240093543Z").ToUniversalTime(), + CorrelationId = "correlatorId", + MessageRedaction = new MessageInboundEventItem() + { + Id = "01W4FFL35P4NC4K35MESSAGE02", + Direction = ConversationDirection.ToApp, + ContactMessage = new ContactMessage(new TextMessage( + "Hi, my real name is {PERSON} and I live in {LOCATION}. My credit card number is 4242 4242 4242 4242. What a beautiful day!")), + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Messenger, + Identity = "7968425018576406", + AppId = "01W4FFL35P4NC4K35CONVAPP01", + }, + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Metadata = "", + ProcessingMode = ProcessingMode.Conversation, + Injected = false, + SenderId = "", + AcceptTime = DateTime.Parse("2024-06-06T14:42:42.165Z").ToUniversalTime(), + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task MessageSubmitMedia() + { + var json = await _httpClient.GetStringAsync("message-submit/media"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new MessageSubmitEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:42.475Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.475Z").ToUniversalTime(), + CorrelationId = "", + MessageSubmitNotification = new MessageSubmitNotification() + { + MessageId = "01W4FFL35P4NC4K35MESSAGE04", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Messenger, + Identity = "7968425018576406", + AppId = "01W4FFL35P4NC4K35CONVAPP01", + }, + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Metadata = "", + ProcessingMode = ProcessingMode.Conversation, + SubmittedMessage = new ContactMessage(new MediaMessage() + { + FilenameOverride = "", + ThumbnailUrl = "", + Url = + "https://scontent.xx.fbcdn.net/v/t1.15752-9/450470563_473474858617216_4192328888545460366_n.png?_nc_cat=102&ccb=1-7&_nc_sid=fc17b8&_nc_ohc=48P1Kdk4UiwQ7kNvgE60fDt&_nc_ad=z-m&_nc_cid=0&_nc_ht=scontent.xx&oh=03_Q7cD1QEkgERuI-tu8rt1GGpOEcNU2-0bFkmG4mQkzbciZss10g&oe=66C0A0E0", + }) + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task MessageSubmitText() + { + var json = await _httpClient.GetStringAsync("message-submit/text"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new MessageSubmitEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:42.721Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.721Z").ToUniversalTime(), + CorrelationId = "correlatorId", + MessageSubmitNotification = new MessageSubmitNotification() + { + MessageId = "01W4FFL35P4NC4K35MESSAGE03", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + + ChannelIdentity = new ChannelIdentity() + { + Channel = ConversationChannel.Rcs, + Identity = "12015555555", + AppId = "", + }, + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Metadata = "", + ProcessingMode = ProcessingMode.Conversation, + SubmittedMessage = new ContactMessage(new TextMessage("I \u2764\ufe0f Sinch") + { + }) + } + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task SmartConversationMedia() + { + var json = await _httpClient.GetStringAsync("smart-conversations/media"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new SmartConversationsEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:42.094Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:44.2069826Z").ToUniversalTime(), + CorrelationId = "", + SmartConversationNotification = new SmartConversationNotification() + { + MessageId = "01W4FFL35P4NC4K35MESSAGE04", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + + ChannelIdentity = "7968425018576406", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Channel = ConversationChannel.Messenger, + AnalysisResults = new AnalysisResult() + { + MlImageRecognitionResult = new List() + { + new MachineLearningImageRecognitionResult() + { + Url = + "https://scontent.xx.fbcdn.net/v/t1.15752-9/450470563_473474858617216_4192328888545460366_n.png?_nc_cat=102&ccb=1-7&_nc_sid=fc17b8&_nc_ohc=48P1Kdk4UiwQ7kNvgE60fDt&_nc_ad=z-m&_nc_cid=0&_nc_ht=scontent.xx&oh=03_Q7cD1QEkgERuI-tu8rt1GGpOEcNU2-0bFkmG4mQkzbciZss10g&oe=66C0A0E0" + } + }, + MlOffensiveAnalysisResult = new List() + { + new OffensiveAnalysis() + { + Message = "", + Url = + "https://scontent.xx.fbcdn.net/v/t1.15752-9/450470563_473474858617216_4192328888545460366_n.png?_nc_cat=102&ccb=1-7&_nc_sid=fc17b8&_nc_ohc=48P1Kdk4UiwQ7kNvgE60fDt&_nc_ad=z-m&_nc_cid=0&_nc_ht=scontent.xx&oh=03_Q7cD1QEkgERuI-tu8rt1GGpOEcNU2-0bFkmG4mQkzbciZss10g&oe=66C0A0E0", + Evaluation = Evaluation.Safe, + Score = 0.3069f + } + } + } + }, + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + + [Fact] + public async Task SmartConversationText() + { + var json = await _httpClient.GetStringAsync("smart-conversations/text"); + + var result = _sinchConversationWebhooks.DeserializeCallbackEvent(json); + + AssertEvent(result); + + var resultPlain = JsonSerializer.Deserialize(json); + + AssertEvent(resultPlain); + + void AssertEvent(ICallbackEvent callbackEvent) + { + callbackEvent.Should().BeEquivalentTo(new SmartConversationsEvent() + { + AppId = "01W4FFL35P4NC4K35CONVAPP01", + ProjectId = "tinyfrog-jump-high-over-lilypadbasin", + EventTime = DateTime.Parse("2024-06-06T14:42:42.1492634Z").ToUniversalTime(), + AcceptedTime = DateTime.Parse("2024-06-06T14:42:42.2198899Z").ToUniversalTime(), + CorrelationId = "", + SmartConversationNotification = new SmartConversationNotification() + { + MessageId = "01W4FFL35P4NC4K35MESSAGE03", + ConversationId = "01W4FFL35P4NC4K35CONVERS01", + + ChannelIdentity = "12015555555", + ContactId = "01W4FFL35P4NC4K35CONTACT01", + Channel = ConversationChannel.Rcs, + AnalysisResults = new AnalysisResult() + { + MlSentimentResult = new List() + { + new MachineLearningSentimentResult() + { + Message = "I \u2764\ufe0f Sinch", + Sentiment = Sentiment.Positive, + Score = 0.9041176f, + Results = new List() + { + new SentimentResult() + { + Sentiment = Sentiment.Negative, + Score = 0.0028852955f + }, + new SentimentResult() + { + Sentiment = Sentiment.Neutral, + Score = 0.09299716f + }, + + new SentimentResult() + { + Sentiment = Sentiment.Positive, + Score = 0.9041176f + }, + } + } + }, + MlNluResult = new List() + { + new MachineLearningNLUResult() + { + Message = "I \u2764\ufe0f Sinch", + Intent = "chitchat.thank_you", + Score = 0.99831617f, + Results = new List() + { + new IntentResult() + { + Intent = "chitchat.thank_you", + Score = 0.99831617f + }, + new IntentResult() + { + Intent = "chitchat.one_moment_please", + Score = 0.00027679664f + }, + new IntentResult() + { + Intent = "chitchat.bye", + Score = 0.0002178006f + } + } + }, + + }, + MlPiiResult = new List() + { + new MachineLearningPIIResult() + { + Message = "I \u2764\ufe0f Sinch", + Masked = "{PERSON} {PERSON} {PERSON}" + } + }, + MlOffensiveAnalysisResult = new List() + { + new OffensiveAnalysis() + { + Message = "I \u2764\ufe0f Sinch", + Url = "", + Evaluation = Evaluation.Safe, + Score = 0.9826318f + } + } + } + }, + }, + x => x.Excluding(m => + m.MessageMetadata)); + var convEvent = callbackEvent.As(); + convEvent.MessageMetadata!.GetValue().Should().BeEmpty(); + } + } + } +} diff --git a/tests/Sinch.Tests/e2e/TestBase.cs b/tests/Sinch.Tests/e2e/TestBase.cs index 4cf6d18..6d6e1eb 100644 --- a/tests/Sinch.Tests/e2e/TestBase.cs +++ b/tests/Sinch.Tests/e2e/TestBase.cs @@ -15,6 +15,8 @@ public class TestBase // MockStudio should be removed and all contract testing should go to mock server version protected readonly ISinchClient SinchClientMockServer; + protected readonly string WebhooksEventsBaseAddress; + protected TestBase() { Env.Load(); @@ -28,6 +30,7 @@ protected TestBase() SmsUrl = "http://localhost:8002" }; }); + var conversationUrl = GetTestUrl("MOCK_CONVERSATION_PORT"); SinchClientMockServer = new SinchClient(ProjectId, "key_id", "key_secret", options => { @@ -35,16 +38,18 @@ protected TestBase() { AuthUrl = GetTestUrl("MOCK_AUTH_PORT"), SmsUrl = GetTestUrl("MOCK_SMS_PORT"), - ConversationUrl = GetTestUrl("MOCK_CONVERSATION_PORT"), + ConversationUrl = conversationUrl, NumbersUrl = GetTestUrl("MOCK_NUMBERS_PORT"), VoiceUrl = GetTestUrl("MOCK_VOICE_PORT"), // Voice Application Management treated the same as voice in doppelganger VoiceApplicationManagementUrl = GetTestUrl("MOCK_VOICE_PORT"), VerificationUrl = GetTestUrl("MOCK_VERIFICATION_PORT"), // templates treated as conversation api in doppelganger - TemplatesUrl = GetTestUrl("MOCK_CONVERSATION_PORT"), + TemplatesUrl = conversationUrl, }; }); + WebhooksEventsBaseAddress = conversationUrl + "/webhooks/conversation/"; + } private string GetTestUrl(string portEnvVar) =>