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