// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.Log; using Xunit; namespace Squidex.Infrastructure.Migrations { public class MigratorTests { private readonly IMigrationStatus status = A.Fake(); private readonly IMigrationPath path = A.Fake(); private readonly ISemanticLog log = A.Fake(); private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); public sealed class InMemoryStatus : IMigrationStatus { private readonly object lockObject = new object(); private int version; private bool isLocked; public Task GetVersionAsync() { return Task.FromResult(version); } public Task TryLockAsync() { var lockAcquired = false; lock (lockObject) { if (!isLocked) { isLocked = true; lockAcquired = true; } } return Task.FromResult(lockAcquired); } public Task UnlockAsync(int newVersion) { lock (lockObject) { isLocked = false; version = newVersion; } return Task.CompletedTask; } } public MigratorTests() { A.CallTo(() => path.GetNext(A._)) .ReturnsLazily((int v) => { var m = migrations.Where(x => x.From == v).ToList(); return m.Count == 0 ? (0, null) : (migrations.Max(x => x.To), migrations.Select(x => x.Migration)); }); A.CallTo(() => status.GetVersionAsync()).Returns(0); A.CallTo(() => status.TryLockAsync()).Returns(true); } [Fact] public async Task Should_migrate_step_by_step() { var migrator_0_1 = BuildMigration(0, 1); var migrator_1_2 = BuildMigration(1, 2); var migrator_2_3 = BuildMigration(2, 3); var sut = new Migrator(status, path, log); await sut.MigrateAsync(); A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); A.CallTo(() => status.UnlockAsync(3)) .MustHaveHappened(); } [Fact] public async Task Should_unlock_when_migration_failed() { var migrator_0_1 = BuildMigration(0, 1); var migrator_1_2 = BuildMigration(1, 2); var migrator_2_3 = BuildMigration(2, 3); var sut = new Migrator(status, path, log); A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); await Assert.ThrowsAsync(() => sut.MigrateAsync()); A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); } [Fact] public async Task Should_log_exception_when_migration_failed() { var migrator_0_1 = BuildMigration(0, 1); var migrator_1_2 = BuildMigration(1, 2); var ex = new InvalidOperationException(); A.CallTo(() => migrator_0_1.UpdateAsync()) .Throws(ex); var sut = new Migrator(status, path, log); await Assert.ThrowsAsync(() => sut.MigrateAsync()); A.CallTo(() => log.Log(SemanticLogLevel.Fatal, ex, A._!)) .MustHaveHappened(); A.CallTo(() => migrator_1_2.UpdateAsync()) .MustNotHaveHappened(); } [Fact] public async Task Should_prevent_multiple_updates() { var migrator_0_1 = BuildMigration(0, 1); var migrator_1_2 = BuildMigration(1, 2); var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); A.CallTo(() => migrator_0_1.UpdateAsync()) .MustHaveHappened(1, Times.Exactly); A.CallTo(() => migrator_1_2.UpdateAsync()) .MustHaveHappened(1, Times.Exactly); } private IMigration BuildMigration(int fromVersion, int toVersion) { var migration = A.Fake(); migrations.Add((fromVersion, toVersion, migration)); return migration; } } }