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;