diff --git a/AdminUi/test/AdminUi.Tests.Integration/HttpClientFactory.cs b/AdminUi/test/AdminUi.Tests.Integration/HttpClientFactory.cs index f7b6d99d36..d2bb38c3c0 100644 --- a/AdminUi/test/AdminUi.Tests.Integration/HttpClientFactory.cs +++ b/AdminUi/test/AdminUi.Tests.Integration/HttpClientFactory.cs @@ -14,7 +14,7 @@ internal HttpClientFactory(CustomWebApplicationFactory factory) internal HttpClient CreateClient() { var baseAddress = Environment.GetEnvironmentVariable("ADMIN_API_BASE_ADDRESS"); - return baseAddress.IsNullOrEmpty() ? _factory.CreateClient() : new HttpClient() { BaseAddress = new Uri(baseAddress!) }; + return baseAddress.IsNullOrEmpty() ? _factory.CreateClient() : new HttpClient() { BaseAddress = new Uri(baseAddress) }; } } diff --git a/Backbone.sln b/Backbone.sln index 565d6bccca..3bd3e578fd 100644 --- a/Backbone.sln +++ b/Backbone.sln @@ -265,7 +265,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdminUi.Infrastructure.Data EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdminUi.Infrastructure", "AdminUi\src\AdminUi.Infrastructure\AdminUi.Infrastructure.csproj", "{5028E85D-E115-4E02-AC94-2B441686E44E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthCheck", "HealthCheck\HealthCheck.csproj", "{EE910828-296B-45CD-BA01-DCABE27BCC4C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthCheck", "HealthCheck\HealthCheck.csproj", "{EE910828-296B-45CD-BA01-DCABE27BCC4C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quotas.Infrastructure.Tests", "Modules\Quotas\test\Quotas.Infrastructure.Tests\Quotas.Infrastructure.Tests.csproj", "{FB38C7C5-9F11-43BB-871F-E2E0360FD993}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -649,6 +651,10 @@ Global {EE910828-296B-45CD-BA01-DCABE27BCC4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE910828-296B-45CD-BA01-DCABE27BCC4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE910828-296B-45CD-BA01-DCABE27BCC4C}.Release|Any CPU.Build.0 = Release|Any CPU + {FB38C7C5-9F11-43BB-871F-E2E0360FD993}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB38C7C5-9F11-43BB-871F-E2E0360FD993}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB38C7C5-9F11-43BB-871F-E2E0360FD993}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB38C7C5-9F11-43BB-871F-E2E0360FD993}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -773,6 +779,7 @@ Global {FEBD339A-7343-47EF-84F1-770EDF7E5E94} = {A8C20813-97C8-42A9-B45A-4A0D650DA647} {4D9FCC6B-0958-45A2-85EA-3992CE29B5CB} = {A8C20813-97C8-42A9-B45A-4A0D650DA647} {5028E85D-E115-4E02-AC94-2B441686E44E} = {A8C20813-97C8-42A9-B45A-4A0D650DA647} + {FB38C7C5-9F11-43BB-871F-E2E0360FD993} = {4192A28C-45C0-4D20-B880-F417B8AB752F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F3BD2C6-7CB3-450F-A21A-23EA520D5B7A} diff --git a/BuildingBlocks/src/Tooling/Extensions/StringExtensions.cs b/BuildingBlocks/src/Tooling/Extensions/StringExtensions.cs index 9b8443a1b7..d5bdea90ce 100644 --- a/BuildingBlocks/src/Tooling/Extensions/StringExtensions.cs +++ b/BuildingBlocks/src/Tooling/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; namespace Backbone.Tooling.Extensions; @@ -9,7 +10,7 @@ public static bool IsEmpty(this string @string) return @string == string.Empty; } - public static bool IsNullOrEmpty(this string? @string) + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? @string) { return string.IsNullOrEmpty(@string); } diff --git a/ConsumerApi.Tests.Integration/HttpClientFactory.cs b/ConsumerApi.Tests.Integration/HttpClientFactory.cs index d037f901a8..c0a1401467 100644 --- a/ConsumerApi.Tests.Integration/HttpClientFactory.cs +++ b/ConsumerApi.Tests.Integration/HttpClientFactory.cs @@ -14,6 +14,6 @@ internal HttpClientFactory(CustomWebApplicationFactory factory) internal HttpClient CreateClient() { var baseAddress = Environment.GetEnvironmentVariable("CONSUMER_API_BASE_ADDRESS"); - return baseAddress.IsNullOrEmpty() ? _factory.CreateClient() : new HttpClient() { BaseAddress = new Uri(baseAddress!) }; + return baseAddress.IsNullOrEmpty() ? _factory.CreateClient() : new HttpClient() { BaseAddress = new Uri(baseAddress) }; } } diff --git a/ConsumerApi/Controllers/AuthorizationController.cs b/ConsumerApi/Controllers/AuthorizationController.cs index e6373a8f52..88bc1a7db2 100644 --- a/ConsumerApi/Controllers/AuthorizationController.cs +++ b/ConsumerApi/Controllers/AuthorizationController.cs @@ -56,7 +56,7 @@ public async Task Exchange() throw new OperationFailedException( ApplicationErrors.Authentication.InvalidOAuthRequest("missing password")); - var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password!, lockoutOnFailure: true); + var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true); if (result.IsLockedOut) return UserLockedOut(); if (!result.Succeeded) diff --git a/Modules/Quotas/src/Quotas.Infrastructure/Persistence/Repository/TiersRepository.cs b/Modules/Quotas/src/Quotas.Infrastructure/Persistence/Repository/TiersRepository.cs index ae6eb87133..7d76a93fdc 100644 --- a/Modules/Quotas/src/Quotas.Infrastructure/Persistence/Repository/TiersRepository.cs +++ b/Modules/Quotas/src/Quotas.Infrastructure/Persistence/Repository/TiersRepository.cs @@ -55,7 +55,19 @@ public async Task RemoveById(TierId tierId) public async Task Update(Tier tier, CancellationToken cancellationToken) { + RemoveOrphanedTierQuotaDefinitions(); _tiers.Update(tier); await _dbContext.SaveChangesAsync(cancellationToken); } + + private void RemoveOrphanedTierQuotaDefinitions() + { + var removedQuotas = _dbContext.ChangeTracker + .Entries() + .Where(e => e.State == EntityState.Modified) + .Where(e => _dbContext.Entry(e.Entity).Property("TierId").CurrentValue == null) + .Select(e => e.Entity); + + _dbContext.RemoveRange(removedQuotas); + } } diff --git a/Modules/Quotas/test/Quotas.Application.Tests/Quotas.Application.Tests.csproj b/Modules/Quotas/test/Quotas.Application.Tests/Quotas.Application.Tests.csproj index 5bceadc4df..6616e43652 100644 --- a/Modules/Quotas/test/Quotas.Application.Tests/Quotas.Application.Tests.csproj +++ b/Modules/Quotas/test/Quotas.Application.Tests/Quotas.Application.Tests.csproj @@ -17,9 +17,7 @@ - - diff --git a/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTierQuotaDefinitionsStubRepository.cs b/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTierQuotaDefinitionsStubRepository.cs index a424c89cf4..f8ff1337b1 100644 --- a/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTierQuotaDefinitionsStubRepository.cs +++ b/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTierQuotaDefinitionsStubRepository.cs @@ -32,6 +32,11 @@ public Task RemoveById(TierId tierId) throw new NotImplementedException(); } + public Task RemoveTierQuotaDefinitionIfOrphaned(TierQuotaDefinitionId tierQuotaDefinitionId) + { + throw new NotImplementedException(); + } + public Task Update(Tier tier, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTiersStubRepository.cs b/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTiersStubRepository.cs index a220368d24..8efdd25b71 100644 --- a/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTiersStubRepository.cs +++ b/Modules/Quotas/test/Quotas.Application.Tests/TestDoubles/FindTiersStubRepository.cs @@ -32,6 +32,11 @@ public Task RemoveById(TierId tierId) throw new NotImplementedException(); } + public Task RemoveTierQuotaDefinitionIfOrphaned(TierQuotaDefinitionId tierQuotaDefinitionId) + { + throw new NotImplementedException(); + } + public Task Update(Tier tier, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/Modules/Quotas/test/Quotas.Infrastructure.Tests/Quotas.Infrastructure.Tests.csproj b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Quotas.Infrastructure.Tests.csproj new file mode 100644 index 0000000000..a75f9ee293 --- /dev/null +++ b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Quotas.Infrastructure.Tests.csproj @@ -0,0 +1,25 @@ + + + + false + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/Modules/Quotas/test/Quotas.Application.Tests/Tests/Repositories/MessagesRepositoryTests.cs b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs similarity index 93% rename from Modules/Quotas/test/Quotas.Application.Tests/Tests/Repositories/MessagesRepositoryTests.cs rename to Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs index ab2f04ed08..5dc4bb828d 100644 --- a/Modules/Quotas/test/Quotas.Application.Tests/Tests/Repositories/MessagesRepositoryTests.cs +++ b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs @@ -11,10 +11,10 @@ using FluentAssertions.Execution; using Xunit; -namespace Backbone.Modules.Quotas.Application.Tests.Tests.Repositories; +namespace Backbone.Modules.Quotas.Infrastructure.Tests.Tests.Repositories; + public class MessagesRepositoryTests { - private readonly IdentityAddress _identityAddress1 = TestDataGenerator.CreateRandomIdentityAddress(); private readonly IdentityAddress _identityAddress2 = TestDataGenerator.CreateRandomIdentityAddress(); @@ -28,6 +28,7 @@ public class MessagesRepositoryTests * */ private static readonly DateTime TODAY = new(2020, 02, 15, 10, 30, 00); + private static readonly DateTime YESTERDAY = TODAY.AddDays(-1); private static readonly DateTime TOMORROW = TODAY.AddDays(1); private static readonly DateTime LAST_YEAR = TODAY.AddYears(-1); @@ -48,7 +49,8 @@ public MessagesRepositoryTests() public async Task Counts_entities_within_timeframe_hour_quotaPeriod() { // Arrange - var messages = new List() { + var messages = new List() + { CreateMessage(TODAY, _identityAddress1), CreateMessage(YESTERDAY, _identityAddress1), CreateMessage(TOMORROW, _identityAddress1) @@ -70,7 +72,8 @@ public async Task Counts_entities_within_timeframe_hour_quotaPeriod() public async Task Counts_entities_within_timeframe_month_quotaPeriod() { // Arrange - var messages = new List() { + var messages = new List() + { CreateMessage(TODAY, _identityAddress1), CreateMessage(YESTERDAY, _identityAddress1), CreateMessage(TOMORROW, _identityAddress1), @@ -94,7 +97,8 @@ public async Task Counts_entities_within_timeframe_month_quotaPeriod() public async Task Counts_entities_total_quotaPeriod() { // Arrange - var messages = new List() { + var messages = new List() + { CreateMessage(TODAY, _identityAddress1), CreateMessage(TOMORROW, _identityAddress1), CreateMessage(NEXT_YEAR, _identityAddress1) @@ -116,7 +120,8 @@ public async Task Counts_entities_total_quotaPeriod() public async Task Counts_entities_only_for_requested_identityAddress() { // Arrange - var messages = new List() { + var messages = new List() + { CreateMessage(TODAY, _identityAddress1), CreateMessage(TOMORROW, _identityAddress2), CreateMessage(NEXT_YEAR, _identityAddress1) @@ -145,9 +150,9 @@ private static Message CreateMessage(DateTime createdAt, IdentityAddress identit DeviceId.New(), null, Array.Empty(), - new List(), - new List() - ); + [], + [] + ); SystemTime.Set(savedDateTime); diff --git a/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/TiersRepositoryTests.cs b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/TiersRepositoryTests.cs new file mode 100644 index 0000000000..d43a6012b0 --- /dev/null +++ b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/TiersRepositoryTests.cs @@ -0,0 +1,45 @@ +using Backbone.Modules.Quotas.Domain.Aggregates.Identities; +using Backbone.Modules.Quotas.Domain.Aggregates.Metrics; +using Backbone.Modules.Quotas.Domain.Aggregates.Tiers; +using Backbone.Modules.Quotas.Infrastructure.Persistence.Database; +using Backbone.Modules.Quotas.Infrastructure.Persistence.Repository; +using Backbone.UnitTestTools.TestDoubles.Fakes; +using FluentAssertions; +using Xunit; + +namespace Backbone.Modules.Quotas.Infrastructure.Tests.Tests.Repositories; + +public class TiersRepositoryTests +{ + /** + * This test makes sure that when the Update method is called on the TiersRepository with a Tier from which + * TierQuotaDefinitions were removed, the TierQuotaDefinitions were removed from the database as well. + * That's not the case by default, because EF Core only sets the foreign key to null, but doesn't remove the + * lines from the TierQuotaDefinitions table. + */ + [Fact] + public async Task Updating_a_Tier_deletes_unassigned_quotas_from_the_TierQuotaDefinitions_table() + { + // Arrange + var (arrangeContext, actContext, assertContext) = FakeDbContextFactory.CreateDbContexts(); + + var arrangedTier = new Tier(new TierId("TIR00000000000000000"), "Test"); + var tierQuotaDefinitionToBeDeleted = arrangedTier.CreateQuota(MetricKey.NumberOfSentMessages, 5, QuotaPeriod.Month).Value; + var otherTierQuotaDefinition = arrangedTier.CreateQuota(MetricKey.NumberOfFiles, 5, QuotaPeriod.Month).Value; + + await arrangeContext.Tiers.AddAsync(arrangedTier); + await arrangeContext.SaveChangesAsync(); + + var repository = new TiersRepository(actContext); + + var actTier = (await repository.Find(arrangedTier.Id, CancellationToken.None, true))!; + actTier.DeleteQuota(tierQuotaDefinitionToBeDeleted.Id); + + // Act + await repository.Update(actTier, CancellationToken.None); + + // Assert + assertContext.Set().Should().NotContain(q => q.Id == tierQuotaDefinitionToBeDeleted.Id); + assertContext.Set().Should().Contain(q => q.Id == otherTierQuotaDefinition.Id); + } +}