From e4f796b2ebcff96f3f5fc6069c621b57f771907c Mon Sep 17 00:00:00 2001 From: Antonello Provenzano Date: Fri, 27 Oct 2023 20:01:46 +0200 Subject: [PATCH] Entity Framework subscription management improvements --- Deveel.Webhooks.sln | 7 + .../Deveel.Webhooks.EntityFramework.csproj | 2 +- .../Webhooks/DbWebhookDeliveryResult.cs | 2 + .../Webhooks/DbWebhookSubscriptionHeader.cs | 6 - .../Webhooks/DbWebhookValueConvert.cs | 6 +- .../Webhooks/DefaultDbWebhookConverter.cs | 44 ---- ... EntityWebhookDeliveryResultRepository.cs} | 8 +- ...ntityWebhookDeliveryResultRepository_T.cs} | 15 +- .../Webhooks/EntityWebhookStorageBuilder.cs | 53 +++-- .../EntityWebhookSubscriptionRepository.cs | 5 +- .../EntityWebhookSubscriptionRepository_T.cs | 62 +++++- .../Webhooks/WebhookJsonUtil.cs | 32 --- ...okDeliveryRepositoryRepositoryProvider.cs} | 3 +- .../MongoDbWebhookDeliveryResultLogger.cs | 12 +- .../MongoDbWebhookSubscriptionRepository_T.cs | 61 ++++- .../Webhooks/MongoWebhookSubscription.cs | 25 ++- .../Webhooks/ObjectIdExtensions.cs | 26 --- .../IWebhookSubscriptionRepository.cs | 210 +++++++++++++++++- .../Webhooks/WebhookSubscriptionManager_T.cs | 49 +++- ...ebhooks.DeliveryResultLogging.Tests.csproj | 24 ++ .../GlobalUsings.cs | 1 + .../Webhooks/DeliveryResultLoggerTestSuite.cs | 147 ++++++++++++ .../Webhooks/WebhookFaker.cs | 15 ++ .../Webhooks/WebhookSubscription.cs | 33 +++ .../Webhooks/WebhookSubscriptionFaker.cs | 19 ++ ...veel.Webhooks.EntityFramework.XUnit.csproj | 1 + .../Webhooks/DbEventInfoFaker.cs | 20 ++ .../Webhooks/DbWebhookDeliveryResultFaker.cs | 9 +- .../Webhooks/DbWebhookFaker.cs | 1 + .../Webhooks/DbWebhookSubscriptionFaker.cs | 1 + .../Webhooks/DbWebhookValueConvertTests.cs | 45 ++++ .../EntityDeliveryResultRepositoryTests.cs | 122 ++++++++++ .../EntityDeliveryResultStoreTests.cs | 162 -------------- .../Webhooks/StorageBuildingTests.cs | 49 ++++ .../Webhooks/WebhookManagementTestSuite.cs | 76 +++++++ .../Deveel.Webhooks.MongoDb.XUnit.csproj | 1 + .../MongoDeliveryResultLoggingTests.cs | 57 +++++ .../Webhooks/MongoWebhookSubscriptionFaker.cs | 1 + ...TenantWebhookDeliveryResultLoggingTests.cs | 1 - 39 files changed, 1058 insertions(+), 355 deletions(-) delete mode 100644 src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DefaultDbWebhookConverter.cs rename src/Deveel.Webhooks.Service.EntityFramework/Webhooks/{EntityWebhookDeliveryResultStore.cs => EntityWebhookDeliveryResultRepository.cs} (70%) rename src/Deveel.Webhooks.Service.EntityFramework/Webhooks/{EntityWebhookDeliveryResultStore_T.cs => EntityWebhookDeliveryResultRepository_T.cs} (78%) delete mode 100644 src/Deveel.Webhooks.Service.EntityFramework/Webhooks/WebhookJsonUtil.cs rename src/Deveel.Webhooks.Service.MongoDb/Webhooks/{MongoDbWebhookDeliveryRepositoryStoreProvider.cs => MongoDbWebhookDeliveryRepositoryRepositoryProvider.cs} (96%) delete mode 100644 src/Deveel.Webhooks.Service.MongoDb/Webhooks/ObjectIdExtensions.cs create mode 100644 test/Deveel.Webhooks.DeliveryResultLogging.Tests/Deveel.Webhooks.DeliveryResultLogging.Tests.csproj create mode 100644 test/Deveel.Webhooks.DeliveryResultLogging.Tests/GlobalUsings.cs create mode 100644 test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/DeliveryResultLoggerTestSuite.cs create mode 100644 test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookFaker.cs create mode 100644 test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscription.cs create mode 100644 test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscriptionFaker.cs create mode 100644 test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbEventInfoFaker.cs create mode 100644 test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookValueConvertTests.cs create mode 100644 test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultRepositoryTests.cs delete mode 100644 test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultStoreTests.cs create mode 100644 test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/StorageBuildingTests.cs create mode 100644 test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDeliveryResultLoggingTests.cs diff --git a/Deveel.Webhooks.sln b/Deveel.Webhooks.sln index 57673e3..e9d9049 100644 --- a/Deveel.Webhooks.sln +++ b/Deveel.Webhooks.sln @@ -75,6 +75,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Webhooks.XUnit", "te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Webhooks.Receiver.TestApi", "test\Deveel.Webhooks.Receiver.TestApi\Deveel.Webhooks.Receiver.TestApi.csproj", "{4942C858-277D-438D-B822-92055B8E8DF7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Webhooks.DeliveryResultLogging.Tests", "test\Deveel.Webhooks.DeliveryResultLogging.Tests\Deveel.Webhooks.DeliveryResultLogging.Tests.csproj", "{DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +203,10 @@ Global {4942C858-277D-438D-B822-92055B8E8DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {4942C858-277D-438D-B822-92055B8E8DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {4942C858-277D-438D-B822-92055B8E8DF7}.Release|Any CPU.Build.0 = Release|Any CPU + {DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -236,6 +242,7 @@ Global {E669EE13-1CBB-453D-B3B0-5DA3B0B51FE6} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1} {EBD3DB50-0E90-47C7-9DD8-FBBAC8696CE1} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1} {4942C858-277D-438D-B822-92055B8E8DF7} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1} + {DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E682A9F5-43D7-4D4C-82EA-953545B8F4DE} diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Deveel.Webhooks.EntityFramework.csproj b/src/Deveel.Webhooks.Service.EntityFramework/Deveel.Webhooks.EntityFramework.csproj index c04a98c..8e5d3f5 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Deveel.Webhooks.EntityFramework.csproj +++ b/src/Deveel.Webhooks.Service.EntityFramework/Deveel.Webhooks.EntityFramework.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookDeliveryResult.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookDeliveryResult.cs index b249e76..1a9cbb5 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookDeliveryResult.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookDeliveryResult.cs @@ -26,6 +26,8 @@ public class DbWebhookDeliveryResult : IWebhookDeliveryResult { /// public string OperationId { get; set; } + public string? TenantId { get; set; } + IEventInfo IWebhookDeliveryResult.EventInfo => EventInfo; /// diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookSubscriptionHeader.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookSubscriptionHeader.cs index ef2c157..a204cd5 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookSubscriptionHeader.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookSubscriptionHeader.cs @@ -16,21 +16,15 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Deveel.Webhooks { - [Table("webhook_subscription_headers")] public class DbWebhookSubscriptionHeader { - [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column("id")] public int Id { get; set; } - [Required, Column("key")] public string Key { get; set; } - [Required, Column("value")] public string Value { get; set; } - // [ForeignKey(nameof(SubscriptionId))] public virtual DbWebhookSubscription? Subscription { get; set; } - [Required, Column("subscription_id")] public string? SubscriptionId { get; set; } } } diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookValueConvert.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookValueConvert.cs index 1fda73a..10d5668 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookValueConvert.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DbWebhookValueConvert.cs @@ -40,8 +40,10 @@ public static string GetValueType(object? value) { return valueType switch { DbWebhookValueTypes.Boolean => ParseBoolean(value), - DbWebhookValueTypes.Integer => Int64.Parse(value, CultureInfo.InvariantCulture), - DbWebhookValueTypes.Number => Double.Parse(value, CultureInfo.InvariantCulture), + var x when x == DbWebhookValueTypes.Integer && Int32.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32) => i32, + var x when x == DbWebhookValueTypes.Integer && Int64.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64) => i64, + var x when x == DbWebhookValueTypes.Number && Double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d, + var x when x == DbWebhookValueTypes.Number && Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var f) => f, DbWebhookValueTypes.String => value, DbWebhookValueTypes.DateTime => DateTime.Parse(value, CultureInfo.InvariantCulture), _ => throw new NotSupportedException($"The value type '{valueType}' is not supported") diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DefaultDbWebhookConverter.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DefaultDbWebhookConverter.cs deleted file mode 100644 index f0ab9d4..0000000 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/DefaultDbWebhookConverter.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022-2023 Deveel -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Deveel.Webhooks { - /// - /// A default implementation of that - /// converts a object into a - /// to be stored in the database. - /// - /// - /// The type of the webhook object to be converted. - /// - public class DefaultDbWebhookConverter : IDbWebhookConverter where TWebhook : class { - /// - public DbWebhook ConvertWebhook(EventInfo eventInfo, TWebhook webhook) { - if (webhook is IWebhook obj) { - return new DbWebhook { - WebhookId = obj.Id, - EventType = obj.EventType, - Data = WebhookJsonUtil.ToJson(obj.Data), - TimeStamp = obj.TimeStamp - }; - } - - return new DbWebhook { - EventType = eventInfo.EventType, - TimeStamp = eventInfo.TimeStamp, - WebhookId = eventInfo.Id, - Data = WebhookJsonUtil.ToJson(webhook) - }; - } - } -} diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultStore.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultRepository.cs similarity index 70% rename from src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultStore.cs rename to src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultRepository.cs index 95737a5..265de72 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultStore.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultRepository.cs @@ -13,6 +13,7 @@ // limitations under the License. using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Deveel.Webhooks { /// @@ -20,10 +21,11 @@ namespace Deveel.Webhooks { /// uses an Entity Framework Core to store the /// delivery results of a webhook of type . /// - /// - public sealed class EntityWebhookDeliveryResultStore : EntityWebhookDeliveryResultStore { + /// + public sealed class EntityWebhookDeliveryResultRepository : EntityWebhookDeliveryResultRepository { /// - public EntityWebhookDeliveryResultStore(WebhookDbContext context) : base(context) { + public EntityWebhookDeliveryResultRepository(WebhookDbContext context, ILogger? logger = null) + : base(context, logger) { } } } diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultStore_T.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultRepository_T.cs similarity index 78% rename from src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultStore_T.cs rename to src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultRepository_T.cs index 4b37dbd..5a79613 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultStore_T.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookDeliveryResultRepository_T.cs @@ -15,6 +15,7 @@ using Deveel.Data; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Deveel.Webhooks { /// @@ -25,26 +26,18 @@ namespace Deveel.Webhooks { /// /// The type of delivery result to be stored in the database. /// - public class EntityWebhookDeliveryResultStore : EntityRepository, + public class EntityWebhookDeliveryResultRepository : EntityRepository, IWebhookDeliveryResultRepository where TResult : DbWebhookDeliveryResult { - private readonly WebhookDbContext context; /// /// Constructs the store with the given . /// /// - public EntityWebhookDeliveryResultStore(WebhookDbContext context) : base(context) { - this.context = context; + public EntityWebhookDeliveryResultRepository(WebhookDbContext context, ILogger>? logger = null) + : base(context, logger) { } - /// - /// Gets the set of results stored in the database. - /// - protected DbSet Results => context.Set(); - - protected override DbSet Entities => Results; - /// public async Task FindByWebhookIdAsync(string webhookId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookStorageBuilder.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookStorageBuilder.cs index 36875f1..6c6c6f3 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookStorageBuilder.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookStorageBuilder.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Deveel.Data; + using Finbuckle.MultiTenant; using Microsoft.EntityFrameworkCore; @@ -48,22 +50,19 @@ internal EntityWebhookStorageBuilder(WebhookSubscriptionBuilder b public IServiceCollection Services => builder.Services; private void AddDefaultStorage() { - Services.TryAddScoped, EntityWebhookSubscriptionRepository>(); - Services.TryAddScoped>(); + Services.AddRepository>(); if (typeof(TSubscription) == typeof(DbWebhookSubscription)) { - Services.TryAddScoped, EntityWebhookSubscriptionRepository>(); - Services.TryAddScoped>(); - Services.AddScoped(); + Services.AddRepository(); } - if (ResultType != null && ResultType == typeof(DbWebhookDeliveryResult)) { - Services.TryAddScoped, EntityWebhookDeliveryResultStore>(); - Services.AddScoped(); - Services.TryAddScoped>(); - } + if (ResultType != null) { + var resultStoreType = typeof(EntityWebhookDeliveryResultRepository<>).MakeGenericType(ResultType); + Services.AddRepository(resultStoreType); - Services.TryAddSingleton(typeof(IDbWebhookConverter<>), typeof(DefaultDbWebhookConverter<>)); + if (ResultType == typeof(DbWebhookDeliveryResult)) + Services.AddRepository(); + } } /// @@ -147,17 +146,27 @@ public EntityWebhookStorageBuilder UseContext(Action - /// + /// /// The type of the storage to use for storing the webhook subscriptions, /// that is derived from . /// /// /// Returns the current instance of the builder for chaining. /// - public EntityWebhookStorageBuilder UseSubscriptionStore() - where TStore : EntityWebhookSubscriptionRepository { - Services.AddScoped, TStore>(); - Services.AddScoped(); + public EntityWebhookStorageBuilder UseSubscriptionRepository() + where TRepository : EntityWebhookSubscriptionRepository { + + Services.RemoveAll>(); + Services.RemoveAll>(); + Services.RemoveAll>(); + Services.RemoveAll>(); + Services.RemoveAll>(); + Services.RemoveAll(); + + Services.AddRepository(); + + if (typeof(EntityWebhookSubscriptionRepository) != typeof(TRepository)) + Services.AddScoped, TRepository>(); return this; } @@ -180,8 +189,7 @@ public EntityWebhookStorageBuilder UseSubscriptionStore() /// . /// public EntityWebhookStorageBuilder UseResultType(Type type) { - if (type is null) - throw new ArgumentNullException(nameof(type)); + ArgumentNullException.ThrowIfNull(type, nameof(type)); if (!typeof(DbWebhookDeliveryResult).IsAssignableFrom(type)) throw new ArgumentException($"The given type '{type}' is not a valid result type"); @@ -195,17 +203,16 @@ public EntityWebhookStorageBuilder UseResult() where TResult : DbWebhookDeliveryResult => UseResultType(typeof(TResult)); - public EntityWebhookStorageBuilder UseResultStore(Type storeType) { + public EntityWebhookStorageBuilder UseResultRepository(Type repositoryType) { if (ResultType == null) throw new InvalidOperationException("No result type was specified for the storage"); var resultStoreType = typeof(IWebhookDeliveryResultRepository<>).MakeGenericType(ResultType); - if (!resultStoreType.IsAssignableFrom(storeType)) - throw new ArgumentException($"The given type '{storeType}' is not a valid result store"); + if (!resultStoreType.IsAssignableFrom(repositoryType)) + throw new ArgumentException($"The given type '{repositoryType}' is not a valid result store"); - Services.AddScoped(resultStoreType, storeType); - Services.AddScoped(storeType); + Services.AddRepository(resultStoreType); return this; } diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository.cs index f91c569..fa75933 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Microsoft.Extensions.Logging; + namespace Deveel.Webhooks { /// /// A default implementation of that @@ -19,7 +21,8 @@ namespace Deveel.Webhooks { /// public class EntityWebhookSubscriptionRepository : EntityWebhookSubscriptionRepository { /// - public EntityWebhookSubscriptionRepository(WebhookDbContext context) : base(context) { + public EntityWebhookSubscriptionRepository(WebhookDbContext context, ILogger? logger = null) + : base(context, logger) { } } } diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository_T.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository_T.cs index 6c7b55d..15ea18f 100644 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository_T.cs +++ b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/EntityWebhookSubscriptionRepository_T.cs @@ -71,11 +71,11 @@ protected override async Task OnEntityFoundByKeyAsync(object key, => await EnsureLoadedAsync(entity, cancellationToken); /// - public Task GetDestinationUrlAsync(TSubscription subscription, CancellationToken cancellationToken = default) { + public Task GetDestinationUrlAsync(TSubscription subscription, CancellationToken cancellationToken = default) { ThrowIfDisposed(); cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(subscription.DestinationUrl); + return Task.FromResult(subscription.DestinationUrl); } /// @@ -119,6 +119,24 @@ public Task SetStatusAsync(TSubscription subscription, WebhookSubscriptionStatus return Task.CompletedTask; } + /// + public Task GetSecretAsync(TSubscription subscription, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + return Task.FromResult(subscription.Secret); + } + + /// + public Task SetSecretAsync(TSubscription subscription, string? secret, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + subscription.Secret = secret; + + return Task.CompletedTask; + } + /// public Task GetEventTypesAsync(TSubscription subscription, CancellationToken cancellationToken = default) { ThrowIfDisposed(); @@ -195,5 +213,45 @@ public Task RemoveHeadersAsync(TSubscription subscription, string[] headerKeys, return Task.CompletedTask; } + + /// + public Task> GetPropertiesAsync(TSubscription subscription, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + var properties = subscription.Properties.ToDictionary(x => x.Key, x => DbWebhookValueConvert.Convert(x.Value, x.ValueType)); + + return Task.FromResult>(properties); + } + + /// + public Task AddPropertiesAsync(TSubscription subscription, IDictionary properties, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var property in properties) { + subscription.Properties.Add(new DbWebhookSubscriptionProperty { + Key = property.Key, + Value = DbWebhookValueConvert.ConvertToString(property.Value), + ValueType = DbWebhookValueConvert.GetValueType(property.Value) + }); + } + + return Task.CompletedTask; + } + + /// + public Task RemovePropertiesAsync(TSubscription subscription, string[] propertyKeys, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var propertyKey in propertyKeys) { + var found = subscription.Properties.FirstOrDefault(x => x.Key == propertyKey); + if (found != null) + subscription.Properties.Remove(found); + } + + return Task.CompletedTask; + } } } diff --git a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/WebhookJsonUtil.cs b/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/WebhookJsonUtil.cs deleted file mode 100644 index 78113ec..0000000 --- a/src/Deveel.Webhooks.Service.EntityFramework/Webhooks/WebhookJsonUtil.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2022-2023 Deveel -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Deveel.Webhooks { - internal static class WebhookJsonUtil { - public static string? ToJson(object? data) { - if (data == null) - return null; - - if (data is string str) - return str; - - return JsonSerializer.Serialize(data, new JsonSerializerOptions { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault - }); - } - } -} diff --git a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryRepositoryStoreProvider.cs b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryRepositoryRepositoryProvider.cs similarity index 96% rename from src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryRepositoryStoreProvider.cs rename to src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryRepositoryRepositoryProvider.cs index 65a9ead..dbccf07 100644 --- a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryRepositoryStoreProvider.cs +++ b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryRepositoryRepositoryProvider.cs @@ -32,7 +32,8 @@ namespace Deveel.Webhooks { /// /// The type of the result that is stored in the database. /// - public class MongoDbWebhookDeliveryResultRepositoryProvider : MongoRepositoryProvider, IWebhookDeliveryResultRepositoryProvider, IDisposable + public class MongoDbWebhookDeliveryResultRepositoryProvider : MongoRepositoryProvider, + IWebhookDeliveryResultRepositoryProvider where TTenantInfo : class, ITenantInfo, new() where TResult : MongoWebhookDeliveryResult { /// diff --git a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryResultLogger.cs b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryResultLogger.cs index 3fa4ab5..f411f6c 100644 --- a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryResultLogger.cs +++ b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookDeliveryResultLogger.cs @@ -55,7 +55,7 @@ public MongoDbWebhookDeliveryResultLogger( IWebhookDeliveryResultRepositoryProvider storeProvider, IMongoWebhookConverter? webhookConverter = null, ILogger>? logger = null) { - StoreProvider = storeProvider; + RepositoryProvider = storeProvider; this.webhookConverter = webhookConverter; Logger = logger ?? NullLogger>.Instance; } @@ -64,7 +64,7 @@ public MongoDbWebhookDeliveryResultLogger( /// Gets the provider used to resolve the MongoDB storage where to log /// the delivery results. /// - protected IWebhookDeliveryResultRepositoryProvider StoreProvider { get; } + protected IWebhookDeliveryResultRepositoryProvider RepositoryProvider { get; } /// /// Gets the logger used to log messages emitted by this service. @@ -184,10 +184,8 @@ protected virtual MongoWebhook ConvertWebhook(EventInfo eventInfo, TWebhook webh /// public async Task LogResultAsync(EventInfo eventInfo, IWebhookSubscription subscription, WebhookDeliveryResult result, CancellationToken cancellationToken) { - if (result is null) - throw new ArgumentNullException(nameof(result)); - if (subscription is null) - throw new ArgumentNullException(nameof(subscription)); + ArgumentNullException.ThrowIfNull(result, nameof(result)); + ArgumentNullException.ThrowIfNull(subscription, nameof(subscription)); // TODO: we should support also non-multi-tenant scenarios... if (String.IsNullOrWhiteSpace(subscription.TenantId)) @@ -199,7 +197,7 @@ public async Task LogResultAsync(EventInfo eventInfo, IWebhookSubscription subsc try { var resultObj = ConvertResult(eventInfo, subscription, result); - var repository = await StoreProvider.GetRepositoryAsync(subscription.TenantId, cancellationToken); + var repository = await RepositoryProvider.GetRepositoryAsync(subscription.TenantId, cancellationToken); await repository.AddAsync(resultObj, cancellationToken); } catch (Exception ex) { diff --git a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookSubscriptionRepository_T.cs b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookSubscriptionRepository_T.cs index d01bdd9..941eceb 100644 --- a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookSubscriptionRepository_T.cs +++ b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoDbWebhookSubscriptionRepository_T.cs @@ -54,9 +54,6 @@ public MongoDbWebhookSubscriptionRepository(IMongoDbWebhookContext context, ILog /// protected IMongoDbSet Subscriptions => base.DbSet; - /// - public IQueryable AsQueryable() => Subscriptions.AsQueryable(); - /// public Task GetDestinationUrlAsync(TSubscription subscription, CancellationToken cancellationToken = default) { ThrowIfDisposed(); @@ -96,6 +93,24 @@ public Task GetEventTypesAsync(TSubscription subscription, Cancellatio return Task.FromResult(subscription.EventTypes?.ToArray() ?? Array.Empty()); } + /// + public Task GetSecretAsync(TSubscription subscription, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + return Task.FromResult(subscription.Secret); + } + + /// + public Task SetSecretAsync(TSubscription subscription, string? secret, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + subscription.Secret = secret; + + return Task.CompletedTask; + } + /// public Task AddEventTypesAsync(TSubscription subscription, string[] eventTypes, CancellationToken cancellationToken = default) { ThrowIfDisposed(); @@ -137,7 +152,6 @@ public Task SetStatusAsync(TSubscription subscription, WebhookSubscriptionStatus cancellationToken.ThrowIfCancellationRequested(); subscription.Status = status; - subscription.LastStatusTime = DateTimeOffset.UtcNow; return Task.CompletedTask; } @@ -179,5 +193,44 @@ public Task RemoveHeadersAsync(TSubscription subscription, string[] headerNames, return Task.CompletedTask; } + + /// + public Task> GetPropertiesAsync(TSubscription subscription, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + var props = subscription.Properties ?? new Dictionary(); + return Task.FromResult>(props); + } + + /// + public Task AddPropertiesAsync(TSubscription subscription, IDictionary properties, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + if (subscription.Properties == null) + subscription.Properties = new Dictionary(); + + foreach (var property in properties) { + subscription.Properties[property.Key] = property.Value; + } + + return Task.CompletedTask; + } + + /// + public Task RemovePropertiesAsync(TSubscription subscription, string[] propertyNames, CancellationToken cancellationToken = default) { + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + if (subscription.Properties == null) + return Task.CompletedTask; + + foreach (var propertyName in propertyNames) { + subscription.Properties.Remove(propertyName); + } + + return Task.CompletedTask; + } } } diff --git a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoWebhookSubscription.cs b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoWebhookSubscription.cs index c8d98e2..57146c8 100644 --- a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoWebhookSubscription.cs +++ b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/MongoWebhookSubscription.cs @@ -19,6 +19,8 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; +using Deveel.Data; + using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Options; @@ -32,7 +34,7 @@ namespace Deveel.Webhooks { /// and that is stored in a MongoDB storage. /// [Table(MongoDbWebhookStorageConstants.SubscriptionCollectionName)] - public class MongoWebhookSubscription : IWebhookSubscription { + public class MongoWebhookSubscription : IWebhookSubscription, IHaveTimeStamp { [ExcludeFromCodeCoverage] string? IWebhookSubscription.SubscriptionId => Id.Equals(ObjectId.Empty) ? null : Id.ToString(); @@ -48,23 +50,16 @@ public class MongoWebhookSubscription : IWebhookSubscription { /// [Column("destination_url")] - public string? DestinationUrl { get; set; } + public string DestinationUrl { get; set; } /// [Column("secret")] - public string Secret { get; set; } + public string? Secret { get; set; } /// [Column("status")] public WebhookSubscriptionStatus Status { get; set; } - /// - /// Gets or sets the time when the last status of the subscription - /// was set. - /// - [Column("last_status_time")] - public DateTimeOffset? LastStatusTime { get; set; } - /// [Column("tenant_id")] public string TenantId { get; set; } @@ -102,10 +97,20 @@ public class MongoWebhookSubscription : IWebhookSubscription { [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public IDictionary Properties { get; set; } + DateTimeOffset? IHaveTimeStamp.CreatedAtUtc { + get => CreatedAt ?? DateTimeOffset.UtcNow; + set => CreatedAt = value; + } + /// [Column("created_at")] public DateTimeOffset? CreatedAt { get; set; } + DateTimeOffset? IHaveTimeStamp.UpdatedAtUtc { + get => UpdatedAt ?? DateTimeOffset.UtcNow; + set => UpdatedAt = value; + } + /// [Column("updated_at")] public DateTimeOffset? UpdatedAt { get; set; } diff --git a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/ObjectIdExtensions.cs b/src/Deveel.Webhooks.Service.MongoDb/Webhooks/ObjectIdExtensions.cs deleted file mode 100644 index 8b81fe4..0000000 --- a/src/Deveel.Webhooks.Service.MongoDb/Webhooks/ObjectIdExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2022-2023 Deveel -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using MongoDB.Bson; - -namespace Deveel.Webhooks { - static class ObjectIdExtensions { - public static string? ToEntityId(this ObjectId objectId) { - if (objectId == ObjectId.Empty) - return null; - - return objectId.ToString(); - } - } -} diff --git a/src/Deveel.Webhooks.Service/Webhooks/IWebhookSubscriptionRepository.cs b/src/Deveel.Webhooks.Service/Webhooks/IWebhookSubscriptionRepository.cs index 6563f53..e8d06a9 100644 --- a/src/Deveel.Webhooks.Service/Webhooks/IWebhookSubscriptionRepository.cs +++ b/src/Deveel.Webhooks.Service/Webhooks/IWebhookSubscriptionRepository.cs @@ -12,11 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - using Deveel.Data; namespace Deveel.Webhooks { @@ -28,8 +23,39 @@ namespace Deveel.Webhooks { /// public interface IWebhookSubscriptionRepository : IRepository where TSubscription : class, IWebhookSubscription { + /// + /// Gets the URL of the destination where to deliver the + /// webhook events for the given subscription. + /// + /// + /// The instance of the subscription to get the destination URL for. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns the URL of the destination where to deliver the + /// webhook events for the given subscription, or null + /// if the subscription has no destination URL set. + /// Task GetDestinationUrlAsync(TSubscription subscription, CancellationToken cancellationToken = default); + /// + /// Sets the URL of the destination where to deliver the + /// webhook events for the given subscription. + /// + /// + /// The instance of the subscription to set the destination URL for. + /// + /// + /// The URL of the destination where to deliver the webhook events. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the destination URL is set. + /// Task SetDestinationUrlAsync(TSubscription subscription, string url, CancellationToken cancellationToken = default); /// @@ -54,6 +80,42 @@ public interface IWebhookSubscriptionRepository : IRepository GetStatusAsync(TSubscription subscription, CancellationToken cancellationToken = default); + /// + /// Gets the secret that is used to sign the webhooks + /// delivered to the given subscription. + /// + /// + /// The instance of the subscription to get the secret for. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns the secret that is used to sign the webhooks + /// delivered to the given subscription, or null + /// if the subscription has no secret set. + /// + Task GetSecretAsync(TSubscription subscription, CancellationToken cancellationToken = default); + + /// + /// Sets the secret that is used to sign the webhooks + /// to be delivered to the given subscription. + /// + /// + /// The instance of the subscription to set the secret for. + /// + /// + /// The secret to set for the subscription, or + /// null to remove the secret from the subscription. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the secret is set. + /// + Task SetSecretAsync(TSubscription subscription, string? secret, CancellationToken cancellationToken = default); + /// /// Sets the state of the given subscription. /// @@ -71,16 +133,154 @@ public interface IWebhookSubscriptionRepository : IRepository Task SetStatusAsync(TSubscription subscription, WebhookSubscriptionStatus status, CancellationToken cancellationToken = default); + /// + /// Gets the list of event types that the given subscription + /// is listening for. + /// + /// + /// The instance of the subscription to get the event types for. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns an array of event types that the subscription is + /// listening for. + /// Task GetEventTypesAsync(TSubscription subscription, CancellationToken cancellationToken = default); + /// + /// Adds the given set of event types to the list of the + /// ones that the given subscription is listening for. + /// + /// + /// The instance of the subscription to add the event types to. + /// + /// + /// The list of the new event types to add to the subscription. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the event types are added. + /// Task AddEventTypesAsync(TSubscription subscription, string[] eventTypes, CancellationToken cancellationToken = default); + /// + /// Removes the given set of event types from the list of the + /// ones that the given subscription is listening for. + /// + /// + /// The instance of the subscription to remove the event types from. + /// + /// + /// The list of the event types to remove from the subscription. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the event types are removed. + /// Task RemoveEventTypesAsync(TSubscription subscription, string[] eventTypes, CancellationToken cancellationToken = default); + /// + /// Gets the list of the headers that are set for the given subscription, + /// to be sent to the destination URL together with the webhook. + /// + /// + /// The instance of the subscription to get the headers for. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a dictionary of the headers that are set for the subscription. + /// Task> GetHeadersAsync(TSubscription subscription, CancellationToken cancellationToken = default); + /// + /// Adds new headers to the list of the ones that are set to be sent + /// to the destination URL together with the webhook. + /// + /// + /// The instance of the subscription to add the headers to. + /// + /// + /// The new headers to add to the subscription. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the headers are added. + /// Task AddHeadersAsync(TSubscription subscription, IDictionary headers, CancellationToken cancellationToken = default); + /// + /// Removes the given set of headers from the list of the ones that + /// will be sent to the destination URL together with the webhook. + /// + /// + /// The instance of the subscription to remove the headers from. + /// + /// + /// The list of the names of the headers to remove from the subscription. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the headers are removed. + /// Task RemoveHeadersAsync(TSubscription subscription, string[] headerNames, CancellationToken cancellationToken = default); + + /// + /// Gets the list of the properties that are set for the given subscription, + /// used to configure the behaviour of the webhook notification. + /// + /// + /// The instance of the subscription to get the properties for. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a dictionary of the properties that are set for the subscription. + /// + Task> GetPropertiesAsync(TSubscription subscription, CancellationToken cancellationToken = default); + + /// + /// Adds new properties to the list of the ones that are set to configure + /// the behaviour of the webhook notification. + /// + /// + /// The instance of the subscription to add the properties to. + /// + /// + /// The new properties to add to the subscription. + /// + /// + /// A cancellation token used to cancel the operation. + /// + /// + /// Returns a task that completes when the properties are added. + /// + Task AddPropertiesAsync(TSubscription subscription, IDictionary properties, CancellationToken cancellationToken = default); + + /// + /// Removes the given set of properties from the list of the ones that + /// are used to configure the behaviour of the webhook notification. + /// + /// + /// The instance of the subscription to remove the properties from. + /// + /// + /// The list of the names of the properties to remove from the subscription. + /// + /// + /// + Task RemovePropertiesAsync(TSubscription subscription, string[] propertyNames, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/Deveel.Webhooks.Service/Webhooks/WebhookSubscriptionManager_T.cs b/src/Deveel.Webhooks.Service/Webhooks/WebhookSubscriptionManager_T.cs index a19b67f..ef3f7c5 100644 --- a/src/Deveel.Webhooks.Service/Webhooks/WebhookSubscriptionManager_T.cs +++ b/src/Deveel.Webhooks.Service/Webhooks/WebhookSubscriptionManager_T.cs @@ -75,8 +75,6 @@ protected virtual IWebhookSubscriptionRepository SubscriptionRepo /// protected override bool AreEqual(TSubscription existing, TSubscription other) => false; - internal object? GetSubscriptionId(TSubscription subscription) => GetEntityKey(subscription); - /// protected override IOperationError OperationError(string errorCode, string? message = null) { errorCode = errorCode switch { @@ -185,16 +183,6 @@ public virtual Task DisableAsync(TSubscription subscription, Ca public virtual Task EnableAsync(TSubscription subscription, CancellationToken? cancellationToken = null) => SetStatusAsync(subscription, WebhookSubscriptionStatus.Active, cancellationToken); - /// - public virtual async Task CountAllAsync() { - try { - return await base.CountAsync(QueryFilter.Empty); - } catch (Exception ex) { - Logger.LogUnknownError(ex); - throw new WebhookException("Could not count the subscriptions", ex); - } - } - public virtual async Task SetEventTypesAsync(TSubscription subscription, string[] eventTypes, CancellationToken? cancellationToken = null) { ThrowIfDisposed(); @@ -265,5 +253,42 @@ public async Task SetDestinationUrlAsync(TSubscription subscrip return Fail(WebhookSubscriptionErrorCodes.UnknownError, "Unhandled error while setting the destination URL"); } } + + public async Task SetSecretAsync(TSubscription subscription, string? secret, CancellationToken? cancellationToken = null) { + try { + var existing = await SubscriptionRepository.GetSecretAsync(subscription, GetCancellationToken(cancellationToken)); + if (existing != null && existing.Equals(secret)) + return OperationResult.NotModified; + + await SubscriptionRepository.SetSecretAsync(subscription, secret, GetCancellationToken(cancellationToken)); + + return await UpdateAsync(subscription); + } catch (Exception ex) { + Logger.LogUnknownSubscriptionError(ex, subscription.SubscriptionId); + return Fail(WebhookSubscriptionErrorCodes.UnknownError, "Unhandled error while setting the secret"); + } + } + + public async Task SetPropertiesAsync(TSubscription subscription, IDictionary properties, CancellationToken? cancellationToken = null) { + try { + var existing = await SubscriptionRepository.GetPropertiesAsync(subscription, GetCancellationToken(cancellationToken)); + + var toAdd = properties.Where(x => !existing.ContainsKey(x.Key)).ToArray(); + var toRemove = existing.Where(x => !properties.ContainsKey(x.Key)).ToArray(); + + if (toAdd.Length == 0 && toRemove.Length == 0) + return OperationResult.NotModified; + + if (toAdd.Length > 0) + await SubscriptionRepository.AddPropertiesAsync(subscription, toAdd.ToDictionary(x => x.Key, x => x.Value), GetCancellationToken(cancellationToken)); + if (toRemove.Length > 0) + await SubscriptionRepository.RemovePropertiesAsync(subscription, toRemove.Select(x => x.Key).ToArray(), GetCancellationToken(cancellationToken)); + + return await UpdateAsync(subscription); + } catch (Exception ex) { + Logger.LogUnknownSubscriptionError(ex, subscription.SubscriptionId); + return Fail(WebhookSubscriptionErrorCodes.UnknownError, "Unhandled error while setting the properties"); + } + } } } diff --git a/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Deveel.Webhooks.DeliveryResultLogging.Tests.csproj b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Deveel.Webhooks.DeliveryResultLogging.Tests.csproj new file mode 100644 index 0000000..9758d94 --- /dev/null +++ b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Deveel.Webhooks.DeliveryResultLogging.Tests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + Deveel + false + false + + + + + + + + + + + + + + + diff --git a/test/Deveel.Webhooks.DeliveryResultLogging.Tests/GlobalUsings.cs b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/DeliveryResultLoggerTestSuite.cs b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/DeliveryResultLoggerTestSuite.cs new file mode 100644 index 0000000..637beeb --- /dev/null +++ b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/DeliveryResultLoggerTestSuite.cs @@ -0,0 +1,147 @@ +using Bogus; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Xunit.Abstractions; + +namespace Deveel.Webhooks { + public abstract class DeliveryResultLoggerTestSuite : IAsyncLifetime { + protected DeliveryResultLoggerTestSuite(ITestOutputHelper testOutput) { + TestOutput = testOutput; + SubscriptionFaker = new WebhookSubscriptionFaker(TenantId); + } + + protected string TenantId { get; } = Guid.NewGuid().ToString(); + + protected IServiceProvider? Services { get; private set; } + + protected IServiceScope? Scope { get; private set; } + + protected ITestOutputHelper TestOutput { get; } + + protected IWebhookDeliveryResultLogger ResultLogger + => Scope!.ServiceProvider.GetRequiredService>(); + + protected Faker WebhookFaker { get; } = new WebhookFaker(); + + protected Faker SubscriptionFaker { get; } + + private IServiceProvider BuildServices() { + var services = new ServiceCollection(); + + services.AddLogging(x => x.AddXUnit(TestOutput)); + + ConfigureService(services); + + return services.BuildServiceProvider(); + } + + async Task IAsyncLifetime.InitializeAsync() { + Services = BuildServices(); + Scope = Services.CreateScope(); + + await InitializeAsync(); + } + + protected virtual Task InitializeAsync() { + return Task.CompletedTask; + } + + async Task IAsyncLifetime.DisposeAsync() { + await DisposeAsync(); + + Scope?.Dispose(); + (Services as IDisposable)?.Dispose(); + Scope = null; + Services = null; + } + + protected virtual Task DisposeAsync() { + return Task.CompletedTask; + } + + protected virtual void ConfigureService(IServiceCollection services) { + } + + protected abstract Task FindResultByOperationIdAsync(string operationId); + + [Fact] + public async Task LogSuccessfulDelivery() { + var webhook = WebhookFaker.Generate(); + var eventInfo = new EventInfo("test", "executed", "1.0.0", new { name = "logTest" }); + var subscription = SubscriptionFaker.Generate(); + var destination = subscription.AsDestination(); + + var result = new WebhookDeliveryResult(Guid.NewGuid().ToString(), destination, webhook); + + var attempt = result.StartAttempt(); + attempt.Complete(200, "OK"); + + Assert.True(result.Successful); + Assert.Single(result.Attempts); + Assert.Equal(200, result.Attempts[0].ResponseCode); + Assert.Equal("OK", result.Attempts[0].ResponseMessage); + + await ResultLogger.LogResultAsync(eventInfo, subscription, result); + + var logged = await FindResultByOperationIdAsync(result.OperationId); + + Assert.NotNull(logged); + Assert.Equal(result.OperationId, logged.OperationId); + Assert.NotNull(logged.Webhook); + Assert.NotNull(logged.DeliveryAttempts); + Assert.NotEmpty(logged.DeliveryAttempts); + Assert.Single(logged.DeliveryAttempts); + Assert.Equal(200, logged.DeliveryAttempts.ElementAt(0).ResponseStatusCode); + Assert.Equal("OK", logged.DeliveryAttempts.ElementAt(0).ResponseMessage); + } + + [Fact] + public async Task LogFailedDelivery() { + var webhook = WebhookFaker.Generate(); + var eventInfo = new EventInfo("test", "executed", "1.0.0", new { name = "logTest" }); + var subscription = SubscriptionFaker.Generate(); + var destination = subscription.AsDestination(); + + var result = new WebhookDeliveryResult(Guid.NewGuid().ToString(), destination, webhook); + + Enumerable.Range(0, 3) + .Select(x => result.StartAttempt()) + .ToList() + .ForEach(x => { + if (x.Number == 3) { + x.Complete(200, "OK"); + } else { + x.Complete(500, "Internal Server Error"); + } + }); + + Assert.True(result.Successful); + Assert.Equal(3, result.Attempts.Count); + Assert.Equal(500, result.Attempts[0].ResponseCode); + Assert.Equal("Internal Server Error", result.Attempts[0].ResponseMessage); + Assert.Equal(500, result.Attempts[1].ResponseCode); + Assert.Equal("Internal Server Error", result.Attempts[1].ResponseMessage); + Assert.Equal(200, result.Attempts[2].ResponseCode); + Assert.Equal("OK", result.Attempts[2].ResponseMessage); + + await ResultLogger.LogResultAsync(eventInfo, subscription, result); + + var logged = await FindResultByOperationIdAsync(result.OperationId); + + Assert.NotNull(logged); + Assert.Equal(result.OperationId, logged.OperationId); + Assert.NotNull(logged.Webhook); + Assert.NotNull(logged.DeliveryAttempts); + Assert.NotEmpty(logged.DeliveryAttempts); + Assert.Equal(3, logged.DeliveryAttempts.Count()); + Assert.Equal(500, logged.DeliveryAttempts.ElementAt(0).ResponseStatusCode); + Assert.Equal("Internal Server Error", logged.DeliveryAttempts.ElementAt(0).ResponseMessage); + Assert.Equal(500, logged.DeliveryAttempts.ElementAt(1).ResponseStatusCode); + Assert.Equal("Internal Server Error", logged.DeliveryAttempts.ElementAt(1).ResponseMessage); + Assert.Equal(200, logged.DeliveryAttempts.ElementAt(2).ResponseStatusCode); + Assert.Equal("OK", logged.DeliveryAttempts.ElementAt(2).ResponseMessage); + } + } +} diff --git a/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookFaker.cs b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookFaker.cs new file mode 100644 index 0000000..22f3843 --- /dev/null +++ b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookFaker.cs @@ -0,0 +1,15 @@ +using Bogus; + +namespace Deveel.Webhooks { + class WebhookFaker : Faker { + public WebhookFaker() { + RuleFor(x => x.Id, f => f.Random.Guid().ToString()); + RuleFor(x => x.Name, f => f.Lorem.Word()); + RuleFor(x => x.EventType, f => f.PickRandom(EventTypes)); + RuleFor(x => x.SubscriptionId, f => f.Random.Guid().ToString()); + RuleFor(x => x.TimeStamp, f => f.Date.Past()); + } + + public static readonly string[] EventTypes = new[] { "data.created", "data.modified" }; + } +} diff --git a/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscription.cs b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscription.cs new file mode 100644 index 0000000..8da9f2e --- /dev/null +++ b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscription.cs @@ -0,0 +1,33 @@ +namespace Deveel.Webhooks { + public sealed class WebhookSubscription : IWebhookSubscription { + public string? SubscriptionId { get; set; } + + public string? TenantId { get; set; } + + public string Name { get; set; } + + IEnumerable IWebhookSubscription.EventTypes => EventTypes; + + public string[] EventTypes { get; set; } + + public string DestinationUrl { get; set; } + + public string? Secret { get; set; } + + public string? Format { get; set; } + + public WebhookSubscriptionStatus Status { get; set; } + + public int? RetryCount { get; set; } + + IEnumerable? IWebhookSubscription.Filters => Enumerable.Empty(); + + public IDictionary? Headers { get; set; } = new Dictionary(); + + public IDictionary? Properties { get; set; } = new Dictionary(); + + public DateTimeOffset? CreatedAt { get; set; } + + public DateTimeOffset? UpdatedAt { get; set; } + } +} diff --git a/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscriptionFaker.cs b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscriptionFaker.cs new file mode 100644 index 0000000..b48f76d --- /dev/null +++ b/test/Deveel.Webhooks.DeliveryResultLogging.Tests/Webhooks/WebhookSubscriptionFaker.cs @@ -0,0 +1,19 @@ +using Bogus; + +namespace Deveel.Webhooks { + class WebhookSubscriptionFaker : Faker { + public WebhookSubscriptionFaker(string? tenantId = null) { + RuleFor(x => x.TenantId, tenantId); + RuleFor(x => x.SubscriptionId, f => f.Random.Guid().ToString()); + RuleFor(x => x.Name, f => f.Lorem.Word()); + RuleFor(x => x.DestinationUrl, f => f.Internet.Url()); + RuleFor(x => x.EventTypes, f => f.Random.ListItems(EventTypes, 2).ToArray()); + RuleFor(x => x.Status, f => f.Random.Enum()); + RuleFor(x => x.RetryCount, f => f.Random.Int(1, 10)); + RuleFor(x => x.Format, f => f.PickRandom(new[] { "json", "xml" })); + RuleFor(x => x.Secret, f => f.Internet.Password()); + } + + public static readonly string[] EventTypes = new[] { "data.created", "data.updated", "data.deleted" }; + } +} diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Deveel.Webhooks.EntityFramework.XUnit.csproj b/test/Deveel.Webhooks.EntityFramework.XUnit/Deveel.Webhooks.EntityFramework.XUnit.csproj index 9705d85..ae6594a 100644 --- a/test/Deveel.Webhooks.EntityFramework.XUnit/Deveel.Webhooks.EntityFramework.XUnit.csproj +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Deveel.Webhooks.EntityFramework.XUnit.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbEventInfoFaker.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbEventInfoFaker.cs new file mode 100644 index 0000000..f31de5c --- /dev/null +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbEventInfoFaker.cs @@ -0,0 +1,20 @@ +using System.Text.Json; + +using Bogus; + +namespace Deveel.Webhooks { + public class DbEventInfoFaker : Faker { + public DbEventInfoFaker() { + RuleFor(x => x.EventId, f => f.Random.Guid().ToString()); + RuleFor(x => x.EventType, f => f.Random.ListItem(new[] { "created", "deleted", "updated" })); + RuleFor(x => x.DataVersion, "1.0"); + RuleFor(x => x.EventId, f => f.Random.Guid().ToString("N")); + RuleFor(x => x.TimeStamp, f => f.Date.PastOffset()); + RuleFor(x => x.Subject, "data"); + RuleFor(x => x.Data, f => JsonSerializer.Serialize(new { + data_type = f.Random.Word(), + users = f.Random.ListItems(new string[] { "user1", "user2" }) + })); + } + } +} diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookDeliveryResultFaker.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookDeliveryResultFaker.cs index b91fd38..ef299e8 100644 --- a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookDeliveryResultFaker.cs +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookDeliveryResultFaker.cs @@ -2,8 +2,13 @@ namespace Deveel.Webhooks { public class DbWebhookDeliveryResultFaker : Faker { - public DbWebhookDeliveryResultFaker(int? eventId = null) { - RuleFor(x => x.EventId, eventId); + public DbWebhookDeliveryResultFaker(DbEventInfo? eventInfo = null) { + RuleFor(x => x.EventInfo, f => { + if (eventInfo != null) + return eventInfo; + + return new DbEventInfoFaker().Generate(); + }); RuleFor(x => x.OperationId, f => f.Random.Guid().ToString()); RuleFor(x => x.Webhook, f => new DbWebhookFaker().Generate()); RuleFor(x => x.Receiver, f => new DbWebhookReceiverFaker().Generate()); diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookFaker.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookFaker.cs index d1a26e3..1df712a 100644 --- a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookFaker.cs +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookFaker.cs @@ -5,6 +5,7 @@ namespace Deveel.Webhooks { public class DbWebhookFaker : Faker { public DbWebhookFaker() { + RuleFor(x => x.WebhookId, f => f.Random.Guid().ToString()); RuleFor(x => x.EventType, f => f.PickRandom(EventTypes)); RuleFor(x => x.TimeStamp, f => f.Date.Past()); RuleFor(x => x.Data, (f, w) => GenerateData(f, w)); diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookSubscriptionFaker.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookSubscriptionFaker.cs index db41972..e6cdf91 100644 --- a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookSubscriptionFaker.cs +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookSubscriptionFaker.cs @@ -6,6 +6,7 @@ public DbWebhookSubscriptionFaker() { RuleFor(x => x.Id, f => f.Random.Guid().ToString()); RuleFor(x => x.Name, f => f.Lorem.Word()); RuleFor(x => x.DestinationUrl, f => f.Internet.Url()); + RuleFor(x => x.Secret, f => f.Internet.Password(20).OrNull(f)); RuleFor(x => x.Status, f => f.Random.Enum()); RuleFor(x => x.Format, f => f.PickRandom("json", "xml")); RuleFor(x => x.Filters, f => new List { diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookValueConvertTests.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookValueConvertTests.cs new file mode 100644 index 0000000..d286720 --- /dev/null +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/DbWebhookValueConvertTests.cs @@ -0,0 +1,45 @@ +namespace Deveel.Webhooks { + public static class DbWebhookValueConvertTests { + [Theory] + [InlineData(null, null)] + [InlineData(true, "true")] + [InlineData(false, "false")] + [InlineData(123, "123")] + [InlineData(123L, "123")] + [InlineData(123.45, "123.45")] + [InlineData(123.45f, "123.45")] + [InlineData("foo", "foo")] + public static void ConvertToString(object? value, string expected) { + var actual = DbWebhookValueConvert.ConvertToString(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, DbWebhookValueTypes.String)] + [InlineData(true, DbWebhookValueTypes.Boolean)] + [InlineData(false, DbWebhookValueTypes.Boolean)] + [InlineData(123, DbWebhookValueTypes.Integer)] + [InlineData(123L, DbWebhookValueTypes.Integer)] + [InlineData(123.45, DbWebhookValueTypes.Number)] + [InlineData(123.45f, DbWebhookValueTypes.Number)] + [InlineData("foo", DbWebhookValueTypes.String)] + public static void GetValueType(object? value, string expected) { + var actual = DbWebhookValueConvert.GetValueType(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, DbWebhookValueTypes.String, null)] + [InlineData("true", DbWebhookValueTypes.Boolean, true)] + [InlineData("false", DbWebhookValueTypes.Boolean, false)] + [InlineData("123", DbWebhookValueTypes.Integer, 123)] + [InlineData("123", DbWebhookValueTypes.Number, 123.0)] + [InlineData("123.45", DbWebhookValueTypes.Number, 123.45)] + [InlineData("123.45678901", DbWebhookValueTypes.Number, 123.45678901d)] + [InlineData("foo", DbWebhookValueTypes.String, "foo")] + public static void GetValue(string? value, string valueType, object? expected) { + var actual = DbWebhookValueConvert.Convert(value, valueType); + Assert.Equal(expected, actual); + } + } +} diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultRepositoryTests.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultRepositoryTests.cs new file mode 100644 index 0000000..9c06303 --- /dev/null +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultRepositoryTests.cs @@ -0,0 +1,122 @@ +using Bogus; + +using Deveel.Data; + +using Microsoft.Extensions.DependencyInjection; + +using Xunit.Abstractions; + +namespace Deveel.Webhooks { + public class EntityDeliveryResultRepositoryTests : EntityWebhookTestBase { + private readonly Faker resultFaker; + private readonly Faker eventFaker; + private List results; + + public EntityDeliveryResultRepositoryTests(SqliteTestDatabase sqlite, ITestOutputHelper outputHelper) : base(sqlite, outputHelper) { + resultFaker = new DbWebhookDeliveryResultFaker(); + eventFaker = new DbEventInfoFaker(); + } + + private IWebhookDeliveryResultRepository Repository + => Services.GetRequiredService>(); + + public override async Task InitializeAsync() { + await base.InitializeAsync(); + + var events = eventFaker.Generate(10).ToList(); + results = new List(10 * 5); + + foreach (var eventInfo in events) { + var faker = new DbWebhookDeliveryResultFaker(eventInfo); + var deliveryResults = resultFaker.Generate(5); + results.AddRange(deliveryResults); + } + + await Repository.AddRangeAsync(results); + } + + private DbWebhookDeliveryResult NextRandom() + => results[Random.Shared.Next(0, results.Count - 1)]; + + [Fact] + public async Task CreateNewResult() { + var result = resultFaker.Generate(); + + await Repository.AddAsync(result); + + Assert.NotNull(result.Id); + } + + [Fact] + public async Task GetExistingResult() { + var result = NextRandom(); + + var found = await Repository.FindByKeyAsync(result.Id!); + + Assert.NotNull(found); + Assert.Equal(result.Id, found.Id); + + var deliveryResult = Assert.IsAssignableFrom(found); + + Assert.Equal(result.Webhook.WebhookId, deliveryResult.Webhook.Id); + Assert.Equal(result.Webhook.EventType, deliveryResult.Webhook.EventType); + Assert.Equal(result.Webhook.TimeStamp, deliveryResult.Webhook.TimeStamp); + Assert.Equal(result.EventInfo.EventType, deliveryResult.EventInfo.EventType); + Assert.Equal(result.EventInfo.EventId, deliveryResult.EventInfo.Id); + Assert.Equal(result.EventInfo.DataVersion, deliveryResult.EventInfo.DataVersion); + Assert.Equal(result.EventInfo.Subject, deliveryResult.EventInfo.Subject); + Assert.Equal(result.EventInfo.TimeStamp, deliveryResult.EventInfo.TimeStamp); + Assert.Equal(result.DeliveryAttempts.Count, deliveryResult.DeliveryAttempts.Count()); + } + + [Fact] + public async Task GetNotExistingResult() { + var resultId = Random.Shared.Next(results.Max(x => x.Id!.Value) + 1, Int32.MaxValue); + + var found = await Repository.FindByKeyAsync(resultId!); + + Assert.Null(found); + } + + [Fact] + public async Task RemoveExistingResult() { + var result = NextRandom(); + + var deleted = await Repository.RemoveAsync(result); + + Assert.True(deleted); + + var found = await Repository.FindByKeyAsync(result.Id!); + + Assert.Null(found); + } + + [Fact] + public async Task RemoveNotExistingResult() { + var resultId = Random.Shared.Next(results.Max(x => x.Id!.Value) + 1, Int32.MaxValue); + var result = resultFaker.Generate(); + result.Id = resultId; + + var removed = await Repository.RemoveAsync(result); + + Assert.False(removed); + } + + [Fact] + public async Task CountAll() { + var count = await Repository.CountAllAsync(); + + Assert.Equal(results.Count, count); + } + + [Fact] + public async Task GetByWebhookId() { + var result = NextRandom(); + + var found = await Repository.FindByWebhookIdAsync(result.Webhook.WebhookId!, default); + + Assert.NotNull(found); + Assert.Equal(result.Id, found.Id); + } + } +} diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultStoreTests.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultStoreTests.cs deleted file mode 100644 index a717327..0000000 --- a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/EntityDeliveryResultStoreTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System.Runtime.Serialization; -using System.Text.Json; - -using Bogus; - -using Deveel.Data; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -using Xunit.Abstractions; - -namespace Deveel.Webhooks { - public class EntityDeliveryResultStoreTests : EntityWebhookTestBase { - private readonly Faker faker; - private readonly List results; - - public EntityDeliveryResultStoreTests(SqliteTestDatabase sqlite, ITestOutputHelper outputHelper) : base(sqlite, outputHelper) { - - var receiver = new Faker() - .RuleFor(x => x.BodyFormat, f => f.Random.ListItem(new[] { "json", "xml" })) - .RuleFor(x => x.DestinationUrl, f => f.Internet.Url()) - // .RuleFor(x => x.SubscriptionId, f => f.Random.Guid().OrNull(f)?.ToString()) - .RuleFor(x => x.SubscriptionName, f => f.Name.JobType()); - - var webhook = new Faker() - .RuleFor(x => x.EventType, f => f.Random.ListItem(new[] { "data.created", "data.deleted", "data.updated" })) - .RuleFor(x => x.WebhookId, f => f.Random.Guid().ToString()) - .RuleFor(x => x.TimeStamp, f => f.Date.PastOffset(1)) - .RuleFor(x => x.Data, "{ \"data-type\", \"test\" }"); - - var attempt = new Faker() - .RuleFor(x => x.ResponseStatusCode, f => f.Random.ListItem(new int?[] { 200, 201, 204, 400, 404, 500, null })) - .RuleFor(x => x.StartedAt, (f, a) => f.Date.PastOffset()) - .RuleFor(x => x.EndedAt, (f, a) => a.StartedAt.AddMilliseconds(200).OrNull(f)); - - var eventInfo = new Faker() - .RuleFor(x => x.EventId, f => f.Random.Guid().ToString()) - .RuleFor(x => x.EventType, f => f.Random.ListItem(new[] { "created", "deleted", "updated" })) - .RuleFor(x => x.DataVersion, "1.0") - .RuleFor(x => x.EventId, f => f.Random.Guid().ToString("N")) - .RuleFor(x => x.TimeStamp, f => f.Date.PastOffset()) - .RuleFor(x => x.Subject, "data") - .RuleFor(x => x.Data, f => JsonSerializer.Serialize(new { - data_type = f.Random.Word(), - users = f.Random.ListItems(new string[]{ "user1", "user2" }) - })); - - faker = new Faker() - .RuleFor(x => x.OperationId, f => f.Random.Guid().ToString()) - .RuleFor(x => x.EventInfo, f => eventInfo.Generate()) - .RuleFor(x => x.Receiver, f => receiver.Generate()) - .RuleFor(x => x.Webhook, f => webhook.Generate()) - .RuleFor(x => x.DeliveryAttempts, f => attempt.Generate(2)); - - results = new List(); - } - - private IWebhookDeliveryResultRepository Store - => Services.GetRequiredService>(); - - public override async Task InitializeAsync() { - await base.InitializeAsync(); - - var fakes = faker.Generate(112).ToList(); - - foreach (var attempt in fakes) { - await AddAttemptAsync(attempt); - - results.Add(attempt); - } - } - - private Task AddAttemptAsync(DbWebhookDeliveryResult attempt) { - return Store.AddAsync(attempt, default); - } - - private DbWebhookDeliveryResult NextRandom() - => results[Random.Shared.Next(0, results.Count - 1)]; - - [Fact] - public async Task CreateNewResult() { - var result = faker.Generate(); - - await Store.AddAsync(result); - - Assert.NotNull(result.Id); - } - - [Fact] - public async Task GetExistingResult() { - var result = NextRandom(); - - var found = await Store.FindByKeyAsync(result.Id!); - - Assert.NotNull(found); - Assert.Equal(result.Id, found.Id); - - var deliveryResult = Assert.IsAssignableFrom(found); - - Assert.Equal(result.Webhook.WebhookId, deliveryResult.Webhook.Id); - Assert.Equal(result.Webhook.EventType, deliveryResult.Webhook.EventType); - Assert.Equal(result.Webhook.TimeStamp, deliveryResult.Webhook.TimeStamp); - Assert.Equal(result.EventInfo.EventType, deliveryResult.EventInfo.EventType); - Assert.Equal(result.EventInfo.EventId, deliveryResult.EventInfo.Id); - Assert.Equal(result.EventInfo.DataVersion, deliveryResult.EventInfo.DataVersion); - Assert.Equal(result.EventInfo.Subject, deliveryResult.EventInfo.Subject); - Assert.Equal(result.EventInfo.TimeStamp, deliveryResult.EventInfo.TimeStamp); - Assert.Equal(result.DeliveryAttempts.Count, deliveryResult.DeliveryAttempts.Count()); - } - - [Fact] - public async Task GetNotExistingResult() { - var resultId = Random.Shared.Next(results.Max(x => x.Id!.Value) + 1, Int32.MaxValue); - - var found = await Store.FindByKeyAsync(resultId!); - - Assert.Null(found); - } - - [Fact] - public async Task RemoveExistingResult() { - var result = NextRandom(); - - var deleted = await Store.RemoveAsync(result); - - Assert.True(deleted); - - var found = await Store.FindByKeyAsync(result.Id!); - - Assert.Null(found); - } - - [Fact] - public async Task RemoveNotExistingResult() { - var resultId = Random.Shared.Next(results.Max(x => x.Id!.Value) + 1, Int32.MaxValue); - var result = faker.Generate(); - result.Id = resultId; - - var removed = await Store.RemoveAsync(result); - - Assert.False(removed); - } - - [Fact] - public async Task CountAll() { - var count = await Store.CountAllAsync(); - - Assert.Equal(results.Count, count); - } - - [Fact] - public async Task GetByWebhookId() { - var result = NextRandom(); - - var found = await Store.FindByWebhookIdAsync(result.Webhook.WebhookId!, default); - - Assert.NotNull(found); - Assert.Equal(result.Id, found.Id); - } - } -} diff --git a/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/StorageBuildingTests.cs b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/StorageBuildingTests.cs new file mode 100644 index 0000000..72f7dc8 --- /dev/null +++ b/test/Deveel.Webhooks.EntityFramework.XUnit/Webhooks/StorageBuildingTests.cs @@ -0,0 +1,49 @@ +using Deveel.Data; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Deveel.Webhooks { + public static class StorageBuildingTests { + [Fact] + public static void UseDefaultRepository() { + var services = new ServiceCollection(); + services.AddWebhookSubscriptions() + .UseEntityFramework(ef => ef.UseContext(options => options.UseSqlite())); + + var provider = services.BuildServiceProvider(); + + Assert.NotNull(provider.GetService>()); + Assert.NotNull(provider.GetService>()); + Assert.NotNull(provider.GetService>()); + Assert.NotNull(provider.GetService()); + } + + [Fact] + public static void UseCustomRepository() { + var services = new ServiceCollection(); + services.AddWebhookSubscriptions() + .UseEntityFramework(ef => ef + .UseContext(options => options.UseSqlite()) + .UseSubscriptionRepository()); + + var provider = services.BuildServiceProvider(); + + Assert.NotNull(provider.GetService>()); + Assert.NotNull(provider.GetService>()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService>()); + Assert.Null(provider.GetService()); + + var repository = provider.GetService>(); + + Assert.IsType(repository); + } + + class MyWebhookSubscriptionRepository : EntityWebhookSubscriptionRepository { + public MyWebhookSubscriptionRepository(WebhookDbContext context, ILogger>? logger = null) : base(context, logger) { + } + } + } +} diff --git a/test/Deveel.Webhooks.Management.Tests/Webhooks/WebhookManagementTestSuite.cs b/test/Deveel.Webhooks.Management.Tests/Webhooks/WebhookManagementTestSuite.cs index 73ade59..baa435a 100644 --- a/test/Deveel.Webhooks.Management.Tests/Webhooks/WebhookManagementTestSuite.cs +++ b/test/Deveel.Webhooks.Management.Tests/Webhooks/WebhookManagementTestSuite.cs @@ -236,6 +236,50 @@ public async Task SetEventTypes_SameEvents() { Assert.True(result.IsNotModified()); } + [Fact] + public async Task SetNewSecret() { + var subscription = Subscriptions.Random(); + + var secret = new Faker().Internet.Password(20); + + var result = await Manager.SetSecretAsync(subscription, secret); + + Assert.True(result.IsSuccess()); + Assert.False(result.IsError()); + + var key = Repository.GetEntityKey(subscription); + var updated = await Repository.FindByKeyAsync(key!); + + Assert.NotNull(updated); + Assert.Equal(secret, updated.Secret); + } + + [Fact] + public async Task SetSameSecret() { + var subscription = Subscriptions.Random(x => x.Secret != null); + + var result = await Manager.SetSecretAsync(subscription, subscription.Secret); + + Assert.False(result.IsSuccess()); + Assert.True(result.IsNotModified()); + } + + [Fact] + public async Task RemoveSecret() { + var subscription = Subscriptions.Random(x => x.Secret != null); + + var result = await Manager.SetSecretAsync(subscription, null); + + Assert.True(result.IsSuccess()); + Assert.False(result.IsError()); + + var key = Repository.GetEntityKey(subscription); + var updated = await Repository.FindByKeyAsync(key!); + + Assert.NotNull(updated); + Assert.Null(updated.Secret); + } + [Fact] public async Task FindExistingSubscription() { var subscription = Subscriptions.Random(); @@ -374,6 +418,30 @@ public async Task AddExistingHeaders() { Assert.True(result.IsNotModified()); } + [Fact] + public async Task AddNewProperties() { + var subscription = Subscriptions.Random(); + + var properties = new Dictionary { + {"testProperty", "test value"}, + { "testProperty2", 220 } + }; + + var result = await Manager.SetPropertiesAsync(subscription, properties); + + Assert.True(result.IsSuccess()); + Assert.False(result.IsNotModified()); + + var key = Repository.GetEntityKey(subscription); + var updated = await Repository.FindByKeyAsync(key!); + + Assert.NotNull(updated); + Assert.NotNull(updated.Properties); + Assert.NotEmpty(updated.Properties); + Assert.Contains(updated.Properties, x => x.Key == "testProperty" && (string) x.Value == "test value"); + Assert.Contains(updated.Properties, x => x.Key == "testProperty2" && (int) x.Value == 220); + } + [Fact] public async Task GetSimplePage() { var totalPages = (int)Math.Ceiling(Subscriptions.Count / (double)10); @@ -408,5 +476,13 @@ public async Task GetPageWithFilter() { Assert.Equal(items.Count, result.TotalItems); Assert.Equal(totalPages, result.TotalPages); } + + [Fact] + public async Task CountAllSubscriptions() { + var subsCount = Subscriptions.Count; + var count = await Manager.CountAsync(); + + Assert.Equal(subsCount, count); + } } } \ No newline at end of file diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj b/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj index 7605f6b..017ed20 100644 --- a/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj +++ b/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj @@ -19,6 +19,7 @@ + diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDeliveryResultLoggingTests.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDeliveryResultLoggingTests.cs new file mode 100644 index 0000000..e5916a4 --- /dev/null +++ b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDeliveryResultLoggingTests.cs @@ -0,0 +1,57 @@ +using Deveel.Data; + +using Finbuckle.MultiTenant; + +using Microsoft.Extensions.DependencyInjection; + +using Xunit.Abstractions; + +namespace Deveel.Webhooks { + [Collection(nameof(MongoTestCollection))] + public class MongoDeliveryResultLoggingTests : DeliveryResultLoggerTestSuite { + private readonly MongoTestDatabase mongo; + + public MongoDeliveryResultLoggingTests(MongoTestDatabase mongo, ITestOutputHelper testOutput) : base(testOutput) { + this.mongo = mongo; + } + + private IRepositoryProvider RepositoryProvider + => Scope!.ServiceProvider.GetRequiredService>(); + + protected override void ConfigureService(IServiceCollection services) { + services.AddSingleton(_ => { + return new TenantInfo { + Id = TenantId, + Identifier = TenantId + }; + }); + + services.AddMultiTenant() + .WithInMemoryStore(store => { + store.Tenants.Add(new TenantInfo { + Id = TenantId, + Identifier = TenantId, + Name = "Test Tenant", + ConnectionString = mongo.GetConnectionString("webhooks1") + }); + + store.Tenants.Add(new TenantInfo { + Id = "tenant2", + Identifier = "tenant2", + Name = "Test Tenant 2", + ConnectionString = mongo.GetConnectionString("webhooks2") + }); + }); + + services.AddSingleton, DefaultMongoWebhookConverter>(); + services.AddMongoDbContext((tenant, builder) => builder.UseConnection(tenant.ConnectionString!)); + services.AddRepositoryProvider>(); + services.AddScoped, MongoDbWebhookDeliveryResultLogger>(); + } + + protected override async Task FindResultByOperationIdAsync(string operationId) { + var respository = await RepositoryProvider.GetRepositoryAsync(TenantId); + return await respository.FindFirstAsync(x => x.OperationId == operationId); + } + } +} diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoWebhookSubscriptionFaker.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoWebhookSubscriptionFaker.cs index e06792f..a479f30 100644 --- a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoWebhookSubscriptionFaker.cs +++ b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoWebhookSubscriptionFaker.cs @@ -6,6 +6,7 @@ public MongoWebhookSubscriptionFaker(string? tenantId = null) { RuleFor(x => x.TenantId, tenantId); RuleFor(x => x.Name, f => f.Name.JobTitle()); RuleFor(x => x.EventTypes, f => f.Random.ListItems(EventTypes)); + RuleFor(x => x.Secret, f => f.Internet.Password(20).OrNull(f)); RuleFor(x => x.Format, f => f.Random.ListItem(new[] { "json", "xml" })); RuleFor(x => x.DestinationUrl, f => f.Internet.UrlWithPath("https")); RuleFor(x => x.Status, f => f.Random.Enum()); diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MultiTenantWebhookDeliveryResultLoggingTests.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MultiTenantWebhookDeliveryResultLoggingTests.cs index 249ce61..a62d79d 100644 --- a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MultiTenantWebhookDeliveryResultLoggingTests.cs +++ b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MultiTenantWebhookDeliveryResultLoggingTests.cs @@ -115,7 +115,6 @@ private Task CreateSubscriptionAsync(string name, string eventType, para private async Task CreateSubscriptionAsync(MongoWebhookSubscription subscription, bool enabled = true) { if (enabled) { subscription.Status = WebhookSubscriptionStatus.Active; - subscription.LastStatusTime = DateTime.Now; } subscription.TenantId = tenantId;