mirror of https://github.com/Squidex/squidex.git
Browse Source
* Flow fixes. * Temp * Remove duplicate timers. * Add dump. * Fix styles and UI bugs. * More and fixed tests * Fix tests * Fix e2e testspull/1221/head
committed by
GitHub
128 changed files with 7405 additions and 682 deletions
File diff suppressed because it is too large
@ -0,0 +1,43 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace Squidex.Providers.MySql.App.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddCronJobs : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "CronJobs", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
DueTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false), |
||||
|
Data = table.Column<string>(type: "longtext", nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4") |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_CronJobs", x => x.Id); |
||||
|
}) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_CronJobs_DueTime", |
||||
|
table: "CronJobs", |
||||
|
column: "DueTime"); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "CronJobs"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace Squidex.Providers.Postgres.App.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddCronJobs : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "CronJobs", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
DueTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), |
||||
|
Data = table.Column<string>(type: "text", nullable: false) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_CronJobs", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_CronJobs_DueTime", |
||||
|
table: "CronJobs", |
||||
|
column: "DueTime"); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "CronJobs"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace Squidex.Providers.SqlServer.App.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddCronJobs : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "CronJobs", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
DueTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false), |
||||
|
Data = table.Column<string>(type: "nvarchar(max)", nullable: false) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_CronJobs", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_CronJobs_DueTime", |
||||
|
table: "CronJobs", |
||||
|
column: "DueTime"); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "CronJobs"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
|
||||
|
public sealed class EnrichedCronJobEvent : EnrichedUserEventBase |
||||
|
{ |
||||
|
public JsonValue Value { get; set; } |
||||
|
|
||||
|
public override long Partition |
||||
|
{ |
||||
|
get => 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
|
||||
|
[TypeName(nameof(CronJobTrigger))] |
||||
|
public sealed record CronJobTrigger : RuleTrigger |
||||
|
{ |
||||
|
public string CronExpression { get; init; } |
||||
|
|
||||
|
public string? CronTimezone { get; init; } |
||||
|
|
||||
|
public JsonValue Value { get; init; } |
||||
|
|
||||
|
public override T Accept<T, TArgs>(IRuleTriggerVisitor<T, TArgs> visitor, TArgs args) |
||||
|
{ |
||||
|
return visitor.Visit(this, args); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules; |
||||
|
|
||||
|
public sealed record CronJobContext(NamedId<DomainId> AppId, DomainId RuleId); |
||||
@ -0,0 +1,44 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Rules; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public sealed class CronJobTriggerHandler : IRuleTriggerHandler |
||||
|
{ |
||||
|
public Type TriggerType => typeof(CronJobTrigger); |
||||
|
|
||||
|
public bool Handles(AppEvent appEvent) |
||||
|
{ |
||||
|
return appEvent is RuleCronJobTriggered; |
||||
|
} |
||||
|
|
||||
|
public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RulesContext context, |
||||
|
[EnumeratorCancellation] CancellationToken ct) |
||||
|
{ |
||||
|
var result = new EnrichedCronJobEvent(); |
||||
|
|
||||
|
// Use the concrete event to map properties that are not part of app event.
|
||||
|
SimpleMapper.Map((RuleCronJobTriggered)@event.Payload, result); |
||||
|
|
||||
|
await Task.Yield(); |
||||
|
yield return result; |
||||
|
} |
||||
|
|
||||
|
public string? GetName(AppEvent @event) |
||||
|
{ |
||||
|
return "CronJob"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Events.Rules; |
||||
|
using Squidex.Events; |
||||
|
using Squidex.Flows.CronJobs; |
||||
|
using Squidex.Hosting; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public sealed class CronJobUpdater( |
||||
|
IAppProvider appProvider, |
||||
|
ICronJobManager<CronJobContext> cronJobs, |
||||
|
IRuleEnqueuer ruleEnqueuer) |
||||
|
: IEventConsumer, IInitializable |
||||
|
{ |
||||
|
public StreamFilter EventsFilter => StreamFilter.Prefix("rule-"); |
||||
|
|
||||
|
public Task InitializeAsync( |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
cronJobs.Subscribe(HandleCronJobAsync); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public async Task HandleCronJobAsync(CronJob<CronJobContext> job, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var (appId, ruleId) = job.Context; |
||||
|
|
||||
|
var rule = await appProvider.GetRuleAsync(appId.Id, ruleId, ct); |
||||
|
|
||||
|
// The rule might have been updated or deleted in the meantime, but we are running asynchronously.
|
||||
|
if (rule == null || rule.Trigger is not CronJobTrigger cronJob) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// The rule enqueue needs an event.
|
||||
|
var @event = new RuleCronJobTriggered { AppId = appId, RuleId = ruleId, Value = cronJob.Value }; |
||||
|
|
||||
|
await ruleEnqueuer.EnqueueAsync(rule, Envelope.Create(@event), ct); |
||||
|
} |
||||
|
|
||||
|
public async Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
if (@event.Payload is RuleCreated created) |
||||
|
{ |
||||
|
if (created.Trigger is CronJobTrigger cronJob) |
||||
|
{ |
||||
|
await AddCronJobAsync(created.AppId, created.RuleId, cronJob, default); |
||||
|
} |
||||
|
} |
||||
|
else if (@event.Payload is RuleUpdated updated && updated.Trigger != null) |
||||
|
{ |
||||
|
if (updated.Trigger is CronJobTrigger cronJob) |
||||
|
{ |
||||
|
await AddCronJobAsync(updated.AppId, updated.RuleId, cronJob, default); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
await cronJobs.RemoveAsync(updated.RuleId.ToString()); |
||||
|
} |
||||
|
} |
||||
|
else if (@event.Payload is RuleDeleted deleted) |
||||
|
{ |
||||
|
await cronJobs.RemoveAsync(deleted.RuleId.ToString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task AddCronJobAsync(NamedId<DomainId> appId, DomainId id, CronJobTrigger trigger, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
await cronJobs.AddAsync(new CronJob<CronJobContext> |
||||
|
{ |
||||
|
Id = id.ToString(), |
||||
|
CronExpression = trigger.CronExpression, |
||||
|
CronTimezone = trigger.CronTimezone, |
||||
|
Context = new CronJobContext(appId, id), |
||||
|
}, ct); |
||||
|
} |
||||
|
} |
||||
@ -1,26 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using NodaTime; |
|
||||
using Squidex.Domain.Apps.Core.HandleRules; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Rules; |
|
||||
|
|
||||
public sealed class RuleJobUpdate |
|
||||
{ |
|
||||
public string? ExecutionDump { get; set; } |
|
||||
|
|
||||
public RuleResult ExecutionResult { get; set; } |
|
||||
|
|
||||
public RuleJobResult JobResult { get; set; } |
|
||||
|
|
||||
public TimeSpan Elapsed { get; set; } |
|
||||
|
|
||||
public Instant Finished { get; set; } |
|
||||
|
|
||||
public Instant? JobNext { get; set; } |
|
||||
} |
|
||||
@ -1,105 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Globalization; |
|
||||
using System.Net; |
|
||||
using System.Net.Http.Headers; |
|
||||
using System.Text; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Http; |
|
||||
|
|
||||
public static class DumpFormatter |
|
||||
{ |
|
||||
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? responseBody) |
|
||||
{ |
|
||||
return BuildDump(request, response, null, responseBody, TimeSpan.Zero); |
|
||||
} |
|
||||
|
|
||||
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody) |
|
||||
{ |
|
||||
return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); |
|
||||
} |
|
||||
|
|
||||
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody, TimeSpan elapsed, bool isTimeout = false) |
|
||||
{ |
|
||||
var writer = new StringBuilder(); |
|
||||
|
|
||||
writer.AppendLine("Request:"); |
|
||||
writer.AppendRequest(request, requestBody); |
|
||||
|
|
||||
writer.AppendLine(); |
|
||||
writer.AppendLine(); |
|
||||
|
|
||||
writer.AppendLine("Response:"); |
|
||||
writer.AppendResponse(response, responseBody, elapsed, isTimeout); |
|
||||
|
|
||||
return writer.ToString(); |
|
||||
} |
|
||||
|
|
||||
private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string? requestBody) |
|
||||
{ |
|
||||
var method = request.Method.ToString().ToUpperInvariant(); |
|
||||
|
|
||||
writer.AppendLine(CultureInfo.InvariantCulture, $"{method}: {request.RequestUri} HTTP/{request.Version}"); |
|
||||
|
|
||||
writer.AppendHeaders(request.Headers); |
|
||||
writer.AppendHeaders(request.Content?.Headers); |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(requestBody)) |
|
||||
{ |
|
||||
writer.AppendLine(); |
|
||||
writer.AppendLine(requestBody); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static void AppendResponse(this StringBuilder writer, HttpResponseMessage? response, string? responseBody, TimeSpan elapsed, bool isTimeout) |
|
||||
{ |
|
||||
if (response != null) |
|
||||
{ |
|
||||
var responseCode = (int)response.StatusCode; |
|
||||
var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); |
|
||||
|
|
||||
writer.AppendLine(CultureInfo.InvariantCulture, $"HTTP/{response.Version} {responseCode} {responseText}"); |
|
||||
|
|
||||
writer.AppendHeaders(response.Headers); |
|
||||
writer.AppendHeaders(response.Content?.Headers); |
|
||||
} |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(responseBody)) |
|
||||
{ |
|
||||
writer.AppendLine(); |
|
||||
writer.AppendLine(responseBody); |
|
||||
} |
|
||||
|
|
||||
if (response != null && elapsed != TimeSpan.Zero) |
|
||||
{ |
|
||||
writer.AppendLine(); |
|
||||
writer.AppendLine(CultureInfo.InvariantCulture, $"Elapsed: {elapsed}"); |
|
||||
} |
|
||||
|
|
||||
if (isTimeout) |
|
||||
{ |
|
||||
writer.AppendLine(CultureInfo.InvariantCulture, $"Timeout after {elapsed}"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static void AppendHeaders(this StringBuilder writer, HttpHeaders? headers) |
|
||||
{ |
|
||||
if (headers == null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
foreach (var (key, value) in headers) |
|
||||
{ |
|
||||
writer.Append(key); |
|
||||
writer.Append(": "); |
|
||||
writer.Append(string.Join("; ", value)); |
|
||||
writer.AppendLine(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,43 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; |
||||
|
|
||||
|
public sealed class CronJobRuleTriggerDto : RuleTriggerDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The cron expression that defines the interval.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string CronExpression { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The optional timezone.
|
||||
|
/// </summary>
|
||||
|
public string? CronTimezone { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The value sent to the flow.
|
||||
|
/// </summary>
|
||||
|
public JsonValue Value { get; init; } |
||||
|
|
||||
|
public static CronJobRuleTriggerDto FromDomain(CronJobTrigger trigger) |
||||
|
{ |
||||
|
return SimpleMapper.Map(trigger, new CronJobRuleTriggerDto()); |
||||
|
} |
||||
|
|
||||
|
public override RuleTrigger ToTrigger() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new CronJobTrigger()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Rules; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public class CronJobTriggerHandlerTests |
||||
|
{ |
||||
|
private readonly IRuleTriggerHandler sut = new CronJobTriggerHandler(); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_false_if_asking_for_snapshot_support() |
||||
|
{ |
||||
|
Assert.False(sut.CanCreateSnapshotEvents); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_calculate_name() |
||||
|
{ |
||||
|
var @event = new RuleCronJobTriggered(); |
||||
|
|
||||
|
Assert.Equal("CronJob", sut.GetName(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_create_event_with_name() |
||||
|
{ |
||||
|
var @event = TestUtils.CreateEvent<RuleCronJobTriggered>(); |
||||
|
var envelope = Envelope.Create<AppEvent>(@event); |
||||
|
|
||||
|
var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); |
||||
|
|
||||
|
var enrichedEvent = (EnrichedCronJobEvent)actual.Single(); |
||||
|
|
||||
|
Assert.Equal(@event.Actor, enrichedEvent.Actor); |
||||
|
Assert.Equal(@event.AppId, enrichedEvent.AppId); |
||||
|
Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_create_event_with_actor() |
||||
|
{ |
||||
|
var actor = RefToken.User("me"); |
||||
|
|
||||
|
var @event = new RuleCronJobTriggered { Actor = actor }; |
||||
|
var envelope = Envelope.Create<AppEvent>(@event); |
||||
|
|
||||
|
var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); |
||||
|
|
||||
|
Assert.Equal(actor, ((EnrichedUserEventBase)actual.Single()).Actor); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_always_trigger() |
||||
|
{ |
||||
|
var @event = new RuleCronJobTriggered(); |
||||
|
|
||||
|
Assert.True(sut.Trigger(Envelope.Create<AppEvent>(@event), null!)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_always_trigger_enriched_event() |
||||
|
{ |
||||
|
var @event = new EnrichedUsageExceededEvent(); |
||||
|
|
||||
|
Assert.True(sut.Trigger(@event, null!)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,221 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Events.Rules; |
||||
|
using Squidex.Events; |
||||
|
using Squidex.Flows.CronJobs; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public sealed class CronJobUpdaterTests : GivenContext |
||||
|
{ |
||||
|
private readonly ICronJobManager<CronJobContext> cronJobs = A.Fake<ICronJobManager<CronJobContext>>(); |
||||
|
private readonly IRuleEnqueuer ruleEnqueuer = A.Fake<IRuleEnqueuer>(); |
||||
|
private readonly CronJobUpdater sut; |
||||
|
|
||||
|
public CronJobUpdaterTests() |
||||
|
{ |
||||
|
sut = new CronJobUpdater(AppProvider, cronJobs, ruleEnqueuer); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_rules_filter_for_events_filter() |
||||
|
{ |
||||
|
Assert.Equal(StreamFilter.Prefix("rule-"), sut.EventsFilter); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_register_handler_when_initialized() |
||||
|
{ |
||||
|
await sut.InitializeAsync(default); |
||||
|
|
||||
|
A.CallTo(() => cronJobs.Subscribe(A<Func<CronJob<CronJobContext>, CancellationToken, Task>>._)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_register_new_cron_job() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create( |
||||
|
new RuleCreated |
||||
|
{ |
||||
|
AppId = AppId, |
||||
|
Trigger = new CronJobTrigger |
||||
|
{ |
||||
|
CronExpression = "* */5 * * *", |
||||
|
CronTimezone = "Europe/Berlin", |
||||
|
}, |
||||
|
RuleId = ruleId, |
||||
|
}); |
||||
|
|
||||
|
await sut.On(@event); |
||||
|
|
||||
|
A.CallTo(() => cronJobs.AddAsync( |
||||
|
A<CronJob<CronJobContext>>.That.Matches(x => |
||||
|
x.Id == ruleId.ToString() && |
||||
|
x.Context.RuleId == ruleId && |
||||
|
x.Context.AppId == AppId && |
||||
|
x.CronExpression == "* */5 * * *" && |
||||
|
x.CronTimezone == "Europe/Berlin"), |
||||
|
default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_register_updated_cron_job() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create( |
||||
|
new RuleUpdated |
||||
|
{ |
||||
|
AppId = AppId, |
||||
|
Trigger = new CronJobTrigger |
||||
|
{ |
||||
|
CronExpression = "* */5 * * *", |
||||
|
CronTimezone = "Europe/Berlin", |
||||
|
}, |
||||
|
RuleId = ruleId, |
||||
|
}); |
||||
|
|
||||
|
await sut.On(@event); |
||||
|
|
||||
|
A.CallTo(() => cronJobs.AddAsync( |
||||
|
A<CronJob<CronJobContext>>.That.Matches(x => |
||||
|
x.Id == ruleId.ToString() && |
||||
|
x.Context.RuleId == ruleId && |
||||
|
x.Context.AppId == AppId && |
||||
|
x.CronExpression == "* */5 * * *" && |
||||
|
x.CronTimezone == "Europe/Berlin"), |
||||
|
default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_unregister_when_trigger_changed() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create( |
||||
|
new RuleUpdated |
||||
|
{ |
||||
|
AppId = AppId, |
||||
|
Trigger = new ManualTrigger |
||||
|
{ |
||||
|
}, |
||||
|
RuleId = ruleId, |
||||
|
}); |
||||
|
|
||||
|
await sut.On(@event); |
||||
|
|
||||
|
A.CallTo(() => cronJobs.RemoveAsync(ruleId.ToString(), default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_unregister_cron_job_when_rule_deleted() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create( |
||||
|
new RuleDeleted |
||||
|
{ |
||||
|
RuleId = ruleId, |
||||
|
}); |
||||
|
|
||||
|
await sut.On(@event); |
||||
|
|
||||
|
A.CallTo(() => cronJobs.RemoveAsync(ruleId.ToString(), default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_do_nothing_when_trigger_not_changed() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create( |
||||
|
new RuleUpdated |
||||
|
{ |
||||
|
RuleId = ruleId, |
||||
|
}); |
||||
|
|
||||
|
await sut.On(@event); |
||||
|
|
||||
|
A.CallTo(cronJobs) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_rule_enqueue_when_cron_job_due() |
||||
|
{ |
||||
|
var rule = CreateAndSetupRule(new CronJobTrigger()); |
||||
|
|
||||
|
var job = new CronJob<CronJobContext> |
||||
|
{ |
||||
|
Id = rule.Id.ToString(), |
||||
|
CronExpression = "* */5 * * *", |
||||
|
CronTimezone = "Europe/Berlin", |
||||
|
Context = new CronJobContext(AppId, rule.Id), |
||||
|
}; |
||||
|
|
||||
|
await sut.HandleCronJobAsync(job, CancellationToken); |
||||
|
|
||||
|
A.CallTo(() => ruleEnqueuer.EnqueueAsync(rule, A<Envelope<IEvent>>._, CancellationToken)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_now_call_rule_enqueue_when_rule_has_no_cron_job_anymore() |
||||
|
{ |
||||
|
var rule = CreateAndSetupRule(); |
||||
|
|
||||
|
var job = new CronJob<CronJobContext> |
||||
|
{ |
||||
|
Id = rule.Id.ToString(), |
||||
|
CronExpression = "* */5 * * *", |
||||
|
CronTimezone = "Europe/Berlin", |
||||
|
Context = new CronJobContext(AppId, rule.Id), |
||||
|
}; |
||||
|
|
||||
|
await sut.HandleCronJobAsync(job, CancellationToken); |
||||
|
|
||||
|
A.CallTo(ruleEnqueuer) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_now_call_rule_enqueue_when_rule_is_not_found() |
||||
|
{ |
||||
|
var rule = CreateAndSetupRule(); |
||||
|
|
||||
|
var job = new CronJob<CronJobContext> |
||||
|
{ |
||||
|
Id = rule.Id.ToString(), |
||||
|
CronExpression = "* */5 * * *", |
||||
|
CronTimezone = "Europe/Berlin", |
||||
|
Context = new CronJobContext(AppId, rule.Id), |
||||
|
}; |
||||
|
|
||||
|
await sut.HandleCronJobAsync(job, CancellationToken); |
||||
|
|
||||
|
A.CallTo(ruleEnqueuer) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Core.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Flows.CronJobs; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers; |
||||
|
|
||||
|
public class CronJobTriggerValidationTests : GivenContext, IClassFixture<TranslationsFixture> |
||||
|
{ |
||||
|
private readonly ICronJobManager<CronJobContext> cronJobs = A.Fake<ICronJobManager<CronJobContext>>(); |
||||
|
private readonly RuleValidator validator; |
||||
|
|
||||
|
public CronJobTriggerValidationTests() |
||||
|
{ |
||||
|
A.CallTo(() => cronJobs.IsValidCronExpression(A<string>._)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
A.CallTo(() => cronJobs.IsValidTimezone(A<string>._)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
validator = new RuleValidator(null!, cronJobs, AppProvider); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_expression_is_not_valid() |
||||
|
{ |
||||
|
var trigger = new CronJobTrigger |
||||
|
{ |
||||
|
CronExpression = "invalid", |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => cronJobs.IsValidCronExpression("invalid")) |
||||
|
.Returns(false); |
||||
|
|
||||
|
var errors = await ValidateAsync(trigger); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
[ |
||||
|
new ValidationError("Cron Expression is invalid.", "CronExpression"), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_timezone_is_not_valid() |
||||
|
{ |
||||
|
var trigger = new CronJobTrigger |
||||
|
{ |
||||
|
CronTimezone = "invalid", |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => cronJobs.IsValidTimezone("invalid")) |
||||
|
.Returns(false); |
||||
|
|
||||
|
var errors = await ValidateAsync(trigger); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
[ |
||||
|
new ValidationError("Timezone is not a valid IANA identifier.", "CronTimezone"), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_add_error_if_valid() |
||||
|
{ |
||||
|
var trigger = new CronJobTrigger(); |
||||
|
|
||||
|
var errors = await ValidateAsync(trigger); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
} |
||||
|
|
||||
|
private async Task<List<ValidationError>> ValidateAsync(RuleTrigger trigger) |
||||
|
{ |
||||
|
var errors = new List<ValidationError>(); |
||||
|
|
||||
|
await validator.ValidateTriggerAsync(trigger, AppId.Id, (m, p) => errors.Add(new ValidationError(m, p)), CancellationToken); |
||||
|
return errors; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Flows.Internal.Execution; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public class RuleFlowTrackingCallbackTests : GivenContext |
||||
|
{ |
||||
|
private readonly IRuleUsageTracker ruleUsageTracker = A.Fake<IRuleUsageTracker>(); |
||||
|
private readonly RuleFlowTrackingCallback sut; |
||||
|
|
||||
|
public RuleFlowTrackingCallbackTests() |
||||
|
{ |
||||
|
sut = new RuleFlowTrackingCallback(ruleUsageTracker); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_track_usage_with_success() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
await sut.OnUpdateAsync( |
||||
|
new FlowExecutionState<FlowEventContext> |
||||
|
{ |
||||
|
InstanceId = default, |
||||
|
Context = new FlowEventContext(), |
||||
|
Definition = null!, |
||||
|
DefinitionId = ruleId.ToString(), |
||||
|
OwnerId = AppId.Id.ToString(), |
||||
|
Status = FlowExecutionStatus.Completed, |
||||
|
}, |
||||
|
CancellationToken); |
||||
|
|
||||
|
A.CallTo(() => ruleUsageTracker.TrackAsync( |
||||
|
AppId.Id, |
||||
|
ruleId, |
||||
|
A<DateOnly>._, |
||||
|
0, |
||||
|
1, |
||||
|
0, |
||||
|
CancellationToken)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_track_usage_with_failure() |
||||
|
{ |
||||
|
var ruleId = DomainId.NewGuid(); |
||||
|
|
||||
|
await sut.OnUpdateAsync( |
||||
|
new FlowExecutionState<FlowEventContext> |
||||
|
{ |
||||
|
InstanceId = default, |
||||
|
Context = new FlowEventContext(), |
||||
|
Definition = null!, |
||||
|
DefinitionId = ruleId.ToString(), |
||||
|
OwnerId = AppId.Id.ToString(), |
||||
|
Status = FlowExecutionStatus.Failed, |
||||
|
}, |
||||
|
CancellationToken); |
||||
|
|
||||
|
A.CallTo(() => ruleUsageTracker.TrackAsync( |
||||
|
AppId.Id, |
||||
|
ruleId, |
||||
|
A<DateOnly>._, |
||||
|
0, |
||||
|
0, |
||||
|
1, |
||||
|
CancellationToken)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
@ -1,127 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Net; |
|
||||
using System.Net.Http.Headers; |
|
||||
using System.Text; |
|
||||
|
|
||||
#pragma warning disable SA1122 // Use string.Empty for empty strings
|
|
||||
|
|
||||
namespace Squidex.Infrastructure.Http; |
|
||||
|
|
||||
public class DumpFormatterTests |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void Should_format_dump_without_response() |
|
||||
{ |
|
||||
var httpRequest = CreateRequest(); |
|
||||
|
|
||||
var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); |
|
||||
|
|
||||
var expected = CreateExpectedDump( |
|
||||
"Request:", |
|
||||
"POST: https://cloud.squidex.io/ HTTP/1.1", |
|
||||
"User-Agent: Squidex/1.0", |
|
||||
"Accept-Language: de; en", |
|
||||
"Accept-Encoding: UTF-8", |
|
||||
"", |
|
||||
"", |
|
||||
"Response:", |
|
||||
"Timeout after 00:01:00"); |
|
||||
|
|
||||
Assert.Equal(expected, dump); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_format_dump_without_content() |
|
||||
{ |
|
||||
var httpRequest = CreateRequest(); |
|
||||
var httpResponse = CreateResponse(); |
|
||||
|
|
||||
var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); |
|
||||
|
|
||||
var expected = CreateExpectedDump( |
|
||||
"Request:", |
|
||||
"POST: https://cloud.squidex.io/ HTTP/1.1", |
|
||||
"User-Agent: Squidex/1.0", |
|
||||
"Accept-Language: de; en", |
|
||||
"Accept-Encoding: UTF-8", |
|
||||
"", |
|
||||
"", |
|
||||
"Response:", |
|
||||
"HTTP/1.1 200 OK", |
|
||||
"Transfer-Encoding: UTF-8", |
|
||||
"Trailer: Expires", |
|
||||
"", |
|
||||
"Elapsed: 00:01:00"); |
|
||||
|
|
||||
Assert.Equal(expected, dump); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_format_dump_with_content_without_timeout() |
|
||||
{ |
|
||||
var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); |
|
||||
var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); |
|
||||
|
|
||||
var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); |
|
||||
|
|
||||
var expected = CreateExpectedDump( |
|
||||
"Request:", |
|
||||
"POST: https://cloud.squidex.io/ HTTP/1.1", |
|
||||
"User-Agent: Squidex/1.0", |
|
||||
"Accept-Language: de; en", |
|
||||
"Accept-Encoding: UTF-8", |
|
||||
"Content-Type: text/plain; charset=utf-8", |
|
||||
"", |
|
||||
"Hello Squidex", |
|
||||
"", |
|
||||
"", |
|
||||
"Response:", |
|
||||
"HTTP/1.1 200 OK", |
|
||||
"Transfer-Encoding: UTF-8", |
|
||||
"Trailer: Expires", |
|
||||
"Content-Type: text/plain; charset=utf-8", |
|
||||
"", |
|
||||
"Hello Back", |
|
||||
"", |
|
||||
"Elapsed: 00:01:00"); |
|
||||
|
|
||||
Assert.Equal(expected, dump); |
|
||||
} |
|
||||
|
|
||||
private static HttpRequestMessage CreateRequest(HttpContent? content = null) |
|
||||
{ |
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); |
|
||||
|
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); |
|
||||
request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); |
|
||||
request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); |
|
||||
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); |
|
||||
|
|
||||
request.Content = content; |
|
||||
|
|
||||
return request; |
|
||||
} |
|
||||
|
|
||||
private static HttpResponseMessage CreateResponse(HttpContent? content = null) |
|
||||
{ |
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK); |
|
||||
|
|
||||
response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); |
|
||||
response.Headers.Trailer.Add("Expires"); |
|
||||
|
|
||||
response.Content = content; |
|
||||
|
|
||||
return response; |
|
||||
} |
|
||||
|
|
||||
private static string CreateExpectedDump(params string[] input) |
|
||||
{ |
|
||||
return string.Join(Environment.NewLine, input) + Environment.NewLine; |
|
||||
} |
|
||||
} |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue