mirror of https://github.com/Squidex/squidex.git
40 changed files with 1823 additions and 3 deletions
@ -0,0 +1,27 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookSchema.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Webhooks |
||||
|
{ |
||||
|
public sealed class WebhookSchema |
||||
|
{ |
||||
|
public Guid SchemaId { get; set; } |
||||
|
|
||||
|
public bool SendCreate { get; set; } |
||||
|
|
||||
|
public bool SendUpdate { get; set; } |
||||
|
|
||||
|
public bool SendDelete { get; set; } |
||||
|
|
||||
|
public bool SendPublish { get; set; } |
||||
|
|
||||
|
public bool SendUnpublish { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookAdded.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Schemas.Old |
||||
|
{ |
||||
|
[TypeName("WebhookAddedEvent")] |
||||
|
[Obsolete] |
||||
|
public sealed class WebhookAdded : SchemaEvent |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
public string SharedSecret { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookDeleted.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Schemas.Old |
||||
|
{ |
||||
|
[TypeName("WebhookDeletedEvent")] |
||||
|
[Obsolete] |
||||
|
public sealed class WebhookDeleted : SchemaEvent |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookCreated.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Webhooks |
||||
|
{ |
||||
|
[TypeName("WebhookCreatedEvent")] |
||||
|
public sealed class WebhookCreated : WebhookEditEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookDeleted.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Webhooks |
||||
|
{ |
||||
|
[TypeName("WebhookDeletedEvent")] |
||||
|
public sealed class WebhookDeleted : WebhookEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookEditEvent.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Core.Webhooks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Webhooks |
||||
|
{ |
||||
|
public abstract class WebhookEditEvent : WebhookEvent |
||||
|
{ |
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
public List<WebhookSchema> Schemas { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookEvent.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Webhooks |
||||
|
{ |
||||
|
public abstract class WebhookEvent : AppEvent |
||||
|
{ |
||||
|
public Guid WebhookId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookUpdated.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Webhooks |
||||
|
{ |
||||
|
[TypeName("WebhookUpdatedEvent")] |
||||
|
public sealed class WebhookUpdated : WebhookEditEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoWebhookEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Domain.Apps.Core.Webhooks; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
// ReSharper disable CollectionNeverUpdated.Global
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks |
||||
|
{ |
||||
|
public class MongoWebhookEntity : MongoEntity, IWebhookEntity |
||||
|
{ |
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string SharedSecret { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public long TotalSucceeded { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public long TotalFailed { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public long TotalTimedout { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public long TotalRequestTime { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public List<WebhookSchema> Schemas { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public List<Guid> SchemaIds { get; set; } |
||||
|
|
||||
|
IEnumerable<WebhookSchema> IWebhookEntity.Schemas |
||||
|
{ |
||||
|
get { return Schemas; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoWebhookEventEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks |
||||
|
{ |
||||
|
public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity |
||||
|
{ |
||||
|
private WebhookJob job; |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Uri RequestUrl { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string RequestBody { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string RequestSignature { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string EventName { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string LastDump { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Instant Expires { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Instant? NextAttempt { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public int NumCalls { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public bool IsSending { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public WebhookResult Result { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public WebhookJobResult JobResult { get; set; } |
||||
|
|
||||
|
public WebhookJob Job |
||||
|
{ |
||||
|
get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoWebhookEventRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks |
||||
|
{ |
||||
|
public sealed class MongoWebhookEventRepository : MongoRepositoryBase<MongoWebhookEventEntity>, IWebhookEventRepository |
||||
|
{ |
||||
|
private readonly IClock clock; |
||||
|
|
||||
|
public MongoWebhookEventRepository(IMongoDatabase database, IClock clock) |
||||
|
: base(database) |
||||
|
{ |
||||
|
Guard.NotNull(clock, nameof(clock)); |
||||
|
|
||||
|
this.clock = clock; |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "WebhookEvents"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoWebhookEventEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending)); |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Created)); |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }); |
||||
|
} |
||||
|
|
||||
|
public Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = new CancellationToken()) |
||||
|
{ |
||||
|
var now = clock.GetCurrentInstant(); |
||||
|
|
||||
|
return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IWebhookEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) |
||||
|
{ |
||||
|
var entities = await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync(); |
||||
|
|
||||
|
return entities; |
||||
|
} |
||||
|
|
||||
|
public async Task<IWebhookEventEntity> FindAsync(Guid id) |
||||
|
{ |
||||
|
var entity = await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); |
||||
|
|
||||
|
return entity; |
||||
|
} |
||||
|
|
||||
|
public async Task<int> CountByAppAsync(Guid appId) |
||||
|
{ |
||||
|
return (int)await Collection.CountAsync(x => x.AppId == appId); |
||||
|
} |
||||
|
|
||||
|
public Task EnqueueAsync(Guid id, Instant nextAttempt) |
||||
|
{ |
||||
|
return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); |
||||
|
} |
||||
|
|
||||
|
public Task TraceSendingAsync(Guid jobId) |
||||
|
{ |
||||
|
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); |
||||
|
} |
||||
|
|
||||
|
public Task EnqueueAsync(WebhookJob job, Instant nextAttempt) |
||||
|
{ |
||||
|
var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt }); |
||||
|
|
||||
|
return Collection.InsertOneIfNotExistsAsync(entity); |
||||
|
} |
||||
|
|
||||
|
public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt) |
||||
|
{ |
||||
|
WebhookJobResult jobResult; |
||||
|
|
||||
|
if (result != WebhookResult.Success && nextAttempt == null) |
||||
|
{ |
||||
|
jobResult = WebhookJobResult.Failed; |
||||
|
} |
||||
|
else if (result != WebhookResult.Success && nextAttempt.HasValue) |
||||
|
{ |
||||
|
jobResult = WebhookJobResult.Retry; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
jobResult = WebhookJobResult.Success; |
||||
|
} |
||||
|
|
||||
|
return Collection.UpdateOneAsync(x => x.Id == jobId, |
||||
|
Update.Set(x => x.Result, result) |
||||
|
.Set(x => x.LastDump, dump) |
||||
|
.Set(x => x.JobResult, jobResult) |
||||
|
.Set(x => x.IsSending, false) |
||||
|
.Set(x => x.NextAttempt, nextAttempt) |
||||
|
.Inc(x => x.NumCalls, 1)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoWebhookRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
// ReSharper disable SwitchStatementMissingSomeCases
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks |
||||
|
{ |
||||
|
public partial class MongoWebhookRepository : MongoRepositoryBase<MongoWebhookEntity>, IWebhookRepository, IEventConsumer |
||||
|
{ |
||||
|
private static readonly List<IWebhookEntity> EmptyWebhooks = new List<IWebhookEntity>(); |
||||
|
private Dictionary<Guid, List<IWebhookEntity>> inMemoryWebhooks; |
||||
|
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
||||
|
|
||||
|
public MongoWebhookRepository(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "Projections_SchemaWebhooks"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoWebhookEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaIds)); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IWebhookEntity>> QueryByAppAsync(Guid appId) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
return inMemoryWebhooks.GetOrDefault(appId) ?? EmptyWebhooks; |
||||
|
} |
||||
|
|
||||
|
public async Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed) |
||||
|
{ |
||||
|
var webhookEntity = |
||||
|
await Collection.Find(x => x.Id == webhookId) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (webhookEntity != null) |
||||
|
{ |
||||
|
switch (result) |
||||
|
{ |
||||
|
case WebhookResult.Success: |
||||
|
webhookEntity.TotalSucceeded++; |
||||
|
break; |
||||
|
case WebhookResult.Failed: |
||||
|
webhookEntity.TotalFailed++; |
||||
|
break; |
||||
|
case WebhookResult.Timeout: |
||||
|
webhookEntity.TotalTimedout++; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
webhookEntity.TotalRequestTime += (long)elapsed.TotalMilliseconds; |
||||
|
|
||||
|
await Collection.ReplaceOneAsync(x => x.Id == webhookId, webhookEntity); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task EnsureWebooksLoadedAsync() |
||||
|
{ |
||||
|
if (inMemoryWebhooks == null) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await lockObject.WaitAsync(); |
||||
|
|
||||
|
if (inMemoryWebhooks == null) |
||||
|
{ |
||||
|
inMemoryWebhooks = new Dictionary<Guid, List<IWebhookEntity>>(); |
||||
|
|
||||
|
var webhooks = await Collection.Find(new BsonDocument()).ToListAsync(); |
||||
|
|
||||
|
foreach (var webhook in webhooks) |
||||
|
{ |
||||
|
inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
lockObject.Release(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoSchemaWebhookRepository_EventHandling.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Events.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.Dispatching; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Domain.Apps.Events.Webhooks; |
||||
|
using Squidex.Domain.Apps.Read.MongoDb.Utils; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks |
||||
|
{ |
||||
|
public partial class MongoWebhookRepository |
||||
|
{ |
||||
|
public string Name |
||||
|
{ |
||||
|
get { return GetType().Name; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "(^webhook-)|(^schema-)"; } |
||||
|
} |
||||
|
|
||||
|
public Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(WebhookCreated @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
await Collection.CreateAsync(@event, headers, w => |
||||
|
{ |
||||
|
SimpleMapper.Map(@event, w); |
||||
|
|
||||
|
w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); |
||||
|
|
||||
|
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
||||
|
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(WebhookUpdated @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
await Collection.UpdateAsync(@event, headers, w => |
||||
|
{ |
||||
|
SimpleMapper.Map(@event, w); |
||||
|
|
||||
|
w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); |
||||
|
|
||||
|
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
||||
|
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
await Collection.UpdateAsync(@event, headers, w => |
||||
|
{ |
||||
|
w.Schemas.RemoveAll(s => s.SchemaId == @event.SchemaId.Id); |
||||
|
|
||||
|
w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); |
||||
|
|
||||
|
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
||||
|
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.WebhookId); |
||||
|
|
||||
|
await Collection.DeleteManyAsync(x => x.Id == @event.WebhookId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IWebhookEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Core.Webhooks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public interface IWebhookEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion |
||||
|
{ |
||||
|
Uri Url { get; } |
||||
|
|
||||
|
long TotalSucceeded { get; } |
||||
|
|
||||
|
long TotalFailed { get; } |
||||
|
|
||||
|
long TotalTimedout { get; } |
||||
|
|
||||
|
long TotalRequestTime { get; } |
||||
|
|
||||
|
string SharedSecret { get; } |
||||
|
|
||||
|
IEnumerable<WebhookSchema> Schemas { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IWebhookEventEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public interface IWebhookEventEntity : IEntity |
||||
|
{ |
||||
|
WebhookJob Job { get; } |
||||
|
|
||||
|
Instant? NextAttempt { get; } |
||||
|
|
||||
|
WebhookResult Result { get; } |
||||
|
|
||||
|
WebhookJobResult JobResult { get; } |
||||
|
|
||||
|
int NumCalls { get; } |
||||
|
|
||||
|
string LastDump { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IWebhookEventRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks.Repositories |
||||
|
{ |
||||
|
public interface IWebhookEventRepository |
||||
|
{ |
||||
|
Task EnqueueAsync(WebhookJob job, Instant nextAttempt); |
||||
|
|
||||
|
Task EnqueueAsync(Guid id, Instant nextAttempt); |
||||
|
|
||||
|
Task TraceSendingAsync(Guid jobId); |
||||
|
|
||||
|
Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextCall); |
||||
|
|
||||
|
Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken)); |
||||
|
|
||||
|
Task<int> CountByAppAsync(Guid appId); |
||||
|
|
||||
|
Task<IReadOnlyList<IWebhookEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); |
||||
|
|
||||
|
Task<IWebhookEventEntity> FindAsync(Guid id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ISchemaWebhookRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks.Repositories |
||||
|
{ |
||||
|
public interface IWebhookRepository |
||||
|
{ |
||||
|
Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed); |
||||
|
|
||||
|
Task<IReadOnlyList<IWebhookEntity>> QueryByAppAsync(Guid appId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookDequeuer.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Threading.Tasks.Dataflow; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Timers; |
||||
|
|
||||
|
// ReSharper disable SwitchStatementMissingSomeCases
|
||||
|
// ReSharper disable MethodSupportsCancellation
|
||||
|
// ReSharper disable InvertIf
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem |
||||
|
{ |
||||
|
private readonly ActionBlock<IWebhookEventEntity> requestBlock; |
||||
|
private readonly TransformBlock<IWebhookEventEntity, IWebhookEventEntity> blockBlock; |
||||
|
private readonly IWebhookEventRepository webhookEventRepository; |
||||
|
private readonly IWebhookRepository webhookRepository; |
||||
|
private readonly WebhookSender webhookSender; |
||||
|
private readonly CompletionTimer timer; |
||||
|
private readonly ISemanticLog log; |
||||
|
private readonly IClock clock; |
||||
|
|
||||
|
public WebhookDequeuer(WebhookSender webhookSender, |
||||
|
IWebhookEventRepository webhookEventRepository, |
||||
|
IWebhookRepository webhookRepository, |
||||
|
IClock clock, |
||||
|
ISemanticLog log) |
||||
|
{ |
||||
|
Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); |
||||
|
Guard.NotNull(webhookRepository, nameof(webhookRepository)); |
||||
|
Guard.NotNull(webhookSender, nameof(webhookSender)); |
||||
|
Guard.NotNull(clock, nameof(clock)); |
||||
|
Guard.NotNull(log, nameof(log)); |
||||
|
|
||||
|
this.webhookEventRepository = webhookEventRepository; |
||||
|
this.webhookRepository = webhookRepository; |
||||
|
this.webhookSender = webhookSender; |
||||
|
|
||||
|
this.clock = clock; |
||||
|
|
||||
|
this.log = log; |
||||
|
|
||||
|
requestBlock = |
||||
|
new ActionBlock<IWebhookEventEntity>(MakeRequestAsync, |
||||
|
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); |
||||
|
|
||||
|
blockBlock = |
||||
|
new TransformBlock<IWebhookEventEntity, IWebhookEventEntity>(x => BlockAsync(x), |
||||
|
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 }); |
||||
|
|
||||
|
blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true }); |
||||
|
|
||||
|
timer = new CompletionTimer(5000, QueryAsync); |
||||
|
} |
||||
|
|
||||
|
protected override void DisposeObject(bool disposing) |
||||
|
{ |
||||
|
if (disposing) |
||||
|
{ |
||||
|
timer.StopAsync().Wait(); |
||||
|
|
||||
|
blockBlock.Complete(); |
||||
|
requestBlock.Completion.Wait(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Connect() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Next() |
||||
|
{ |
||||
|
timer.SkipCurrentDelay(); |
||||
|
} |
||||
|
|
||||
|
private async Task QueryAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "QueueWebhookEvents") |
||||
|
.WriteProperty("status", "Failed")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<IWebhookEventEntity> BlockAsync(IWebhookEventEntity @event) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await webhookEventRepository.TraceSendingAsync(@event.Id); |
||||
|
|
||||
|
return @event; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "BlockWebhookEvent") |
||||
|
.WriteProperty("status", "Failed")); |
||||
|
|
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task MakeRequestAsync(IWebhookEventEntity @event) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var response = await webhookSender.SendAsync(@event.Job); |
||||
|
|
||||
|
Instant? nextCall = null; |
||||
|
|
||||
|
if (response.Result != WebhookResult.Success) |
||||
|
{ |
||||
|
var now = clock.GetCurrentInstant(); |
||||
|
|
||||
|
switch (@event.NumCalls) |
||||
|
{ |
||||
|
case 0: |
||||
|
nextCall = now.Plus(Duration.FromMinutes(5)); |
||||
|
break; |
||||
|
case 1: |
||||
|
nextCall = now.Plus(Duration.FromHours(1)); |
||||
|
break; |
||||
|
case 2: |
||||
|
nextCall = now.Plus(Duration.FromHours(5)); |
||||
|
break; |
||||
|
case 3: |
||||
|
nextCall = now.Plus(Duration.FromHours(6)); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await Task.WhenAll( |
||||
|
webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed), |
||||
|
webhookEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall)); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "SendWebhookEvent") |
||||
|
.WriteProperty("status", "Failed")); |
||||
|
|
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,140 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookEnqueuer.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Webhooks; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public sealed class WebhookEnqueuer : IEventConsumer |
||||
|
{ |
||||
|
private const string ContentPrefix = "Content"; |
||||
|
private static readonly Duration ExpirationTime = Duration.FromDays(2); |
||||
|
private readonly IWebhookEventRepository webhookEventRepository; |
||||
|
private readonly IWebhookRepository webhookRepository; |
||||
|
private readonly IClock clock; |
||||
|
private readonly TypeNameRegistry typeNameRegistry; |
||||
|
private readonly JsonSerializer webhookSerializer; |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get { return GetType().Name; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "^content-"; } |
||||
|
} |
||||
|
|
||||
|
public WebhookEnqueuer(TypeNameRegistry typeNameRegistry, |
||||
|
IWebhookEventRepository webhookEventRepository, |
||||
|
IWebhookRepository webhookRepository, |
||||
|
IClock clock, |
||||
|
JsonSerializer webhookSerializer) |
||||
|
{ |
||||
|
Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); |
||||
|
Guard.NotNull(webhookSerializer, nameof(webhookSerializer)); |
||||
|
Guard.NotNull(webhookRepository, nameof(webhookRepository)); |
||||
|
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); |
||||
|
Guard.NotNull(clock, nameof(clock)); |
||||
|
|
||||
|
this.webhookEventRepository = webhookEventRepository; |
||||
|
this.webhookSerializer = webhookSerializer; |
||||
|
this.webhookRepository = webhookRepository; |
||||
|
|
||||
|
this.clock = clock; |
||||
|
|
||||
|
this.typeNameRegistry = typeNameRegistry; |
||||
|
} |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public async Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
if (@event.Payload is ContentEvent contentEvent) |
||||
|
{ |
||||
|
var eventType = typeNameRegistry.GetName(@event.Payload.GetType()); |
||||
|
|
||||
|
var webhooks = await webhookRepository.QueryByAppAsync(contentEvent.AppId.Id); |
||||
|
|
||||
|
var matchingWebhooks = webhooks.Where(w => w.Schemas.Any(s => Matchs(s, contentEvent))).ToList(); |
||||
|
|
||||
|
if (matchingWebhooks.Count > 0) |
||||
|
{ |
||||
|
var now = clock.GetCurrentInstant(); |
||||
|
|
||||
|
var eventPayload = CreatePayload(@event, eventType); |
||||
|
var eventName = $"{contentEvent.SchemaId.Name.ToPascalCase()}{CreateContentEventName(eventType)}"; |
||||
|
|
||||
|
foreach (var webhook in matchingWebhooks) |
||||
|
{ |
||||
|
await EnqueueJobAsync(eventPayload, webhook, contentEvent, eventName, now); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task EnqueueJobAsync(string payload, IWebhookEntity webhook, AppEvent contentEvent, string eventName, Instant now) |
||||
|
{ |
||||
|
var signature = $"{payload}{webhook.SharedSecret}".Sha256Base64(); |
||||
|
|
||||
|
var job = new WebhookJob |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
AppId = contentEvent.AppId.Id, |
||||
|
RequestUrl = webhook.Url, |
||||
|
RequestBody = payload, |
||||
|
RequestSignature = signature, |
||||
|
EventName = eventName, |
||||
|
Expires = now.Plus(ExpirationTime), |
||||
|
WebhookId = webhook.Id |
||||
|
}; |
||||
|
|
||||
|
await webhookEventRepository.EnqueueAsync(job, now); |
||||
|
} |
||||
|
|
||||
|
private static bool Matchs(WebhookSchema webhookSchema, SchemaEvent @event) |
||||
|
{ |
||||
|
return |
||||
|
(@event.SchemaId.Id == webhookSchema.SchemaId) && |
||||
|
(@event is ContentCreated && webhookSchema.SendCreate || |
||||
|
@event is ContentUpdated && webhookSchema.SendUpdate || |
||||
|
@event is ContentDeleted && webhookSchema.SendDelete || |
||||
|
@event is ContentPublished && webhookSchema.SendPublish || |
||||
|
@event is ContentUnpublished && webhookSchema.SendUnpublish); |
||||
|
} |
||||
|
|
||||
|
private string CreatePayload(Envelope<IEvent> @event, string eventType) |
||||
|
{ |
||||
|
return new JObject( |
||||
|
new JProperty("type", eventType), |
||||
|
new JProperty("payload", JObject.FromObject(@event.Payload, webhookSerializer)), |
||||
|
new JProperty("timestamp", @event.Headers.Timestamp().ToString())) |
||||
|
.ToString(Formatting.Indented); |
||||
|
} |
||||
|
|
||||
|
private static string CreateContentEventName(string eventType) |
||||
|
{ |
||||
|
return eventType.StartsWith(ContentPrefix) ? eventType.Substring(ContentPrefix.Length) : eventType; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookJob.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public sealed class WebhookJob |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
public Guid WebhookId { get; set; } |
||||
|
|
||||
|
public Uri RequestUrl { get; set; } |
||||
|
|
||||
|
public string RequestBody { get; set; } |
||||
|
|
||||
|
public string RequestSignature { get; set; } |
||||
|
|
||||
|
public string EventName { get; set; } |
||||
|
|
||||
|
public Instant Expires { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookJobResult.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public enum WebhookJobResult |
||||
|
{ |
||||
|
Pending, |
||||
|
Success, |
||||
|
Retry, |
||||
|
Failed |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookResult.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public enum WebhookResult |
||||
|
{ |
||||
|
Pending, |
||||
|
Success, |
||||
|
Failed, |
||||
|
Timeout |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookSender.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Net.Http; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Http; |
||||
|
|
||||
|
// ReSharper disable SuggestVarOrType_SimpleTypes
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public class WebhookSender |
||||
|
{ |
||||
|
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); |
||||
|
|
||||
|
public virtual async Task<(string Dump, WebhookResult Result, TimeSpan Elapsed)> SendAsync(WebhookJob job) |
||||
|
{ |
||||
|
HttpRequestMessage request = BuildRequest(job); |
||||
|
HttpResponseMessage response = null; |
||||
|
|
||||
|
var isTimeout = false; |
||||
|
|
||||
|
var watch = Stopwatch.StartNew(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
using (var client = new HttpClient { Timeout = Timeout }) |
||||
|
{ |
||||
|
response = await client.SendAsync(request); |
||||
|
} |
||||
|
} |
||||
|
catch (TimeoutException) |
||||
|
{ |
||||
|
isTimeout = true; |
||||
|
} |
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
isTimeout = true; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
watch.Stop(); |
||||
|
} |
||||
|
|
||||
|
var responseString = string.Empty; |
||||
|
|
||||
|
if (response != null) |
||||
|
{ |
||||
|
responseString = await response.Content.ReadAsStringAsync(); |
||||
|
} |
||||
|
|
||||
|
var dump = DumpFormatter.BuildDump(request, response, job.RequestBody, responseString, watch.Elapsed); |
||||
|
|
||||
|
var result = WebhookResult.Failed; |
||||
|
|
||||
|
if (isTimeout) |
||||
|
{ |
||||
|
result = WebhookResult.Timeout; |
||||
|
} |
||||
|
else if (response?.IsSuccessStatusCode == true) |
||||
|
{ |
||||
|
result = WebhookResult.Success; |
||||
|
} |
||||
|
|
||||
|
return (dump, result, watch.Elapsed); |
||||
|
} |
||||
|
|
||||
|
private static HttpRequestMessage BuildRequest(WebhookJob job) |
||||
|
{ |
||||
|
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) |
||||
|
{ |
||||
|
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") |
||||
|
}; |
||||
|
|
||||
|
request.Headers.Add("X-Signature", job.RequestSignature); |
||||
|
request.Headers.Add("User-Agent", "Squidex Webhook"); |
||||
|
|
||||
|
return request; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// CreateWebhook.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
||||
|
{ |
||||
|
public sealed class CreateWebhook : WebhookEditCommand |
||||
|
{ |
||||
|
public string SharedSecret { get; } = RandomHash.New(); |
||||
|
|
||||
|
public CreateWebhook() |
||||
|
{ |
||||
|
WebhookId = Guid.NewGuid(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// DeleteWebhook.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
||||
|
{ |
||||
|
public sealed class DeleteWebhook : WebhookAggregateCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// UpdateWebhook.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
||||
|
{ |
||||
|
public sealed class UpdateWebhook : WebhookEditCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookAggregateCommand.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.CQRS.Commands; |
||||
|
|
||||
|
// ReSharper disable MemberCanBeProtected.Global
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
||||
|
{ |
||||
|
public abstract class WebhookAggregateCommand : AppCommand, IAggregateCommand |
||||
|
{ |
||||
|
public Guid WebhookId { get; set; } |
||||
|
|
||||
|
Guid IAggregateCommand.AggregateId |
||||
|
{ |
||||
|
get { return WebhookId; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookEditCommand.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Core.Webhooks; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
||||
|
{ |
||||
|
public abstract class WebhookEditCommand : WebhookAggregateCommand, IValidatable |
||||
|
{ |
||||
|
private List<WebhookSchema> schemas = new List<WebhookSchema>(); |
||||
|
|
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
public List<WebhookSchema> Schemas |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return schemas ?? (schemas = new List<WebhookSchema>()); |
||||
|
} |
||||
|
set |
||||
|
{ |
||||
|
schemas = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public virtual void Validate(IList<ValidationError> errors) |
||||
|
{ |
||||
|
if (Url == null || !Url.IsAbsoluteUri) |
||||
|
{ |
||||
|
errors.Add(new ValidationError("Url must be specified and absolute", nameof(Url))); |
||||
|
} |
||||
|
|
||||
|
if (Schemas == null) |
||||
|
{ |
||||
|
errors.Add(new ValidationError("Schemas must be specified.", nameof(Schemas))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookCommandMiddleware.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Read.Schemas.Services; |
||||
|
using Squidex.Domain.Apps.Write.Webhooks.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Commands; |
||||
|
using Squidex.Infrastructure.Dispatching; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks |
||||
|
{ |
||||
|
public class WebhookCommandMiddleware : ICommandMiddleware |
||||
|
{ |
||||
|
private readonly IAggregateHandler handler; |
||||
|
private readonly ISchemaProvider schemas; |
||||
|
|
||||
|
public WebhookCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) |
||||
|
{ |
||||
|
Guard.NotNull(handler, nameof(handler)); |
||||
|
Guard.NotNull(schemas, nameof(schemas)); |
||||
|
|
||||
|
this.handler = handler; |
||||
|
this.schemas = schemas; |
||||
|
} |
||||
|
|
||||
|
protected async Task On(CreateWebhook command, CommandContext context) |
||||
|
{ |
||||
|
await ValidateAsync(command, () => "Failed to create webhook"); |
||||
|
|
||||
|
await handler.UpdateAsync<WebhookDomainObject>(context, c => c.Create(command)); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(UpdateWebhook command, CommandContext context) |
||||
|
{ |
||||
|
await ValidateAsync(command, () => "Failed to update content"); |
||||
|
|
||||
|
await handler.UpdateAsync<WebhookDomainObject>(context, c => c.Update(command)); |
||||
|
} |
||||
|
|
||||
|
protected Task On(DeleteWebhook command, CommandContext context) |
||||
|
{ |
||||
|
return handler.UpdateAsync<WebhookDomainObject>(context, c => c.Delete(command)); |
||||
|
} |
||||
|
|
||||
|
public async Task HandleAsync(CommandContext context, Func<Task> next) |
||||
|
{ |
||||
|
if (!await this.DispatchActionAsync(context.Command, context)) |
||||
|
{ |
||||
|
await next(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task ValidateAsync(WebhookEditCommand command, Func<string> message) |
||||
|
{ |
||||
|
var results = await Task.WhenAll( |
||||
|
command.Schemas.Select(async schema => |
||||
|
await schemas.FindSchemaByIdAsync(schema.SchemaId) == null |
||||
|
? new ValidationError($"Schema {schema.SchemaId} does not exist.") |
||||
|
: null)); |
||||
|
|
||||
|
var errors = results.Where(x => x != null).ToArray(); |
||||
|
|
||||
|
if (errors.Length > 0) |
||||
|
{ |
||||
|
throw new ValidationException(message(), errors); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookDomainObject.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Domain.Apps.Events.Webhooks; |
||||
|
using Squidex.Domain.Apps.Write.Webhooks.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.Dispatching; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Webhooks |
||||
|
{ |
||||
|
public class WebhookDomainObject : DomainObjectBase |
||||
|
{ |
||||
|
private bool isDeleted; |
||||
|
private bool isCreated; |
||||
|
|
||||
|
public WebhookDomainObject(Guid id, int version) |
||||
|
: base(id, version) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected void On(WebhookCreated @event) |
||||
|
{ |
||||
|
isCreated = true; |
||||
|
} |
||||
|
|
||||
|
protected void On(WebhookDeleted @event) |
||||
|
{ |
||||
|
isDeleted = true; |
||||
|
} |
||||
|
|
||||
|
public void Create(CreateWebhook command) |
||||
|
{ |
||||
|
Guard.Valid(command, nameof(command), () => "Cannot create webhook"); |
||||
|
|
||||
|
VerifyNotCreated(); |
||||
|
|
||||
|
RaiseEvent(SimpleMapper.Map(command, new WebhookCreated())); |
||||
|
} |
||||
|
|
||||
|
public void Update(UpdateWebhook command) |
||||
|
{ |
||||
|
Guard.Valid(command, nameof(command), () => "Cannot update webhook"); |
||||
|
|
||||
|
VerifyCreatedAndNotDeleted(); |
||||
|
|
||||
|
RaiseEvent(SimpleMapper.Map(command, new WebhookUpdated())); |
||||
|
} |
||||
|
|
||||
|
public void Delete(DeleteWebhook command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
VerifyCreatedAndNotDeleted(); |
||||
|
|
||||
|
RaiseEvent(SimpleMapper.Map(command, new WebhookDeleted())); |
||||
|
} |
||||
|
|
||||
|
private void VerifyNotCreated() |
||||
|
{ |
||||
|
if (isCreated) |
||||
|
{ |
||||
|
throw new DomainException("Webhook has already been created."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void VerifyCreatedAndNotDeleted() |
||||
|
{ |
||||
|
if (isDeleted || isCreated) |
||||
|
{ |
||||
|
throw new DomainException("Webhook has already been deleted or not created yet."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void DispatchEvent(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
this.DispatchAction(@event.Payload); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
// ==========================================================================
|
||||
|
// UpdateWebhookDto.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Squidex.Controllers.Api.Webhooks.Models |
||||
|
{ |
||||
|
public class UpdateWebhookDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The url of the webhook.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The schema settings.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public List<WebhookSchemaDto> Schemas { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookSchemaDto.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Controllers.Api.Webhooks.Models |
||||
|
{ |
||||
|
public class WebhookSchemaDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The id of the schema.
|
||||
|
/// </summary>
|
||||
|
public Guid SchemaId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// True, when to send a message for created events.
|
||||
|
/// </summary>
|
||||
|
public bool SendCreate { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// True, when to send a message for updated events.
|
||||
|
/// </summary>
|
||||
|
public bool SendUpdate { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// True, when to send a message for deleted events.
|
||||
|
/// </summary>
|
||||
|
public bool SendDelete { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// True, when to send a message for published events.
|
||||
|
/// </summary>
|
||||
|
public bool SendPublish { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// True, when to send a message for unpublished events.
|
||||
|
/// </summary>
|
||||
|
public bool SendUnpublish { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookDequeuerTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Read.Webhooks.Repositories; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Xunit; |
||||
|
|
||||
|
// ReSharper disable MethodSupportsCancellation
|
||||
|
// ReSharper disable ImplicitlyCapturedClosure
|
||||
|
// ReSharper disable ConvertToConstant.Local
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Webhooks |
||||
|
{ |
||||
|
public class WebhookDequeuerTests |
||||
|
{ |
||||
|
private readonly IClock clock = A.Fake<IClock>(); |
||||
|
private readonly IWebhookRepository webhookRepository = A.Fake<IWebhookRepository>(); |
||||
|
private readonly IWebhookEventRepository webhookEventRepository = A.Fake<IWebhookEventRepository>(); |
||||
|
private readonly WebhookSender webhookSender = A.Fake<WebhookSender>(); |
||||
|
private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); |
||||
|
|
||||
|
public WebhookDequeuerTests() |
||||
|
{ |
||||
|
A.CallTo(() => clock.GetCurrentInstant()).Returns(now); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_update_repositories_on_successful_requests() |
||||
|
{ |
||||
|
var @event = CreateEvent(0); |
||||
|
|
||||
|
var requestResult = WebhookResult.Success; |
||||
|
var requestTime = TimeSpan.FromMinutes(1); |
||||
|
var requestDump = "Dump"; |
||||
|
|
||||
|
SetupSender(@event, requestDump, requestResult, requestTime); |
||||
|
SetupPendingEvents(@event); |
||||
|
|
||||
|
var sut = new WebhookDequeuer( |
||||
|
webhookSender, |
||||
|
webhookEventRepository, |
||||
|
webhookRepository, |
||||
|
clock, A.Fake<ISemanticLog>()); |
||||
|
|
||||
|
sut.Next(); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
VerifyRepositories(@event, requestDump, requestResult, requestTime, null); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0, 5)] |
||||
|
[InlineData(1, 60)] |
||||
|
[InlineData(2, 300)] |
||||
|
[InlineData(3, 360)] |
||||
|
public void Should_set_next_attempt_based_on_num_calls(int calls, int minutes) |
||||
|
{ |
||||
|
var @event = CreateEvent(calls); |
||||
|
|
||||
|
var requestResult = WebhookResult.Failed; |
||||
|
var requestTime = TimeSpan.FromMinutes(1); |
||||
|
var requestDump = "Dump"; |
||||
|
|
||||
|
SetupSender(@event, requestDump, requestResult, requestTime); |
||||
|
SetupPendingEvents(@event); |
||||
|
|
||||
|
var sut = new WebhookDequeuer( |
||||
|
webhookSender, |
||||
|
webhookEventRepository, |
||||
|
webhookRepository, |
||||
|
clock, A.Fake<ISemanticLog>()); |
||||
|
|
||||
|
sut.Next(); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
VerifyRepositories(@event, requestDump, requestResult, requestTime, now.Plus(Duration.FromMinutes(minutes))); |
||||
|
} |
||||
|
|
||||
|
private void SetupSender(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime) |
||||
|
{ |
||||
|
A.CallTo(() => webhookSender.SendAsync(@event.Job)) |
||||
|
.Returns(Task.FromResult((requestDump, requestResult, requestTime))); |
||||
|
} |
||||
|
|
||||
|
private void SetupPendingEvents(IWebhookEventEntity @event) |
||||
|
{ |
||||
|
A.CallTo(() => webhookEventRepository.QueryPendingAsync(A<Func<IWebhookEventEntity, Task>>.Ignored, A<CancellationToken>.Ignored)) |
||||
|
.Invokes(async (Func<IWebhookEventEntity, Task> callback, CancellationToken ct) => |
||||
|
{ |
||||
|
await callback(@event); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void VerifyRepositories(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime, Instant? nextAttempt) |
||||
|
{ |
||||
|
A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => webhookEventRepository.TraceSentAsync(@event.Id, requestDump, requestResult, requestTime, nextAttempt)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => webhookRepository.TraceSentAsync(@event.Job.WebhookId, requestResult, requestTime)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private static IWebhookEventEntity CreateEvent(int numCalls) |
||||
|
{ |
||||
|
var @event = A.Fake<IWebhookEventEntity>(); |
||||
|
|
||||
|
A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); |
||||
|
A.CallTo(() => @event.Job).Returns(new WebhookJob { WebhookId = Guid.NewGuid() }); |
||||
|
A.CallTo(() => @event.NumCalls).Returns(numCalls); |
||||
|
|
||||
|
return @event; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
//// ==========================================================================
|
||||
|
//// WebhookEnqueuerTests.cs
|
||||
|
//// Squidex Headless CMS
|
||||
|
//// ==========================================================================
|
||||
|
//// Copyright (c) Squidex Group
|
||||
|
//// All rights reserved.
|
||||
|
//// ==========================================================================
|
||||
|
|
||||
|
//using System;
|
||||
|
//using System.Collections.Generic;
|
||||
|
//using System.Threading.Tasks;
|
||||
|
//using FakeItEasy;
|
||||
|
//using Newtonsoft.Json;
|
||||
|
//using NodaTime;
|
||||
|
//using Squidex.Domain.Apps.Events.Contents;
|
||||
|
//using Squidex.Domain.Apps.Read.Webhooks.Repositories;
|
||||
|
//using Squidex.Infrastructure;
|
||||
|
//using Squidex.Infrastructure.CQRS.Events;
|
||||
|
//using Xunit;
|
||||
|
|
||||
|
//// ReSharper disable MethodSupportsCancellation
|
||||
|
//// ReSharper disable ImplicitlyCapturedClosure
|
||||
|
//// ReSharper disable ConvertToConstant.Local
|
||||
|
|
||||
|
//namespace Squidex.Domain.Apps.Read.Webhooks
|
||||
|
//{
|
||||
|
// public class WebhookEnqueuerTests
|
||||
|
// {
|
||||
|
// private readonly IClock clock = A.Fake<IClock>();
|
||||
|
// private readonly IWebhookRepository webhookRepository = A.Fake<IWebhookRepository>();
|
||||
|
// private readonly IWebhookEventRepository webhookEventRepository = A.Fake<IWebhookEventRepository>();
|
||||
|
// private readonly TypeNameRegistry typeNameRegisty = new TypeNameRegistry();
|
||||
|
// private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
// private readonly WebhookEnqueuer sut;
|
||||
|
|
||||
|
// public WebhookEnqueuerTests()
|
||||
|
// {
|
||||
|
// A.CallTo(() => clock.GetCurrentInstant()).Returns(now);
|
||||
|
|
||||
|
// typeNameRegisty.Map(typeof(ContentCreated));
|
||||
|
|
||||
|
// sut = new WebhookEnqueuer(
|
||||
|
// typeNameRegisty,
|
||||
|
// webhookEventRepository,
|
||||
|
// webhookRepository,
|
||||
|
// clock, new JsonSerializer());
|
||||
|
// }
|
||||
|
|
||||
|
// [Fact]
|
||||
|
// public void Should_return_contents_filter_for_events_filter()
|
||||
|
// {
|
||||
|
// Assert.Equal("^content-", sut.EventsFilter);
|
||||
|
// }
|
||||
|
|
||||
|
// [Fact]
|
||||
|
// public void Should_return_type_name_for_name()
|
||||
|
// {
|
||||
|
// Assert.Equal(typeof(WebhookEnqueuer).Name, sut.Name);
|
||||
|
// }
|
||||
|
|
||||
|
// [Fact]
|
||||
|
// public Task Should_do_nothing_on_clear()
|
||||
|
// {
|
||||
|
// return sut.ClearAsync();
|
||||
|
// }
|
||||
|
|
||||
|
// [Fact]
|
||||
|
// public async Task Should_update_repositories_on_successful_requests()
|
||||
|
// {
|
||||
|
// var appId = new NamedId<Guid>(Guid.NewGuid(), "my-app");
|
||||
|
|
||||
|
// var schemaId = new NamedId<Guid>(Guid.NewGuid(), "my-schema");
|
||||
|
|
||||
|
// var @event = Envelope.Create(new ContentCreated { AppId = appId, SchemaId = schemaId });
|
||||
|
|
||||
|
// var webhook1 = CreateWebhook(1);
|
||||
|
// var webhook2 = CreateWebhook(2);
|
||||
|
|
||||
|
// A.CallTo(() => webhookRepository.QueryByAppAsync(appId.Id))
|
||||
|
// .Returns(Task.FromResult<IReadOnlyList<IWebhookEntity>>(new List<IWebhookEntity> { webhook1, webhook2 }));
|
||||
|
|
||||
|
// await sut.On(@event);
|
||||
|
|
||||
|
// A.CallTo(() => webhookEventRepository.EnqueueAsync(
|
||||
|
// A<WebhookJob>.That.Matches(webhookJob =>
|
||||
|
// !string.IsNullOrWhiteSpace(webhookJob.RequestSignature)
|
||||
|
// && !string.IsNullOrWhiteSpace(webhookJob.RequestBody)
|
||||
|
// && webhookJob.Id != Guid.Empty
|
||||
|
// && webhookJob.Expires == now.Plus(Duration.FromDays(2))
|
||||
|
// && webhookJob.AppId == appId.Id
|
||||
|
// && webhookJob.EventName == "MySchemaCreatedEvent"
|
||||
|
// && webhookJob.RequestUrl == webhook1.Url
|
||||
|
// && webhookJob.WebhookId == webhook1.Id), now)).MustHaveHappened();
|
||||
|
|
||||
|
// A.CallTo(() => webhookEventRepository.EnqueueAsync(
|
||||
|
// A<WebhookJob>.That.Matches(webhookJob =>
|
||||
|
// !string.IsNullOrWhiteSpace(webhookJob.RequestSignature)
|
||||
|
// && !string.IsNullOrWhiteSpace(webhookJob.RequestBody)
|
||||
|
// && webhookJob.Id != Guid.Empty
|
||||
|
// && webhookJob.Expires == now.Plus(Duration.FromDays(2))
|
||||
|
// && webhookJob.AppId == appId.Id
|
||||
|
// && webhookJob.EventName == "MySchemaCreatedEvent"
|
||||
|
// && webhookJob.RequestUrl == webhook2.Url
|
||||
|
// && webhookJob.WebhookId == webhook2.Id), now)).MustHaveHappened();
|
||||
|
// }
|
||||
|
|
||||
|
// private static ISchemaWebhookUrlEntity CreateWebhook(int offset)
|
||||
|
// {
|
||||
|
// var webhook = A.Dummy<ISchemaWebhookUrlEntity>();
|
||||
|
|
||||
|
// A.CallTo(() => webhook.Id).Returns(Guid.NewGuid());
|
||||
|
// A.CallTo(() => webhook.Url).Returns(new Uri($"http://domain{offset}.com"));
|
||||
|
// A.CallTo(() => webhook.SharedSecret).Returns($"secret{offset}");
|
||||
|
|
||||
|
// return webhook;
|
||||
|
// }
|
||||
|
//}
|
||||
|
//}
|
||||
Loading…
Reference in new issue