Browse Source

Solve deadlock in rules.

pull/283/head
Sebastian 8 years ago
parent
commit
a699cdcf07
  1. 3
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  2. 16
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs
  3. 87
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
  4. 52
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs
  5. 108
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs
  6. 58
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs
  7. 66
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
  8. 67
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  9. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs
  10. 22
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  11. 16
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  12. 7
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  13. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  14. 1
      src/Squidex.Infrastructure/CachingProviderBase.cs
  15. 1
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  16. 1
      src/Squidex/Config/Orleans/SiloWrapper.cs
  17. 1
      src/Squidex/Program.cs
  18. 86
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  19. 41
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  20. 5
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs

3
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using Newtonsoft.Json.Linq;
using NodaTime;
namespace Squidex.Domain.Apps.Core.Rules
@ -28,6 +29,6 @@ namespace Squidex.Domain.Apps.Core.Rules
public Instant Expires { get; set; }
public RuleJobData ActionData { get; set; }
public JObject ActionData { get; set; }
}
}

16
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.Rules
{
public sealed class RuleJobData : Dictionary<string, JToken>
{
}
}

87
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs

@ -11,17 +11,32 @@ using Algolia.Search;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class AlgoliaActionHandler : RuleActionHandler<AlgoliaAction>
public sealed class AlgoliaJob
{
public string AppId { get; set; }
public string ApiKey { get; set; }
public string ContentId { get; set; }
public string IndexName { get; set; }
public JObject Content { get; set; }
}
public sealed class AlgoliaActionHandler : RuleActionHandler<AlgoliaAction, AlgoliaJob>
{
private const string DescriptionIgnore = "Ignore";
private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients;
private readonly RuleEventFormatter formatter;
@ -39,20 +54,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
});
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, AlgoliaAction action)
protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, AlgoliaAction action)
{
var ruleDescription = string.Empty;
var ruleData = new RuleJobData
if (@event.Payload is ContentEvent contentEvent)
{
["AppId"] = action.AppId,
["ApiKey"] = action.ApiKey
};
var contentId = contentEvent.ContentId.ToString();
if (@event.Payload is ContentEvent contentEvent)
var ruleDescription = string.Empty;
var ruleJob = new AlgoliaJob
{
ruleData["ContentId"] = contentEvent.ContentId.ToString();
ruleData["Operation"] = "Upsert";
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
AppId = action.AppId,
ApiKey = action.ApiKey,
ContentId = contentId,
IndexName = await formatter.FormatStringAsync(action.IndexName, @event)
};
var timestamp = @event.Headers.Timestamp().ToString();
@ -62,8 +77,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
new JProperty("id", contentEvent.ContentId),
ruleJob.Content = new JObject(
new JProperty("objectID", contentId),
new JProperty("id", contentId),
new JProperty("created", timestamp),
new JProperty("createdBy", created.Actor.ToString()),
new JProperty("lastModified", timestamp),
@ -77,7 +93,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
ruleJob.Content = new JObject(
new JProperty("objectID", contentId),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()),
new JProperty("data", formatter.ToRouteData(updated.Data)));
@ -88,7 +105,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
ruleJob.Content = new JObject(
new JProperty("objectID", contentId),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()),
new JProperty("status", statusChanged.Status.ToString()));
@ -99,56 +117,39 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject();
ruleData["Operation"] = "Delete";
break;
}
}
return (ruleDescription, ruleJob);
}
return (ruleDescription, ruleData);
return (DescriptionIgnore, new AlgoliaJob());
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AlgoliaJob job)
{
if (!job.TryGetValue("Operation", out var operationToken))
if (string.IsNullOrWhiteSpace(job.AppId))
{
return (null, new InvalidOperationException("The action cannot handle this event."));
}
var appId = job["AppId"].Value<string>();
var apiKey = job["ApiKey"].Value<string>();
var indexName = job["IndexName"].Value<string>();
var index = clients.GetClient((appId, apiKey, indexName));
var operation = operationToken.Value<string>();
var content = job["Content"].Value<JObject>();
var contentId = job["ContentId"].Value<string>();
var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName));
try
{
switch (operation)
if (job.Content != null)
{
case "Upsert":
{
content["objectID"] = contentId;
var response = await index.PartialUpdateObjectAsync(content);
var response = await index.PartialUpdateObjectAsync(job.Content);
return (response.ToString(Formatting.Indented), null);
}
case "Delete":
else
{
var response = await index.DeleteObjectAsync(contentId);
var response = await index.DeleteObjectAsync(job.ContentId);
return (response.ToString(Formatting.Indented), null);
}
default:
return (null, null);
}
}
catch (AlgoliaException ex)
{

52
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs

@ -11,15 +11,34 @@ using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class AzureQueueActionHandler : RuleActionHandler<AzureQueueAction>
public sealed class AzureQueueJob
{
public string QueueConnectionString { get; set; }
public string QueueName { get; set; }
public string MessageBodyV2 { get; set; }
public JObject MessageBody { get; set; }
public string Body
{
get
{
return MessageBodyV2 ?? MessageBody.ToString(Formatting.Indented);
}
}
}
public sealed class AzureQueueActionHandler : RuleActionHandler<AzureQueueAction, AzureQueueJob>
{
private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients;
private readonly RuleEventFormatter formatter;
@ -41,31 +60,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
});
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, AzureQueueAction action)
protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, AzureQueueAction action)
{
var body = formatter.ToRouteData(@event, eventName);
var body = formatter.ToRouteData(@event, eventName).ToString(Formatting.Indented);
var ruleDescription = $"Send event to azure queue '{action.Queue}'";
var ruleData = new RuleJobData
var queueName = await formatter.FormatStringAsync(action.Queue, @event);
var ruleDescription = $"Send AzureQueueJob to azure queue '{action.Queue}'";
var ruleJob = new AzureQueueJob
{
["QueueConnectionString"] = action.ConnectionString,
["QueueName"] = action.Queue,
["MessageBody"] = body
QueueConnectionString = action.ConnectionString,
QueueName = queueName,
MessageBodyV2 = body,
};
return (ruleDescription, ruleData);
return (ruleDescription, ruleJob);
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AzureQueueJob job)
{
var queueConnectionString = job["QueueConnectionString"].Value<string>();
var queueName = job["QueueName"].Value<string>();
var queue = clients.GetClient((queueConnectionString, queueName));
var messageBody = job["MessageBody"].ToString(Formatting.Indented);
var queue = clients.GetClient((job.QueueConnectionString, job.QueueName));
await queue.AddMessageAsync(new CloudQueueMessage(messageBody));
await queue.AddMessageAsync(new CloudQueueMessage(job.Body));
return ("Completed", null);
}

108
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs

@ -10,17 +10,37 @@ using System.Threading.Tasks;
using Elasticsearch.Net;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class ElasticSearchActionHandler : RuleActionHandler<ElasticSearchAction>
public sealed class ElasticSearchJob
{
public string Host { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string ContentId { get; set; }
public string IndexName { get; set; }
public string IndexType { get; set; }
public string Operation { get; set; }
public JObject Content { get; set; }
}
public sealed class ElasticSearchActionHandler : RuleActionHandler<ElasticSearchAction, ElasticSearchJob>
{
private const string DescriptionIgnore = "Ignore";
private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients;
private readonly RuleEventFormatter formatter;
@ -43,37 +63,40 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
});
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, ElasticSearchAction action)
{
var ruleDescription = string.Empty;
var ruleData = new RuleJobData
protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, ElasticSearchAction action)
{
["Host"] = action.Host,
["Username"] = action.Username,
["Password"] = action.Password
};
if (@event.Payload is ContentEvent contentEvent)
{
ruleData["ContentId"] = contentEvent.ContentId.ToString();
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
ruleData["IndexType"] = formatter.FormatString(action.IndexType, @event);
var contentId = contentEvent.ContentId.ToString();
var ruleDescription = string.Empty;
var ruleJob = new ElasticSearchJob
{
Host = action.Host.ToString(),
Username = action.Username,
Password = action.Password,
ContentId = contentId,
IndexName = await formatter.FormatStringAsync(action.IndexName, @event),
IndexType = await formatter.FormatStringAsync(action.IndexType, @event),
};
var timestamp = @event.Headers.Timestamp().ToString();
var actor = @event.Payload.Actor.ToString();
switch (@event.Payload)
{
case ContentCreated created:
{
ruleDescription = $"Add entry to ES index: {action.IndexName}";
ruleData["Operation"] = "Create";
ruleData["Content"] = new JObject(
new JProperty("id", contentEvent.ContentId),
ruleJob.Operation = "Create";
ruleJob.Content = new JObject(
new JProperty("id", contentId),
new JProperty("created", timestamp),
new JProperty("createdBy", created.Actor.ToString()),
new JProperty("createdBy", actor),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", created.Actor.ToString()),
new JProperty("lastModifiedBy", actor),
new JProperty("status", Status.Draft.ToString()),
new JProperty("data", formatter.ToRouteData(created.Data)));
break;
@ -83,10 +106,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update";
ruleData["Content"] = new JObject(
ruleJob.Operation = "Update";
ruleJob.Content = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()),
new JProperty("lastModifiedBy", actor),
new JProperty("data", formatter.ToRouteData(updated.Data)));
break;
}
@ -95,10 +118,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update";
ruleData["Content"] = new JObject(
ruleJob.Operation = "Update";
ruleJob.Content = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()),
new JProperty("lastModifiedBy", actor),
new JProperty("status", statusChanged.Status.ToString()));
break;
}
@ -107,62 +130,49 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Delete entry from ES index: {action.IndexName}";
ruleData["Operation"] = "Delete";
ruleData["Content"] = new JObject();
ruleJob.Operation = "Delete";
break;
}
}
}
return (ruleDescription, ruleData);
return (DescriptionIgnore, new ElasticSearchJob());
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(ElasticSearchJob job)
{
if (!job.TryGetValue("Operation", out var operationToken))
if (string.IsNullOrWhiteSpace(job.Operation))
{
return (null, new InvalidOperationException("The action cannot handle this event."));
}
var host = new Uri(job["Host"].Value<string>(), UriKind.Absolute);
var username = job["Username"].Value<string>();
var password = job["Password"].Value<string>();
var client = clients.GetClient((host, username, password));
var indexName = job["IndexName"].Value<string>();
var indexType = job["IndexType"].Value<string>();
var operation = operationToken.Value<string>();
var content = job["Content"].Value<JObject>();
var contentId = job["ContentId"].Value<string>();
var client = clients.GetClient((new Uri(job.Host, UriKind.Absolute), job.Username, job.Password));
try
{
switch (operation)
switch (job.Operation)
{
case "Create":
{
var doc = content.ToString();
var doc = job.Content.ToString();
var response = await client.IndexAsync<StringResponse>(indexName, indexType, contentId, doc);
var response = await client.IndexAsync<StringResponse>(job.IndexName, job.IndexType, job.ContentId, doc);
return (response.Body, response.OriginalException);
}
case "Update":
{
var doc = new JObject(new JProperty("doc", content)).ToString();
var doc = new JObject(new JProperty("doc", job.Content)).ToString();
var response = await client.UpdateAsync<StringResponse>(indexName, indexType, contentId, doc);
var response = await client.UpdateAsync<StringResponse>(job.IndexName, job.IndexType, job.ContentId, doc);
return (response.Body, response.OriginalException);
}
case "Delete":
{
var response = await client.DeleteAsync<StringResponse>(indexName, indexType, contentId);
var response = await client.DeleteAsync<StringResponse>(job.IndexName, job.IndexType, job.ContentId);
return (response.Body, response.OriginalException);
}

58
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs

@ -6,45 +6,54 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class FastlyActionHandler : RuleActionHandler<FastlyAction>
{
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, FastlyAction action)
public sealed class FastlyJob
{
var ruleDescription = "Purge key in fastly";
var ruleData = new RuleJobData
public string Key { get; set; }
public string FastlyApiKey { get; set; }
public string FastlyServiceID { get; set; }
}
public sealed class FastlyActionHandler : RuleActionHandler<FastlyAction, FastlyJob>
{
["FastlyApiKey"] = action.ApiKey,
["FastlyServiceID"] = action.ServiceId
};
private const string Description = "Purge key in fastly";
private const string DescriptionIgnore = "Ignore";
protected override Task<(string Description, FastlyJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, FastlyAction action)
{
if (@event.Headers.Contains(CommonHeaders.AggregateId))
{
ruleData["Key"] = @event.Headers.AggregateId().ToString();
var ruleData = new FastlyJob
{
Key = @event.Headers.AggregateId().ToString(),
FastlyApiKey = action.ApiKey,
FastlyServiceID = action.ServiceId
};
return Task.FromResult((Description, ruleData));
}
return (ruleDescription, ruleData);
return Task.FromResult((DescriptionIgnore, new FastlyJob()));
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(FastlyJob job)
{
if (!job.TryGetValue("Key", out var keyToken))
if (string.IsNullOrWhiteSpace(job.Key))
{
return (null, new InvalidOperationException("The action cannot handle this event."));
}
var requestMsg = BuildRequest(job, keyToken.Value<string>());
var requestMsg = BuildRequest(job);
HttpResponseMessage response = null;
@ -58,30 +67,19 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
return (requestDump, null);
}
catch (Exception ex)
{
if (requestMsg != null)
{
var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, ex.ToString(), TimeSpan.Zero, false);
return (requestDump, ex);
}
else
{
var requestDump = ex.ToString();
return (requestDump, ex);
}
}
}
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string key)
private static HttpRequestMessage BuildRequest(FastlyJob job)
{
var serviceId = job["FastlyServiceID"].Value<string>();
var requestUrl = $"https://api.fastly.com/service/{serviceId}/purge/{key}";
var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}";
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
request.Headers.Add("Fastly-Key", job["FastlyApiKey"].Value<string>());
request.Headers.Add("Fastly-Key", job.FastlyApiKey);
return request;
}

66
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs

@ -6,23 +6,41 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class SlackActionHandler : RuleActionHandler<SlackAction>
public sealed class SlackJob
{
public string RequestUrl { get; set; }
public string RequestBodyV2 { get; set; }
public JObject RequestBody { get; set; }
public string Body
{
get
{
return RequestBodyV2 ?? RequestBody.ToString(Formatting.Indented);
}
}
}
public sealed class SlackActionHandler : RuleActionHandler<SlackAction, SlackJob>
{
private const string Description = "Send message to slack";
private readonly RuleEventFormatter formatter;
public SlackActionHandler(RuleEventFormatter formatter)
@ -32,61 +50,51 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
this.formatter = formatter;
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, SlackAction action)
protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, SlackAction action)
{
var body = CreatePayload(@event, action.Text);
var body = await CreatePayloadAsync(@event, action.Text);
var ruleDescription = "Send message to slack";
var ruleData = new RuleJobData
var ruleJob = new SlackJob
{
["RequestUrl"] = action.WebhookUrl,
["RequestBody"] = body
RequestUrl = action.WebhookUrl.ToString(),
RequestBodyV2 = body.ToString(Formatting.Indented),
};
return (ruleDescription, ruleData);
return (Description, ruleJob);
}
private JObject CreatePayload(Envelope<AppEvent> @event, string text)
private async Task<JObject> CreatePayloadAsync(Envelope<AppEvent> @event, string text)
{
return new JObject(new JProperty("text", formatter.FormatString(text, @event)));
return new JObject(new JProperty("text", await formatter.FormatStringAsync(text, @event)));
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job)
{
var requestBody = job["RequestBody"].ToString(Formatting.Indented);
var requestMsg = BuildRequest(job, requestBody);
var requestBody = job.Body;
var requestMessage = BuildRequest(job, requestBody);
HttpResponseMessage response = null;
try
{
response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg);
response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage);
var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false);
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false);
return (requestDump, null);
}
catch (Exception ex)
{
if (requestMsg != null)
{
var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
return (requestDump, ex);
}
else
{
throw;
}
}
}
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string requestBody)
private static HttpRequestMessage BuildRequest(SlackJob job, string requestBody)
{
var requestUrl = job["RequestUrl"].Value<string>();
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};

67
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs

@ -6,22 +6,39 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction>
public sealed class WebhookJob
{
public string RequestUrl { get; set; }
public string RequestSignature { get; set; }
public string RequestBodyV2 { get; set; }
public JObject RequestBody { get; set; }
public string Body
{
get
{
return RequestBodyV2 ?? RequestBody.ToString(Formatting.Indented);
}
}
}
public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction, WebhookJob>
{
private readonly RuleEventFormatter formatter;
@ -32,36 +49,34 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
this.formatter = formatter;
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, WebhookAction action)
protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, WebhookAction action)
{
var body = formatter.ToRouteData(@event, eventName);
var signature = $"{body.ToString(Formatting.Indented)}{action.SharedSecret}".Sha256Base64();
var body = formatter.ToRouteData(@event, eventName).ToString(Formatting.Indented);
var ruleDescription = $"Send event to webhook '{action.Url}'";
var ruleData = new RuleJobData
var ruleJob = new WebhookJob
{
["RequestUrl"] = action.Url,
["RequestBody"] = body,
["RequestSignature"] = signature
RequestUrl = await formatter.FormatStringAsync(action.Url.ToString(), @event),
RequestSignature = $"{body}{action.SharedSecret}".Sha256Base64(),
RequestBodyV2 = body
};
return (ruleDescription, ruleData);
return (ruleDescription, ruleJob);
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job)
{
var requestBody = job["RequestBody"].ToString(Formatting.Indented);
var requestMsg = BuildRequest(job, requestBody);
var requestBody = job.Body;
var requestMessage = BuildRequest(job, requestBody);
HttpResponseMessage response = null;
try
{
response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg);
response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage);
var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false);
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false);
Exception ex = null;
@ -74,30 +89,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
}
catch (Exception ex)
{
if (requestMsg != null)
{
var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
return (requestDump, ex);
}
else
{
throw;
}
}
}
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string requestBody)
private static HttpRequestMessage BuildRequest(WebhookJob job, string requestBody)
{
var requestUrl = job["RequestUrl"].Value<string>();
var requestSig = job["RequestSignature"].Value<string>();
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};
request.Headers.Add("X-Signature", requestSig);
request.Headers.Add("X-Signature", job.RequestSignature);
request.Headers.Add("User-Agent", "Squidex Webhook");
return request;

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs

@ -7,6 +7,7 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
@ -17,8 +18,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
Type ActionType { get; }
(string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, RuleAction action);
Task<(string Description, JObject Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, RuleAction action);
Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data);
Task<(string Dump, Exception Exception)> ExecuteJobAsync(JObject data);
}
}

22
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs

@ -7,26 +7,36 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public abstract class RuleActionHandler<T> : IRuleActionHandler where T : RuleAction
public abstract class RuleActionHandler<TAction, TData> : IRuleActionHandler where TAction : RuleAction
{
Type IRuleActionHandler.ActionType
{
get { return typeof(T); }
get { return typeof(TAction); }
}
(string Description, RuleJobData Data) IRuleActionHandler.CreateJob(Envelope<AppEvent> @event, string eventName, RuleAction action)
async Task<(string Description, JObject Data)> IRuleActionHandler.CreateJobAsync(Envelope<AppEvent> @event, string eventName, RuleAction action)
{
return CreateJob(@event, eventName, (T)action);
var (description, data) = await CreateJobAsync(@event, eventName, (TAction)action);
return (description, JObject.FromObject(data));
}
async Task<(string Dump, Exception Exception)> IRuleActionHandler.ExecuteJobAsync(JObject data)
{
var typedData = data.ToObject<TData>();
return await ExecuteJobAsync(typedData);
}
protected abstract (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, T action);
protected abstract Task<(string Description, TData Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, TAction action);
public abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job);
protected abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(TData job);
}
}

16
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -9,6 +9,7 @@ using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -67,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
}
public virtual string FormatString(string text, Envelope<AppEvent> @event)
public async virtual Task<string> FormatStringAsync(string text, Envelope<AppEvent> @event)
{
var sb = new StringBuilder(text);
@ -96,7 +97,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
sb.Replace(ContentUrlPlaceholder, urlGenerator.GenerateContentUIUrl(@event.Payload.AppId, contentEvent.SchemaId, contentEvent.ContentId));
}
FormatUserInfo(@event, sb);
await FormatUserInfoAsync(@event, sb);
FormatContentAction(@event, sb);
var result = sb.ToString();
@ -114,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return result;
}
private void FormatUserInfo(Envelope<AppEvent> @event, StringBuilder sb)
private async Task FormatUserInfoAsync(Envelope<AppEvent> @event, StringBuilder sb)
{
var text = sb.ToString();
@ -131,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
}
else
{
var user = FindUser(actor);
var user = await FindUserAsync(actor);
if (user != null)
{
@ -222,17 +224,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
});
}
private IUser FindUser(RefToken actor)
private Task<IUser> FindUserAsync(RefToken actor)
{
var key = $"RuleEventFormatter_Users_${actor.Identifier}";
return memoryCache.GetOrCreate(key, x =>
return memoryCache.GetOrCreateAsync(key, async x =>
{
x.AbsoluteExpirationRelativeToNow = UserCacheDuration;
try
{
return userResolver.FindByIdOrEmailAsync(actor.Identifier).Result;
return await userResolver.FindByIdOrEmailAsync(actor.Identifier);
}
catch
{

7
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -11,6 +11,7 @@ using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
this.clock = clock;
}
public virtual RuleJob CreateJob(Rule rule, Envelope<IEvent> @event)
public virtual async Task<RuleJob> CreateJobAsync(Rule rule, Envelope<IEvent> @event)
{
Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event));
@ -80,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var now = clock.GetCurrentInstant();
var actionName = typeNameRegistry.GetName(actionType);
var actionData = actionHandler.CreateJob(appEventEnvelope, eventName, rule.Action);
var actionData = await actionHandler.CreateJobAsync(appEventEnvelope, eventName, rule.Action);
var eventTime =
@event.Headers.Contains(CommonHeaders.Timestamp) ?
@ -113,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return job;
}
public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData job)
public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, JObject job)
{
try
{

2
src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
foreach (var ruleEntity in rules)
{
var job = ruleService.CreateJob(ruleEntity.RuleDef, @event);
var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, @event);
if (job != null)
{

1
src/Squidex.Infrastructure/CachingProviderBase.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure;
namespace Squidex.Infrastructure
{

1
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users;

1
src/Squidex/Config/Orleans/SiloWrapper.cs

@ -7,7 +7,6 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

1
src/Squidex/Program.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;

86
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -79,88 +79,88 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
}
[Fact]
public void Should_replace_app_information_from_event()
public async Task Should_replace_app_information_from_event()
{
var @event = new ContentCreated { AppId = appId };
var result = sut.FormatString("Name $APP_NAME has id $APP_ID", AsEnvelope(@event));
var result = await sut.FormatStringAsync("Name $APP_NAME has id $APP_ID", AsEnvelope(@event));
Assert.Equal($"Name my-app has id {appId.Id}", result);
}
[Fact]
public void Should_replace_schema_information_from_event()
public async Task Should_replace_schema_information_from_event()
{
var @event = new ContentCreated { SchemaId = schemaId };
var result = sut.FormatString("Name $SCHEMA_NAME has id $SCHEMA_ID", AsEnvelope(@event));
var result = await sut.FormatStringAsync("Name $SCHEMA_NAME has id $SCHEMA_ID", AsEnvelope(@event));
Assert.Equal($"Name my-schema has id {schemaId.Id}", result);
}
[Fact]
public void Should_replace_timestamp_information_from_event()
public async Task Should_replace_timestamp_information_from_event()
{
var now = DateTime.UtcNow;
var envelope = AsEnvelope(new ContentCreated()).SetTimestamp(Instant.FromDateTimeUtc(now));
var result = sut.FormatString("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope);
var result = await sut.FormatStringAsync("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope);
Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result);
}
[Fact]
public void Should_format_email_and_display_name_from_user()
public async Task Should_format_email_and_display_name_from_user()
{
A.CallTo(() => userResolver.FindByIdOrEmailAsync("123"))
.Returns(user);
var @event = new ContentCreated { Actor = new RefToken("subject", "123") };
var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
Assert.Equal($"From me (me@email.com)", result);
}
[Fact]
public void Should_return_undefined_if_user_is_not_found()
public async Task Should_return_undefined_if_user_is_not_found()
{
A.CallTo(() => userResolver.FindByIdOrEmailAsync("123"))
.Returns(Task.FromResult<IUser>(null));
var @event = new ContentCreated { Actor = new RefToken("subject", "123") };
var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
Assert.Equal($"From UNDEFINED (UNDEFINED)", result);
}
[Fact]
public void Should_return_undefined_if_user_failed_to_resolve()
public async Task Should_return_undefined_if_user_failed_to_resolve()
{
A.CallTo(() => userResolver.FindByIdOrEmailAsync("123"))
.Throws(new InvalidOperationException());
var @event = new ContentCreated { Actor = new RefToken("subject", "123") };
var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
Assert.Equal($"From UNDEFINED (UNDEFINED)", result);
}
[Fact]
public void Should_format_email_and_display_name_from_client()
public async Task Should_format_email_and_display_name_from_client()
{
var @event = new ContentCreated { Actor = new RefToken("client", "android") };
var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event));
Assert.Equal($"From client:android (client:android)", result);
}
[Fact]
public void Should_replacecontent_url_from_event()
public async Task Should_replace_content_url_from_event()
{
var url = "http://content";
@ -169,13 +169,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
var @event = new ContentCreated { AppId = appId, ContentId = contentId, SchemaId = schemaId };
var result = sut.FormatString("Go to $CONTENT_URL", AsEnvelope(@event));
var result = await sut.FormatStringAsync("Go to $CONTENT_URL", AsEnvelope(@event));
Assert.Equal($"Go to {url}", result);
}
[Fact]
public void Should_return_undefined_when_field_not_found()
public async Task Should_return_undefined_when_field_not_found()
{
var @event = new ContentCreated
{
@ -186,13 +186,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
var result = sut.FormatString("$CONTENT_DATA.country.iv", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.country.iv", AsEnvelope(@event));
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_undefined_when_partition_not_found()
public async Task Should_return_undefined_when_partition_not_found()
{
var @event = new ContentCreated
{
@ -203,13 +203,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
var result = sut.FormatString("$CONTENT_DATA.city.de", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.de", AsEnvelope(@event));
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_undefined_when_array_item_not_found()
public async Task Should_return_undefined_when_array_item_not_found()
{
var @event = new ContentCreated
{
@ -220,13 +220,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", new JArray()))
};
var result = sut.FormatString("$CONTENT_DATA.city.de.10", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.de.10", AsEnvelope(@event));
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_undefined_when_property_not_found()
public async Task Should_return_undefined_when_property_not_found()
{
var @event = new ContentCreated
{
@ -238,13 +238,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new JProperty("name", "Berlin"))))
};
var result = sut.FormatString("$CONTENT_DATA.city.de.Name", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.de.Name", AsEnvelope(@event));
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_plain_value_when_found()
public async Task Should_return_plain_value_when_found()
{
var @event = new ContentCreated
{
@ -255,13 +255,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event));
Assert.Equal("Berlin", result);
}
[Fact]
public void Should_return_plain_value_when_found_from_update_event()
public async Task Should_return_plain_value_when_found_from_update_event()
{
var @event = new ContentUpdated
{
@ -272,13 +272,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event));
Assert.Equal("Berlin", result);
}
[Fact]
public void Should_return_undefined_when_null()
public async Task Should_return_undefined_when_null()
{
var @event = new ContentCreated
{
@ -289,13 +289,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JValue.CreateNull()))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event));
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_undefined_when_undefined()
public async Task Should_return_undefined_when_undefined()
{
var @event = new ContentCreated
{
@ -306,13 +306,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JValue.CreateUndefined()))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event));
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_string_when_object()
public async Task Should_return_string_when_object()
{
var @event = new ContentCreated
{
@ -324,13 +324,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new JProperty("name", "Berlin"))))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event));
Assert.Equal(JObject.FromObject(new { name = "Berlin" }).ToString(Formatting.Indented), result);
}
[Fact]
public void Should_return_plain_value_from_array_when_found()
public async Task Should_return_plain_value_from_array_when_found()
{
var @event = new ContentCreated
{
@ -342,13 +342,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
"Berlin")))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv.0", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv.0", AsEnvelope(@event));
Assert.Equal("Berlin", result);
}
[Fact]
public void Should_return_plain_value_from_object_when_found()
public async Task Should_return_plain_value_from_object_when_found()
{
var @event = new ContentCreated
{
@ -360,19 +360,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new JProperty("name", "Berlin"))))
};
var result = sut.FormatString("$CONTENT_DATA.city.iv.name", AsEnvelope(@event));
var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv.name", AsEnvelope(@event));
Assert.Equal("Berlin", result);
}
[Fact]
public void Should_format_content_actions_when_found()
public async Task Should_format_content_actions_when_found()
{
Assert.Equal("created", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentCreated())));
Assert.Equal("updated", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentUpdated())));
Assert.Equal("deleted", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentDeleted())));
Assert.Equal("created", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentCreated())));
Assert.Equal("updated", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentUpdated())));
Assert.Equal("deleted", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentDeleted())));
Assert.Equal("set to archived", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentStatusChanged { Status = Status.Archived })));
Assert.Equal("set to archived", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentStatusChanged { Status = Status.Archived })));
}
private static Envelope<AppEvent> AsEnvelope<T>(T @event) where T : AppEvent

41
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -8,6 +8,7 @@
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
@ -66,40 +67,40 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
}
[Fact]
public void Should_not_create_job_for_invalid_event()
public async Task Should_not_create_job_for_invalid_event()
{
var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction());
var ruleEnvelope = Envelope.Create(new InvalidEvent());
var job = sut.CreateJob(ruleConfig, ruleEnvelope);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
Assert.Null(job);
}
[Fact]
public void Should_not_create_job_if_no_trigger_handler_registered()
public async Task Should_not_create_job_if_no_trigger_handler_registered()
{
var ruleConfig = new Rule(new InvalidTrigger(), new WebhookAction());
var ruleEnvelope = Envelope.Create(new ContentCreated());
var job = sut.CreateJob(ruleConfig, ruleEnvelope);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
Assert.Null(job);
}
[Fact]
public void Should_not_create_job_if_no_action_handler_registered()
public async Task Should_not_create_job_if_no_action_handler_registered()
{
var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction());
var ruleEnvelope = Envelope.Create(new ContentCreated());
var job = sut.CreateJob(ruleConfig, ruleEnvelope);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
Assert.Null(job);
}
[Fact]
public void Should_not_create_if_not_triggered()
public async Task Should_not_create_if_not_triggered()
{
var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction());
var ruleEnvelope = Envelope.Create(new ContentCreated());
@ -107,13 +108,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger))
.Returns(false);
var job = sut.CreateJob(ruleConfig, ruleEnvelope);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
Assert.Null(job);
}
[Fact]
public void Should_not_create_job_if_too_old()
public async Task Should_not_create_job_if_too_old()
{
var e = new ContentCreated { SchemaId = new NamedId<Guid>(Guid.NewGuid(), "my-schema"), AppId = new NamedId<Guid>(Guid.NewGuid(), "my-event") };
@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
ruleEnvelope.SetTimestamp(now.Minus(Duration.FromDays(3)));
var actionData = new RuleJobData();
var actionData = new JObject();
var actionDescription = "MyDescription";
var eventName = "MySchemaCreatedEvent";
@ -135,16 +136,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger))
.Returns(true);
A.CallTo(() => ruleActionHandler.CreateJob(A<Envelope<AppEvent>>.Ignored, eventName, ruleConfig.Action))
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<Envelope<AppEvent>>.Ignored, eventName, ruleConfig.Action))
.Returns((actionDescription, actionData));
var job = sut.CreateJob(ruleConfig, ruleEnvelope);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
Assert.Null(job);
}
[Fact]
public void Should_create_job_if_triggered()
public async Task Should_create_job_if_triggered()
{
var e = new ContentCreated { SchemaId = new NamedId<Guid>(Guid.NewGuid(), "my-schema"), AppId = new NamedId<Guid>(Guid.NewGuid(), "my-event") };
@ -156,7 +157,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
ruleEnvelope.SetTimestamp(now);
var actionName = "WebhookAction";
var actionData = new RuleJobData();
var actionData = new JObject();
var actionDescription = "MyDescription";
var eventName = "MySchemaCreatedEvent";
@ -167,10 +168,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger))
.Returns(true);
A.CallTo(() => ruleActionHandler.CreateJob(A<Envelope<AppEvent>>.Ignored, eventName, ruleConfig.Action))
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<Envelope<AppEvent>>.Ignored, eventName, ruleConfig.Action))
.Returns((actionDescription, actionData));
var job = sut.CreateJob(ruleConfig, ruleEnvelope);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
Assert.Equal(eventName, job.EventName);
@ -189,7 +190,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Fact]
public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception()
{
var ruleJob = new RuleJobData();
var ruleJob = new JObject();
var ruleEx = new InvalidOperationException();
var actionDump = "MyDump";
@ -208,7 +209,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Fact]
public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception()
{
var ruleJob = new RuleJobData();
var ruleJob = new JObject();
var ruleEx = new InvalidOperationException();
var actionDump = "MyDump";
@ -227,7 +228,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Fact]
public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout()
{
var ruleJob = new RuleJobData();
var ruleJob = new JObject();
var ruleEx = new InvalidOperationException();
var actionDump = "MyDump";
@ -247,7 +248,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Fact]
public async Task Should_create_exception_details_when_job_to_execute_failed()
{
var ruleJob = new RuleJobData();
var ruleJob = new JObject();
var ruleEx = new InvalidOperationException();
A.CallTo(() => ruleActionHandler.ExecuteJobAsync(ruleJob))

5
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs

@ -8,6 +8,7 @@
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
@ -49,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
[InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)]
public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult)
{
var actionData = new RuleJobData();
var actionData = new JObject();
var actionName = "MyAction";
var @event = CreateEvent(calls, actionName, actionData);
@ -73,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
.MustHaveHappened();
}
private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData)
private IRuleEventEntity CreateEvent(int numCalls, string actionName, JObject actionData)
{
var @event = A.Fake<IRuleEventEntity>();

Loading…
Cancel
Save