diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a30bf93 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Restore dependencies + run: dotnet restore ./src/AspNetCore.Identity.Mongo/ + - name: Build + run: dotnet build ./src/AspNetCore.Identity.Mongo/ -c Release --no-restore + - name: Test + run: dotnet test ./Tests -c Release diff --git a/AspNetCore.Identity.Mongo.sln b/AspNetCore.Identity.Mongo.sln index b711a93..b6cf31b 100644 --- a/AspNetCore.Identity.Mongo.sln +++ b/AspNetCore.Identity.Mongo.sln @@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{767BB7BE EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7282BE10-249F-4308-BB55-EA2346584782}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{E3254B66-8E82-463F-9550-DFE8563341F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,6 +42,10 @@ Global {25627014-8963-42D8-B1EC-A56FBDE16937}.Debug|Any CPU.Build.0 = Debug|Any CPU {25627014-8963-42D8-B1EC-A56FBDE16937}.Release|Any CPU.ActiveCfg = Release|Any CPU {25627014-8963-42D8-B1EC-A56FBDE16937}.Release|Any CPU.Build.0 = Release|Any CPU + {E3254B66-8E82-463F-9550-DFE8563341F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3254B66-8E82-463F-9550-DFE8563341F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3254B66-8E82-463F-9550-DFE8563341F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3254B66-8E82-463F-9550-DFE8563341F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -47,6 +53,7 @@ Global GlobalSection(NestedProjects) = preSolution {39E2704C-B0DE-4BD2-849F-5B51332EE03F} = {7282BE10-249F-4308-BB55-EA2346584782} {25627014-8963-42D8-B1EC-A56FBDE16937} = {238A25AE-691E-4A86-9B5E-727DFC186F33} + {E3254B66-8E82-463F-9550-DFE8563341F8} = {767BB7BE-35C8-424C-B873-FEBD69EE6C1A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3806160E-9B49-41B7-A532-95CD600CE2CF} diff --git a/Tests/MigrationTests.cs b/Tests/MigrationTests.cs new file mode 100644 index 0000000..8de4d4f --- /dev/null +++ b/Tests/MigrationTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AspNetCore.Identity.Mongo.Migrations; +using AspNetCore.Identity.Mongo.Model; +using MongoDB.Bson; +using MongoDB.Driver; +using NUnit.Framework; + +namespace Tests +{ + [TestFixture] + public class MigrationTests + { + private IDisposable _runner; + private IMongoClient _client; + private IMongoDatabase _db; + + [OneTimeSetUp] + public void OneTimeSetup() + { + var runner = Mongo2Go.MongoDbRunner.Start(); + _client = new MongoClient(runner.ConnectionString); + _db = _client.GetDatabase("migration-tests"); + _runner = runner; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _runner.Dispose(); + } + + [Test, Category("unit")] + public void Apply_Schema4_AllMigrationsApplied() + { + // ARRANGE + var history = _db.GetCollection("migrations"); + var users = _db.GetCollection("users"); + var roles = _db.GetCollection>("roles"); + var initialVersion = 4; + var existingHistory = new List + { + new MigrationHistory + { + Id = ObjectId.GenerateNewId(), + DatabaseVersion = 3, + InstalledOn = DateTime.UtcNow.AddDays(-2) + }, + new MigrationHistory + { + Id = ObjectId.GenerateNewId(), + DatabaseVersion = initialVersion, + InstalledOn = DateTime.UtcNow.AddDays(-1) + } + }; + history.InsertMany(existingHistory); + + + // ACT + Migrator.Apply, ObjectId>(history, users, roles); + + // ASSERT + var historyAfter = history + .Find("{}") + .SortBy(h => h.DatabaseVersion) + .ToList(); + + var expectedHistoryObjectsAfter = Migrator.CurrentVersion - initialVersion + existingHistory.Count; + Assert.That(historyAfter.Count, Is.EqualTo(expectedHistoryObjectsAfter), + () => "Expected all migrations to run"); + Assert.That(historyAfter.Last().DatabaseVersion, Is.EqualTo(Migrator.CurrentVersion)); + } + } +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 0000000..2bd1609 --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + diff --git a/src/AspNetCore.Identity.Mongo/Migrations/BaseMigration.cs b/src/AspNetCore.Identity.Mongo/Migrations/BaseMigration.cs new file mode 100644 index 0000000..ad49018 --- /dev/null +++ b/src/AspNetCore.Identity.Mongo/Migrations/BaseMigration.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AspNetCore.Identity.Mongo.Model; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace AspNetCore.Identity.Mongo.Migrations +{ + internal abstract class BaseMigration + { + private static List _migrations; + public static List Migrations { + get + { + if (_migrations == null) + { + _migrations = typeof(BaseMigration) + .Assembly + .GetTypes() + .Where(t => typeof(BaseMigration).IsAssignableFrom(t)) + .Select(t => t.GetConstructor(Type.EmptyTypes)?.Invoke(Array.Empty())) + .Where(o => o != null) + .Cast() + .OrderBy(m => m.Version) + .ToList(); + if (_migrations.Count != _migrations.Select(m => m.Version).Distinct().Count()) + { + throw new InvalidOperationException("Migration versions must be unique, please check versions"); + } + } + + return _migrations; + } + } + + + public abstract int Version { get; } + + public MigrationHistory Apply(IMongoCollection usersCollection, + IMongoCollection rolesCollection) + where TKey : IEquatable + where TUser : MigrationMongoUser + where TRole : MongoRole + { + DoApply(usersCollection, rolesCollection); + return new MigrationHistory + { + Id = ObjectId.GenerateNewId(), + InstalledOn = DateTime.UtcNow, + DatabaseVersion = Version + 1 + }; + } + + protected abstract void DoApply( + IMongoCollection usersCollection, IMongoCollection rolesCollection) + where TKey : IEquatable + where TUser : MigrationMongoUser + where TRole : MongoRole; + } +} \ No newline at end of file diff --git a/src/AspNetCore.Identity.Mongo/Migrations/Migrator.cs b/src/AspNetCore.Identity.Mongo/Migrations/Migrator.cs index b47b588..22ae708 100644 --- a/src/AspNetCore.Identity.Mongo/Migrations/Migrator.cs +++ b/src/AspNetCore.Identity.Mongo/Migrations/Migrator.cs @@ -1,68 +1,41 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using AspNetCore.Identity.Mongo.Model; -using AspNetCore.Identity.Mongo.Mongo; -using MongoDB.Driver; - -namespace AspNetCore.Identity.Mongo.Migrations -{ - internal static class Migrator - { - //Starting from 4 in case we want to implement migrations for previous versions - public static int CurrentVersion = 6; - - public static void Apply(IMongoCollection migrationCollection, IMongoCollection usersCollection, IMongoCollection rolesCollection) - where TKey : IEquatable - where TUser : MigrationMongoUser - where TRole : MongoRole - { - var history = migrationCollection.Find(_ => true).ToList(); - - if (history.Count > 0) - { - var lastHistory = history.OrderBy(x => x.DatabaseVersion).Last(); - - if (lastHistory.DatabaseVersion == CurrentVersion) - { - return; - } - - // 4 -> 5 - if (lastHistory.DatabaseVersion == 4) - { - var users = usersCollection.Find(x => !string.IsNullOrEmpty(x.AuthenticatorKey)).ToList(); - foreach (var user in users) - { - var tokens = user.Tokens; - tokens.Add(new Microsoft.AspNetCore.Identity.IdentityUserToken() - { - UserId = user.Id.ToString(), - Value = user.AuthenticatorKey, - LoginProvider = "[AspNetUserStore]", - Name = "AuthenticatorKey" - }); - usersCollection.UpdateOne(x => x.Id.Equals(user.Id), - Builders.Update.Set(x => x.Tokens, tokens) - .Set(x => x.AuthenticatorKey, null)); - } - } - - // 5 -> 6 - if (lastHistory.DatabaseVersion == 5) - { - usersCollection.UpdateMany(x => true, - Builders.Update.Unset(x => x.AuthenticatorKey) - .Unset(x => x.RecoveryCodes)); - } - } - - migrationCollection.InsertOne(new MigrationHistory - { - InstalledOn = DateTime.Now, - DatabaseVersion = CurrentVersion - }); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using AspNetCore.Identity.Mongo.Model; +using AspNetCore.Identity.Mongo.Mongo; +using MongoDB.Driver; + +[assembly: InternalsVisibleTo("Tests")] + +namespace AspNetCore.Identity.Mongo.Migrations +{ + internal static class Migrator + { + //Starting from 4 in case we want to implement migrations for previous versions + public static int CurrentVersion = 6; + + public static void Apply(IMongoCollection migrationCollection, + IMongoCollection usersCollection, IMongoCollection rolesCollection) + where TKey : IEquatable + where TUser : MigrationMongoUser + where TRole : MongoRole + { + var version = migrationCollection + .Find(h => true) + .SortByDescending(h => h.DatabaseVersion) + .Project(h => h.DatabaseVersion) + .FirstOrDefault(); + + var appliedMigrations = BaseMigration.Migrations + .Where(m => m.Version >= version) + .Select(migration => migration.Apply(usersCollection, rolesCollection)) + .ToList(); + + migrationCollection.InsertMany(appliedMigrations); + + } + } +} \ No newline at end of file diff --git a/src/AspNetCore.Identity.Mongo/Migrations/Schema4Migration.cs b/src/AspNetCore.Identity.Mongo/Migrations/Schema4Migration.cs new file mode 100644 index 0000000..581635d --- /dev/null +++ b/src/AspNetCore.Identity.Mongo/Migrations/Schema4Migration.cs @@ -0,0 +1,31 @@ +using MongoDB.Driver; + +namespace AspNetCore.Identity.Mongo.Migrations +{ + internal class Schema4Migration: BaseMigration + { + public override int Version { get; } = 4; + + protected override void DoApply( + IMongoCollection usersCollection, + IMongoCollection rolesCollection) + { + var users = usersCollection.Find(x => !string.IsNullOrEmpty(x.AuthenticatorKey)).ToList(); + foreach (var user in users) + { + var tokens = user.Tokens; + tokens.Add(new Microsoft.AspNetCore.Identity.IdentityUserToken() + { + UserId = user.Id.ToString(), + Value = user.AuthenticatorKey, + LoginProvider = "[AspNetUserStore]", + Name = "AuthenticatorKey" + }); + usersCollection.UpdateOne(x => x.Id.Equals(user.Id), + Builders.Update.Set(x => x.Tokens, tokens) + .Set(x => x.AuthenticatorKey, null)); + + } + } + } +} \ No newline at end of file diff --git a/src/AspNetCore.Identity.Mongo/Migrations/Schema5Migration.cs b/src/AspNetCore.Identity.Mongo/Migrations/Schema5Migration.cs new file mode 100644 index 0000000..eb022c1 --- /dev/null +++ b/src/AspNetCore.Identity.Mongo/Migrations/Schema5Migration.cs @@ -0,0 +1,18 @@ +using MongoDB.Driver; + +namespace AspNetCore.Identity.Mongo.Migrations +{ + internal class Schema5Migration : BaseMigration + { + public override int Version { get; } = 5; + + protected override void DoApply( + IMongoCollection usersCollection, + IMongoCollection rolesCollection) + { + usersCollection.UpdateMany(x => true, + Builders.Update.Unset(x => x.AuthenticatorKey) + .Unset(x => x.RecoveryCodes)); + } + } +} \ No newline at end of file