mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
75 changed files with 1102 additions and 191 deletions
@ -0,0 +1,90 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization; |
||||
|
using MongoDB.Bson.Serialization.Serializers; |
||||
|
using MongoDB.Driver; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Rules |
||||
|
{ |
||||
|
public sealed class MongoRuleStatisticsCollection : MongoRepositoryBase<RuleStatistics> |
||||
|
{ |
||||
|
static MongoRuleStatisticsCollection() |
||||
|
{ |
||||
|
var guidSerializer = new GuidSerializer().WithRepresentation(BsonType.String); |
||||
|
|
||||
|
BsonClassMap.RegisterClassMap<RuleStatistics>(map => |
||||
|
{ |
||||
|
map.AutoMap(); |
||||
|
|
||||
|
map.MapProperty(x => x.AppId).SetSerializer(guidSerializer); |
||||
|
map.MapProperty(x => x.RuleId).SetSerializer(guidSerializer); |
||||
|
|
||||
|
map.SetIgnoreExtraElements(true); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public MongoRuleStatisticsCollection(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "RuleStatistics"; |
||||
|
} |
||||
|
|
||||
|
protected override Task SetupCollectionAsync(IMongoCollection<RuleStatistics> collection, CancellationToken ct = default) |
||||
|
{ |
||||
|
return collection.Indexes.CreateOneAsync( |
||||
|
new CreateIndexModel<RuleStatistics>( |
||||
|
Index |
||||
|
.Ascending(x => x.AppId) |
||||
|
.Ascending(x => x.RuleId)), |
||||
|
cancellationToken: ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<RuleStatistics>> QueryByAppAsync(Guid appId) |
||||
|
{ |
||||
|
var statistics = await Collection.Find(x => x.AppId == appId).ToListAsync(); |
||||
|
|
||||
|
return statistics; |
||||
|
} |
||||
|
|
||||
|
public Task IncrementSuccess(Guid appId, Guid ruleId, Instant now) |
||||
|
{ |
||||
|
return Collection.UpdateOneAsync( |
||||
|
x => x.AppId == appId && x.RuleId == ruleId, |
||||
|
Update |
||||
|
.Inc(x => x.NumSucceeded, 1) |
||||
|
.Set(x => x.LastExecuted, now) |
||||
|
.SetOnInsert(x => x.AppId, appId) |
||||
|
.SetOnInsert(x => x.RuleId, ruleId), |
||||
|
Upsert); |
||||
|
} |
||||
|
|
||||
|
public Task IncrementFailed(Guid appId, Guid ruleId, Instant now) |
||||
|
{ |
||||
|
return Collection.UpdateOneAsync( |
||||
|
x => x.AppId == appId && x.RuleId == ruleId, |
||||
|
Update |
||||
|
.Inc(x => x.NumFailed, 1) |
||||
|
.Set(x => x.LastExecuted, now) |
||||
|
.SetOnInsert(x => x.AppId, appId) |
||||
|
.SetOnInsert(x => x.RuleId, ruleId), |
||||
|
Upsert); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public interface IEnrichedRuleEntity : IRuleEntity, IEntityWithCacheDependencies |
||||
|
{ |
||||
|
int NumSucceeded { get; } |
||||
|
|
||||
|
int NumFailed { get; } |
||||
|
|
||||
|
Instant? LastExecuted { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public interface IRuleEnricher |
||||
|
{ |
||||
|
Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context); |
||||
|
|
||||
|
Task<IReadOnlyList<IEnrichedRuleEntity>> EnrichAsync(IEnumerable<IRuleEntity> rules, Context context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public interface IRuleQueryService |
||||
|
{ |
||||
|
Task<IReadOnlyList<IEnrichedRuleEntity>> QueryAsync(Context context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.Queries |
||||
|
{ |
||||
|
public sealed class RuleEnricher : IRuleEnricher |
||||
|
{ |
||||
|
private readonly IRuleEventRepository ruleEventRepository; |
||||
|
|
||||
|
public RuleEnricher(IRuleEventRepository ruleEventRepository) |
||||
|
{ |
||||
|
Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); |
||||
|
|
||||
|
this.ruleEventRepository = ruleEventRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context) |
||||
|
{ |
||||
|
Guard.NotNull(rule, nameof(rule)); |
||||
|
|
||||
|
var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context); |
||||
|
|
||||
|
return enriched[0]; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IEnrichedRuleEntity>> EnrichAsync(IEnumerable<IRuleEntity> rules, Context context) |
||||
|
{ |
||||
|
Guard.NotNull(rules, nameof(rules)); |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
|
||||
|
using (Profiler.TraceMethod<RuleEnricher>()) |
||||
|
{ |
||||
|
var results = new List<RuleEntity>(); |
||||
|
|
||||
|
foreach (var rule in rules) |
||||
|
{ |
||||
|
var result = SimpleMapper.Map(rule, new RuleEntity()); |
||||
|
|
||||
|
results.Add(result); |
||||
|
} |
||||
|
|
||||
|
foreach (var group in results.GroupBy(x => x.AppId.Id)) |
||||
|
{ |
||||
|
var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key); |
||||
|
|
||||
|
foreach (var rule in group) |
||||
|
{ |
||||
|
var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); |
||||
|
|
||||
|
if (statistic != null) |
||||
|
{ |
||||
|
rule.LastExecuted = statistic.LastExecuted; |
||||
|
rule.NumFailed = statistic.NumFailed; |
||||
|
rule.NumSucceeded = statistic.NumSucceeded; |
||||
|
|
||||
|
rule.CacheDependencies = new HashSet<object> |
||||
|
{ |
||||
|
statistic.LastExecuted |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Indexes; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.Queries |
||||
|
{ |
||||
|
public sealed class RuleQueryService : IRuleQueryService |
||||
|
{ |
||||
|
private readonly IRulesIndex rulesIndex; |
||||
|
private readonly IRuleEnricher ruleEnricher; |
||||
|
|
||||
|
public RuleQueryService(IRulesIndex rulesIndex, IRuleEnricher ruleEnricher) |
||||
|
{ |
||||
|
Guard.NotNull(rulesIndex, nameof(rulesIndex)); |
||||
|
Guard.NotNull(ruleEnricher, nameof(ruleEnricher)); |
||||
|
|
||||
|
this.rulesIndex = rulesIndex; |
||||
|
this.ruleEnricher = ruleEnricher; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IEnrichedRuleEntity>> QueryAsync(Context context) |
||||
|
{ |
||||
|
var rules = await rulesIndex.GetRulesAsync(context.App.Id); |
||||
|
|
||||
|
var enriched = await ruleEnricher.EnrichAsync(rules, context); |
||||
|
|
||||
|
return enriched; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.Repositories |
||||
|
{ |
||||
|
public class RuleStatistics |
||||
|
{ |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
public Guid RuleId { get; set; } |
||||
|
|
||||
|
public int NumSucceeded { get; set; } |
||||
|
|
||||
|
public int NumFailed { get; set; } |
||||
|
|
||||
|
public Instant? LastExecuted { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Orleans; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public sealed class RuleCommandMiddleware : GrainCommandMiddleware<RuleCommand, IRuleGrain> |
||||
|
{ |
||||
|
private readonly IRuleEnricher ruleEnricher; |
||||
|
private readonly IContextProvider contextProvider; |
||||
|
|
||||
|
public RuleCommandMiddleware(IGrainFactory grainFactory, IRuleEnricher ruleEnricher, IContextProvider contextProvider) |
||||
|
: base(grainFactory) |
||||
|
{ |
||||
|
Guard.NotNull(ruleEnricher, nameof(ruleEnricher)); |
||||
|
Guard.NotNull(contextProvider, nameof(contextProvider)); |
||||
|
|
||||
|
this.ruleEnricher = ruleEnricher; |
||||
|
this.contextProvider = contextProvider; |
||||
|
} |
||||
|
|
||||
|
public override async Task HandleAsync(CommandContext context, Func<Task> next) |
||||
|
{ |
||||
|
await base.HandleAsync(context, next); |
||||
|
|
||||
|
if (context.PlainResult is IRuleEntity rule && NotEnriched(context)) |
||||
|
{ |
||||
|
var enriched = await ruleEnricher.EnrichAsync(rule, contextProvider.Context); |
||||
|
|
||||
|
context.Complete(enriched); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool NotEnriched(CommandContext context) |
||||
|
{ |
||||
|
return !(context.PlainResult is IEnrichedRuleEntity); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public sealed class RuleEntity : IEnrichedRuleEntity |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public NamedId<Guid> AppId { get; set; } |
||||
|
|
||||
|
public NamedId<Guid> SchemaId { get; set; } |
||||
|
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public Instant Created { get; set; } |
||||
|
|
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
public Rule RuleDef { get; set; } |
||||
|
|
||||
|
public bool IsDeleted { get; set; } |
||||
|
|
||||
|
public int NumSucceeded { get; set; } |
||||
|
|
||||
|
public int NumFailed { get; set; } |
||||
|
|
||||
|
public Instant? LastExecuted { get; set; } |
||||
|
|
||||
|
public HashSet<object> CacheDependencies { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
<div class="card"> |
||||
|
<div class="card-header"> |
||||
|
<div class="row"> |
||||
|
<div class="col col-name"> |
||||
|
<sqx-editable-title |
||||
|
fallback="Unnamed Rule" |
||||
|
[name]="rule.name" |
||||
|
(nameChange)="rename($event)" |
||||
|
[maxLength]="60" |
||||
|
[isRequired]="false" |
||||
|
[disabled]="!rule.canUpdate"> |
||||
|
</sqx-editable-title> |
||||
|
</div> |
||||
|
<div class="col-auto"> |
||||
|
<button type="button" class="btn btn-text-danger" |
||||
|
[disabled]="!rule.canDelete" |
||||
|
(sqxConfirmClick)="delete()" |
||||
|
confirmTitle="Delete rule" |
||||
|
confirmText="Do you really want to delete the rule?"> |
||||
|
<i class="icon-bin2"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<div class="row align-items-center"> |
||||
|
<div class="col col-auto"> |
||||
|
<h3>If</h3> |
||||
|
</div> |
||||
|
<div class="col"> |
||||
|
<span (click)="editTrigger.emit()"> |
||||
|
<sqx-rule-element [type]="rule.triggerType" [element]="ruleTriggers[rule.triggerType]"></sqx-rule-element> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="col col-auto"> |
||||
|
<h3>then</h3> |
||||
|
</div> |
||||
|
<div class="col"> |
||||
|
<span (click)="editAction.emit()"> |
||||
|
<sqx-rule-element [type]="rule.actionType" [element]="ruleActions[rule.actionType]"></sqx-rule-element> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="col col-auto"> |
||||
|
<sqx-toggle [disabled]="!rule.canDisable && !rule.canEnable" [ngModel]="rule.isEnabled" (ngModelChange)="toggle()"></sqx-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-footer"> |
||||
|
<div class="row"> |
||||
|
<div class="col-3"> |
||||
|
Succeeded: <strong>{{rule.numSucceeded}}</strong> |
||||
|
</div> |
||||
|
<div class="col-3"> |
||||
|
Failed: <strong>{{rule.numFailed}}</strong> |
||||
|
</div> |
||||
|
<div class="col"> |
||||
|
Last Executed: |
||||
|
|
||||
|
<ng-container *ngIf="rule.lastExecuted; else notExecuted"> |
||||
|
{{rule.lastExecuted | sqxFromNow}} |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-template #notExecuted> |
||||
|
- |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
<div class="col-auto"> |
||||
|
<a routerLink="events" [queryParams]="{ ruleId: rule.id }"> |
||||
|
Logs |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,22 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.card { |
||||
|
& { |
||||
|
@include border-radius(0); |
||||
|
border-bottom-width: 2px; |
||||
|
border-top-width: 0; |
||||
|
margin-bottom: .25rem; |
||||
|
} |
||||
|
|
||||
|
&-header, |
||||
|
&-body { |
||||
|
padding: .75rem 1.25rem; |
||||
|
} |
||||
|
|
||||
|
&-footer { |
||||
|
background: $color-border; |
||||
|
font-weight: normal; |
||||
|
font-size: 90%; |
||||
|
} |
||||
|
} |
||||
@ -1,23 +1,30 @@ |
|||||
<div class="title"> |
<div class="title"> |
||||
<form *ngIf="isRenaming; else noRenaming" class="form-inline" [formGroup]="renameForm" (ngSubmit)="rename()"> |
<form *ngIf="renaming; else noRenaming" [formGroup]="renameForm" (ngSubmit)="rename()"> |
||||
<div class="form-group mr-1"> |
<div class="row no-gutters"> |
||||
|
<div class="col"> |
||||
|
<div class="form-group mr-2"> |
||||
<sqx-control-errors for="name"></sqx-control-errors> |
<sqx-control-errors for="name"></sqx-control-errors> |
||||
|
|
||||
<input type="text" class="form-control form-underlined" formControlName="name" maxlength="20" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" spellcheck="false" /> |
<input type="text" class="form-control form-underlined" formControlName="name" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" spellcheck="false" /> |
||||
</div> |
</div> |
||||
|
</div> |
||||
|
<div class="col-auto"> |
||||
|
<button type="submit" class="btn btn-primary mr-1" [disabled]="!renameForm.valid || !renameForm.dirty">Save</button> |
||||
|
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.valid || !renameForm.dirty">Save</button> |
<button type="button" class="btn btn-text-secondary btn-cancel mr-4" (click)="toggleRename()"> |
||||
|
|
||||
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="toggleRename()"> |
|
||||
<i class="icon-close"></i> |
<i class="icon-close"></i> |
||||
</button> |
</button> |
||||
|
</div> |
||||
|
</div> |
||||
</form> |
</form> |
||||
|
|
||||
<ng-template #noRenaming> |
<ng-template #noRenaming> |
||||
<h3 class="title-name" (dblclick)="toggleRename()"> |
<div class="title-view"> |
||||
{{name}} |
<h3 class="title-name" [class.fallback]="!name" (dblclick)="toggleRename()"> |
||||
|
{{name || fallback}} |
||||
</h3> |
</h3> |
||||
|
|
||||
<i class="title-edit icon-pencil" *ngIf="!disabled" (click)="toggleRename()"></i> |
<i class="title-edit icon-pencil" *ngIf="!disabled" (click)="toggleRename()"></i> |
||||
|
</div> |
||||
</ng-template> |
</ng-template> |
||||
</div> |
</div> |
||||
@ -0,0 +1,76 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.Queries |
||||
|
{ |
||||
|
public class RuleEnricherTests |
||||
|
{ |
||||
|
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly Context requestContext = Context.Anonymous(); |
||||
|
private readonly RuleEnricher sut; |
||||
|
|
||||
|
public RuleEnricherTests() |
||||
|
{ |
||||
|
sut = new RuleEnricher(ruleEventRepository); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_enrich_if_statistics_not_found() |
||||
|
{ |
||||
|
var source = new RuleEntity { AppId = appId }; |
||||
|
|
||||
|
var result = await sut.EnrichAsync(source, requestContext); |
||||
|
|
||||
|
Assert.Equal(0, result.NumFailed); |
||||
|
Assert.Equal(0, result.NumSucceeded); |
||||
|
Assert.Null(result.LastExecuted); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_rules_with_found_statistics() |
||||
|
{ |
||||
|
var source1 = new RuleEntity { AppId = appId, Id = Guid.NewGuid() }; |
||||
|
var source2 = new RuleEntity { AppId = appId, Id = Guid.NewGuid() }; |
||||
|
|
||||
|
var stats = new RuleStatistics |
||||
|
{ |
||||
|
RuleId = source1.Id, |
||||
|
NumFailed = 12, |
||||
|
NumSucceeded = 17, |
||||
|
LastExecuted = SystemClock.Instance.GetCurrentInstant() |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id)) |
||||
|
.Returns(new List<RuleStatistics> { stats }); |
||||
|
|
||||
|
var result = await sut.EnrichAsync(new[] { source1, source2 }, requestContext); |
||||
|
|
||||
|
var enriched1 = result.ElementAt(0); |
||||
|
|
||||
|
Assert.Equal(12, enriched1.NumFailed); |
||||
|
Assert.Equal(17, enriched1.NumSucceeded); |
||||
|
Assert.Equal(stats.LastExecuted, enriched1.LastExecuted); |
||||
|
|
||||
|
var enriched2 = result.ElementAt(1); |
||||
|
|
||||
|
Assert.Equal(0, enriched2.NumFailed); |
||||
|
Assert.Equal(0, enriched2.NumSucceeded); |
||||
|
Assert.Null(enriched2.LastExecuted); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Indexes; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.Queries |
||||
|
{ |
||||
|
public class RuleQueryServiceTests |
||||
|
{ |
||||
|
private readonly IRulesIndex rulesIndex = A.Fake<IRulesIndex>(); |
||||
|
private readonly IRuleEnricher ruleEnricher = A.Fake<IRuleEnricher>(); |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly Context requestContext = Context.Anonymous(); |
||||
|
private readonly RuleQueryService sut; |
||||
|
|
||||
|
public RuleQueryServiceTests() |
||||
|
{ |
||||
|
requestContext.App = Mocks.App(appId); |
||||
|
|
||||
|
sut = new RuleQueryService(rulesIndex, ruleEnricher); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_rules_from_index_and_enrich() |
||||
|
{ |
||||
|
var original = new List<IRuleEntity> |
||||
|
{ |
||||
|
new RuleEntity() |
||||
|
}; |
||||
|
|
||||
|
var enriched = new List<IEnrichedRuleEntity> |
||||
|
{ |
||||
|
new RuleEntity() |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => rulesIndex.GetRulesAsync(appId.Id)) |
||||
|
.Returns(original); |
||||
|
|
||||
|
A.CallTo(() => ruleEnricher.EnrichAsync(original, requestContext)) |
||||
|
.Returns(enriched); |
||||
|
|
||||
|
var result = await sut.QueryAsync(requestContext); |
||||
|
|
||||
|
Assert.Same(enriched, result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Orleans; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.State; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public sealed class RuleCommandMiddlewareTests : HandlerTestBase<RuleState> |
||||
|
{ |
||||
|
private readonly IRuleEnricher ruleEnricher = A.Fake<IRuleEnricher>(); |
||||
|
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); |
||||
|
private readonly Guid ruleId = Guid.NewGuid(); |
||||
|
private readonly Context requestContext = Context.Anonymous(); |
||||
|
private readonly RuleCommandMiddleware sut; |
||||
|
|
||||
|
public sealed class MyCommand : SquidexCommand |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override Guid Id |
||||
|
{ |
||||
|
get { return ruleId; } |
||||
|
} |
||||
|
|
||||
|
public RuleCommandMiddlewareTests() |
||||
|
{ |
||||
|
A.CallTo(() => contextProvider.Context) |
||||
|
.Returns(requestContext); |
||||
|
|
||||
|
sut = new RuleCommandMiddleware(A.Fake<IGrainFactory>(), ruleEnricher, contextProvider); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_invoke_enricher_for_other_result() |
||||
|
{ |
||||
|
var command = CreateCommand(new MyCommand()); |
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
context.Complete(12); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
A.CallTo(() => ruleEnricher.EnrichAsync(A<IEnrichedRuleEntity>.Ignored, requestContext)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_invoke_enricher_if_already_enriched() |
||||
|
{ |
||||
|
var result = new RuleEntity(); |
||||
|
|
||||
|
var command = CreateCommand(new MyCommand()); |
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
context.Complete(result); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
Assert.Same(result, context.Result<IEnrichedRuleEntity>()); |
||||
|
|
||||
|
A.CallTo(() => ruleEnricher.EnrichAsync(A<IEnrichedRuleEntity>.Ignored, requestContext)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_rule_result() |
||||
|
{ |
||||
|
var result = A.Fake<IRuleEntity>(); |
||||
|
|
||||
|
var command = CreateCommand(new MyCommand()); |
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
context.Complete(result); |
||||
|
|
||||
|
var enriched = new RuleEntity(); |
||||
|
|
||||
|
A.CallTo(() => ruleEnricher.EnrichAsync(result, requestContext)) |
||||
|
.Returns(enriched); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
Assert.Same(enriched, context.Result<IEnrichedRuleEntity>()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue