Browse Source

Fix restore and backup.

pull/909/head
Sebastian 3 years ago
parent
commit
b1dd7c19e7
  1. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
  2. 19
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs
  3. 55
      backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.Run.cs
  4. 99
      backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs
  5. 58
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.Run.cs
  6. 172
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs
  7. 32
      backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs
  8. 34
      backend/src/Squidex.Infrastructure/States/SimpleState.cs
  9. 20
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs
  10. 36
      backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs
  11. 25
      backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs

6
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -15,7 +15,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string Role { get; set; } = Roles.Editor;
public bool Restoring { get; set; }
public bool IgnoreActor { get; set; }
public bool IgnorePlans { get; set; }
public bool Invite { get; set; }
}

19
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -43,19 +43,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
throw new DomainObjectNotFoundException(command.ContributorId);
}
if (!command.Restoring)
if (!command.IgnoreActor && string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
{
throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself"));
}
throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself"));
}
if (!contributors.TryGetValue(command.ContributorId, out _))
if (!command.IgnorePlans && !contributors.TryGetValue(command.ContributorId, out _))
{
if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors)
{
if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors)
{
e(T.Get("apps.contributors.maxReached"));
}
e(T.Get("apps.contributors.maxReached"));
}
}
}

55
backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.Run.cs

@ -0,0 +1,55 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Backup.State;
using Squidex.Infrastructure;
#pragma warning disable MA0040 // Flow the cancellation token
namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed partial class BackupProcessor
{
// Use a run to store all state that is necessary for a single run.
private sealed class Run : IDisposable
{
private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource();
private readonly CancellationTokenSource cancellationLinked;
public IEnumerable<IBackupHandler> Handlers { get; init; }
public RefToken Actor { get; init; }
public BackupJob Job { get; init; }
public CancellationToken CancellationToken => cancellationLinked.Token;
public Run(CancellationToken ct)
{
cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token);
}
public void Dispose()
{
cancellationSource.Dispose();
cancellationLinked.Dispose();
}
public void Cancel()
{
try
{
cancellationSource.Cancel();
}
catch (ObjectDisposedException)
{
// Cancellation token might have been disposed, if the run is completed.
}
}
}
}
}

99
backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs

@ -21,9 +21,8 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed class BackupProcessor
public sealed partial class BackupProcessor
{
private static readonly Duration UpdateDuration = Duration.FromSeconds(1);
private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IBackupArchiveStore backupArchiveStore;
private readonly IBackupHandlerFactory backupHandlerFactory;
@ -36,44 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly DomainId appId;
private Run? currentRun;
// Use a run to store all state that is necessary for a single run.
private sealed class Run : IDisposable
{
private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource();
private readonly CancellationTokenSource cancellationLinked;
public IEnumerable<IBackupHandler> Handlers { get; init; }
public RefToken Actor { get; init; }
public BackupJob Job { get; init; }
public CancellationToken CancellationToken => cancellationLinked.Token;
public Run(CancellationToken ct)
{
cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token);
}
public void Dispose()
{
cancellationSource.Dispose();
cancellationLinked.Dispose();
}
public void Cancel()
{
try
{
cancellationSource.Cancel();
}
catch (ObjectDisposedException)
{
// Cancellation token might have been disposed, if the run is completed.
}
}
}
public IClock Clock { get; set; } = SystemClock.Instance;
public BackupProcessor(
@ -195,10 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
writer.WriteEvent(storedEvent, ct);
run.Job.HandledEvents = writer.WrittenEvents;
run.Job.HandledAssets = writer.WrittenAttachments;
lastTimestamp = await WritePeriodically(lastTimestamp);
await LogAsync(run, writer.WrittenEvents, writer.WrittenAttachments);
}
foreach (var handler in run.Handlers)
@ -224,8 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
await backupArchiveStore.UploadAsync(run.Job.Id, stream, ct);
}
run.Job.Status = JobStatus.Completed;
await SetStatusAsync(run, JobStatus.Completed);
}
catch (OperationCanceledException)
{
@ -233,15 +190,9 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
catch (Exception ex)
{
log.LogError(ex, "Faield to make backup with backup id '{backupId}'.", run.Job.Id);
run.Job.Status = JobStatus.Failed;
}
finally
{
run.Job.Stopped = Clock.GetCurrentInstant();
await SetStatusAsync(run, JobStatus.Failed);
await state.WriteAsync(default);
log.LogError(ex, "Faield to make backup with backup id '{backupId}'.", run.Job.Id);
}
}
@ -250,20 +201,6 @@ namespace Squidex.Domain.Apps.Entities.Backup
return $"^[^\\-]*-{Regex.Escape(appId.ToString())}";
}
private async Task<Instant> WritePeriodically(Instant lastTimestamp)
{
var now = Clock.GetCurrentInstant();
if ((now - lastTimestamp) >= UpdateDuration)
{
lastTimestamp = now;
await state.WriteAsync();
}
return lastTimestamp;
}
public Task DeleteAsync(DomainId id)
{
return scheduler.ScheduleAsync(async _ =>
@ -301,5 +238,31 @@ namespace Squidex.Domain.Apps.Entities.Backup
await state.WriteAsync();
}
private Task SetStatusAsync(Run run, JobStatus status)
{
var now = Clock.GetCurrentInstant();
run.Job.Status = status;
if (status == JobStatus.Failed || status == JobStatus.Completed)
{
run.Job.Stopped = now;
}
else if (status == JobStatus.Started)
{
run.Job.Started = now;
}
return state.WriteAsync(ct: default);
}
private Task LogAsync(Run run, int numEvents, int numAttachments)
{
run.Job.HandledEvents = numEvents;
run.Job.HandledAssets = numAttachments;
return state.WriteAsync(100, run.CancellationToken);
}
}
}

58
backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.Run.cs

@ -0,0 +1,58 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Backup.State;
namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed partial class RestoreProcessor
{
// Use a run to store all state that is necessary for a single run.
private sealed class Run : IDisposable
{
private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource();
private readonly CancellationTokenSource cancellationLinked;
public IEnumerable<IBackupHandler> Handlers { get; init; }
public IBackupReader Reader { get; set; }
public RestoreJob Job { get; init; }
public RestoreContext Context { get; set; }
public StreamMapper StreamMapper { get; set; }
public CancellationToken CancellationToken => cancellationLinked.Token;
public Run(CancellationToken ct)
{
cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token);
}
public void Dispose()
{
Reader?.Dispose();
cancellationSource.Dispose();
cancellationLinked.Dispose();
}
public void Cancel()
{
try
{
cancellationSource.Cancel();
}
catch (ObjectDisposedException)
{
// Cancellation token might have been disposed, if the run is completed.
}
}
}
}
}

172
backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs

@ -23,7 +23,7 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed class RestoreProcessor
public sealed partial class RestoreProcessor
{
private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IBackupHandlerFactory backupHandlerFactory;
@ -37,65 +37,6 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly SimpleState<BackupRestoreState> state;
private Run? currentRun;
// Use a run to store all state that is necessary for a single run.
private sealed class Run : IDisposable
{
private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource();
private readonly CancellationTokenSource cancellationLinked;
private readonly IClock clock;
public IEnumerable<IBackupHandler> Handlers { get; init; }
public IBackupReader Reader { get; set; }
public RestoreJob Job { get; init; }
public RestoreContext Context { get; set; }
public StreamMapper StreamMapper { get; set; }
public CancellationToken CancellationToken => cancellationLinked.Token;
public Run(IClock clock, CancellationToken ct)
{
cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token);
this.clock = clock;
}
public void Log(string message, bool replace = false)
{
if (replace && Job.Log.Count > 0)
{
Job.Log[^1] = $"{clock.GetCurrentInstant()}: {message}";
}
else
{
Job.Log.Add($"{clock.GetCurrentInstant()}: {message}");
}
}
public void Dispose()
{
Reader?.Dispose();
cancellationSource.Dispose();
cancellationLinked.Dispose();
}
public void Cancel()
{
try
{
cancellationSource.Cancel();
}
catch (ObjectDisposedException)
{
// Cancellation token might have been disposed, if the run is completed.
}
}
}
public IClock Clock { get; set; } = SystemClock.Instance;
public RestoreProcessor(
@ -155,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
state.Value.Job?.EnsureCanStart();
// Set the current run first to indicate that we are running a rule at the moment.
var run = currentRun = new Run(Clock, ct)
var run = currentRun = new Run(ct)
{
Job = new RestoreJob
{
@ -192,11 +133,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
try
{
run.Log("Started. The restore process has the following steps:");
run.Log(" * Download backup");
run.Log(" * Restore events and attachments.");
run.Log(" * Restore all objects like app, schemas and contents");
run.Log(" * Complete the restore operation for all objects");
await LogAsync(run, "Started. The restore process has the following steps:");
await LogAsync(run, " * Download backup");
await LogAsync(run, " * Restore events and attachments.");
await LogAsync(run, " * Restore all objects like app, schemas and contents");
await LogAsync(run, " * Complete the restore operation for all objects");
log.LogInformation("Backup with job id {backupId} with from URL '{url}' started.", run.Job.Id, run.Job.Url);
@ -216,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
await handler.RestoreAsync(run.Context, ct);
}
run.Log($"Restored {handler.Name}");
await LogAsync(run, $"Restored {handler.Name}");
}
foreach (var handler in run.Handlers)
@ -226,43 +167,35 @@ namespace Squidex.Domain.Apps.Entities.Backup
await handler.CompleteRestoreAsync(run.Context, run.Job.NewAppName!);
}
run.Log($"Completed {handler.Name}");
await LogAsync(run, $"Completed {handler.Name}");
}
await AssignContributorAsync(run);
run.Job.Status = JobStatus.Completed;
run.Log("Completed, Yeah!");
await SetStatusAsync(run, JobStatus.Completed, "Completed, Yeah!");
log.LogInformation("Backup with job id {backupId} from URL '{url}' completed.", run.Job.Id, run.Job.Url);
}
catch (Exception ex)
{
var message = "Failed with internal error.";
switch (ex)
{
case BackupRestoreException backupException:
run.Log(backupException.Message);
message = backupException.Message;
break;
case FileNotFoundException fileNotFoundException:
run.Log(fileNotFoundException.Message);
break;
default:
run.Log("Failed with internal error");
message = fileNotFoundException.Message;
break;
}
await CleanupAsync(run);
run.Job.Status = JobStatus.Failed;
await SetStatusAsync(run, JobStatus.Failed, message);
log.LogError(ex, "Backup with job id {backupId} from URL '{url}' failed.", run.Job.Id, run.Job.Url);
}
finally
{
run.Job.Stopped = Clock.GetCurrentInstant();
await state.WriteAsync(ct);
}
}
}
@ -270,32 +203,41 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
if (run.Job.Actor?.IsUser != true)
{
run.Log("Current user not assigned because restore was triggered by client.");
await LogAsync(run, "Current user not assigned because restore was triggered by client.");
return;
}
try
{
// Add the current user to the app, so that the admin can see it and verify integrity.
var command = new AssignContributor
await PublishAsync(run, new AssignContributor
{
Actor = run.Job.Actor,
AppId = run.Job.AppId,
ContributorId = run.Job.Actor.Identifier,
Restoring = true,
IgnoreActor = true,
IgnorePlans = true,
Role = Role.Owner
};
await commandBus.PublishAsync(command, default);
});
run.Log("Assigned current user.");
await LogAsync(run, "Assigned current user.");
}
catch (DomainException ex)
{
run.Log($"Failed to assign contributor: {ex.Message}");
await LogAsync(run, $"Failed to assign contributor: {ex.Message}");
}
}
private Task PublishAsync(Run run, AppCommand command)
{
command.Actor = run.Job.Actor;
if (command is IAppCommand appCommand)
{
appCommand.AppId = run.Job.AppId;
}
return commandBus.PublishAsync(command, default);
}
private async Task CleanupAsync(Run run)
{
if (run.Job.AppId == null)
@ -321,11 +263,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
using (Telemetry.Activities.StartActivity("Download"))
{
run.Log("Downloading Backup");
await LogAsync(run, "Downloading Backup");
var reader = await backupArchiveLocation.OpenReaderAsync(run.Job.Url, run.Job.Id, ct);
run.Log("Downloaded Backup");
await LogAsync(run, "Downloaded Backup");
return reader;
}
@ -355,7 +297,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
handled += commits.Count;
run.Log($"Reading {run.Reader.ReadEvents}/{handled} events and {run.Reader.ReadAttachments} attachments completed.", true);
await LogAsync(run, $"Reading {run.Reader.ReadEvents}/{handled} events and {run.Reader.ReadAttachments} attachments completed.", true);
}
catch (OperationCanceledException ex)
{
@ -453,14 +395,50 @@ namespace Squidex.Domain.Apps.Entities.Backup
using (Telemetry.Activities.StartActivity("CreateUsers"))
{
run.Log("Creating Users");
await LogAsync(run, "Creating Users");
await userMapping.RestoreAsync(run.Reader, userResolver, ct);
run.Log("Created Users");
await LogAsync(run, "Created Users");
}
run.Context = new RestoreContext(run.Job.AppId.Id, userMapping, run.Reader, previousAppId);
}
private Task SetStatusAsync(Run run, JobStatus status, string message)
{
var now = Clock.GetCurrentInstant();
run.Job.Status = status;
if (status == JobStatus.Failed || status == JobStatus.Completed)
{
run.Job.Stopped = now;
}
else if (status == JobStatus.Started)
{
run.Job.Started = now;
}
run.Job.Log.Add($"{now}: {message}");
return state.WriteAsync(ct: default);
}
private Task LogAsync(Run run, string message, bool replace = false)
{
var now = Clock.GetCurrentInstant();
if (replace && run.Job.Log.Count > 0)
{
run.Job.Log[^1] = $"{now}: {message}";
}
else
{
run.Job.Log.Add($"{now}: {message}");
}
return state.WriteAsync(100, run.CancellationToken);
}
}
}

32
backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs

@ -32,15 +32,31 @@ namespace Squidex.Infrastructure.Json.System
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var text = reader.GetString();
try
switch (reader.TokenType)
{
return convertFromString(text!);
}
catch (Exception ex)
{
ThrowHelper.JsonException("Error while converting from string.", ex);
return default;
case JsonTokenType.String:
var text = reader.GetString();
try
{
return convertFromString(text!);
}
catch (Exception ex)
{
ThrowHelper.JsonException("Error while converting from string.", ex);
return default;
}
case JsonTokenType.StartObject:
var optionsWithoutSelf = new JsonSerializerOptions(options);
// Remove the current converter, otherwise we would create a stackoverflow exception.
optionsWithoutSelf.Converters.Remove(this);
return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf);
default:
ThrowHelper.JsonException($"Expected string or object, got {reader.TokenType}.");
return default;
}
}

34
backend/src/Squidex.Infrastructure/States/SimpleState.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
@ -13,6 +14,7 @@ namespace Squidex.Infrastructure.States
{
private readonly IPersistence<T> persistence;
private bool isLoaded;
private Instant lastWrite;
public T Value { get; set; } = new T();
@ -21,6 +23,8 @@ namespace Squidex.Infrastructure.States
get => persistence.Version;
}
public IClock Clock { get; set; } = SystemClock.Instance;
public SimpleState(IPersistenceFactory<T> persistenceFactory, Type ownerType, string id)
: this(persistenceFactory, ownerType, DomainId.Create(id))
{
@ -52,16 +56,35 @@ namespace Squidex.Infrastructure.States
return persistence.DeleteAsync(ct);
}
public Task WriteAsync(
public async Task WriteAsync(int ifNotWrittenWithinMs,
CancellationToken ct = default)
{
return persistence.WriteSnapshotAsync(Value, ct);
var now = Clock.GetCurrentInstant();
if (ifNotWrittenWithinMs > 0 && now.Minus(lastWrite).TotalMilliseconds < ifNotWrittenWithinMs)
{
return;
}
await persistence.WriteSnapshotAsync(Value, ct);
lastWrite = now;
}
public Task WriteEventAsync(Envelope<IEvent> envelope,
public async Task WriteAsync(
CancellationToken ct = default)
{
return persistence.WriteEventAsync(envelope, ct);
await persistence.WriteSnapshotAsync(Value, ct);
lastWrite = Clock.GetCurrentInstant();
}
public async Task WriteEventAsync(Envelope<IEvent> envelope,
CancellationToken ct = default)
{
await persistence.WriteEventAsync(envelope, ct);
lastWrite = Clock.GetCurrentInstant();
}
public Task UpdateAsync(Func<T, bool> updater, int retries = 20,
@ -73,6 +96,9 @@ namespace Squidex.Infrastructure.States
public async Task<TResult> UpdateAsync<TResult>(Func<T, (bool, TResult)> updater, int retries = 20,
CancellationToken ct = default)
{
Guard.GreaterEquals(retries, 1);
Guard.LessThan(retries, 100);
if (!isLoaded)
{
await LoadAsync(ct);

20
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs

@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
[Fact]
public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore()
{
var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, Restoring = true };
var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IgnoreActor = true };
var contributors_1 = contributors_0.Assign("1", Role.Owner);
@ -126,6 +126,20 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
new ValidationError("You have reached the maximum number of contributors for your plan."));
}
[Fact]
public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ignored()
{
A.CallTo(() => appPlan.MaxContributors)
.Returns(2);
var command = new AssignContributor { ContributorId = "3", IgnorePlans = true };
var contributors_1 = contributors_0.Assign("1", Role.Owner);
var contributors_2 = contributors_1.Assign("2", Role.Editor);
await GuardAppContributors.CanAssign(command, App(contributors_2), users, appPlan);
}
[Fact]
public async Task CanAssign_should_not_throw_exception_if_user_found()
{
@ -162,12 +176,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
}
[Fact]
public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_from_restore()
public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored()
{
A.CallTo(() => appPlan.MaxContributors)
.Returns(2);
var command = new AssignContributor { ContributorId = "3", Restoring = true };
var command = new AssignContributor { ContributorId = "3", IgnorePlans = true };
var contributors_1 = contributors_0.Assign("1", Role.Editor);
var contributors_2 = contributors_1.Assign("2", Role.Editor);

36
backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json.Serialization;
using Squidex.Infrastructure.Json.System;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
@ -12,6 +14,12 @@ namespace Squidex.Infrastructure
{
public class NamedIdTests
{
internal sealed record Wrapper
{
[JsonConverter(typeof(StringConverter<NamedId<long>>))]
public NamedId<long> Value { get; set; }
}
[Fact]
public void Should_instantiate_token()
{
@ -113,6 +121,34 @@ namespace Squidex.Infrastructure
Assert.Equal(value, serialized);
}
[Fact]
public void Should_serialize_and_deserialize_old_object()
{
var value = new { id = 42L, name = "my-name" };
var serialized = value.SerializeAndDeserialize<NamedId<long>>();
Assert.Equal(NamedId.Of(42L, "my-name"), serialized);
}
[Fact]
public void Should_deserialize_from_old_object_with_explicit_converter()
{
var value = new
{
value = new { id = 42, name = "my-name" }
};
var expected = new Wrapper
{
Value = NamedId.Of(42L, "my-name")
};
var serialized = value.SerializeAndDeserialize<Wrapper>();
Assert.Equal(expected, serialized);
}
[Fact]
public void Should_throw_exception_if_string_id_is_not_valid()
{

25
backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using FakeItEasy;
using NodaTime;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
@ -204,5 +205,29 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct))
.MustHaveHappenedANumberOfTimesMatching(x => x == 1);
}
[Fact]
public async Task Should_not_written_if_period_not_over()
{
var now = SystemClock.Instance.GetCurrentInstant();
var clock = A.Fake<IClock>();
A.CallTo(() => clock.GetCurrentInstant())
.Returns(now).NumberOfTimes(5).Then
.Returns(now.Plus(Duration.FromSeconds(2)));
sut.Clock = clock;
for (var i = 0; i < 10; i++)
{
await sut.WriteAsync(1000, ct);
}
await sut.UpdateAsync(x => true, ct: ct);
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct))
.MustHaveHappenedANumberOfTimesMatching(x => x == 3);
}
}
}

Loading…
Cancel
Save