Browse Source

Improved repair.

pull/681/head
Sebastian Stehle 5 years ago
parent
commit
6b867865ff
  1. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs
  2. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  4. 11
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs
  7. 4
      backend/src/Squidex.Domain.Users/DefaultKeyStore.cs
  8. 6
      backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  9. 16
      backend/src/Squidex.Infrastructure/DomainException.cs
  10. 4
      backend/src/Squidex.Infrastructure/DomainForbiddenException.cs
  11. 4
      backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs
  12. 4
      backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs
  13. 4
      backend/src/Squidex.Infrastructure/DomainObjectException.cs
  14. 4
      backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs
  15. 4
      backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs
  16. 2
      backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs
  17. 15
      backend/src/Squidex.Infrastructure/States/Persistence.cs
  18. 4
      backend/src/Squidex.Infrastructure/Validation/ValidationException.cs
  19. 28
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  20. 3
      backend/src/Squidex.Web/ErrorDto.cs
  21. 4
      backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs
  22. 28
      backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs
  23. 13
      backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs
  24. 82
      backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs
  25. 29
      backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs
  26. 35
      backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs
  27. 18
      frontend/app/framework/angular/forms/error-validator.spec.ts
  28. 18
      frontend/app/framework/angular/http/http-extensions.spec.ts
  29. 8
      frontend/app/framework/angular/http/http-extensions.ts
  30. 12
      frontend/app/framework/utils/error.spec.ts
  31. 1
      frontend/app/framework/utils/error.ts
  32. 4
      frontend/app/shared/state/assets.state.spec.ts
  33. 2
      frontend/app/shared/state/assets.state.ts
  34. 2
      frontend/app/shared/state/contents.state.ts
  35. 19826
      frontend/package-lock.json

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetFolderRepository : ISnapshotStore<AssetFolderDomainObject.State> public sealed partial class MongoAssetFolderRepository : ISnapshotStore<AssetFolderDomainObject.State>
{ {
async Task<(AssetFolderDomainObject.State Value, long Version)> ISnapshotStore<AssetFolderDomainObject.State>.ReadAsync(DomainId key) async Task<(AssetFolderDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<AssetFolderDomainObject.State>.ReadAsync(DomainId key)
{ {
using (Profiler.TraceMethod<MongoAssetFolderRepository>()) using (Profiler.TraceMethod<MongoAssetFolderRepository>())
{ {
@ -33,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null) if (existing != null)
{ {
return (Map(existing), existing.Version); return (Map(existing), true, existing.Version);
} }
return (null!, EtagVersion.Empty); return (null!, true, EtagVersion.Empty);
} }
} }

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetRepository : ISnapshotStore<AssetDomainObject.State> public sealed partial class MongoAssetRepository : ISnapshotStore<AssetDomainObject.State>
{ {
async Task<(AssetDomainObject.State Value, long Version)> ISnapshotStore<AssetDomainObject.State>.ReadAsync(DomainId key) async Task<(AssetDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<AssetDomainObject.State>.ReadAsync(DomainId key)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>()) using (Profiler.TraceMethod<MongoAssetRepository>())
{ {
@ -33,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null) if (existing != null)
{ {
return (Map(existing), existing.Version); return (Map(existing), true, existing.Version);
} }
return (null!, EtagVersion.Empty); return (null!, true, EtagVersion.Empty);
} }
} }

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -168,9 +168,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public Task<MongoContentEntity> FindAsync(DomainId documentId) public async Task<long> FindVersionAsync(DomainId documentId)
{ {
return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync(); var result = await Collection.Find(x => x.DocumentId == documentId).Only(x => x.Version).FirstOrDefaultAsync();
return result?["vs"].AsInt64 ?? EtagVersion.Empty;
} }
public Task ResetScheduledAsync(DomainId documentId) public Task ResetScheduledAsync(DomainId documentId)

11
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -23,12 +23,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
Task ISnapshotStore<ContentDomainObject.State>.ReadAllAsync(Func<ContentDomainObject.State, long, Task> callback, CancellationToken ct) Task ISnapshotStore<ContentDomainObject.State>.ReadAllAsync(Func<ContentDomainObject.State, long, Task> callback, CancellationToken ct)
{ {
throw new NotSupportedException(); return Task.CompletedTask;
} }
Task<(ContentDomainObject.State Value, long Version)> ISnapshotStore<ContentDomainObject.State>.ReadAsync(DomainId key) async Task<(ContentDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<ContentDomainObject.State>.ReadAsync(DomainId key)
{ {
return Task.FromResult<(ContentDomainObject.State, long Version)>((null!, EtagVersion.Empty)); using (Profiler.TraceMethod<MongoContentRepository>())
{
var version = await collectionAll.FindVersionAsync(key);
return (null!, false, version);
}
} }
async Task ISnapshotStore<ContentDomainObject.State>.ClearAsync() async Task ISnapshotStore<ContentDomainObject.State>.ClearAsync()

2
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
if (hasReferrer) if (hasReferrer)
{ {
throw new DomainException(T.Get("assets.referenced")); throw new DomainException(T.Get("assets.referenced"), "OBJECT_REFERENCED");
} }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs

@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
if (hasReferrer) if (hasReferrer)
{ {
throw new DomainException(T.Get("contents.referenced")); throw new DomainException(T.Get("contents.referenced", "OBJECT_REFERENCED"));
} }
} }

4
backend/src/Squidex.Domain.Users/DefaultKeyStore.cs

@ -60,7 +60,7 @@ namespace Squidex.Domain.Users
return (cachedKeyInfo, cachedKey); return (cachedKeyInfo, cachedKey);
} }
var (state, _) = await store.ReadAsync(default); var (state, _, _) = await store.ReadAsync(default);
RsaSecurityKey securityKey; RsaSecurityKey securityKey;
@ -92,7 +92,7 @@ namespace Squidex.Domain.Users
} }
catch (InconsistentStateException) catch (InconsistentStateException)
{ {
(state, _) = await store.ReadAsync(default); (state, _, _) = await store.ReadAsync(default);
} }
} }

6
backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.States
return $"States_{name}"; return $"States_{name}";
} }
public async Task<(T Value, long Version)> ReadAsync(DomainId key) public async Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key)
{ {
using (Profiler.TraceMethod<MongoSnapshotStore<T>>()) using (Profiler.TraceMethod<MongoSnapshotStore<T>>())
{ {
@ -53,10 +53,10 @@ namespace Squidex.Infrastructure.States
if (existing != null) if (existing != null)
{ {
return (existing.Doc, existing.Version); return (existing.Doc, true, existing.Version);
} }
return (default!, EtagVersion.Empty); return (default!, true, EtagVersion.Empty);
} }
} }

16
backend/src/Squidex.Infrastructure/DomainException.cs

@ -13,14 +13,30 @@ namespace Squidex.Infrastructure
[Serializable] [Serializable]
public class DomainException : Exception public class DomainException : Exception
{ {
public string? ErrorCode { get; }
public DomainException(string message, Exception? inner = null) public DomainException(string message, Exception? inner = null)
: base(message, inner) : base(message, inner)
{ {
} }
public DomainException(string message, string? errorCode, Exception? inner = null)
: base(message, inner)
{
ErrorCode = errorCode;
}
protected DomainException(SerializationInfo info, StreamingContext context) protected DomainException(SerializationInfo info, StreamingContext context)
: base(info, context) : base(info, context)
{ {
ErrorCode = info.GetString(nameof(ErrorCode));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(ErrorCode), ErrorCode);
base.GetObjectData(info, context);
} }
} }
} }

4
backend/src/Squidex.Infrastructure/DomainForbiddenException.cs

@ -13,8 +13,10 @@ namespace Squidex.Infrastructure
[Serializable] [Serializable]
public class DomainForbiddenException : DomainException public class DomainForbiddenException : DomainException
{ {
private const string ValidationError = "FORBIDDEN";
public DomainForbiddenException(string message, Exception? inner = null) public DomainForbiddenException(string message, Exception? inner = null)
: base(message, inner) : base(message, ValidationError, inner)
{ {
} }

4
backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs

@ -14,8 +14,10 @@ namespace Squidex.Infrastructure
[Serializable] [Serializable]
public class DomainObjectConflictException : DomainObjectException public class DomainObjectConflictException : DomainObjectException
{ {
private const string ValidationError = "OBJECT_CONFLICT";
public DomainObjectConflictException(string id, Exception? inner = null) public DomainObjectConflictException(string id, Exception? inner = null)
: base(FormatMessage(id), id, inner) : base(FormatMessage(id), id, ValidationError, inner)
{ {
} }

4
backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs

@ -14,8 +14,10 @@ namespace Squidex.Infrastructure
[Serializable] [Serializable]
public class DomainObjectDeletedException : DomainObjectException public class DomainObjectDeletedException : DomainObjectException
{ {
private const string ValidationError = "OBJECT_DELETED";
public DomainObjectDeletedException(string id, Exception? inner = null) public DomainObjectDeletedException(string id, Exception? inner = null)
: base(FormatMessage(id), id, inner) : base(FormatMessage(id), id, ValidationError, inner)
{ {
} }

4
backend/src/Squidex.Infrastructure/DomainObjectException.cs

@ -15,8 +15,8 @@ namespace Squidex.Infrastructure
{ {
public string Id { get; } public string Id { get; }
public DomainObjectException(string message, string id, Exception? inner = null) public DomainObjectException(string message, string id, string errorCode, Exception? inner = null)
: base(message, inner) : base(message, errorCode, inner)
{ {
Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNullOrEmpty(id, nameof(id));

4
backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs

@ -14,8 +14,10 @@ namespace Squidex.Infrastructure
[Serializable] [Serializable]
public class DomainObjectNotFoundException : DomainObjectException public class DomainObjectNotFoundException : DomainObjectException
{ {
private const string ValidationError = "OBJECT_NOTFOUND";
public DomainObjectNotFoundException(string id, Exception? inner = null) public DomainObjectNotFoundException(string id, Exception? inner = null)
: base(FormatMessage(id), id, inner) : base(FormatMessage(id), id, ValidationError, inner)
{ {
} }

4
backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs

@ -14,12 +14,14 @@ namespace Squidex.Infrastructure
[Serializable] [Serializable]
public class DomainObjectVersionException : DomainObjectException public class DomainObjectVersionException : DomainObjectException
{ {
private const string ValidationError = "OBJECT_VERSION_CONFLICT";
public long CurrentVersion { get; } public long CurrentVersion { get; }
public long ExpectedVersion { get; } public long ExpectedVersion { get; }
public DomainObjectVersionException(string id, long currentVersion, long expectedVersion, Exception? inner = null) public DomainObjectVersionException(string id, long currentVersion, long expectedVersion, Exception? inner = null)
: base(FormatMessage(id, currentVersion, expectedVersion), id, inner) : base(FormatMessage(id, currentVersion, expectedVersion), id, ValidationError, inner)
{ {
CurrentVersion = currentVersion; CurrentVersion = currentVersion;

2
backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs

@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.States
Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots); Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots);
Task<(T Value, long Version)> ReadAsync(DomainId key); Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key);
Task ClearAsync(); Task ClearAsync();

15
backend/src/Squidex.Infrastructure/States/Persistence.cs

@ -46,7 +46,7 @@ namespace Squidex.Infrastructure.States
public bool IsSnapshotStale public bool IsSnapshotStale
{ {
get => persistenceMode == PersistenceMode.SnapshotsAndEventSourcing && versionSnapshot < versionEvents && versionSnapshot > EtagVersion.Empty; get => UseSnapshots && UseEventSourcing && versionSnapshot < versionEvents;
} }
public Persistence(DomainId ownerKey, Type ownerType, public Persistence(DomainId ownerKey, Type ownerType,
@ -114,14 +114,17 @@ namespace Squidex.Infrastructure.States
private async Task ReadSnapshotAsync() private async Task ReadSnapshotAsync()
{ {
var (state, version) = await snapshotStore.ReadAsync(ownerKey); var (state, valid, version) = await snapshotStore.ReadAsync(ownerKey);
version = Math.Max(version, EtagVersion.Empty); version = Math.Max(version, EtagVersion.Empty);
versionSnapshot = version; versionSnapshot = version;
versionEvents = version;
if (applyState != null && version >= 0) if (valid)
{
versionEvents = version;
}
if (applyState != null && version > EtagVersion.Empty && valid)
{ {
applyState(state, version); applyState(state, version);
} }
@ -162,7 +165,7 @@ namespace Squidex.Infrastructure.States
if (oldVersion == EtagVersion.Empty && UseEventSourcing) if (oldVersion == EtagVersion.Empty && UseEventSourcing)
{ {
oldVersion = (versionEvents - 1); oldVersion = versionEvents - 1;
} }
var newVersion = UseEventSourcing ? versionEvents : oldVersion + 1; var newVersion = UseEventSourcing ? versionEvents : oldVersion + 1;

4
backend/src/Squidex.Infrastructure/Validation/ValidationException.cs

@ -15,6 +15,8 @@ namespace Squidex.Infrastructure.Validation
[Serializable] [Serializable]
public class ValidationException : DomainException public class ValidationException : DomainException
{ {
private const string ValidationError = "VALIDATION_ERROR";
public IReadOnlyList<ValidationError> Errors { get; } public IReadOnlyList<ValidationError> Errors { get; }
public ValidationException(string error, Exception? inner = null) public ValidationException(string error, Exception? inner = null)
@ -28,7 +30,7 @@ namespace Squidex.Infrastructure.Validation
} }
public ValidationException(IReadOnlyList<ValidationError> errors, Exception? inner = null) public ValidationException(IReadOnlyList<ValidationError> errors, Exception? inner = null)
: base(FormatMessage(errors), inner) : base(FormatMessage(errors), ValidationError, inner)
{ {
Errors = errors; Errors = errors;
} }

28
backend/src/Squidex.Web/ApiExceptionConverter.cs

@ -29,6 +29,7 @@ namespace Squidex.Web
[404] = "https://tools.ietf.org/html/rfc7231#section-6.5.4", [404] = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
[406] = "https://tools.ietf.org/html/rfc7231#section-6.5.6", [406] = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
[409] = "https://tools.ietf.org/html/rfc7231#section-6.5.8", [409] = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
[410] = "https://tools.ietf.org/html/rfc7231#section-6.5.9",
[412] = "https://tools.ietf.org/html/rfc7231#section-6.5.10", [412] = "https://tools.ietf.org/html/rfc7231#section-6.5.10",
[415] = "https://tools.ietf.org/html/rfc7231#section-6.5.13", [415] = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
[422] = "https://tools.ietf.org/html/rfc4918#section-11.2", [422] = "https://tools.ietf.org/html/rfc4918#section-11.2",
@ -85,20 +86,23 @@ namespace Squidex.Web
case ValidationException ex: case ValidationException ex:
return (CreateError(400, T.Get("common.httpValidationError"), ToErrors(ex.Errors).ToArray()), true); return (CreateError(400, T.Get("common.httpValidationError"), ToErrors(ex.Errors).ToArray()), true);
case DomainObjectNotFoundException: case DomainObjectNotFoundException ex:
return (CreateError(404), true); return (CreateError(404, errorCode: ex.ErrorCode), true);
case DomainObjectVersionException: case DomainObjectVersionException ex:
return (CreateError(412, exception.Message), true); return (CreateError(412, exception.Message, errorCode: ex.ErrorCode), true);
case DomainObjectConflictException: case DomainObjectDeletedException ex:
return (CreateError(409, exception.Message), true); return (CreateError(410, exception.Message, errorCode: ex.ErrorCode), true);
case DomainForbiddenException: case DomainObjectConflictException ex:
return (CreateError(403, exception.Message), true); return (CreateError(409, exception.Message, errorCode: ex.ErrorCode), true);
case DomainException: case DomainForbiddenException ex:
return (CreateError(400, exception.Message), true); return (CreateError(403, exception.Message, errorCode: ex.ErrorCode), true);
case DomainException ex:
return (CreateError(400, exception.Message, errorCode: ex.ErrorCode), true);
case SecurityException: case SecurityException:
return (CreateError(403), false); return (CreateError(403), false);
@ -114,9 +118,9 @@ namespace Squidex.Web
} }
} }
private static ErrorDto CreateError(int status, string? message = null, string[]? details = null) private static ErrorDto CreateError(int status, string? message = null, string[]? details = null, string? errorCode = null)
{ {
var error = new ErrorDto { StatusCode = status, Message = message, Details = details }; var error = new ErrorDto { StatusCode = status, Message = message, Details = details, ErrorCode = errorCode };
return error; return error;
} }

3
backend/src/Squidex.Web/ErrorDto.cs

@ -16,6 +16,9 @@ namespace Squidex.Web
[Display(Description = "Error message.")] [Display(Description = "Error message.")]
public string? Message { get; set; } public string? Message { get; set; }
[Display(Description = "The error code.")]
public string? ErrorCode { get; set; }
[Display(Description = "The optional trace id.")] [Display(Description = "The optional trace id.")]
public string? TraceId { get; set; } public string? TraceId { get; set; }

4
backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Users
public async Task Should_generate_signing_credentials_once() public async Task Should_generate_signing_credentials_once()
{ {
A.CallTo(() => store.ReadAsync(A<DomainId>._)) A.CallTo(() => store.ReadAsync(A<DomainId>._))
.Returns((null!, 0)); .Returns((null!, true, 0));
var credentials1 = await sut.GetSigningCredentialsAsync(); var credentials1 = await sut.GetSigningCredentialsAsync();
var credentials2 = await sut.GetSigningCredentialsAsync(); var credentials2 = await sut.GetSigningCredentialsAsync();
@ -45,7 +45,7 @@ namespace Squidex.Domain.Users
public async Task Should_generate_validation_keys_once() public async Task Should_generate_validation_keys_once()
{ {
A.CallTo(() => store.ReadAsync(A<DomainId>._)) A.CallTo(() => store.ReadAsync(A<DomainId>._))
.Returns((null!, 0)); .Returns((null!, true, 0));
var credentials1 = await sut.GetValidationKeysAsync(); var credentials1 = await sut.GetValidationKeysAsync();
var credentials2 = await sut.GetValidationKeysAsync(); var credentials2 = await sut.GetValidationKeysAsync();

28
backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs

@ -35,6 +35,34 @@ namespace Squidex.Infrastructure.Commands
AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty); AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty);
} }
[Fact]
public async Task Should_repair_when_stale()
{
A.CallTo(() => persistence.IsSnapshotStale)
.Returns(true);
SetupCreated(1);
await sut.EnsureLoadedAsync();
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_repair_when_not_stale()
{
A.CallTo(() => persistence.IsSnapshotStale)
.Returns(false);
SetupCreated(1);
await sut.EnsureLoadedAsync();
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>._))
.MustNotHaveHappened();
}
[Fact] [Fact]
public async Task Should_write_state_and_events_when_created() public async Task Should_write_state_and_events_when_created()
{ {

13
backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs

@ -12,6 +12,16 @@ namespace Squidex.Infrastructure
{ {
public class DomainObjectExceptionTests public class DomainObjectExceptionTests
{ {
[Fact]
public void Should_serialize_and_deserialize_DomainException()
{
var source = new DomainException("Message", "ErrorCode");
var result = source.SerializeAndDeserializeBinary();
Assert.Equal(result.ErrorCode, source.ErrorCode);
Assert.Equal(result.Message, source.Message);
}
[Fact] [Fact]
public void Should_serialize_and_deserialize_DomainObjectDeletedException() public void Should_serialize_and_deserialize_DomainObjectDeletedException()
{ {
@ -19,7 +29,6 @@ namespace Squidex.Infrastructure
var result = source.SerializeAndDeserializeBinary(); var result = source.SerializeAndDeserializeBinary();
Assert.Equal(result.Id, source.Id); Assert.Equal(result.Id, source.Id);
Assert.Equal(result.Message, source.Message); Assert.Equal(result.Message, source.Message);
} }
@ -30,7 +39,6 @@ namespace Squidex.Infrastructure
var result = source.SerializeAndDeserializeBinary(); var result = source.SerializeAndDeserializeBinary();
Assert.Equal(result.Id, source.Id); Assert.Equal(result.Id, source.Id);
Assert.Equal(result.Message, source.Message); Assert.Equal(result.Message, source.Message);
} }
@ -43,7 +51,6 @@ namespace Squidex.Infrastructure
Assert.Equal(result.Id, source.Id); Assert.Equal(result.Id, source.Id);
Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); Assert.Equal(result.ExpectedVersion, source.ExpectedVersion);
Assert.Equal(result.CurrentVersion, source.CurrentVersion); Assert.Equal(result.CurrentVersion, source.CurrentVersion);
Assert.Equal(result.Message, source.Message); Assert.Equal(result.Message, source.Message);
} }
} }

82
backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs

@ -19,18 +19,18 @@ namespace Squidex.Infrastructure.States
public class PersistenceEventSourcingTests public class PersistenceEventSourcingTests
{ {
private readonly DomainId key = DomainId.NewGuid(); private readonly DomainId key = DomainId.NewGuid();
private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>(); private readonly ISnapshotStore<string> snapshotStore = A.Fake<ISnapshotStore<string>>();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>(); private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>(); private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>(); private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly IStore<int> sut; private readonly IStore<string> sut;
public PersistenceEventSourcingTests() public PersistenceEventSourcingTests()
{ {
A.CallTo(() => streamNameResolver.GetStreamName(None.Type, A<string>._)) A.CallTo(() => streamNameResolver.GetStreamName(None.Type, A<string>._))
.ReturnsLazily(x => x.GetArgument<string>(1)!); .ReturnsLazily(x => x.GetArgument<string>(1)!);
sut = new Store<int>(snapshotStore, eventStore, eventDataFormatter, streamNameResolver); sut = new Store<string>(snapshotStore, eventStore, eventDataFormatter, streamNameResolver);
} }
[Fact] [Fact]
@ -86,32 +86,54 @@ namespace Squidex.Infrastructure.States
} }
[Fact] [Fact]
public async Task Should_read_status_from_snapshot() public async Task Should_read_read_from_snapshot_store()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L)); .Returns(("2", true, 2L));
SetupEventStore(3, 2); SetupEventStore(3, 2);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
await persistence.ReadAsync(); await persistence.ReadAsync();
Assert.False(persistence.IsSnapshotStale);
A.CallTo(() => eventStore.QueryAsync(key.ToString(), 3)) A.CallTo(() => eventStore.QueryAsync(key.ToString(), 3))
.MustHaveHappened(); .MustHaveHappened();
} }
[Fact]
public async Task Should_mark_as_stale_when_snapshot_old_than_events()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns(("2", true, 1L));
SetupEventStore(3, 2, 2);
var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
await persistence.ReadAsync();
Assert.True(persistence.IsSnapshotStale);
A.CallTo(() => eventStore.QueryAsync(key.ToString(), 2))
.MustHaveHappened();
}
[Fact] [Fact]
public async Task Should_throw_exception_if_events_are_older_than_snapshot() public async Task Should_throw_exception_if_events_are_older_than_snapshot()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L)); .Returns(("2", true, 2L));
SetupEventStore(3, 0, 3); SetupEventStore(3, 0, 3);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
@ -122,11 +144,11 @@ namespace Squidex.Infrastructure.States
public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() public async Task Should_throw_exception_if_events_have_gaps_to_snapshot()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L)); .Returns(("2", true, 2L));
SetupEventStore(3, 4, 3); SetupEventStore(3, 4, 3);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
@ -159,11 +181,11 @@ namespace Squidex.Infrastructure.States
public async Task Should_throw_exception_if_other_version_found_from_snapshot() public async Task Should_throw_exception_if_other_version_found_from_snapshot()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L)); .Returns(("2", true, 2L));
SetupEventStore(0); SetupEventStore(0);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
@ -175,7 +197,7 @@ namespace Squidex.Infrastructure.States
{ {
SetupEventStore(0); SetupEventStore(0);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
@ -200,7 +222,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1))) A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => snapshotStore.WriteAsync(A<DomainId>._, A<int>._, A<long>._, A<long>._)) A.CallTo(() => snapshotStore.WriteAsync(A<DomainId>._, A<string>._, A<long>._, A<long>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -219,25 +241,25 @@ namespace Squidex.Infrastructure.States
public async Task Should_write_snapshot_to_store() public async Task Should_write_snapshot_to_store()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L)); .Returns(("2", true, 2L));
SetupEventStore(3); SetupEventStore(3);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
await persistence.ReadAsync(); await persistence.ReadAsync();
await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteEventAsync(Envelope.Create(new MyEvent()));
await persistence.WriteSnapshotAsync(4); await persistence.WriteSnapshotAsync("4");
await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteEventAsync(Envelope.Create(new MyEvent()));
await persistence.WriteSnapshotAsync(5); await persistence.WriteSnapshotAsync("5");
A.CallTo(() => snapshotStore.WriteAsync(key, 4, 2, 3)) A.CallTo(() => snapshotStore.WriteAsync(key, "4", 2, 3))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => snapshotStore.WriteAsync(key, 5, 3, 4)) A.CallTo(() => snapshotStore.WriteAsync(key, "5", 3, 4))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -245,25 +267,25 @@ namespace Squidex.Infrastructure.States
public async Task Should_write_snapshot_to_store_when_not_read_before() public async Task Should_write_snapshot_to_store_when_not_read_before()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((default, EtagVersion.Empty)); .Returns((null!, true, EtagVersion.Empty));
SetupEventStore(3); SetupEventStore(3);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
await persistence.ReadAsync(); await persistence.ReadAsync();
await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteEventAsync(Envelope.Create(new MyEvent()));
await persistence.WriteSnapshotAsync(4); await persistence.WriteSnapshotAsync("4");
await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteEventAsync(Envelope.Create(new MyEvent()));
await persistence.WriteSnapshotAsync(5); await persistence.WriteSnapshotAsync("5");
A.CallTo(() => snapshotStore.WriteAsync(key, 4, 2, 3)) A.CallTo(() => snapshotStore.WriteAsync(key, "4", 2, 3))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => snapshotStore.WriteAsync(key, 5, 3, 4)) A.CallTo(() => snapshotStore.WriteAsync(key, "5", 3, 4))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -271,19 +293,19 @@ namespace Squidex.Infrastructure.States
public async Task Should_not_write_snapshot_to_store_when_not_changed() public async Task Should_not_write_snapshot_to_store_when_not_changed()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, 2)); .Returns(("0", true, 2));
SetupEventStore(3); SetupEventStore(3);
var persistedState = Save.Snapshot(-1); var persistedState = Save.Snapshot(string.Empty);
var persistedEvents = Save.Events(); var persistedEvents = Save.Events();
var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write);
await persistence.ReadAsync(); await persistence.ReadAsync();
await persistence.WriteSnapshotAsync(4); await persistence.WriteSnapshotAsync("4");
A.CallTo(() => snapshotStore.WriteAsync(key, A<int>._, A<long>._, A<long>._)) A.CallTo(() => snapshotStore.WriteAsync(key, A<string>._, A<long>._, A<long>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }

29
backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs

@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.States
public async Task Should_read_from_store() public async Task Should_read_from_store()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((20, 10)); .Returns((20, true, 10));
var persistedState = Save.Snapshot(0); var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
@ -42,11 +42,26 @@ namespace Squidex.Infrastructure.States
Assert.Equal(20, persistedState.Value); Assert.Equal(20, persistedState.Value);
} }
[Fact]
public async Task Should_not_read_from_store_when_not_valid()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((20, false, 10));
var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
await persistence.ReadAsync();
Assert.Equal(10, persistence.Version);
Assert.Equal(0, persistedState.Value);
}
[Fact] [Fact]
public async Task Should_return_empty_version_when_version_negative() public async Task Should_return_empty_version_when_version_negative()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((20, -10)); .Returns((20, true, -10));
var persistedState = Save.Snapshot(0); var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
@ -60,7 +75,7 @@ namespace Squidex.Infrastructure.States
public async Task Should_set_to_empty_when_store_returns_not_found() public async Task Should_set_to_empty_when_store_returns_not_found()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((20, EtagVersion.Empty)); .Returns((20, true, EtagVersion.Empty));
var persistedState = Save.Snapshot(0); var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
@ -75,7 +90,7 @@ namespace Squidex.Infrastructure.States
public async Task Should_throw_exception_if_not_found_and_version_expected() public async Task Should_throw_exception_if_not_found_and_version_expected()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, EtagVersion.Empty)); .Returns((123, true, EtagVersion.Empty));
var persistedState = Save.Snapshot(0); var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
@ -87,7 +102,7 @@ namespace Squidex.Infrastructure.States
public async Task Should_throw_exception_if_other_version_found() public async Task Should_throw_exception_if_other_version_found()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 2)); .Returns((123, true, 2));
var persistedState = Save.Snapshot(0); var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
@ -99,7 +114,7 @@ namespace Squidex.Infrastructure.States
public async Task Should_write_to_store_with_previous_version() public async Task Should_write_to_store_with_previous_version()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((20, 10)); .Returns((20, true, 10));
var persistedState = Save.Snapshot(0); var persistedState = Save.Snapshot(0);
var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write);
@ -130,7 +145,7 @@ namespace Squidex.Infrastructure.States
public async Task Should_not_wrap_exception_when_writing_to_store_with_previous_version() public async Task Should_not_wrap_exception_when_writing_to_store_with_previous_version()
{ {
A.CallTo(() => snapshotStore.ReadAsync(key)) A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((20, 10)); .Returns((20, true, 10));
A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11)) A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11))
.Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); .Throws(new InconsistentStateException(1, 1, new InvalidOperationException()));

35
backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs

@ -103,6 +103,19 @@ namespace Squidex.Web
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
[Fact]
public void Should_generate_400_for_DomainException_with_error_code()
{
var context = Error(new DomainException("NotAllowed", "ERROR_CODE_XYZ"));
sut.OnException(context);
Validate(400, context.Result, context.Exception, "ERROR_CODE_XYZ");
A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!))
.MustNotHaveHappened();
}
[Fact] [Fact]
public void Should_generate_400_for_DecoderFallbackException() public void Should_generate_400_for_DecoderFallbackException()
{ {
@ -123,7 +136,20 @@ namespace Squidex.Web
sut.OnException(context); sut.OnException(context);
Validate(409, context.Result, context.Exception); Validate(409, context.Result, context.Exception, "OBJECT_CONFLICT");
A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!))
.MustNotHaveHappened();
}
[Fact]
public void Should_generate_410_for_DomainObjectDeletedException()
{
var context = Error(new DomainObjectDeletedException("1"));
sut.OnException(context);
Validate(410, context.Result, context.Exception, "OBJECT_DELETED");
A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!)) A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -136,7 +162,7 @@ namespace Squidex.Web
sut.OnException(context); sut.OnException(context);
Validate(412, context.Result, context.Exception); Validate(412, context.Result, context.Exception, "OBJECT_VERSION_CONFLICT");
A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!)) A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -149,7 +175,7 @@ namespace Squidex.Web
sut.OnException(context); sut.OnException(context);
Validate(403, context.Result, context.Exception); Validate(403, context.Result, context.Exception, "FORBIDDEN");
A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!)) A.CallTo(() => log.Log(A<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -229,7 +255,7 @@ namespace Squidex.Web
return actionContext; return actionContext;
} }
private static void Validate(int statusCode, IActionResult? actionResult, Exception? exception) private static void Validate(int statusCode, IActionResult? actionResult, Exception? exception, string? errorCode = null)
{ {
var result = actionResult as ObjectResult; var result = actionResult as ObjectResult;
@ -239,6 +265,7 @@ namespace Squidex.Web
Assert.Equal(statusCode, result?.StatusCode); Assert.Equal(statusCode, result?.StatusCode);
Assert.Equal(statusCode, error?.StatusCode); Assert.Equal(statusCode, error?.StatusCode);
Assert.Equal(errorCode, error?.ErrorCode);
if (exception != null) if (exception != null)
{ {

18
frontend/app/framework/angular/forms/error-validator.spec.ts

@ -33,7 +33,7 @@ describe('ErrorValidator', () => {
}); });
it('should return no message when error does not match', () => { it('should return no message when error does not match', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1Property: My Error.' 'nested1Property: My Error.'
])); ]));
@ -43,7 +43,7 @@ describe('ErrorValidator', () => {
}); });
it('should return matching error', () => { it('should return matching error', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'other, nested1: My Error.' 'other, nested1: My Error.'
])); ]));
@ -57,7 +57,7 @@ describe('ErrorValidator', () => {
}); });
it('should return matching error twice if value does not change', () => { it('should return matching error twice if value does not change', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1: My Error.' 'nested1: My Error.'
])); ]));
@ -78,7 +78,7 @@ describe('ErrorValidator', () => {
}); });
it('should not return matching error again if value has changed', () => { it('should not return matching error again if value has changed', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1[1].nested2: My Error.' 'nested1[1].nested2: My Error.'
])); ]));
@ -100,7 +100,7 @@ describe('ErrorValidator', () => {
}); });
it('should not return matching error again if value has changed to initial', () => { it('should not return matching error again if value has changed to initial', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1[1].nested2: My Error.' 'nested1[1].nested2: My Error.'
])); ]));
@ -126,7 +126,7 @@ describe('ErrorValidator', () => {
}); });
it('should return matching errors', () => { it('should return matching errors', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1: My Error1.', 'nested1: My Error1.',
'nested1: My Error2.' 'nested1: My Error2.'
])); ]));
@ -141,7 +141,7 @@ describe('ErrorValidator', () => {
}); });
it('should return deeply matching error', () => { it('should return deeply matching error', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1[1].nested2: My Error.' 'nested1[1].nested2: My Error.'
])); ]));
@ -155,7 +155,7 @@ describe('ErrorValidator', () => {
}); });
it('should return partial matching error', () => { it('should return partial matching error', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1[1].nested2: My Error.' 'nested1[1].nested2: My Error.'
])); ]));
@ -169,7 +169,7 @@ describe('ErrorValidator', () => {
}); });
it('should return partial matching index error', () => { it('should return partial matching index error', () => {
validator.setError(new ErrorDto(500, 'Error', [ validator.setError(new ErrorDto(500, 'Error', null, [
'nested1[1].nested2: My Error.' 'nested1[1].nested2: My Error.'
])); ]));

18
frontend/app/framework/angular/http/http-extensions.spec.ts

@ -13,11 +13,11 @@ describe('ErrorParsing', () => {
const response: any = new Error(); const response: any = new Error();
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(500, 'Fallback', [], response)); expect(result).toEqual(new ErrorDto(500, 'Fallback', null, [], response));
}); });
it('should just forward error dto', () => { it('should just forward error dto', () => {
const response: any = new ErrorDto(500, 'error', []); const response: any = new ErrorDto(500, 'error', null, []);
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toBe(response); expect(result).toBe(response);
@ -27,23 +27,23 @@ describe('ErrorParsing', () => {
const response: any = { status: 412 }; const response: any = { status: 412 };
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(412, 'i18n:common.httpConflict', [], response)); expect(result).toEqual(new ErrorDto(412, 'i18n:common.httpConflict', null, [], response));
}); });
it('should return default 429 error', () => { it('should return default 429 error', () => {
const response: any = { status: 429 }; const response: any = { status: 429 };
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(429, 'i18n:common.httpLimit', [], response)); expect(result).toEqual(new ErrorDto(429, 'i18n:common.httpLimit', null, [], response));
}); });
it('should return error from error object', () => { it('should return error from error object', () => {
const error = { message: 'My-Message', details: ['My-Detail'] }; const error = { message: 'My-Message', details: ['My-Detail'], errorCode: 'ERROR_CODE_XYZ' };
const response: any = { status: 400, error }; const response: any = { status: 400, error };
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(400, 'My-Message', ['My-Detail'], response)); expect(result).toEqual(new ErrorDto(400, 'My-Message', 'ERROR_CODE_XYZ', ['My-Detail'], response));
}); });
it('should return error from error json', () => { it('should return error from error json', () => {
@ -52,7 +52,7 @@ describe('ErrorParsing', () => {
const response: any = { status: 400, error: JSON.stringify(error) }; const response: any = { status: 400, error: JSON.stringify(error) };
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(400, 'My-Message', ['My-Detail'], response)); expect(result).toEqual(new ErrorDto(400, 'My-Message', undefined, ['My-Detail'], response));
}); });
it('should return default when object is invalid', () => { it('should return default when object is invalid', () => {
@ -61,7 +61,7 @@ describe('ErrorParsing', () => {
const response: any = { status: 400, error }; const response: any = { status: 400, error };
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(500, 'Fallback', [], response)); expect(result).toEqual(new ErrorDto(500, 'Fallback', null, [], response));
}); });
it('should return default when json is invalid', () => { it('should return default when json is invalid', () => {
@ -70,6 +70,6 @@ describe('ErrorParsing', () => {
const response: any = { status: 400, error }; const response: any = { status: 400, error };
const result = parseError(response, 'Fallback'); const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(500, 'Fallback', [], response)); expect(result).toEqual(new ErrorDto(500, 'Fallback', null, [], response));
}); });
}); });

8
frontend/app/framework/angular/http/http-extensions.ts

@ -97,11 +97,11 @@ export function parseError(response: HttpErrorResponse, fallback: string) {
const { error, status } = response; const { error, status } = response;
if (status === 412) { if (status === 412) {
return new ErrorDto(412, 'i18n:common.httpConflict', [], response); return new ErrorDto(412, 'i18n:common.httpConflict', null, [], response);
} }
if (status === 429) { if (status === 429) {
return new ErrorDto(429, 'i18n:common.httpLimit', [], response); return new ErrorDto(429, 'i18n:common.httpLimit', null, [], response);
} }
let parsed: any; let parsed: any;
@ -117,8 +117,8 @@ export function parseError(response: HttpErrorResponse, fallback: string) {
} }
if (parsed && Types.isString(parsed.message)) { if (parsed && Types.isString(parsed.message)) {
return new ErrorDto(status, parsed.message, parsed.details, response); return new ErrorDto(status, parsed.message, parsed.errorCode, parsed.details, response);
} }
return new ErrorDto(500, fallback, [], response); return new ErrorDto(500, fallback, null, [], response);
} }

12
frontend/app/framework/utils/error.spec.ts

@ -20,6 +20,12 @@ describe('ErrorDto', () => {
.returns((key: string) => key.substr(5)); .returns((key: string) => key.substr(5));
}); });
it('should create simple message with error code', () => {
const error = new ErrorDto(500, 'i18n:error.', 'ERROR_CODE_XYZ');
expect(error.errorCode).toBe('ERROR_CODE_XYZ');
});
it('should create simple message when no details are specified.', () => { it('should create simple message when no details are specified.', () => {
const error = new ErrorDto(500, 'i18n:error.'); const error = new ErrorDto(500, 'i18n:error.');
@ -37,7 +43,7 @@ describe('ErrorDto', () => {
}); });
it('should append dot to detail', () => { it('should append dot to detail', () => {
const error = new ErrorDto(500, 'i18n:error.', ['i18n:detail']); const error = new ErrorDto(500, 'i18n:error.', null, ['i18n:detail']);
const result = error.translate(localizer.object); const result = error.translate(localizer.object);
@ -45,7 +51,7 @@ describe('ErrorDto', () => {
}); });
it('should ccreate html list when detail has one item', () => { it('should ccreate html list when detail has one item', () => {
const error = new ErrorDto(500, 'i18n:error.', ['i18n:detail.']); const error = new ErrorDto(500, 'i18n:error.', null, ['i18n:detail.']);
const result = error.translate(localizer.object); const result = error.translate(localizer.object);
@ -53,7 +59,7 @@ describe('ErrorDto', () => {
}); });
it('should create html list when error has more items.', () => { it('should create html list when error has more items.', () => {
const error = new ErrorDto(500, 'i18n:error.', ['i18n:detail1.', 'i18n:detail2.']); const error = new ErrorDto(500, 'i18n:error.', null, ['i18n:detail1.', 'i18n:detail2.']);
const result = error.translate(localizer.object); const result = error.translate(localizer.object);

1
frontend/app/framework/utils/error.ts

@ -39,6 +39,7 @@ export class ErrorDto {
constructor( constructor(
public readonly statusCode: number, public readonly statusCode: number,
public readonly message: string, public readonly message: string,
public readonly errorCode?: string | null,
details?: ReadonlyArray<string> | ReadonlyArray<ErrorDetailsDto>, details?: ReadonlyArray<string> | ReadonlyArray<ErrorDetailsDto>,
public readonly inner?: any public readonly inner?: any
) { ) {

4
frontend/app/shared/state/assets.state.spec.ts

@ -342,7 +342,7 @@ describe('AssetsState', () => {
it('should remove asset from snapshot when when referenced and not confirmed', () => { it('should remove asset from snapshot when when referenced and not confirmed', () => {
assetsService.setup(x => x.deleteAssetItem(app, asset1, false, asset1.version)) assetsService.setup(x => x.deleteAssetItem(app, asset1, false, asset1.version))
.returns(() => throwError(new ErrorDto(404, 'Referenced'))); .returns(() => throwError(new ErrorDto(404, 'Referenced', 'OBJECT_REFERENCED')));
assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version)) assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version))
.returns(() => of(versioned(newVersion))); .returns(() => of(versioned(newVersion)));
@ -359,7 +359,7 @@ describe('AssetsState', () => {
it('should not remove asset when referenced and not confirmed', () => { it('should not remove asset when referenced and not confirmed', () => {
assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version)) assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version))
.returns(() => throwError(new ErrorDto(404, 'Referenced'))); .returns(() => throwError(new ErrorDto(404, 'Referenced', 'OBJECT_REFERENCED')));
dialogs.setup(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAnyString())) dialogs.setup(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAnyString()))
.returns(() => of(false)); .returns(() => of(false));

2
frontend/app/shared/state/assets.state.ts

@ -402,7 +402,7 @@ export abstract class AssetsStateBase extends State<Snapshot> {
} }
function isReferrerError(error?: ErrorDto) { function isReferrerError(error?: ErrorDto) {
return error?.statusCode === 400 && (!error?.details || error?.details.length === 0); return error?.errorCode === 'OBJECT_REFERENCED';
} }
function updateTags(snapshot: Snapshot, newAsset?: AssetDto, oldAsset?: AssetDto) { function updateTags(snapshot: Snapshot, newAsset?: AssetDto, oldAsset?: AssetDto) {

2
frontend/app/shared/state/contents.state.ts

@ -396,7 +396,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
} }
function isReferrerError(error?: ErrorDto) { function isReferrerError(error?: ErrorDto) {
return error?.statusCode === 400 && (!error?.details || error?.details.length === 0); return error?.errorCode === 'OBJECT_REFERENCED';
} }
@Injectable() @Injectable()

19826
frontend/package-lock.json

File diff suppressed because it is too large
Loading…
Cancel
Save