mirror of https://github.com/Squidex/squidex.git
64 changed files with 1374 additions and 1747 deletions
@ -1,25 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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; } |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookCreated.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Webhooks |
|||
{ |
|||
[EventType(nameof(WebhookCreated))] |
|||
public sealed class WebhookCreated : WebhookEditEvent |
|||
{ |
|||
public string SharedSecret { get; set; } |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookDeleted.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Webhooks |
|||
{ |
|||
[EventType(nameof(WebhookDeleted), 2)] |
|||
public sealed class WebhookDeleted : WebhookEvent |
|||
{ |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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; } |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookUpdated.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Webhooks |
|||
{ |
|||
[EventType(nameof(WebhookUpdated))] |
|||
public sealed class WebhookUpdated : WebhookEditEvent |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
// ==========================================================================
|
|||
// MongoRuleEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using MongoDB.Bson.Serialization.Attributes; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Read.Rules; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Rules |
|||
{ |
|||
public class MongoRuleEntity : MongoEntity, IRuleEntity |
|||
{ |
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid AppId { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public RefToken CreatedBy { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public RefToken LastModifiedBy { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public long Version { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
[BsonJson] |
|||
public Rule Rule { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
// ==========================================================================
|
|||
// MongoRuleRepository.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.Rules; |
|||
using Squidex.Domain.Apps.Read.Rules.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Rules |
|||
{ |
|||
public partial class MongoRuleRepository : MongoRepositoryBase<MongoRuleEntity>, IRuleRepository, IEventConsumer |
|||
{ |
|||
private static readonly List<IRuleEntity> EmptyRules = new List<IRuleEntity>(); |
|||
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
|||
private Dictionary<Guid, List<IRuleEntity>> inMemoryWebhooks; |
|||
|
|||
public MongoRuleRepository(IMongoDatabase database) |
|||
: base(database) |
|||
{ |
|||
} |
|||
|
|||
protected override string CollectionName() |
|||
{ |
|||
return "Projections_Rules"; |
|||
} |
|||
|
|||
protected override Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection) |
|||
{ |
|||
return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IRuleEntity>> QueryByAppAsync(Guid appId) |
|||
{ |
|||
var entities = |
|||
await Collection.Find(x => x.AppId == appId) |
|||
.ToListAsync(); |
|||
|
|||
return entities.OfType<IRuleEntity>().ToList(); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IRuleEntity>> QueryCachedByAppAsync(Guid appId) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
return inMemoryWebhooks.GetOrDefault(appId) ?? EmptyRules; |
|||
} |
|||
|
|||
private async Task EnsureRulesLoadedAsync() |
|||
{ |
|||
if (inMemoryWebhooks == null) |
|||
{ |
|||
try |
|||
{ |
|||
await lockObject.WaitAsync(); |
|||
|
|||
if (inMemoryWebhooks == null) |
|||
{ |
|||
inMemoryWebhooks = new Dictionary<Guid, List<IRuleEntity>>(); |
|||
|
|||
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,97 @@ |
|||
// ==========================================================================
|
|||
// MongoRuleRepository_EventHandling.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Events.Rules; |
|||
using Squidex.Domain.Apps.Events.Rules.Utils; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Rules |
|||
{ |
|||
public partial class MongoRuleRepository |
|||
{ |
|||
public string Name |
|||
{ |
|||
get { return GetType().Name; } |
|||
} |
|||
|
|||
public string EventsFilter |
|||
{ |
|||
get { return "^rules-"; } |
|||
} |
|||
|
|||
public Task On(Envelope<IEvent> @event) |
|||
{ |
|||
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
|||
} |
|||
|
|||
protected async Task On(RuleCreated @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.CreateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule = RuleEventDispatcher.Create(@event); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleUpdated @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.UpdateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule.Apply(@event); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleEnabled @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.UpdateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule.Apply(@event); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleDisabled @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.UpdateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule.Apply(@event); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleDeleted @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); |
|||
|
|||
await Collection.DeleteManyAsync(x => x.Id == @event.RuleId); |
|||
} |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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; |
|||
|
|||
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; } |
|||
} |
|||
} |
|||
} |
|||
@ -1,119 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoWebhookRepository.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.Webhooks; |
|||
using Squidex.Domain.Apps.Read.Webhooks.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
|
|||
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 readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
|||
private Dictionary<Guid, List<IWebhookEntity>> inMemoryWebhooks; |
|||
|
|||
public MongoWebhookRepository(IMongoDatabase database) |
|||
: base(database) |
|||
{ |
|||
} |
|||
|
|||
protected override string CollectionName() |
|||
{ |
|||
return "Projections_SchemaWebhooks"; |
|||
} |
|||
|
|||
protected override Task SetupCollectionAsync(IMongoCollection<MongoWebhookEntity> collection) |
|||
{ |
|||
return Task.WhenAll( |
|||
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)), |
|||
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaIds))); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IWebhookEntity>> QueryByAppAsync(Guid appId) |
|||
{ |
|||
var entities = |
|||
await Collection.Find(x => x.AppId == appId) |
|||
.ToListAsync(); |
|||
|
|||
return entities.OfType<IWebhookEntity>().ToList(); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IWebhookEntity>> QueryCachedByAppAsync(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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,99 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Domain.Apps.Events.Webhooks; |
|||
using Squidex.Domain.Apps.Read.MongoDb.Utils; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
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(); |
|||
|
|||
var webhooks = |
|||
await Collection.Find(t => t.SchemaIds.Contains(@event.SchemaId.Id)) |
|||
.ToListAsync(); |
|||
|
|||
foreach (var webhook in webhooks) |
|||
{ |
|||
webhook.Schemas.RemoveAll(s => s.SchemaId == @event.SchemaId.Id); |
|||
|
|||
webhook.SchemaIds = webhook.Schemas.Select(x => x.SchemaId).ToList(); |
|||
|
|||
inMemoryWebhooks.GetOrAddNew(webhook.AppId).RemoveAll(x => x.Id == webhook.Id); |
|||
inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); |
|||
|
|||
await Collection.ReplaceOneAsync(x => x.Id == webhook.Id, webhook); |
|||
} |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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; } |
|||
} |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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; } |
|||
} |
|||
} |
|||
@ -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.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); |
|||
} |
|||
} |
|||
@ -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.Webhooks.Repositories |
|||
{ |
|||
public interface IWebhookRepository |
|||
{ |
|||
Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed); |
|||
|
|||
Task<IReadOnlyList<IWebhookEntity>> QueryByAppAsync(Guid appId); |
|||
|
|||
Task<IReadOnlyList<IWebhookEntity>> QueryCachedByAppAsync(Guid appId); |
|||
} |
|||
} |
|||
@ -1,160 +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.Webhooks.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Log; |
|||
using Squidex.Infrastructure.Timers; |
|||
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,140 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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.Contents; |
|||
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.QueryCachedByAppAsync(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) && |
|||
(webhookSchema.SendCreate && @event is ContentCreated || |
|||
webhookSchema.SendUpdate && @event is ContentUpdated || |
|||
webhookSchema.SendDelete && @event is ContentDeleted || |
|||
webhookSchema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); |
|||
} |
|||
|
|||
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, StringComparison.Ordinal) ? 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.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; } |
|||
} |
|||
} |
|||
@ -1,98 +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; |
|||
|
|||
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) |
|||
{ |
|||
try |
|||
{ |
|||
var request = BuildRequest(job); |
|||
HttpResponseMessage response = null; |
|||
|
|||
var responseString = string.Empty; |
|||
|
|||
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; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
responseString = ex.Message; |
|||
} |
|||
finally |
|||
{ |
|||
watch.Stop(); |
|||
} |
|||
|
|||
if (response != null) |
|||
{ |
|||
responseString = await response.Content.ReadAsStringAsync(); |
|||
} |
|||
|
|||
var dump = DumpFormatter.BuildDump(request, response, job.RequestBody, responseString, watch.Elapsed, isTimeout); |
|||
|
|||
var result = WebhookResult.Failed; |
|||
|
|||
if (isTimeout) |
|||
{ |
|||
result = WebhookResult.Timeout; |
|||
} |
|||
else if (response?.IsSuccessStatusCode == true) |
|||
{ |
|||
result = WebhookResult.Success; |
|||
} |
|||
|
|||
return (dump, result, watch.Elapsed); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return (ex.Message, WebhookResult.Failed, TimeSpan.Zero); |
|||
} |
|||
} |
|||
|
|||
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,14 +1,14 @@ |
|||
// ==========================================================================
|
|||
// UpdateWebhook.cs
|
|||
// CreateRule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
|||
namespace Squidex.Domain.Apps.Write.Rules.Commands |
|||
{ |
|||
public sealed class UpdateWebhook : WebhookEditCommand |
|||
public sealed class CreateRule : RuleEditCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -1,14 +1,14 @@ |
|||
// ==========================================================================
|
|||
// DeleteWebhook.cs
|
|||
// DeleteRule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
|||
namespace Squidex.Domain.Apps.Write.Rules.Commands |
|||
{ |
|||
public sealed class DeleteWebhook : WebhookAggregateCommand |
|||
public sealed class DeleteRule : RuleAggregateCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -1,18 +1,14 @@ |
|||
// ==========================================================================
|
|||
// WebhookResult.cs
|
|||
// DisableRule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Webhooks |
|||
namespace Squidex.Domain.Apps.Write.Rules.Commands |
|||
{ |
|||
public enum WebhookResult |
|||
public sealed class DisableRule : RuleAggregateCommand |
|||
{ |
|||
Pending, |
|||
Success, |
|||
Failed, |
|||
Timeout |
|||
} |
|||
} |
|||
@ -1,18 +1,14 @@ |
|||
// ==========================================================================
|
|||
// WebhookJobResult.cs
|
|||
// EnableRule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Webhooks |
|||
namespace Squidex.Domain.Apps.Write.Rules.Commands |
|||
{ |
|||
public enum WebhookJobResult |
|||
public sealed class EnableRule : RuleAggregateCommand |
|||
{ |
|||
Pending, |
|||
Success, |
|||
Retry, |
|||
Failed |
|||
} |
|||
} |
|||
@ -1,17 +1,19 @@ |
|||
// ==========================================================================
|
|||
// WebhookEvent.cs
|
|||
// RuleEditCommand.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Webhooks |
|||
namespace Squidex.Domain.Apps.Write.Rules.Commands |
|||
{ |
|||
public abstract class WebhookEvent : AppEvent |
|||
public abstract class RuleEditCommand : RuleAggregateCommand |
|||
{ |
|||
public Guid WebhookId { get; set; } |
|||
public RuleTrigger Trigger { get; set; } |
|||
|
|||
public RuleAction Action { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// ==========================================================================
|
|||
// UpdateRule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Commands |
|||
{ |
|||
public sealed class UpdateRule : RuleEditCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
// ==========================================================================
|
|||
// GuardRule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.Rules.Commands; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Guards |
|||
{ |
|||
public static class GuardRule |
|||
{ |
|||
public static Task CanCreate(CreateRule command, ISchemaProvider schemas) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
return Validate.It(() => "Cannot create rule.", async error => |
|||
{ |
|||
if (command.Trigger == null) |
|||
{ |
|||
error(new ValidationError("Trigger must be defined.", nameof(command.Trigger))); |
|||
} |
|||
else |
|||
{ |
|||
var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); |
|||
|
|||
errors.Foreach(error); |
|||
} |
|||
|
|||
if (command.Action == null) |
|||
{ |
|||
error(new ValidationError("Trigger must be defined.", nameof(command.Action))); |
|||
} |
|||
else |
|||
{ |
|||
var errors = await RuleActionValidator.ValidateAsync(command.Action); |
|||
|
|||
errors.Foreach(error); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static Task CanUpdate(UpdateRule command, ISchemaProvider schemas) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
return Validate.It(() => "Cannot update rule.", async error => |
|||
{ |
|||
if (command.Trigger == null && command.Action == null) |
|||
{ |
|||
error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action))); |
|||
} |
|||
|
|||
if (command.Trigger != null) |
|||
{ |
|||
var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); |
|||
|
|||
errors.Foreach(error); |
|||
} |
|||
|
|||
if (command.Action != null) |
|||
{ |
|||
var errors = await RuleActionValidator.ValidateAsync(command.Action); |
|||
|
|||
errors.Foreach(error); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanEnable(EnableRule command, Rule rule) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot enable rule.", error => |
|||
{ |
|||
if (rule.IsEnabled) |
|||
{ |
|||
error(new ValidationError("Rule is already enabled.")); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanDisable(DisableRule command, Rule rule) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot disable rule.", error => |
|||
{ |
|||
if (!rule.IsEnabled) |
|||
{ |
|||
error(new ValidationError("Rule is already disabled.")); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanDelete(DeleteRule command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// ==========================================================================
|
|||
// RuleActionValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Guards |
|||
{ |
|||
public sealed class RuleActionValidator : IRuleActionVisitor<Task<IEnumerable<ValidationError>>> |
|||
{ |
|||
public static Task<IEnumerable<ValidationError>> ValidateAsync(RuleAction action) |
|||
{ |
|||
Guard.NotNull(action, nameof(action)); |
|||
|
|||
var visitor = new RuleActionValidator(); |
|||
|
|||
return action.Accept(visitor); |
|||
} |
|||
|
|||
public Task<IEnumerable<ValidationError>> Visit(WebhookAction action) |
|||
{ |
|||
var errors = new List<ValidationError>(); |
|||
|
|||
if (action.Url == null || !action.Url.IsAbsoluteUri) |
|||
{ |
|||
errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); |
|||
} |
|||
|
|||
return Task.FromResult<IEnumerable<ValidationError>>(errors); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// RuleTriggerValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Guards |
|||
{ |
|||
public sealed class RuleTriggerValidator : IRuleTriggerVisitor<Task<IEnumerable<ValidationError>>> |
|||
{ |
|||
public ISchemaProvider Schemas { get; } |
|||
|
|||
public RuleTriggerValidator(ISchemaProvider schemas) |
|||
{ |
|||
Schemas = schemas; |
|||
} |
|||
|
|||
public static Task<IEnumerable<ValidationError>> ValidateAsync(RuleTrigger action, ISchemaProvider schemas) |
|||
{ |
|||
Guard.NotNull(action, nameof(action)); |
|||
Guard.NotNull(schemas, nameof(schemas)); |
|||
|
|||
var visitor = new RuleTriggerValidator(schemas); |
|||
|
|||
return action.Accept(visitor); |
|||
} |
|||
|
|||
public async Task<IEnumerable<ValidationError>> Visit(ContentChangedTrigger trigger) |
|||
{ |
|||
if (trigger.Schemas != null) |
|||
{ |
|||
var schemaErrors = await Task.WhenAll( |
|||
trigger.Schemas.Select(async s => |
|||
await Schemas.FindSchemaByIdAsync(s.SchemaId) == null |
|||
? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) |
|||
: null)); |
|||
|
|||
return schemaErrors.Where(x => x != null).ToList(); |
|||
} |
|||
|
|||
return new List<ValidationError>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
// ==========================================================================
|
|||
// RuleCommandMiddleware.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.Rules.Commands; |
|||
using Squidex.Domain.Apps.Write.Rules.Guards; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Commands; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules |
|||
{ |
|||
public class RuleCommandMiddleware : ICommandMiddleware |
|||
{ |
|||
private readonly IAggregateHandler handler; |
|||
private readonly ISchemaProvider schemas; |
|||
|
|||
public RuleCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) |
|||
{ |
|||
Guard.NotNull(handler, nameof(handler)); |
|||
Guard.NotNull(schemas, nameof(schemas)); |
|||
|
|||
this.handler = handler; |
|||
this.schemas = schemas; |
|||
} |
|||
|
|||
protected Task On(CreateRule command, CommandContext context) |
|||
{ |
|||
return handler.CreateAsync<RuleDomainObject>(context, async w => |
|||
{ |
|||
await GuardRule.CanCreate(command, schemas); |
|||
|
|||
w.Create(command); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(UpdateRule command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<RuleDomainObject>(context, async c => |
|||
{ |
|||
await GuardRule.CanUpdate(command, schemas); |
|||
|
|||
c.Update(command); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(EnableRule command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<RuleDomainObject>(context, r => |
|||
{ |
|||
GuardRule.CanEnable(command, r.Rule); |
|||
|
|||
r.Enable(command); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(DisableRule command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<RuleDomainObject>(context, r => |
|||
{ |
|||
GuardRule.CanDisable(command, r.Rule); |
|||
|
|||
r.Disable(command); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(DeleteRule command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<RuleDomainObject>(context, c => |
|||
{ |
|||
GuardRule.CanDelete(command); |
|||
|
|||
c.Delete(command); |
|||
}); |
|||
} |
|||
|
|||
public async Task HandleAsync(CommandContext context, Func<Task> next) |
|||
{ |
|||
if (!await this.DispatchActionAsync(context.Command, context)) |
|||
{ |
|||
await next(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
// ==========================================================================
|
|||
// RuleDomainObject.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Events.Rules; |
|||
using Squidex.Domain.Apps.Events.Rules.Utils; |
|||
using Squidex.Domain.Apps.Write.Rules.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.Rules |
|||
{ |
|||
public class RuleDomainObject : DomainObjectBase |
|||
{ |
|||
private Rule rule; |
|||
private bool isDeleted; |
|||
|
|||
public Rule Rule |
|||
{ |
|||
get { return rule; } |
|||
} |
|||
|
|||
public RuleDomainObject(Guid id, int version) |
|||
: base(id, version) |
|||
{ |
|||
} |
|||
|
|||
protected void On(RuleCreated @event) |
|||
{ |
|||
rule = RuleEventDispatcher.Create(@event); |
|||
} |
|||
|
|||
protected void On(RuleUpdated @event) |
|||
{ |
|||
rule.Apply(@event); |
|||
} |
|||
|
|||
protected void On(RuleEnabled @event) |
|||
{ |
|||
rule.Apply(@event); |
|||
} |
|||
|
|||
protected void On(RuleDisabled @event) |
|||
{ |
|||
rule.Apply(@event); |
|||
} |
|||
|
|||
protected void On(RuleDeleted @event) |
|||
{ |
|||
isDeleted = true; |
|||
} |
|||
|
|||
public void Create(CreateRule command) |
|||
{ |
|||
VerifyNotCreated(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); |
|||
} |
|||
|
|||
public void Update(UpdateRule command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); |
|||
} |
|||
|
|||
public void Enable(EnableRule command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); |
|||
} |
|||
|
|||
public void Disable(DisableRule command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); |
|||
} |
|||
|
|||
public void Delete(DeleteRule command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); |
|||
} |
|||
|
|||
private void VerifyNotCreated() |
|||
{ |
|||
if (rule != null) |
|||
{ |
|||
throw new DomainException("Webhook has already been created."); |
|||
} |
|||
} |
|||
|
|||
private void VerifyCreatedAndNotDeleted() |
|||
{ |
|||
if (isDeleted || rule == null) |
|||
{ |
|||
throw new DomainException("Webhook has already been deleted or not created yet."); |
|||
} |
|||
} |
|||
|
|||
protected override void DispatchEvent(Envelope<IEvent> @event) |
|||
{ |
|||
this.DispatchAction(@event.Payload); |
|||
} |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookEditCommand.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.Write.Webhooks.Commands |
|||
{ |
|||
public abstract class WebhookEditCommand : WebhookAggregateCommand |
|||
{ |
|||
public Uri Url { get; set; } |
|||
|
|||
public List<WebhookSchema> Schemas { get; set; } = new List<WebhookSchema>(); |
|||
} |
|||
} |
|||
@ -1,61 +0,0 @@ |
|||
// ==========================================================================
|
|||
// GuardWebhook.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; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Webhooks.Guards |
|||
{ |
|||
public static class GuardWebhook |
|||
{ |
|||
public static Task CanCreate(CreateWebhook command, ISchemaProvider schemas) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
return Validate.It(() => "Cannot create webhook.", error => ValidateCommandAsync(command, error, schemas)); |
|||
} |
|||
|
|||
public static Task CanUpdate(UpdateWebhook command, ISchemaProvider schemas) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
return Validate.It(() => "Cannot update webhook.", error => ValidateCommandAsync(command, error, schemas)); |
|||
} |
|||
|
|||
public static void CanDelete(DeleteWebhook command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
} |
|||
|
|||
private static async Task ValidateCommandAsync(WebhookEditCommand command, Action<ValidationError> error, ISchemaProvider schemas) |
|||
{ |
|||
if (command.Url == null || !command.Url.IsAbsoluteUri) |
|||
{ |
|||
error(new ValidationError("Url must be specified and absolute.", nameof(command.Url))); |
|||
} |
|||
|
|||
if (command.Schemas != null) |
|||
{ |
|||
var schemaErrors = await Task.WhenAll( |
|||
command.Schemas.Select(async s => |
|||
await schemas.FindSchemaByIdAsync(s.SchemaId) == null |
|||
? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(command.Schemas)) |
|||
: null)); |
|||
|
|||
foreach (var schemaError in schemaErrors.Where(x => x != null)) |
|||
{ |
|||
error(schemaError); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookCommandMiddleware.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.Webhooks.Commands; |
|||
using Squidex.Domain.Apps.Write.Webhooks.Guards; |
|||
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 handler.CreateAsync<WebhookDomainObject>(context, async w => |
|||
{ |
|||
await GuardWebhook.CanCreate(command, schemas); |
|||
|
|||
w.Create(command); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(UpdateWebhook command, CommandContext context) |
|||
{ |
|||
await handler.UpdateAsync<WebhookDomainObject>(context, async c => |
|||
{ |
|||
await GuardWebhook.CanUpdate(command, schemas); |
|||
|
|||
c.Update(command); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(DeleteWebhook command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<WebhookDomainObject>(context, c => |
|||
{ |
|||
GuardWebhook.CanDelete(command); |
|||
|
|||
c.Delete(command); |
|||
}); |
|||
} |
|||
|
|||
public async Task HandleAsync(CommandContext context, Func<Task> next) |
|||
{ |
|||
if (!await this.DispatchActionAsync(context.Command, context)) |
|||
{ |
|||
await next(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,82 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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) |
|||
{ |
|||
VerifyNotCreated(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new WebhookCreated())); |
|||
} |
|||
|
|||
public void Update(UpdateWebhook command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new WebhookUpdated())); |
|||
} |
|||
|
|||
public void Delete(DeleteWebhook 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,48 @@ |
|||
// ==========================================================================
|
|||
// WebhookActionTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Guards.Actions |
|||
{ |
|||
public sealed class WebhookActionTests |
|||
{ |
|||
[Fact] |
|||
public async Task Should_add_error_if_url_is_null() |
|||
{ |
|||
var action = new WebhookAction { Url = null }; |
|||
|
|||
var errors = await RuleActionValidator.ValidateAsync(action); |
|||
|
|||
Assert.NotEmpty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_url_is_relative() |
|||
{ |
|||
var action = new WebhookAction { Url = new Uri("/invalid", UriKind.Relative) }; |
|||
|
|||
var errors = await RuleActionValidator.ValidateAsync(action); |
|||
|
|||
Assert.NotEmpty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_url_is_absolute() |
|||
{ |
|||
var action = new WebhookAction { Url = new Uri("https://squidex.io", UriKind.Absolute) }; |
|||
|
|||
var errors = await RuleActionValidator.ValidateAsync(action); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
// ==========================================================================
|
|||
// GuardRuleTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.Rules.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Guards |
|||
{ |
|||
public class GuardRuleTests |
|||
{ |
|||
private readonly Uri validUrl = new Uri("https://squidex.io"); |
|||
private readonly Rule rule = new Rule(new ContentChangedTrigger(), new WebhookAction()); |
|||
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>(); |
|||
|
|||
public GuardRuleTests() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(A.Fake<ISchemaEntity>()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_throw_exception_if_trigger_null() |
|||
{ |
|||
var command = new CreateRule |
|||
{ |
|||
Trigger = null, |
|||
Action = new WebhookAction |
|||
{ |
|||
Url = validUrl |
|||
} |
|||
}; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardRule.CanCreate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_throw_exception_if_action_null() |
|||
{ |
|||
var command = new CreateRule |
|||
{ |
|||
Trigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema>() |
|||
}, |
|||
Action = null |
|||
}; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardRule.CanCreate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() |
|||
{ |
|||
var command = new CreateRule |
|||
{ |
|||
Trigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema>() |
|||
}, |
|||
Action = new WebhookAction |
|||
{ |
|||
Url = validUrl |
|||
} |
|||
}; |
|||
|
|||
await GuardRule.CanCreate(command, schemas); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() |
|||
{ |
|||
var command = new UpdateRule(); |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardRule.CanUpdate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid() |
|||
{ |
|||
var command = new UpdateRule |
|||
{ |
|||
Trigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema>() |
|||
}, |
|||
Action = new WebhookAction |
|||
{ |
|||
Url = validUrl |
|||
} |
|||
}; |
|||
|
|||
await GuardRule.CanUpdate(command, schemas); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanEnable_should_throw_exception_if_rule_enabled() |
|||
{ |
|||
var command = new EnableRule(); |
|||
|
|||
rule.Enable(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardRule.CanEnable(command, rule)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanEnable_should_not_throw_exception_if_rule_disabled() |
|||
{ |
|||
var command = new EnableRule(); |
|||
|
|||
rule.Disable(); |
|||
|
|||
GuardRule.CanEnable(command, rule); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanDisable_should_throw_exception_if_rule_disabled() |
|||
{ |
|||
var command = new DisableRule(); |
|||
|
|||
rule.Disable(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardRule.CanDisable(command, rule)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanDisable_should_not_throw_exception_if_rule_enabled() |
|||
{ |
|||
var command = new DisableRule(); |
|||
|
|||
rule.Enable(); |
|||
|
|||
GuardRule.CanDisable(command, rule); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanDelete_should_not_throw_exception() |
|||
{ |
|||
var command = new DeleteRule(); |
|||
|
|||
GuardRule.CanDelete(command); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
// ==========================================================================
|
|||
// ContentChangedTriggerTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules.Guards.Triggers |
|||
{ |
|||
public class ContentChangedTriggerTests |
|||
{ |
|||
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>(); |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_schemas_ids_are_not_valid() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(Task.FromResult<ISchemaEntity>(null)); |
|||
|
|||
var trigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema> |
|||
{ |
|||
new ContentChangedTriggerSchema() |
|||
} |
|||
}; |
|||
|
|||
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); |
|||
|
|||
Assert.NotEmpty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_schemas_is_null() |
|||
{ |
|||
var trigger = new ContentChangedTrigger(); |
|||
|
|||
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_schemas_is_empty() |
|||
{ |
|||
var trigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema>() |
|||
}; |
|||
|
|||
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_schemas_ids_are_valid() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(A.Fake<ISchemaEntity>()); |
|||
|
|||
var trigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema> |
|||
{ |
|||
new ContentChangedTriggerSchema() |
|||
} |
|||
}; |
|||
|
|||
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
// ==========================================================================
|
|||
// RuleCommandMiddlewareTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.Rules.Commands; |
|||
using Squidex.Domain.Apps.Write.TestHelpers; |
|||
using Squidex.Infrastructure.CQRS.Commands; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules |
|||
{ |
|||
public class RuleCommandMiddlewareTests : HandlerTestBase<RuleDomainObject> |
|||
{ |
|||
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>(); |
|||
private readonly RuleCommandMiddleware sut; |
|||
private readonly RuleDomainObject rule; |
|||
private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); |
|||
private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; |
|||
private readonly Guid ruleId = Guid.NewGuid(); |
|||
|
|||
public RuleCommandMiddlewareTests() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(A.Fake<ISchemaEntity>()); |
|||
|
|||
rule = new RuleDomainObject(ruleId, -1); |
|||
|
|||
sut = new RuleCommandMiddleware(Handler, schemas); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_create_domain_object() |
|||
{ |
|||
var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); |
|||
|
|||
await TestCreate(rule, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_update_domain_object() |
|||
{ |
|||
var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }); |
|||
|
|||
CreateRule(); |
|||
|
|||
await TestUpdate(rule, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Enable_should_update_domain_object() |
|||
{ |
|||
CreateRule(); |
|||
DisableRule(); |
|||
|
|||
var command = CreateContextForCommand(new EnableRule { RuleId = ruleId }); |
|||
|
|||
await TestUpdate(rule, async _ => |
|||
{ |
|||
await sut.HandleAsync(command); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Disable_should_update_domain_object() |
|||
{ |
|||
CreateRule(); |
|||
|
|||
var command = CreateContextForCommand(new DisableRule { RuleId = ruleId }); |
|||
|
|||
await TestUpdate(rule, async _ => |
|||
{ |
|||
await sut.HandleAsync(command); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Delete_should_update_domain_object() |
|||
{ |
|||
CreateRule(); |
|||
|
|||
var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId }); |
|||
|
|||
await TestUpdate(rule, async _ => |
|||
{ |
|||
await sut.HandleAsync(command); |
|||
}); |
|||
} |
|||
|
|||
private void DisableRule() |
|||
{ |
|||
rule.Disable(new DisableRule()); |
|||
} |
|||
|
|||
private void CreateRule() |
|||
{ |
|||
rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,240 @@ |
|||
// ==========================================================================
|
|||
// RuleDomainObjectTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Events.Rules; |
|||
using Squidex.Domain.Apps.Write.Rules.Commands; |
|||
using Squidex.Domain.Apps.Write.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Rules |
|||
{ |
|||
public class RuleDomainObjectTests : HandlerTestBase<RuleDomainObject> |
|||
{ |
|||
private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); |
|||
private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; |
|||
private readonly RuleDomainObject sut; |
|||
|
|||
public Guid RuleId { get; } = Guid.NewGuid(); |
|||
|
|||
public RuleDomainObjectTests() |
|||
{ |
|||
sut = new RuleDomainObject(RuleId, 0); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_throw_exception_if_created() |
|||
{ |
|||
sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_create_events() |
|||
{ |
|||
var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction }; |
|||
|
|||
sut.Create(CreateRuleCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_throw_exception_if_rule_is_deleted() |
|||
{ |
|||
CreateRule(); |
|||
DeleteRule(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_create_events() |
|||
{ |
|||
var newTrigger = new ContentChangedTrigger |
|||
{ |
|||
Schemas = new List<ContentChangedTriggerSchema>() |
|||
}; |
|||
|
|||
var newAction = new WebhookAction |
|||
{ |
|||
Url = new Uri("https://squidex.io/v2") |
|||
}; |
|||
|
|||
CreateRule(); |
|||
|
|||
var command = new UpdateRule { Trigger = newTrigger, Action = newAction }; |
|||
|
|||
sut.Update(CreateRuleCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enable_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Enable(CreateRuleCommand(new EnableRule())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enable_should_throw_exception_if_rule_is_deleted() |
|||
{ |
|||
CreateRule(); |
|||
DeleteRule(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Enable(CreateRuleCommand(new EnableRule())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enable_should_create_events() |
|||
{ |
|||
CreateRule(); |
|||
|
|||
var command = new EnableRule(); |
|||
|
|||
sut.Enable(CreateRuleCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateRuleEvent(new RuleEnabled()) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Disable_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Disable(CreateRuleCommand(new DisableRule())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Disable_should_throw_exception_if_rule_is_deleted() |
|||
{ |
|||
CreateRule(); |
|||
DeleteRule(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Disable(CreateRuleCommand(new DisableRule())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Disable_should_create_events() |
|||
{ |
|||
CreateRule(); |
|||
|
|||
var command = new DisableRule(); |
|||
|
|||
sut.Disable(CreateRuleCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateRuleEvent(new RuleDisabled()) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Delete(CreateRuleCommand(new DeleteRule())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_throw_exception_if_already_deleted() |
|||
{ |
|||
CreateRule(); |
|||
DeleteRule(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Delete(CreateRuleCommand(new DeleteRule())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_update_create_events() |
|||
{ |
|||
CreateRule(); |
|||
|
|||
sut.Delete(CreateRuleCommand(new DeleteRule())); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateRuleEvent(new RuleDeleted()) |
|||
); |
|||
} |
|||
|
|||
private void CreateRule() |
|||
{ |
|||
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); |
|||
|
|||
((IAggregate)sut).ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void DeleteRule() |
|||
{ |
|||
sut.Delete(CreateRuleCommand(new DeleteRule())); |
|||
|
|||
((IAggregate)sut).ClearUncommittedEvents(); |
|||
} |
|||
|
|||
protected T CreateRuleEvent<T>(T @event) where T : RuleEvent |
|||
{ |
|||
@event.RuleId = RuleId; |
|||
|
|||
return CreateEvent(@event); |
|||
} |
|||
|
|||
protected T CreateRuleCommand<T>(T command) where T : RuleAggregateCommand |
|||
{ |
|||
command.RuleId = RuleId; |
|||
|
|||
return CreateCommand(command); |
|||
} |
|||
} |
|||
} |
|||
@ -1,138 +0,0 @@ |
|||
// ==========================================================================
|
|||
// GuardWebhookTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Webhooks; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.Webhooks.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Webhooks.Guards |
|||
{ |
|||
public class GuardWebhookTests |
|||
{ |
|||
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>(); |
|||
|
|||
public GuardWebhookTests() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(A.Fake<ISchemaEntity>()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_throw_exception_if_url_defined() |
|||
{ |
|||
var command = new CreateWebhook(); |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_throw_exception_if_url_not_valid() |
|||
{ |
|||
var command = new CreateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_throw_exception_if_schema_id_not_found() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(Task.FromResult<ISchemaEntity>(null)); |
|||
|
|||
var command = new CreateWebhook |
|||
{ |
|||
Schemas = new List<WebhookSchema> |
|||
{ |
|||
new WebhookSchema() |
|||
}, |
|||
Url = new Uri("/invalid", UriKind.Relative) |
|||
}; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanCreate_should_not_throw_exception_if_schema_id_found() |
|||
{ |
|||
var command = new CreateWebhook |
|||
{ |
|||
Schemas = new List<WebhookSchema> |
|||
{ |
|||
new WebhookSchema() |
|||
}, |
|||
Url = new Uri("/invalid", UriKind.Relative) |
|||
}; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanUpdate_should_throw_exception_if_url_not_defined() |
|||
{ |
|||
var command = new UpdateWebhook(); |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanUpdate_should_throw_exception_if_url_not_valid() |
|||
{ |
|||
var command = new UpdateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanUpdate_should_throw_exception_if_schema_id_not_found() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false)) |
|||
.Returns(Task.FromResult<ISchemaEntity>(null)); |
|||
|
|||
var command = new UpdateWebhook |
|||
{ |
|||
Schemas = new List<WebhookSchema> |
|||
{ |
|||
new WebhookSchema() |
|||
}, |
|||
Url = new Uri("/invalid", UriKind.Relative) |
|||
}; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task CanUpdate_should_not_throw_exception_if_schema_id_found() |
|||
{ |
|||
var command = new UpdateWebhook |
|||
{ |
|||
Schemas = new List<WebhookSchema> |
|||
{ |
|||
new WebhookSchema() |
|||
}, |
|||
Url = new Uri("/invalid", UriKind.Relative) |
|||
}; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanDelete_should_not_throw_exception() |
|||
{ |
|||
var command = new DeleteWebhook(); |
|||
|
|||
GuardWebhook.CanDelete(command); |
|||
} |
|||
} |
|||
} |
|||
@ -1,115 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookCommandMiddlewareTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Webhooks; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Domain.Apps.Read.Schemas.Services; |
|||
using Squidex.Domain.Apps.Write.TestHelpers; |
|||
using Squidex.Domain.Apps.Write.Webhooks.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Commands; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Webhooks |
|||
{ |
|||
public class WebhookCommandMiddlewareTests : HandlerTestBase<WebhookDomainObject> |
|||
{ |
|||
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>(); |
|||
private readonly WebhookCommandMiddleware sut; |
|||
private readonly WebhookDomainObject webhook; |
|||
private readonly Uri url = new Uri("http://squidex.io"); |
|||
private readonly Guid schemaId = Guid.NewGuid(); |
|||
private readonly Guid webhookId = Guid.NewGuid(); |
|||
private readonly List<WebhookSchema> webhookSchemas; |
|||
|
|||
public WebhookCommandMiddlewareTests() |
|||
{ |
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)) |
|||
.Returns(A.Fake<ISchemaEntity>()); |
|||
|
|||
webhook = new WebhookDomainObject(webhookId, -1); |
|||
|
|||
webhookSchemas = new List<WebhookSchema> |
|||
{ |
|||
new WebhookSchema { SchemaId = schemaId } |
|||
}; |
|||
|
|||
sut = new WebhookCommandMiddleware(Handler, schemas); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_create_domain_object() |
|||
{ |
|||
var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); |
|||
|
|||
await TestCreate(webhook, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_update_domain_object() |
|||
{ |
|||
var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); |
|||
|
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake<ISchemaEntity>()); |
|||
|
|||
CreateWebhook(); |
|||
|
|||
await TestUpdate(webhook, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_throw_exception_when_schema_is_not_found() |
|||
{ |
|||
var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); |
|||
|
|||
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null); |
|||
|
|||
CreateWebhook(); |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(async () => |
|||
{ |
|||
await TestCreate(webhook, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Delete_should_update_domain_object() |
|||
{ |
|||
CreateWebhook(); |
|||
|
|||
var command = CreateContextForCommand(new DeleteWebhook { WebhookId = webhookId }); |
|||
|
|||
await TestUpdate(webhook, async _ => |
|||
{ |
|||
await sut.HandleAsync(command); |
|||
}); |
|||
} |
|||
|
|||
private void CreateWebhook() |
|||
{ |
|||
webhook.Create(new CreateWebhook { Url = url }); |
|||
} |
|||
} |
|||
} |
|||
@ -1,159 +0,0 @@ |
|||
// ==========================================================================
|
|||
// WebhookDomainObjectTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Domain.Apps.Events.Webhooks; |
|||
using Squidex.Domain.Apps.Write.TestHelpers; |
|||
using Squidex.Domain.Apps.Write.Webhooks.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Webhooks |
|||
{ |
|||
public class WebhookDomainObjectTests : HandlerTestBase<WebhookDomainObject> |
|||
{ |
|||
private readonly Uri url = new Uri("http://squidex.io"); |
|||
private readonly WebhookDomainObject sut; |
|||
|
|||
public Guid WebhookId { get; } = Guid.NewGuid(); |
|||
|
|||
public WebhookDomainObjectTests() |
|||
{ |
|||
sut = new WebhookDomainObject(WebhookId, 0); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_throw_exception_if_created() |
|||
{ |
|||
sut.Create(new CreateWebhook { Url = url }); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_create_events() |
|||
{ |
|||
var command = new CreateWebhook { Url = url }; |
|||
|
|||
sut.Create(CreateWebhookCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateWebhookEvent(new WebhookCreated |
|||
{ |
|||
Url = url, |
|||
Schemas = command.Schemas, |
|||
SharedSecret = command.SharedSecret, |
|||
WebhookId = command.WebhookId |
|||
}) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_throw_exception_if_webhook_is_deleted() |
|||
{ |
|||
CreateWebhook(); |
|||
DeleteWebhook(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_create_events() |
|||
{ |
|||
CreateWebhook(); |
|||
|
|||
var command = new UpdateWebhook { Url = url }; |
|||
|
|||
sut.Update(CreateWebhookCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateWebhookEvent(new WebhookUpdated { Url = url, Schemas = command.Schemas }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Delete(CreateWebhookCommand(new DeleteWebhook())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_throw_exception_if_already_deleted() |
|||
{ |
|||
CreateWebhook(); |
|||
DeleteWebhook(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Delete(CreateWebhookCommand(new DeleteWebhook())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_update_properties_create_events() |
|||
{ |
|||
CreateWebhook(); |
|||
|
|||
sut.Delete(CreateWebhookCommand(new DeleteWebhook())); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateWebhookEvent(new WebhookDeleted()) |
|||
); |
|||
} |
|||
|
|||
private void CreateWebhook() |
|||
{ |
|||
sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); |
|||
|
|||
((IAggregate)sut).ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void DeleteWebhook() |
|||
{ |
|||
sut.Delete(CreateWebhookCommand(new DeleteWebhook())); |
|||
|
|||
((IAggregate)sut).ClearUncommittedEvents(); |
|||
} |
|||
|
|||
protected T CreateWebhookEvent<T>(T @event) where T : WebhookEvent |
|||
{ |
|||
@event.WebhookId = WebhookId; |
|||
|
|||
return CreateEvent(@event); |
|||
} |
|||
|
|||
protected T CreateWebhookCommand<T>(T command) where T : WebhookAggregateCommand |
|||
{ |
|||
command.WebhookId = WebhookId; |
|||
|
|||
return CreateCommand(command); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue