mirror of https://github.com/Squidex/squidex.git
30 changed files with 1019 additions and 376 deletions
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IOnRead |
||||
|
{ |
||||
|
ValueTask OnReadAsync(); |
||||
|
} |
||||
|
} |
||||
@ -1,136 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using FakeItEasy; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Core.Tags; |
|
||||
using Squidex.Domain.Apps.Core.TestHelpers; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Json.Objects; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Core.Operations.Tags |
|
||||
{ |
|
||||
public class TagNormalizerTests |
|
||||
{ |
|
||||
private readonly ITagService tagService = A.Fake<ITagService>(); |
|
||||
private readonly DomainId appId = DomainId.NewGuid(); |
|
||||
private readonly DomainId schemaId = DomainId.NewGuid(); |
|
||||
private readonly Schema schema; |
|
||||
|
|
||||
public TagNormalizerTests() |
|
||||
{ |
|
||||
schema = |
|
||||
new Schema("my-schema") |
|
||||
.AddTags(1, "tags1", Partitioning.Invariant) |
|
||||
.AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) |
|
||||
.AddString(3, "string", Partitioning.Invariant) |
|
||||
.AddArray(4, "array", Partitioning.Invariant, f => f |
|
||||
.AddTags(401, "nestedTags1") |
|
||||
.AddTags(402, "nestedTags2", new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) |
|
||||
.AddString(403, "string")); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_normalize_tags_with_old_data() |
|
||||
{ |
|
||||
var newData = GenerateData("n_raw"); |
|
||||
var oldData = GenerateData("o_raw"); |
|
||||
|
|
||||
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), |
|
||||
A<HashSet<string>>.That.Is("n_raw2_1", "n_raw2_2", "n_raw4"), |
|
||||
A<HashSet<string>>.That.Is("o_raw2_1", "o_raw2_2", "o_raw4"), |
|
||||
default)) |
|
||||
.Returns(new Dictionary<string, string> |
|
||||
{ |
|
||||
["n_raw2_2"] = "id2_2", |
|
||||
["n_raw2_1"] = "id2_1", |
|
||||
["n_raw4"] = "id4" |
|
||||
}); |
|
||||
|
|
||||
await tagService.NormalizeAsync(appId, schemaId, schema, newData, oldData); |
|
||||
|
|
||||
Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); |
|
||||
Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_normalize_tags_without_old_data() |
|
||||
{ |
|
||||
var newData = GenerateData("name"); |
|
||||
|
|
||||
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), |
|
||||
A<HashSet<string>>.That.Is("name2_1", "name2_2", "name4"), |
|
||||
A<HashSet<string>>.That.IsEmpty(), |
|
||||
default)) |
|
||||
.Returns(new Dictionary<string, string> |
|
||||
{ |
|
||||
["name2_2"] = "id2_2", |
|
||||
["name2_1"] = "id2_1", |
|
||||
["name4"] = "id4" |
|
||||
}); |
|
||||
|
|
||||
await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); |
|
||||
|
|
||||
Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); |
|
||||
Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_denormalize_tags() |
|
||||
{ |
|
||||
var newData = GenerateData("id"); |
|
||||
|
|
||||
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), |
|
||||
A<HashSet<string>>.That.Is("id2_1", "id2_2", "id4"), |
|
||||
A<HashSet<string>>.That.IsEmpty(), |
|
||||
default)) |
|
||||
.Returns(new Dictionary<string, string> |
|
||||
{ |
|
||||
["id2_2"] = "name2_2", |
|
||||
["id2_1"] = "name2_1", |
|
||||
["id4"] = "name4" |
|
||||
}); |
|
||||
|
|
||||
await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); |
|
||||
|
|
||||
Assert.Equal(JsonValue.Array("name2_1", "name2_2"), newData["tags2"]!["iv"]); |
|
||||
Assert.Equal(JsonValue.Array("name4"), GetNestedTags(newData)); |
|
||||
} |
|
||||
|
|
||||
private static JsonValue GetNestedTags(ContentData newData) |
|
||||
{ |
|
||||
var arrayValue = newData["array"]!["iv"].AsArray; |
|
||||
var arrayItem = arrayValue[0].AsObject; |
|
||||
|
|
||||
return arrayItem["nestedTags2"]; |
|
||||
} |
|
||||
|
|
||||
private static ContentData GenerateData(string prefix) |
|
||||
{ |
|
||||
return new ContentData() |
|
||||
.AddField("tags1", |
|
||||
new ContentFieldData() |
|
||||
.AddInvariant(JsonValue.Array($"{prefix}1"))) |
|
||||
.AddField("tags2", |
|
||||
new ContentFieldData() |
|
||||
.AddInvariant(JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) |
|
||||
.AddField("string", |
|
||||
new ContentFieldData() |
|
||||
.AddInvariant($"{prefix}stringValue")) |
|
||||
.AddField("array", |
|
||||
new ContentFieldData() |
|
||||
.AddInvariant( |
|
||||
JsonValue.Array( |
|
||||
new JsonObject() |
|
||||
.Add("nestedTags1", JsonValue.Array($"{prefix}3")) |
|
||||
.Add("nestedTags2", JsonValue.Array($"{prefix}4")) |
|
||||
.Add("string", $"{prefix}nestedStringValue")))); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,208 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using FakeItEasy; |
||||
|
using Squidex.Infrastructure.TestHelpers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public class SimpleStateTests |
||||
|
{ |
||||
|
private readonly CancellationTokenSource cts = new CancellationTokenSource(); |
||||
|
private readonly CancellationToken ct; |
||||
|
private readonly TestState<MyDomainState> testState = new TestState<MyDomainState>(DomainId.NewGuid()); |
||||
|
private readonly SimpleState<MyDomainState> sut; |
||||
|
|
||||
|
public SimpleStateTests() |
||||
|
{ |
||||
|
ct = cts.Token; |
||||
|
|
||||
|
sut = new SimpleState<MyDomainState>(testState.PersistenceFactory, GetType(), testState.Id); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_init_with_base_data() |
||||
|
{ |
||||
|
Assert.Equal(-1, sut.Version); |
||||
|
Assert.NotNull(sut.Value); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_state_from_persistence_on_load() |
||||
|
{ |
||||
|
testState.Version = 42; |
||||
|
testState.Snapshot = new MyDomainState { Value = 50 }; |
||||
|
|
||||
|
await sut.LoadAsync(ct); |
||||
|
|
||||
|
Assert.Equal(42, sut.Version); |
||||
|
Assert.Equal(50, sut.Value.Value); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_delete_when_clearing() |
||||
|
{ |
||||
|
await sut.ClearAsync(ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.DeleteAsync(ct)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_persistence_when_writing_state() |
||||
|
{ |
||||
|
await sut.WriteAsync(ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_load_once_on_update() |
||||
|
{ |
||||
|
await sut.UpdateAsync(x => true, ct: ct); |
||||
|
await sut.UpdateAsync(x => true, ct: ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) |
||||
|
.MustHaveHappenedOnceExactly(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_write_state_on_update_when_callback_returns_true() |
||||
|
{ |
||||
|
await sut.UpdateAsync(x => true, ct: ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_write_state_on_update_when_callback_returns_false() |
||||
|
{ |
||||
|
await sut.UpdateAsync(x => true, ct: ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_write_state_on_update_and_return_when_callback_returns_true() |
||||
|
{ |
||||
|
var result = await sut.UpdateAsync(x => (true, 42), ct: ct); |
||||
|
|
||||
|
Assert.Equal(42, result); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_write_state_on_update_and_return_when_callback_returns_false() |
||||
|
{ |
||||
|
var result = await sut.UpdateAsync(x => (false, 42), ct: ct); |
||||
|
|
||||
|
Assert.Equal(42, result); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_retry_update_when_failed_with_inconsistency_issue() |
||||
|
{ |
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); |
||||
|
|
||||
|
await sut.UpdateAsync(x => true, ct: ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_give_up_update_after_too_many_inconsistency_issues() |
||||
|
{ |
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => true, ct: ct)); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_retry_update_with_other_exception() |
||||
|
{ |
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.Throws(new InvalidOperationException()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => true, ct: ct)); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_retry_update_and_return_when_failed_with_inconsistency_issue() |
||||
|
{ |
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); |
||||
|
|
||||
|
await sut.UpdateAsync(x => (true, 42), ct: ct); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_give_up_update_and_return_after_too_many_inconsistency_issues() |
||||
|
{ |
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_retry_update_and_return_with_other_exception() |
||||
|
{ |
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.Throws(new InvalidOperationException()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
||||
|
|
||||
|
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
||||
|
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue