mirror of https://github.com/Squidex/squidex.git
88 changed files with 157 additions and 1680 deletions
@ -1,23 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookAdded.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Schemas |
|||
{ |
|||
[TypeName("WebhookAddedEvent")] |
|||
public sealed class WebhookAdded : SchemaEvent |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public Uri Url { get; set; } |
|||
|
|||
public string SharedSecret { get; set; } |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookDeleted.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Schemas |
|||
{ |
|||
[TypeName("WebhookDeletedEvent")] |
|||
public sealed class WebhookDeleted : SchemaEvent |
|||
{ |
|||
public Guid Id { get; set; } |
|||
} |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoSchemaWebhookEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using MongoDB.Bson; |
|||
using MongoDB.Bson.Serialization.Attributes; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
|
|||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas |
|||
{ |
|||
public class MongoSchemaWebhookEntity : ISchemaWebhookEntity |
|||
{ |
|||
[BsonId] |
|||
[BsonElement] |
|||
[BsonRepresentation(BsonType.String)] |
|||
public Guid Id { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Uri Url { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid AppId { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid SchemaId { 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; } |
|||
} |
|||
} |
|||
@ -1,127 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoSchemaWebhookRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using MongoDB.Bson; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
// ReSharper disable SwitchStatementMissingSomeCases
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas |
|||
{ |
|||
public partial class MongoSchemaWebhookRepository : MongoRepositoryBase<MongoSchemaWebhookEntity>, ISchemaWebhookRepository, IEventConsumer |
|||
{ |
|||
private static readonly List<ShortInfo> EmptyWebhooks = new List<ShortInfo>(); |
|||
private Dictionary<Guid, Dictionary<Guid, List<ShortInfo>>> inMemoryWebhooks; |
|||
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
|||
|
|||
public sealed class ShortInfo : ISchemaWebhookUrlEntity |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public Uri Url { get; set; } |
|||
|
|||
public string SharedSecret { get; set; } |
|||
} |
|||
|
|||
public MongoSchemaWebhookRepository(IMongoDatabase database) |
|||
: base(database) |
|||
{ |
|||
} |
|||
|
|||
protected override string CollectionName() |
|||
{ |
|||
return "Projections_SchemaWebhooks"; |
|||
} |
|||
|
|||
protected override Task SetupCollectionAsync(IMongoCollection<MongoSchemaWebhookEntity> collection) |
|||
{ |
|||
return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId)); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId) |
|||
{ |
|||
return await Collection.Find(Filter.Eq(x => x.AppId, appId)).ToListAsync(); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId) |
|||
{ |
|||
await EnsureWebooksLoadedAsync(); |
|||
|
|||
return inMemoryWebhooks.GetOrDefault(appId)?.GetOrDefault(schemaId)?.ToList() ?? 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) |
|||
{ |
|||
var result = new Dictionary<Guid, Dictionary<Guid, List<ShortInfo>>>(); |
|||
|
|||
var webhooks = await Collection.Find(new BsonDocument()).ToListAsync(); |
|||
|
|||
foreach (var webhook in webhooks) |
|||
{ |
|||
var list = result.GetOrAddNew(webhook.AppId).GetOrAddNew(webhook.SchemaId); |
|||
|
|||
list.Add(SimpleMapper.Map(webhook, new ShortInfo())); |
|||
} |
|||
|
|||
inMemoryWebhooks = result; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
lockObject.Release(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoSchemaWebhookRepository_EventHandling.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
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; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas |
|||
{ |
|||
public partial class MongoSchemaWebhookRepository |
|||
{ |
|||
public string Name |
|||
{ |
|||
get { return GetType().Name; } |
|||
} |
|||
|
|||
public string EventsFilter |
|||
{ |
|||
get { return "^schema-"; } |
|||
} |
|||
|
|||
public Task On(Envelope<IEvent> @event) |
|||
{ |
|||
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
|||
} |
|||
|
|||
protected async Task On(WebhookAdded @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureWebooksLoadedAsync(); |
|||
|
|||
var theAppId = @event.AppId.Id; |
|||
var theSchemaId = @event.SchemaId.Id; |
|||
|
|||
var webhook = SimpleMapper.Map(@event, new MongoSchemaWebhookEntity { AppId = theAppId, SchemaId = theSchemaId }); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(theAppId).GetOrAddNew(theSchemaId).Add(SimpleMapper.Map(@event, new ShortInfo())); |
|||
|
|||
await Collection.InsertOneAsync(webhook); |
|||
} |
|||
|
|||
protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureWebooksLoadedAsync(); |
|||
|
|||
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.Remove(@event.SchemaId.Id); |
|||
|
|||
await Collection.DeleteManyAsync(x => x.Id == @event.Id); |
|||
} |
|||
|
|||
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureWebooksLoadedAsync(); |
|||
|
|||
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.Remove(@event.SchemaId.Id); |
|||
|
|||
await Collection.DeleteManyAsync(x => x.SchemaId == @event.SchemaId.Id); |
|||
} |
|||
} |
|||
} |
|||
@ -1,79 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas |
|||
{ |
|||
public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity |
|||
{ |
|||
private WebhookJob job; |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid AppId { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid WebhookId { 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())); } |
|||
} |
|||
} |
|||
} |
|||
@ -1,116 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas |
|||
{ |
|||
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)); |
|||
} |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
// ==========================================================================
|
|||
// ISchemaWebhookEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public interface ISchemaWebhookEntity : ISchemaWebhookUrlEntity |
|||
{ |
|||
Guid SchemaId { get; } |
|||
|
|||
long TotalSucceeded { get; } |
|||
|
|||
long TotalFailed { get; } |
|||
|
|||
long TotalTimedout { get; } |
|||
|
|||
long TotalRequestTime { get; } |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
// ==========================================================================
|
|||
// ISchemaWebhookUrlEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public interface ISchemaWebhookUrlEntity |
|||
{ |
|||
Guid Id { get; } |
|||
|
|||
Uri Url { get; } |
|||
|
|||
string SharedSecret { get; } |
|||
} |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
// ==========================================================================
|
|||
// IWebhookEventEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using NodaTime; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public interface IWebhookEventEntity : IEntity |
|||
{ |
|||
WebhookJob Job { get; } |
|||
|
|||
Instant? NextAttempt { get; } |
|||
|
|||
WebhookResult Result { get; } |
|||
|
|||
WebhookJobResult JobResult { get; } |
|||
|
|||
int NumCalls { get; } |
|||
|
|||
string LastDump { get; } |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas.Repositories |
|||
{ |
|||
public interface ISchemaWebhookRepository |
|||
{ |
|||
Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed); |
|||
|
|||
Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId); |
|||
|
|||
Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId); |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas.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); |
|||
} |
|||
} |
|||
@ -1,164 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas.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.Schemas |
|||
{ |
|||
public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem |
|||
{ |
|||
private readonly ActionBlock<IWebhookEventEntity> requestBlock; |
|||
private readonly TransformBlock<IWebhookEventEntity, IWebhookEventEntity> blockBlock; |
|||
private readonly IWebhookEventRepository webhookEventRepository; |
|||
private readonly ISchemaWebhookRepository webhookRepository; |
|||
private readonly WebhookSender webhookSender; |
|||
private readonly CompletionTimer timer; |
|||
private readonly ISemanticLog log; |
|||
private readonly IClock clock; |
|||
|
|||
public WebhookDequeuer(WebhookSender webhookSender, |
|||
IWebhookEventRepository webhookEventRepository, |
|||
ISchemaWebhookRepository 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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,126 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookEnqueuer.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json; |
|||
using Newtonsoft.Json.Linq; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Domain.Apps.Read.Schemas.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public sealed class WebhookEnqueuer : IEventConsumer |
|||
{ |
|||
private const string ContentPrefix = "Content"; |
|||
private static readonly Duration ExpirationTime = Duration.FromDays(2); |
|||
private readonly IWebhookEventRepository webhookEventRepository; |
|||
private readonly ISchemaWebhookRepository 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, |
|||
ISchemaWebhookRepository 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.QueryUrlsBySchemaAsync(contentEvent.AppId.Id, contentEvent.SchemaId.Id); |
|||
|
|||
if (webhooks.Count > 0) |
|||
{ |
|||
var now = clock.GetCurrentInstant(); |
|||
|
|||
var payload = CreatePayload(@event, eventType); |
|||
|
|||
var eventName = $"{contentEvent.SchemaId.Name.ToPascalCase()}{CreateContentEventName(eventType)}"; |
|||
|
|||
foreach (var webhook in webhooks) |
|||
{ |
|||
await EnqueueJobAsync(payload, webhook, contentEvent, eventName, now); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private async Task EnqueueJobAsync(string payload, ISchemaWebhookUrlEntity 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 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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookJob.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using NodaTime; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
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; } |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookJobResult.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public enum WebhookJobResult |
|||
{ |
|||
Pending, |
|||
Success, |
|||
Retry, |
|||
Failed |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookResult.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public enum WebhookResult |
|||
{ |
|||
Pending, |
|||
Success, |
|||
Failed, |
|||
Timeout |
|||
} |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas |
|||
{ |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
// ==========================================================================
|
|||
// AddWebhook.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Schemas.Commands |
|||
{ |
|||
public sealed class AddWebhook : SchemaAggregateCommand, IValidatable |
|||
{ |
|||
public Guid Id { get; } = Guid.NewGuid(); |
|||
|
|||
public Uri Url { get; set; } |
|||
|
|||
public string SharedSecret { get; } = RandomHash.New(); |
|||
|
|||
public void Validate(IList<ValidationError> errors) |
|||
{ |
|||
if (Url == null || !Url.IsAbsoluteUri) |
|||
{ |
|||
errors.Add(new ValidationError("Url must be specified and absolute", nameof(Url))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
// ==========================================================================
|
|||
// DeleteWebhook.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Schemas.Commands |
|||
{ |
|||
public class DeleteWebhook : SchemaAggregateCommand |
|||
{ |
|||
public Guid Id { get; set; } |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookCreatedDto.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Squidex.Controllers.Api.Webhooks.Models |
|||
{ |
|||
public class WebhookCreatedDto |
|||
{ |
|||
/// <summary>
|
|||
/// The id of the webhook.
|
|||
/// </summary>
|
|||
public Guid Id { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The shared secret that is used to calculate the signature.
|
|||
/// </summary>
|
|||
[Required] |
|||
public string SharedSecret { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The id of the schema.
|
|||
/// </summary>
|
|||
public string SchemaId { get; set; } |
|||
} |
|||
} |
|||
@ -1,130 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas.Repositories; |
|||
using Squidex.Infrastructure.Log; |
|||
using Xunit; |
|||
|
|||
// ReSharper disable MethodSupportsCancellation
|
|||
// ReSharper disable ImplicitlyCapturedClosure
|
|||
// ReSharper disable ConvertToConstant.Local
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Schemas |
|||
{ |
|||
public class WebhookDequeuerTests |
|||
{ |
|||
private readonly IClock clock = A.Fake<IClock>(); |
|||
private readonly ISchemaWebhookRepository webhookRepository = A.Fake<ISchemaWebhookRepository>(); |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,118 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Schemas.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.Schemas |
|||
{ |
|||
public class WebhookEnqueuerTests |
|||
{ |
|||
private readonly IClock clock = A.Fake<IClock>(); |
|||
private readonly ISchemaWebhookRepository webhookRepository = A.Fake<ISchemaWebhookRepository>(); |
|||
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.QueryUrlsBySchemaAsync(appId.Id, schemaId.Id)) |
|||
.Returns(Task.FromResult<IReadOnlyList<ISchemaWebhookUrlEntity>>(new List<ISchemaWebhookUrlEntity> { 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