From f4c9fc6015cc8f67415a581e172c5b8f76e3946f Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Wed, 7 Feb 2024 23:03:04 -0500 Subject: [PATCH] Fixed ApiKeyEntity. (#48) --- .../Entities/ApiKeyEntity.cs | 4 +- ...tity.EntityFrameworkCore.Relational.csproj | 6 +- .../AssertApiKeys.cs | 56 +++++ .../Repositories/ApiKeyRepositoryTests.cs | 230 ++++++++++++++++++ .../Repositories/UserRepositoryTests.cs | 2 +- 5 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/AssertApiKeys.cs create mode 100644 tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/ApiKeyRepositoryTests.cs diff --git a/src/Logitar.Identity.EntityFrameworkCore.Relational/Entities/ApiKeyEntity.cs b/src/Logitar.Identity.EntityFrameworkCore.Relational/Entities/ApiKeyEntity.cs index d582719..65282d8 100644 --- a/src/Logitar.Identity.EntityFrameworkCore.Relational/Entities/ApiKeyEntity.cs +++ b/src/Logitar.Identity.EntityFrameworkCore.Relational/Entities/ApiKeyEntity.cs @@ -77,7 +77,7 @@ public void Authenticate(ApiKeyAuthenticatedEvent @event) { Update(@event); - AuthenticatedOn = @event.OccurredOn; + AuthenticatedOn = @event.OccurredOn.ToUniversalTime(); } public void RemoveRole(ApiKeyRoleRemovedEvent @event) @@ -105,7 +105,7 @@ public void Update(ApiKeyUpdatedEvent @event) } if (@event.ExpiresOn.HasValue) { - ExpiresOn = @event.ExpiresOn.Value; + ExpiresOn = @event.ExpiresOn.Value.ToUniversalTime(); } foreach (KeyValuePair customAttribute in @event.CustomAttributes) diff --git a/src/Logitar.Identity.EntityFrameworkCore.Relational/Logitar.Identity.EntityFrameworkCore.Relational.csproj b/src/Logitar.Identity.EntityFrameworkCore.Relational/Logitar.Identity.EntityFrameworkCore.Relational.csproj index f86f7eb..eeef8b8 100644 --- a/src/Logitar.Identity.EntityFrameworkCore.Relational/Logitar.Identity.EntityFrameworkCore.Relational.csproj +++ b/src/Logitar.Identity.EntityFrameworkCore.Relational/Logitar.Identity.EntityFrameworkCore.Relational.csproj @@ -16,14 +16,14 @@ README.md https://github.com/Logitar/Identity git - 0.11.3.0 + 0.11.4.0 $(AssemblyVersion) LICENSE True - 0.11.3 + 0.11.4 en-CA False - Remove custom attributes when deleting an entity. + Fixed ApiKeyEntity. logitar;net;framework;identity;entityframeworkcore;relational https://github.com/Logitar/Identity/tree/main/src/Logitar.Identity.EntityFrameworkCore.Relational diff --git a/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/AssertApiKeys.cs b/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/AssertApiKeys.cs new file mode 100644 index 0000000..b40b1de --- /dev/null +++ b/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/AssertApiKeys.cs @@ -0,0 +1,56 @@ +using Logitar.Identity.Domain.ApiKeys; +using Logitar.Identity.Domain.Roles; +using Logitar.Identity.EntityFrameworkCore.Relational; +using Logitar.Identity.EntityFrameworkCore.Relational.Entities; + +namespace Logitar.Identity.EntityFrameworkCore.SqlServer; + +internal static class AssertApiKeys +{ + public static void AreEqual(ApiKeyAggregate? apiKey, ApiKeyEntity? entity) + { + if (apiKey == null || entity == null) + { + Assert.Null(apiKey); + Assert.Null(entity); + return; + } + + Assert.Equal(apiKey.Version, entity.Version); + Assert.Equal(apiKey.CreatedBy.Value, entity.CreatedBy); + Assertions.Equal(apiKey.CreatedOn, entity.CreatedOn, TimeSpan.FromMinutes(1)); + Assert.Equal(apiKey.UpdatedBy.Value, entity.UpdatedBy); + Assertions.Equal(apiKey.UpdatedOn, entity.UpdatedOn, TimeSpan.FromMinutes(1)); + + Assert.Equal(apiKey.TenantId?.Value, entity.TenantId); + Assert.NotEmpty(entity.SecretHash); + Assert.Equal(apiKey.DisplayName.Value, entity.DisplayName); + Assert.Equal(apiKey.Description?.Value, entity.Description); + Assert.Equal(apiKey.ExpiresOn?.ToUniversalTime(), entity.ExpiresOn); + Assert.Equal(apiKey.AuthenticatedOn?.ToUniversalTime(), entity.AuthenticatedOn); + + Assert.Equal(apiKey.CustomAttributes, entity.CustomAttributes); + + foreach (RoleId roleId in apiKey.Roles) + { + Assert.Contains(entity.Roles, role => role.AggregateId == roleId.Value); + } + } + + public static void AreEquivalent(ApiKeyEntity? apiKey, ActorEntity? actor) + { + if (apiKey == null || actor == null) + { + Assert.Null(apiKey); + Assert.Null(actor); + return; + } + + Assert.Equal(apiKey.AggregateId, actor.Id); + Assert.Equal(ActorType.ApiKey, actor.Type); + Assert.False(actor.IsDeleted); + Assert.Equal(apiKey.DisplayName, actor.DisplayName); + Assert.Null(actor.EmailAddress); + Assert.Null(actor.PictureUrl); + } +} diff --git a/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/ApiKeyRepositoryTests.cs b/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/ApiKeyRepositoryTests.cs new file mode 100644 index 0000000..c6c582b --- /dev/null +++ b/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/ApiKeyRepositoryTests.cs @@ -0,0 +1,230 @@ +using Logitar.Data; +using Logitar.Data.SqlServer; +using Logitar.EventSourcing; +using Logitar.EventSourcing.EntityFrameworkCore.Relational; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.ApiKeys; +using Logitar.Identity.Domain.Passwords; +using Logitar.Identity.Domain.Roles; +using Logitar.Identity.Domain.Settings; +using Logitar.Identity.Domain.Shared; +using Logitar.Identity.EntityFrameworkCore.Relational; +using Logitar.Identity.EntityFrameworkCore.Relational.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Identity.EntityFrameworkCore.SqlServer.Repositories; + +[Trait(Traits.Category, Categories.Integration)] +public class ApiKeyRepositoryTests : RepositoryTests, IAsyncLifetime +{ + private readonly IApiKeyRepository _apiKeyRepository; + private readonly IPasswordManager _passwordManager; + private readonly IRoleRepository _roleRepository; + private readonly IRoleSettings _roleSettings; + + private readonly Password _secret; + private readonly string _secretString; + + private readonly ApiKeyAggregate _apiKey; + private readonly RoleAggregate _role; + + public ApiKeyRepositoryTests() : base() + { + _apiKeyRepository = ServiceProvider.GetRequiredService(); + _passwordManager = ServiceProvider.GetRequiredService(); + _roleRepository = ServiceProvider.GetRequiredService(); + _roleSettings = ServiceProvider.GetRequiredService().Resolve(); + + ApiKeyId apiKeyId = ApiKeyId.NewId(); + ActorId actorId = new(apiKeyId.Value); + TenantId tenantId = new("tests"); + + _role = new(new UniqueNameUnit(_roleSettings.UniqueName, "clerk"), tenantId, actorId); + + DisplayNameUnit displayName = new("Default"); + _secret = _passwordManager.GenerateBase64(32, out _secretString); + _apiKey = new(displayName, _secret, tenantId, actorId, apiKeyId) + { + Description = new DescriptionUnit("This is the default API key.") + }; + _apiKey.SetExpiration(DateTime.Now.AddYears(1)); + _apiKey.SetCustomAttribute("TODO", "TODO"); + _apiKey.Update(actorId); + + _apiKey.AddRole(_role, actorId); + + _apiKey.Authenticate(_secretString, actorId); + } + + public async Task InitializeAsync() + { + await EventContext.Database.MigrateAsync(); + await IdentityContext.Database.MigrateAsync(); + + TableId[] tables = + [ + IdentityDb.ApiKeys.Table, + IdentityDb.Roles.Table, + IdentityDb.CustomAttributes.Table, + IdentityDb.Actors.Table, + EventDb.Events.Table + ]; + foreach (TableId table in tables) + { + ICommand command = SqlServerDeleteBuilder.From(table).Build(); + await IdentityContext.Database.ExecuteSqlRawAsync(command.Text, command.Parameters.ToArray()); + } + + await _roleRepository.SaveAsync(_role); + await _apiKeyRepository.SaveAsync(_apiKey); + } + + [Fact(DisplayName = "LoadAsync: it should load all the API keys.")] + public async Task LoadAsync_it_should_load_all_the_Api_keys() + { + ApiKeyAggregate deleted = new(_apiKey.DisplayName, _secret, tenantId: null); + deleted.Delete(); + await _apiKeyRepository.SaveAsync(deleted); + + IEnumerable apiKeys = await _apiKeyRepository.LoadAsync(includeDeleted: true); + Assert.Equal(2, apiKeys.Count()); + Assert.Contains(apiKeys, _apiKey.Equals); + Assert.Contains(apiKeys, deleted.Equals); + } + + [Fact(DisplayName = "LoadAsync: it should load the API key by identifier.")] + public async Task LoadAsync_it_should_load_the_Api_key_by_identifier() + { + Assert.Null(await _apiKeyRepository.LoadAsync(ApiKeyId.NewId())); + + _apiKey.Delete(); + long version = _apiKey.Version; + + DateTime? authenticatedOn = _apiKey.AuthenticatedOn; + _apiKey.Authenticate(_secretString); + await _apiKeyRepository.SaveAsync(_apiKey); + + Assert.Null(await _apiKeyRepository.LoadAsync(_apiKey.Id, version)); + + ApiKeyAggregate? apiKey = await _apiKeyRepository.LoadAsync(_apiKey.Id, version, includeDeleted: true); + Assert.NotNull(apiKey); + Assert.Equal(authenticatedOn, apiKey.AuthenticatedOn); + Assert.Equal(_apiKey, apiKey); + } + + [Fact(DisplayName = "LoadAsync: it should load the API key by role.")] + public async Task LoadAsync_it_should_load_the_Api_key_by_role() + { + RoleAggregate admin = new(new UniqueNameUnit(_roleSettings.UniqueName, "admin"), _apiKey.TenantId); + await _roleRepository.SaveAsync(admin); + + Assert.Empty(await _apiKeyRepository.LoadAsync(admin)); + + IEnumerable apiKeys = await _apiKeyRepository.LoadAsync(_role); + Assert.Equal(_apiKey, apiKeys.Single()); + } + + [Fact(DisplayName = "LoadAsync: it should load the API keys by tenant identifier.")] + public async Task LoadAsync_it_should_load_the_apiKeys_by_tenant_identifier() + { + ApiKeyAggregate apiKey = new(new DisplayNameUnit("Other API key"), _secret, tenantId: null); + ApiKeyAggregate deleted = new(new DisplayNameUnit("deleted"), _secret, _apiKey.TenantId); + deleted.Delete(); + await _apiKeyRepository.SaveAsync([apiKey, deleted]); + + IEnumerable apiKeys = await _apiKeyRepository.LoadAsync(_apiKey.TenantId); + Assert.Equal(_apiKey, apiKeys.Single()); + } + + [Fact(DisplayName = "LoadAsync: it should load the API keys by identifiers.")] + public async Task LoadAsync_it_should_load_the_Api_keys_by_identifiers() + { + ApiKeyAggregate deleted = new(new DisplayNameUnit("deleted"), _secret, tenantId: null); + deleted.Delete(); + await _apiKeyRepository.SaveAsync(deleted); + + IEnumerable apiKeys = await _apiKeyRepository.LoadAsync([_apiKey.Id, deleted.Id, ApiKeyId.NewId()], includeDeleted: true); + Assert.Equal(2, apiKeys.Count()); + Assert.Contains(apiKeys, _apiKey.Equals); + Assert.Contains(apiKeys, deleted.Equals); + } + + [Fact(DisplayName = "SaveAsync: it should save the deleted API key.")] + public async Task SaveAsync_it_should_save_the_deleted_Api_key() + { + _apiKey.SetCustomAttribute("TODO", "TODO"); + _apiKey.SetCustomAttribute("TODO", "TODO"); + _apiKey.Update(); + await _apiKeyRepository.SaveAsync(_apiKey); + + ApiKeyEntity? entity = await IdentityContext.ApiKeys.AsNoTracking() + .SingleOrDefaultAsync(x => x.AggregateId == _apiKey.Id.Value); + Assert.NotNull(entity); + + CustomAttributeEntity[] customAttributes = await IdentityContext.CustomAttributes.AsNoTracking() + .Where(x => x.EntityType == nameof(IdentityContext.ApiKeys) && x.EntityId == entity.ApiKeyId) + .ToArrayAsync(); + Assert.Equal(_apiKey.CustomAttributes.Count, customAttributes.Length); + foreach (KeyValuePair customAttribute in _apiKey.CustomAttributes) + { + Assert.Contains(customAttributes, c => c.Key == customAttribute.Key && c.Value == customAttribute.Value); + } + + _apiKey.Delete(); + await _apiKeyRepository.SaveAsync(_apiKey); + + customAttributes = await IdentityContext.CustomAttributes.AsNoTracking() + .Where(x => x.EntityType == nameof(IdentityContext.ApiKeys) && x.EntityId == entity.ApiKeyId) + .ToArrayAsync(); + Assert.Empty(customAttributes); + } + + [Fact(DisplayName = "SaveAsync: it should save the specified API key.")] + public async Task SaveAsync_it_should_save_the_specified_Api_key() + { + ApiKeyEntity? entity = await IdentityContext.ApiKeys.AsNoTracking() + .Include(x => x.Roles) + .SingleOrDefaultAsync(x => x.AggregateId == _apiKey.Id.Value); + Assert.NotNull(entity); + AssertApiKeys.AreEqual(_apiKey, entity); + + ActorEntity? actor = await IdentityContext.Actors.AsNoTracking() + .SingleOrDefaultAsync(x => x.Id == _apiKey.Id.Value); + AssertApiKeys.AreEquivalent(entity, actor); + + Dictionary customAttributes = await IdentityContext.CustomAttributes.AsNoTracking() + .Where(x => x.EntityType == nameof(IdentityContext.ApiKeys) && x.EntityId == entity.ApiKeyId) + .ToDictionaryAsync(x => x.Key, x => x.Value); + Assert.Equal(_apiKey.CustomAttributes, customAttributes); + } + + [Fact(DisplayName = "SaveAsync: it should save the specified API keys.")] + public async Task SaveAsync_it_should_save_the_specified_Api_keys() + { + ApiKeyAggregate authenticated = new(new DisplayNameUnit("authenticated"), _secret); + ApiKeyAggregate deleted = new(new DisplayNameUnit("deleted"), _secret); + await _apiKeyRepository.SaveAsync([authenticated, deleted]); + + Dictionary entities = await IdentityContext.ApiKeys.AsNoTracking().ToDictionaryAsync(x => x.AggregateId, x => x); + Assert.True(entities.ContainsKey(authenticated.Id.Value)); + Assert.True(entities.ContainsKey(deleted.Id.Value)); + + authenticated.Authenticate(_secretString); + deleted.Delete(); + await _apiKeyRepository.SaveAsync([authenticated, deleted]); + + entities = await IdentityContext.ApiKeys.AsNoTracking().ToDictionaryAsync(x => x.AggregateId, x => x); + Assert.True(entities.ContainsKey(authenticated.Id.Value)); + Assert.False(entities.ContainsKey(deleted.Id.Value)); + + AssertApiKeys.AreEqual(authenticated, entities[authenticated.Id.Value]); + + ActorEntity? actor = await IdentityContext.Actors.AsNoTracking() + .SingleOrDefaultAsync(x => x.Id == deleted.Id.Value); + Assert.NotNull(actor); + Assert.True(actor.IsDeleted); + } + + public Task DisposeAsync() => Task.CompletedTask; +} diff --git a/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/UserRepositoryTests.cs b/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/UserRepositoryTests.cs index 1a98645..4baa25b 100644 --- a/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/UserRepositoryTests.cs +++ b/tests/Logitar.Identity.EFCore.SqlServer.IntegrationTests/Repositories/UserRepositoryTests.cs @@ -33,8 +33,8 @@ public class UserRepositoryTests : RepositoryTests, IAsyncLifetime public UserRepositoryTests() : base() { - _roleRepository = ServiceProvider.GetRequiredService(); _passwordManager = ServiceProvider.GetRequiredService(); + _roleRepository = ServiceProvider.GetRequiredService(); _roleSettings = ServiceProvider.GetRequiredService().Resolve(); _userRepository = ServiceProvider.GetRequiredService(); _userSettings = ServiceProvider.GetRequiredService().Resolve();