From 90834260e8aec402ab0075c90b5bb9051d1e15ca Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sat, 18 Oct 2025 21:51:08 +0000 Subject: [PATCH 1/5] Refactor to use TimeProvider via injected service - Replaced direct calls to TimeProvider.System.GetUtcNow() with injected TimeProvider service for better testability. - Added TimeProviderExtensions NuGet package. - Updated tests to utilize TimeProvider for timestamp control. - Introduced AddTimeProvider method in SharedDependencyConfiguration for service registration. --- .cursor/rules/backend/backend.mdc | 4 ++- .../AuthenticationCookieMiddleware.cs | 16 +++++---- application/Directory.Packages.props | 1 + .../Authentication/Commands/CompleteLogin.cs | 4 ++- .../Commands/CompleteEmailConfirmation.cs | 8 +++-- .../Commands/ResendEmailConfirmationCode.cs | 6 ++-- .../Commands/StartEmailConfirmation.cs | 7 ++-- .../Domain/EmailConfirmation.cs | 12 +++---- .../Users/Commands/DeclineInvitation.cs | 7 ++-- .../Features/Users/Domain/UserRepository.cs | 18 ++++++---- .../Authentication/CompleteLoginTests.cs | 18 +++++----- .../Tests/Authentication/StartLoginTests.cs | 8 ++--- .../Tests/Authentication/SwitchTenantTests.cs | 26 +++++++------- .../Tests/EndpointBaseTest.cs | 35 ++++++++++--------- .../Tests/Signups/CompleteSignupTests.cs | 8 ++--- .../Tests/Signups/StartSignupTests.cs | 12 ++++--- .../Tests/Tenants/DeleteTenantTests.cs | 6 ++-- .../Tests/Tenants/GetTenantsForUserTests.cs | 22 ++++++------ .../Tests/Users/BulkDeleteUsersTests.cs | 8 ++--- .../Tests/Users/DeclineInvitationTests.cs | 18 +++++----- .../Tests/Users/DeleteUserTests.cs | 10 +++--- .../Tests/Users/GetUserByIdTests.cs | 6 ++-- .../Tests/Users/GetUsersTests.cs | 6 ++-- .../TokenGeneration/AccessTokenGenerator.cs | 6 ++-- .../TokenGeneration/RefreshTokenGenerator.cs | 6 ++-- .../SharedDependencyConfiguration.cs | 14 +++++++- .../SharedKernel/Domain/AudibleEntity.cs | 2 +- .../SharedKernel/SharedKernel.csproj | 1 + 28 files changed, 165 insertions(+), 130 deletions(-) diff --git a/.cursor/rules/backend/backend.mdc b/.cursor/rules/backend/backend.mdc index a555934ac..dc95f938b 100644 --- a/.cursor/rules/backend/backend.mdc +++ b/.cursor/rules/backend/backend.mdc @@ -34,8 +34,10 @@ Carefully follow these instructions for C# backend development, including code s - Avoid try-catch unless we cannot fix the reason. We have global exception handling to handle unknown exceptions. - Use `SharedInfrastructureConfiguration.IsRunningInAzure` to determine if we are running in Azure. - Don't add comments unless the code is truly not expressing the intent. +- Do add comments explains "the why" of code. In other words, never add comments that explains what code does, but explaining why code is there, what non-obvious problem it solves, is expected. - Never add XML comments. -- Use `TimeProvider.System.GetUtcNow()` and not `DateTime.UtcNow()`. +- Inject TimeProvider from keyed service "shared" and `timeProvider.GetUtcNow()` and not `DateTimeOffset.UtcNow()` in all business code. +- Use `DateTimeOffset.UtcNow()` for code that needs timestamps and does not support/work with fake TimeProvider during testing. ## Implementation diff --git a/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs b/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs index 80a5064f0..e4df2965a 100644 --- a/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs +++ b/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs @@ -1,15 +1,16 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Net.Http.Headers; -using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using PlatformPlatform.SharedKernel.Authentication; using PlatformPlatform.SharedKernel.Authentication.TokenSigning; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; +using System.Security.Claims; namespace PlatformPlatform.AppGateway.Middleware; public class AuthenticationCookieMiddleware( ITokenSigningClient tokenSigningClient, IHttpClientFactory httpClientFactory, + [FromKeyedServices("shared")] TimeProvider timeProvider, ILogger logger ) : IMiddleware @@ -51,9 +52,9 @@ private async Task ValidateAuthenticationCookieAndConvertToHttpBearerHeader(Http try { - if (accessToken is null || ExtractExpirationFromToken(accessToken) < TimeProvider.System.GetUtcNow()) + if (accessToken is null || ExtractExpirationFromToken(accessToken) < timeProvider.GetUtcNow()) { - if (ExtractExpirationFromToken(refreshToken) < TimeProvider.System.GetUtcNow()) + if (ExtractExpirationFromToken(refreshToken) < timeProvider.GetUtcNow()) { context.Response.Cookies.Delete(AuthenticationTokenHttpKeys.RefreshTokenCookieName); context.Response.Cookies.Delete(AuthenticationTokenHttpKeys.AccessTokenCookieName); @@ -114,7 +115,10 @@ private void ReplaceAuthenticationHeaderWithCookie(HttpContext context, string r // having to first serve the SPA. This is only secure if iFrames are not allowed to host the site. var refreshTokenCookieOptions = new CookieOptions { - HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, Expires = refreshTokenExpires + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Lax, + Expires = refreshTokenExpires }; context.Response.Cookies.Append(AuthenticationTokenHttpKeys.RefreshTokenCookieName, refreshToken, refreshTokenCookieOptions); diff --git a/application/Directory.Packages.props b/application/Directory.Packages.props index 81243917f..be4a0809e 100644 --- a/application/Directory.Packages.props +++ b/application/Directory.Packages.props @@ -74,6 +74,7 @@ + all diff --git a/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs b/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs index c52b0b7bf..285c7eba9 100644 --- a/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs +++ b/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using PlatformPlatform.AccountManagement.Features.Authentication.Domain; using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Commands; using PlatformPlatform.AccountManagement.Features.Users.Domain; @@ -27,6 +28,7 @@ public sealed class CompleteLoginHandler( AvatarUpdater avatarUpdater, GravatarClient gravatarClient, ITelemetryEventsCollector events, + [FromKeyedServices("shared")] TimeProvider timeProvider, ILogger logger ) : IRequestHandler { @@ -98,7 +100,7 @@ private void CompleteUserInvite(User user) { user.ConfirmEmail(); userRepository.Update(user); - var inviteAcceptedTimeInMinutes = (int)(TimeProvider.System.GetUtcNow() - user.CreatedAt).TotalMinutes; + var inviteAcceptedTimeInMinutes = (int)(timeProvider.GetUtcNow() - user.CreatedAt).TotalMinutes; events.CollectEvent(new UserInviteAccepted(user.Id, inviteAcceptedTimeInMinutes)); } } diff --git a/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs b/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs index 636bbc159..b4c56daa2 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Domain; using PlatformPlatform.SharedKernel.Authentication; using PlatformPlatform.SharedKernel.Cqrs; @@ -17,6 +18,7 @@ public sealed class CompleteEmailConfirmationHandler( IEmailConfirmationRepository emailConfirmationRepository, OneTimePasswordHelper oneTimePasswordHelper, ITelemetryEventsCollector events, + [FromKeyedServices("shared")] TimeProvider timeProvider, ILogger logger ) : IRequestHandler> { @@ -51,14 +53,14 @@ public async Task> Handle(CompleteEmai return Result.BadRequest("The code is wrong or no longer valid.", true); } - var confirmationTimeInSeconds = (int)(TimeProvider.System.GetUtcNow() - emailConfirmation.CreatedAt).TotalSeconds; - if (emailConfirmation.HasExpired()) + var confirmationTimeInSeconds = (int)(timeProvider.GetUtcNow() - emailConfirmation.CreatedAt).TotalSeconds; + if (emailConfirmation.HasExpired(timeProvider)) { events.CollectEvent(new EmailConfirmationExpired(emailConfirmation.Id, emailConfirmation.Type, confirmationTimeInSeconds)); return Result.BadRequest("The code is no longer valid, please request a new code.", true); } - emailConfirmation.MarkAsCompleted(); + emailConfirmation.MarkAsCompleted(timeProvider); emailConfirmationRepository.Update(emailConfirmation); return new CompleteEmailConfirmationResponse(emailConfirmation.Email, confirmationTimeInSeconds); diff --git a/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs b/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs index 94f395f6b..280e190ac 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Domain; using PlatformPlatform.SharedKernel.Authentication; using PlatformPlatform.SharedKernel.Cqrs; @@ -23,6 +24,7 @@ public sealed class ResendEmailConfirmationCodeHandler( IEmailClient emailClient, IPasswordHasher passwordHasher, ITelemetryEventsCollector events, + [FromKeyedServices("shared")] TimeProvider timeProvider, ILogger logger ) : IRequestHandler> { @@ -45,10 +47,10 @@ public async Task> Handle(ResendEmai var oneTimePassword = OneTimePasswordHelper.GenerateOneTimePassword(6); var oneTimePasswordHash = passwordHasher.HashPassword(this, oneTimePassword); - emailConfirmation.UpdateVerificationCode(oneTimePasswordHash); + emailConfirmation.UpdateVerificationCode(oneTimePasswordHash, timeProvider); emailConfirmationRepository.Update(emailConfirmation); - var secondsSinceSignupStarted = (TimeProvider.System.GetUtcNow() - emailConfirmation.CreatedAt).TotalSeconds; + var secondsSinceSignupStarted = (timeProvider.GetUtcNow() - emailConfirmation.CreatedAt).TotalSeconds; events.CollectEvent(new EmailConfirmationResend((int)secondsSinceSignupStarted)); await emailClient.SendAsync(emailConfirmation.Email, "Your verification code (resend)", diff --git a/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs b/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs index f3c7b2018..ed75d5a97 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs @@ -1,6 +1,7 @@ using FluentValidation; using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Domain; using PlatformPlatform.SharedKernel.Authentication; using PlatformPlatform.SharedKernel.Cqrs; @@ -29,15 +30,15 @@ public StartEmailConfirmationValidator() public sealed class StartEmailConfirmationHandler( IEmailConfirmationRepository emailConfirmationRepository, IEmailClient emailClient, - IPasswordHasher passwordHasher -) : IRequestHandler> + IPasswordHasher passwordHasher, + [FromKeyedServices("shared")] TimeProvider timeProvider) : IRequestHandler> { public async Task> Handle(StartEmailConfirmationCommand command, CancellationToken cancellationToken) { var existingConfirmations = emailConfirmationRepository.GetByEmail(command.Email).ToArray(); var lockoutMinutes = command.Type == EmailConfirmationType.Signup ? -60 : -15; - if (existingConfirmations.Count(r => r.CreatedAt > TimeProvider.System.GetUtcNow().AddMinutes(lockoutMinutes)) >= 3) + if (existingConfirmations.Count(r => r.CreatedAt > timeProvider.GetUtcNow().AddMinutes(lockoutMinutes)) >= 3) { return Result.TooManyRequests("Too many attempts to confirm this email address. Please try again later."); } diff --git a/application/account-management/Core/Features/EmailConfirmations/Domain/EmailConfirmation.cs b/application/account-management/Core/Features/EmailConfirmations/Domain/EmailConfirmation.cs index d3e8472ce..c9bb521ec 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Domain/EmailConfirmation.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Domain/EmailConfirmation.cs @@ -34,9 +34,9 @@ private EmailConfirmation(string email, EmailConfirmationType type, string oneTi public bool Completed { get; private set; } - public bool HasExpired() + public bool HasExpired(TimeProvider timeProvider) { - return ValidUntil < TimeProvider.System.GetUtcNow(); + return ValidUntil < timeProvider.GetUtcNow(); } public static EmailConfirmation Create(string email, string oneTimePasswordHash, EmailConfirmationType type) @@ -49,9 +49,9 @@ public void RegisterInvalidPasswordAttempt() RetryCount++; } - public void MarkAsCompleted() + public void MarkAsCompleted(TimeProvider timeProvider) { - if (HasExpired() || RetryCount >= MaxAttempts) + if (HasExpired(timeProvider) || RetryCount >= MaxAttempts) { throw new UnreachableException("This email confirmation has expired."); } @@ -61,7 +61,7 @@ public void MarkAsCompleted() Completed = true; } - public void UpdateVerificationCode(string oneTimePasswordHash) + public void UpdateVerificationCode(string oneTimePasswordHash, TimeProvider timeProvider) { if (Completed) { @@ -73,7 +73,7 @@ public void UpdateVerificationCode(string oneTimePasswordHash) throw new UnreachableException("Cannot regenerate verification code for email confirmation that has been resent too many times."); } - ValidUntil = TimeProvider.System.GetUtcNow().AddSeconds(ValidForSeconds); + ValidUntil = timeProvider.GetUtcNow().AddSeconds(ValidForSeconds); OneTimePasswordHash = oneTimePasswordHash; ResendCount++; } diff --git a/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs b/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs index 2027be85c..e705d9a48 100644 --- a/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs +++ b/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using PlatformPlatform.AccountManagement.Features.Users.Domain; using PlatformPlatform.SharedKernel.Cqrs; using PlatformPlatform.SharedKernel.Domain; @@ -13,8 +14,8 @@ public sealed record DeclineInvitationCommand(TenantId TenantId) : ICommand, IRe public sealed class DeclineInvitationHandler( IUserRepository userRepository, IExecutionContext executionContext, - ITelemetryEventsCollector events -) : IRequestHandler + ITelemetryEventsCollector events, + [FromKeyedServices("shared")] TimeProvider timeProvider) : IRequestHandler { public async Task Handle(DeclineInvitationCommand command, CancellationToken cancellationToken) { @@ -34,7 +35,7 @@ public async Task Handle(DeclineInvitationCommand command, CancellationT } // Calculate how long the invitation existed - var inviteExistedTimeInMinutes = (int)(TimeProvider.System.GetUtcNow() - user.CreatedAt).TotalMinutes; + var inviteExistedTimeInMinutes = (int)(timeProvider.GetUtcNow() - user.CreatedAt).TotalMinutes; // Delete the user to decline the invitation userRepository.Remove(user); diff --git a/application/account-management/Core/Features/Users/Domain/UserRepository.cs b/application/account-management/Core/Features/Users/Domain/UserRepository.cs index 5d57c52a1..f4153437f 100644 --- a/application/account-management/Core/Features/Users/Domain/UserRepository.cs +++ b/application/account-management/Core/Features/Users/Domain/UserRepository.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.ExecutionContext; @@ -40,7 +41,10 @@ CancellationToken cancellationToken Task GetUsersByEmailUnfilteredAsync(string email, CancellationToken cancellationToken); } -internal sealed class UserRepository(AccountManagementDbContext accountManagementDbContext, IExecutionContext executionContext) +internal sealed class UserRepository( + AccountManagementDbContext accountManagementDbContext, + IExecutionContext executionContext, + [FromKeyedServices("shared")] TimeProvider timeProvider) : RepositoryBase(accountManagementDbContext), IUserRepository { /// @@ -89,16 +93,16 @@ public async Task GetByIdsAsync(UserId[] ids, CancellationToken cancella public async Task<(int TotalUsers, int ActiveUsers, int PendingUsers)> GetUserSummaryAsync(CancellationToken cancellationToken) { - var thirtyDaysAgo = TimeProvider.System.GetUtcNow().AddDays(-30); + var thirtyDaysAgo = timeProvider.GetUtcNow().AddDays(-30); var summary = await DbSet .GroupBy(_ => 1) // Group all records into a single group to calculate multiple COUNT aggregates in one query .Select(g => new - { - TotalUsers = g.Count(), - ActiveUsers = g.Count(u => u.EmailConfirmed && u.ModifiedAt >= thirtyDaysAgo), - PendingUsers = g.Count(u => !u.EmailConfirmed) - } + { + TotalUsers = g.Count(), + ActiveUsers = g.Count(u => u.EmailConfirmed && u.ModifiedAt >= thirtyDaysAgo), + PendingUsers = g.Count(u => !u.EmailConfirmed) + } ) .SingleAsync(cancellationToken); diff --git a/application/account-management/Tests/Authentication/CompleteLoginTests.cs b/application/account-management/Tests/Authentication/CompleteLoginTests.cs index 57e71a67c..2844edad3 100644 --- a/application/account-management/Tests/Authentication/CompleteLoginTests.cs +++ b/application/account-management/Tests/Authentication/CompleteLoginTests.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; using FluentAssertions; using Microsoft.AspNetCore.Identity; using PlatformPlatform.AccountManagement.Database; @@ -13,6 +10,9 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Authentication; @@ -159,7 +159,7 @@ public async Task CompleteLogin_WhenLoginExpired_ShouldReturnBadRequest() ("TenantId", DatabaseSeeder.Tenant1Owner.TenantId.ToString()), ("UserId", DatabaseSeeder.Tenant1Owner.Id.ToString()), ("Id", loginId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("EmailConfirmationId", emailConfirmationId.ToString()), ("Completed", false) @@ -167,12 +167,12 @@ public async Task CompleteLogin_WhenLoginExpired_ShouldReturnBadRequest() ); Connection.Insert("EmailConfirmations", [ ("Id", emailConfirmationId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Owner.Email), ("Type", EmailConfirmationType.Signup), ("OneTimePasswordHash", new PasswordHasher().HashPassword(this, CorrectOneTimePassword)), - ("ValidUntil", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("ValidUntil", TimeProvider.GetUtcNow().AddMinutes(-10)), ("RetryCount", 0), ("ResendCount", 0), ("Completed", false) @@ -234,7 +234,7 @@ public async Task CompleteLogin_WithValidPreferredTenant_ShouldLoginToPreferredT Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Active.ToString()), @@ -245,7 +245,7 @@ public async Task CompleteLogin_WithValidPreferredTenant_ShouldLoginToPreferredT Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Owner.Email), ("EmailConfirmed", true), @@ -307,7 +307,7 @@ public async Task CompleteLogin_WithPreferredTenantUserDoesNotHaveAccess_ShouldL Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Active.ToString()), diff --git a/application/account-management/Tests/Authentication/StartLoginTests.cs b/application/account-management/Tests/Authentication/StartLoginTests.cs index 09d0cf64b..b072f42af 100644 --- a/application/account-management/Tests/Authentication/StartLoginTests.cs +++ b/application/account-management/Tests/Authentication/StartLoginTests.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Net.Http.Json; using FluentAssertions; using Microsoft.AspNetCore.Identity; using NSubstitute; @@ -10,6 +8,8 @@ using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; using PlatformPlatform.SharedKernel.Validation; +using System.Net; +using System.Net.Http.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Authentication; @@ -124,12 +124,12 @@ public async Task StartLogin_WhenTooManyAttempts_ShouldReturnTooManyRequests() var oneTimePasswordHash = new PasswordHasher().HashPassword(this, OneTimePasswordHelper.GenerateOneTimePassword(6)); Connection.Insert("EmailConfirmations", [ ("Id", EmailConfirmationId.NewId().ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-i)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-i)), ("ModifiedAt", null), ("Email", email.ToLower()), ("Type", EmailConfirmationType.Login.ToString()), ("OneTimePasswordHash", oneTimePasswordHash), - ("ValidUntil", TimeProvider.System.GetUtcNow().AddMinutes(-i - 1)), // All should be expired + ("ValidUntil", TimeProvider.GetUtcNow().AddMinutes(-i - 1)), // All should be expired ("RetryCount", 0), ("ResendCount", 0), ("Completed", false) diff --git a/application/account-management/Tests/Authentication/SwitchTenantTests.cs b/application/account-management/Tests/Authentication/SwitchTenantTests.cs index e83e2ca7c..5a9a48901 100644 --- a/application/account-management/Tests/Authentication/SwitchTenantTests.cs +++ b/application/account-management/Tests/Authentication/SwitchTenantTests.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Authentication.Commands; @@ -9,6 +6,9 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Authentication; @@ -25,7 +25,7 @@ public async Task SwitchTenant_WhenUserExistsInTargetTenant_ShouldSwitchSuccessf Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", tenant2Name), ("State", TenantState.Active.ToString()), @@ -36,7 +36,7 @@ public async Task SwitchTenant_WhenUserExistsInTargetTenant_ShouldSwitchSuccessf Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", true), @@ -93,7 +93,7 @@ public async Task SwitchTenant_WhenUserDoesNotExistInTargetTenant_ShouldReturnFo Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Active.ToString()), @@ -104,7 +104,7 @@ public async Task SwitchTenant_WhenUserDoesNotExistInTargetTenant_ShouldReturnFo Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", UserId.NewId().ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("EmailConfirmed", true), @@ -152,7 +152,7 @@ public async Task SwitchTenant_WhenUserEmailNotConfirmed_ShouldConfirmEmail() Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", tenant2Name), ("State", TenantState.Active.ToString()), @@ -163,7 +163,7 @@ public async Task SwitchTenant_WhenUserEmailNotConfirmed_ShouldConfirmEmail() Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", false), // User's email is not confirmed @@ -219,7 +219,7 @@ public async Task SwitchTenant_WhenAcceptingInvite_ShouldCopyProfileData() Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", tenant2Name), ("State", TenantState.Active.ToString()), @@ -231,7 +231,7 @@ public async Task SwitchTenant_WhenAcceptingInvite_ShouldCopyProfileData() Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", false), // Unconfirmed - invitation pending @@ -294,7 +294,7 @@ public async Task SwitchTenant_RapidSwitching_ShouldHandleCorrectly() Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Active.ToString()), @@ -305,7 +305,7 @@ public async Task SwitchTenant_RapidSwitching_ShouldHandleCorrectly() Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", true), diff --git a/application/account-management/Tests/EndpointBaseTest.cs b/application/account-management/Tests/EndpointBaseTest.cs index 99de8f12c..28e921eb2 100644 --- a/application/account-management/Tests/EndpointBaseTest.cs +++ b/application/account-management/Tests/EndpointBaseTest.cs @@ -1,4 +1,3 @@ -using System.Net.Http.Headers; using Bogus; using JetBrains.Annotations; using Mapster; @@ -19,6 +18,8 @@ using PlatformPlatform.SharedKernel.SinglePageApp; using PlatformPlatform.SharedKernel.Telemetry; using PlatformPlatform.SharedKernel.Tests.Telemetry; +using System.Net.Http.Headers; +using TimeProviderExtensions; namespace PlatformPlatform.AccountManagement.Tests; @@ -29,6 +30,7 @@ public abstract class EndpointBaseTest : IDisposable where TContext : protected readonly IEmailClient EmailClient; protected readonly Faker Faker = new(); protected readonly ServiceCollection Services; + protected readonly ManualTimeProvider TimeProvider; private ServiceProvider? _provider; protected TelemetryEventsCollectorSpy TelemetryEventsCollectorSpy; @@ -42,7 +44,7 @@ protected EndpointBaseTest() ); Services = new ServiceCollection(); - + TimeProvider = new ManualTimeProvider(); Services.AddLogging(); Services.AddTransient(); @@ -96,26 +98,25 @@ protected EndpointBaseTest() AccessTokenGenerator = serviceScope.ServiceProvider.GetRequiredService(); _webApplicationFactory = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => { - builder.ConfigureTestServices(services => - { - // Replace the default DbContext in the WebApplication to use an in-memory SQLite database - services.Remove(services.Single(d => d.ServiceType == typeof(IDbContextOptionsConfiguration))); - services.AddDbContext(options => { options.UseSqlite(Connection); }); + // Replace the default DbContext in the WebApplication to use an in-memory SQLite database + services.Remove(services.Single(d => d.ServiceType == typeof(IDbContextOptionsConfiguration))); + services.AddDbContext(options => { options.UseSqlite(Connection); }); - TelemetryEventsCollectorSpy = new TelemetryEventsCollectorSpy(new TelemetryEventsCollector()); - services.AddScoped(_ => TelemetryEventsCollectorSpy); + TelemetryEventsCollectorSpy = new TelemetryEventsCollectorSpy(new TelemetryEventsCollector()); + services.AddScoped(_ => TelemetryEventsCollectorSpy); - services.Remove(services.Single(d => d.ServiceType == typeof(IEmailClient))); - services.AddTransient(_ => EmailClient); + services.Remove(services.Single(d => d.ServiceType == typeof(IEmailClient))); + services.AddTransient(_ => EmailClient); - RegisterMockLoggers(services); + RegisterMockLoggers(services); + services.AddKeyedSingleton("shared", TimeProvider); - services.AddScoped(); - } - ); - } - ); + services.AddScoped(); + }); + }); AnonymousHttpClient = _webApplicationFactory.CreateClient(); diff --git a/application/account-management/Tests/Signups/CompleteSignupTests.cs b/application/account-management/Tests/Signups/CompleteSignupTests.cs index 2ba0fe338..f1f636e46 100644 --- a/application/account-management/Tests/Signups/CompleteSignupTests.cs +++ b/application/account-management/Tests/Signups/CompleteSignupTests.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Net.Http.Json; using FluentAssertions; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +8,8 @@ using PlatformPlatform.AccountManagement.Features.Tenants.EventHandlers; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Net.Http.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Signups; @@ -142,12 +142,12 @@ public async Task CompleteSignup_WhenSignupExpired_ShouldReturnBadRequest() var emailConfirmationId = EmailConfirmationId.NewId(); Connection.Insert("EmailConfirmations", [ ("Id", emailConfirmationId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", email), ("Type", EmailConfirmationType.Signup), ("OneTimePasswordHash", new PasswordHasher().HashPassword(this, CorrectOneTimePassword)), - ("ValidUntil", TimeProvider.System.GetUtcNow().AddMinutes(-5)), + ("ValidUntil", TimeProvider.GetUtcNow().AddMinutes(-5)), ("RetryCount", 0), ("ResendCount", 0), ("Completed", false) diff --git a/application/account-management/Tests/Signups/StartSignupTests.cs b/application/account-management/Tests/Signups/StartSignupTests.cs index 88ed9646e..8f078ee68 100644 --- a/application/account-management/Tests/Signups/StartSignupTests.cs +++ b/application/account-management/Tests/Signups/StartSignupTests.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Net.Http.Json; using FluentAssertions; using Microsoft.AspNetCore.Identity; using NSubstitute; @@ -10,6 +8,8 @@ using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; using PlatformPlatform.SharedKernel.Validation; +using System.Net; +using System.Net.Http.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Signups; @@ -78,12 +78,12 @@ public async Task StartSignup_WhenTooManyAttempts_ShouldReturnTooManyRequests() var oneTimePasswordHash = new PasswordHasher().HashPassword(this, OneTimePasswordHelper.GenerateOneTimePassword(6)); Connection.Insert("EmailConfirmations", [ ("Id", EmailConfirmationId.NewId().ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-i)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-i)), ("ModifiedAt", null), ("Email", email), ("Type", EmailConfirmationType.Signup.ToString()), ("OneTimePasswordHash", oneTimePasswordHash), - ("ValidUntil", TimeProvider.System.GetUtcNow().AddMinutes(-i - 1)), // All should be expired + ("ValidUntil", TimeProvider.GetUtcNow().AddMinutes(-i - 1)), // All should be expired ("RetryCount", 0), ("ResendCount", 0), ("Completed", false) @@ -100,6 +100,8 @@ public async Task StartSignup_WhenTooManyAttempts_ShouldReturnTooManyRequests() await response.ShouldHaveErrorStatusCode(HttpStatusCode.TooManyRequests, "Too many attempts to confirm this email address. Please try again later."); TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse(); - await EmailClient.DidNotReceive().SendAsync(Arg.Any(), Arg.Any(), Arg.Any(), CancellationToken.None); + await EmailClient + .DidNotReceive() + .SendAsync(Arg.Any(), Arg.Any(), Arg.Any(), CancellationToken.None); } } diff --git a/application/account-management/Tests/Tenants/DeleteTenantTests.cs b/application/account-management/Tests/Tenants/DeleteTenantTests.cs index a67fe4e7a..272010fa8 100644 --- a/application/account-management/Tests/Tenants/DeleteTenantTests.cs +++ b/application/account-management/Tests/Tenants/DeleteTenantTests.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Users.Domain; @@ -7,6 +5,8 @@ using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; using PlatformPlatform.SharedKernel.Validation; +using System.Net; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Tenants; @@ -36,7 +36,7 @@ public async Task DeleteTenant_WhenTenantHasUsers_ShouldReturnBadRequest() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", UserId.NewId().ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("FirstName", Faker.Person.FirstName), diff --git a/application/account-management/Tests/Tenants/GetTenantsForUserTests.cs b/application/account-management/Tests/Tenants/GetTenantsForUserTests.cs index a1b413fb1..4aeefa618 100644 --- a/application/account-management/Tests/Tenants/GetTenantsForUserTests.cs +++ b/application/account-management/Tests/Tenants/GetTenantsForUserTests.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Tenants.Domain; @@ -9,6 +6,9 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Tenants; @@ -25,7 +25,7 @@ public async Task GetTenants_UserWithMultipleTenants_ReturnsAllTenants() Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", tenant2Name), ("State", TenantState.Active.ToString()), @@ -36,7 +36,7 @@ public async Task GetTenants_UserWithMultipleTenants_ReturnsAllTenants() Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", true), @@ -98,7 +98,7 @@ public async Task GetTenants_CurrentTenantIncluded_VerifyCurrentTenantInResponse Connection.Insert("Tenants", [ ("Id", otherTenantId.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", "Other Tenant"), ("State", TenantState.Active.ToString()), @@ -109,7 +109,7 @@ public async Task GetTenants_CurrentTenantIncluded_VerifyCurrentTenantInResponse Connection.Insert("Users", [ ("TenantId", otherTenantId.Value), ("Id", otherUserId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", email), ("EmailConfirmed", true), @@ -142,7 +142,7 @@ public async Task GetTenants_UsersOnlySeeTheirOwnTenants_DoesNotReturnOtherUsers Connection.Insert("Tenants", [ ("Id", otherUserTenantId.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", "Other User Tenant"), ("State", TenantState.Active.ToString()), @@ -153,7 +153,7 @@ public async Task GetTenants_UsersOnlySeeTheirOwnTenants_DoesNotReturnOtherUsers Connection.Insert("Users", [ ("TenantId", otherUserTenantId.Value), ("Id", otherUserId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", otherUserEmail), ("EmailConfirmed", true), @@ -187,7 +187,7 @@ public async Task GetTenants_UserWithUnconfirmedEmail_ShowsAsNewTenant() Connection.Insert("Tenants", [ ("Id", tenant2Id.Value), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", tenant2Name), ("State", TenantState.Active.ToString()), @@ -198,7 +198,7 @@ public async Task GetTenants_UserWithUnconfirmedEmail_ShowsAsNewTenant() Connection.Insert("Users", [ ("TenantId", tenant2Id.Value), ("Id", user2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", false), // User has not confirmed email in this tenant diff --git a/application/account-management/Tests/Users/BulkDeleteUsersTests.cs b/application/account-management/Tests/Users/BulkDeleteUsersTests.cs index f1ecf8366..ca432f987 100644 --- a/application/account-management/Tests/Users/BulkDeleteUsersTests.cs +++ b/application/account-management/Tests/Users/BulkDeleteUsersTests.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Users.Commands; @@ -9,6 +6,9 @@ using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; using PlatformPlatform.SharedKernel.Validation; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Users; @@ -27,7 +27,7 @@ public async Task BulkDeleteUsers_WhenUsersExist_ShouldDeleteUsers() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", userId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("FirstName", Faker.Person.FirstName), diff --git a/application/account-management/Tests/Users/DeclineInvitationTests.cs b/application/account-management/Tests/Users/DeclineInvitationTests.cs index 46a2fb0ab..d1f7608e3 100644 --- a/application/account-management/Tests/Users/DeclineInvitationTests.cs +++ b/application/account-management/Tests/Users/DeclineInvitationTests.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Tenants.Domain; @@ -9,6 +6,9 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Users; @@ -24,7 +24,7 @@ public async Task DeclineInvitation_WhenValidInviteExists_ShouldDeleteUserAndCol Connection.Insert("Tenants", [ ("Id", newTenantId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Trial.ToString()), @@ -35,7 +35,7 @@ public async Task DeclineInvitation_WhenValidInviteExists_ShouldDeleteUserAndCol Connection.Insert("Users", [ ("TenantId", newTenantId.ToString()), ("Id", userId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", false), @@ -94,7 +94,7 @@ public async Task DeclineInvitation_WhenMultipleInvitesExist_ShouldDeclineSpecif Connection.Insert("Tenants", [ ("Id", tenant2Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Trial.ToString()), @@ -104,7 +104,7 @@ public async Task DeclineInvitation_WhenMultipleInvitesExist_ShouldDeclineSpecif Connection.Insert("Tenants", [ ("Id", tenant3Id.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow()), + ("CreatedAt", TimeProvider.GetUtcNow()), ("ModifiedAt", null), ("Name", Faker.Company.CompanyName()), ("State", TenantState.Trial.ToString()), @@ -115,7 +115,7 @@ public async Task DeclineInvitation_WhenMultipleInvitesExist_ShouldDeclineSpecif Connection.Insert("Users", [ ("TenantId", tenant2Id.ToString()), ("Id", userId2.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", false), @@ -131,7 +131,7 @@ public async Task DeclineInvitation_WhenMultipleInvitesExist_ShouldDeclineSpecif Connection.Insert("Users", [ ("TenantId", tenant3Id.ToString()), ("Id", userId3.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-5)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-5)), ("ModifiedAt", null), ("Email", DatabaseSeeder.Tenant1Member.Email), ("EmailConfirmed", false), diff --git a/application/account-management/Tests/Users/DeleteUserTests.cs b/application/account-management/Tests/Users/DeleteUserTests.cs index ef3bd9af7..5c7c5637a 100644 --- a/application/account-management/Tests/Users/DeleteUserTests.cs +++ b/application/account-management/Tests/Users/DeleteUserTests.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Authentication.Domain; @@ -8,6 +6,8 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Users; @@ -35,7 +35,7 @@ public async Task DeleteUser_WhenUserExists_ShouldDeleteUser() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", userId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("FirstName", Faker.Person.FirstName), @@ -77,7 +77,7 @@ public async Task DeleteUser_WhenUserHasLoginHistory_ShouldDeleteUserAndLogins() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", userId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("FirstName", Faker.Person.FirstName), @@ -97,7 +97,7 @@ public async Task DeleteUser_WhenUserHasLoginHistory_ShouldDeleteUserAndLogins() ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", loginId.ToString()), ("UserId", userId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-5)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-5)), ("ModifiedAt", null), ("EmailConfirmationId", emailConfirmationId.ToString()), ("Completed", true) diff --git a/application/account-management/Tests/Users/GetUserByIdTests.cs b/application/account-management/Tests/Users/GetUserByIdTests.cs index a94150c45..0d095d6d3 100644 --- a/application/account-management/Tests/Users/GetUserByIdTests.cs +++ b/application/account-management/Tests/Users/GetUserByIdTests.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Users.Domain; @@ -7,6 +5,8 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Net; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Users; @@ -20,7 +20,7 @@ public GetUserByIdTests() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", _userId.ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("FirstName", Faker.Name.FirstName()), diff --git a/application/account-management/Tests/Users/GetUsersTests.cs b/application/account-management/Tests/Users/GetUsersTests.cs index a5ae6a0cd..ac60455fb 100644 --- a/application/account-management/Tests/Users/GetUsersTests.cs +++ b/application/account-management/Tests/Users/GetUsersTests.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using FluentAssertions; using PlatformPlatform.AccountManagement.Database; using PlatformPlatform.AccountManagement.Features.Users.Domain; @@ -6,6 +5,7 @@ using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using PlatformPlatform.SharedKernel.Tests.Persistence; +using System.Text.Json; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Users; @@ -22,7 +22,7 @@ public GetUsersTests() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", UserId.NewId().ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Email), ("FirstName", FirstName), @@ -37,7 +37,7 @@ public GetUsersTests() Connection.Insert("Users", [ ("TenantId", DatabaseSeeder.Tenant1.Id.ToString()), ("Id", UserId.NewId().ToString()), - ("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)), + ("CreatedAt", TimeProvider.GetUtcNow().AddMinutes(-10)), ("ModifiedAt", null), ("Email", Faker.Internet.Email()), ("FirstName", Faker.Name.FirstName()), diff --git a/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/AccessTokenGenerator.cs b/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/AccessTokenGenerator.cs index 8ec213138..492a2f387 100644 --- a/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/AccessTokenGenerator.cs +++ b/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/AccessTokenGenerator.cs @@ -1,7 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using PlatformPlatform.SharedKernel.Authentication.TokenSigning; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; namespace PlatformPlatform.SharedKernel.Authentication.TokenGeneration; @@ -32,7 +32,7 @@ public string Generate(UserInfo userInfo) }; return tokenDescriptor.GenerateToken( - TimeProvider.System.GetUtcNow().AddMinutes(ValidForMinutes).UtcDateTime, + DateTimeOffset.UtcNow.AddMinutes(ValidForMinutes).UtcDateTime, tokenSigningClient.Issuer, tokenSigningClient.Audience, tokenSigningClient.GetSigningCredentials() diff --git a/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/RefreshTokenGenerator.cs b/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/RefreshTokenGenerator.cs index 5cbf9fec2..f4a9cbcd4 100644 --- a/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/RefreshTokenGenerator.cs +++ b/application/shared-kernel/SharedKernel/Authentication/TokenGeneration/RefreshTokenGenerator.cs @@ -1,7 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using PlatformPlatform.SharedKernel.Authentication.TokenSigning; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; namespace PlatformPlatform.SharedKernel.Authentication.TokenGeneration; @@ -13,7 +13,7 @@ public sealed class RefreshTokenGenerator(ITokenSigningClient tokenSigningClient public string Generate(UserInfo userInfo) { - return GenerateRefreshToken(userInfo, RefreshTokenId.NewId(), 1, TimeProvider.System.GetUtcNow().AddHours(ValidForHours)); + return GenerateRefreshToken(userInfo, RefreshTokenId.NewId(), 1, DateTimeOffset.UtcNow.AddHours(ValidForHours)); } public string Update(UserInfo userInfo, RefreshTokenId refreshTokenId, int currentRefreshTokenVersion, DateTimeOffset expires) diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs index 2b8bfa50e..b81f5adf2 100644 --- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs +++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Azure.Security.KeyVault.Keys; using Azure.Security.KeyVault.Keys.Cryptography; using Azure.Security.KeyVault.Secrets; @@ -17,6 +16,7 @@ using PlatformPlatform.SharedKernel.PipelineBehaviors; using PlatformPlatform.SharedKernel.Platform; using PlatformPlatform.SharedKernel.Telemetry; +using System.Text.Json; namespace PlatformPlatform.SharedKernel.Configuration; @@ -41,6 +41,7 @@ public static IServiceCollection AddSharedServices(this IServiceCollection se .AddServiceDiscovery() .AddSingleton(GetTokenSigningService()) .AddSingleton(Settings.Current) + .AddTimeProvider() .AddAuthentication() .AddDefaultJsonSerializerOptions() .AddPersistenceHelpers() @@ -113,6 +114,17 @@ private static IServiceCollection AddDefaultHealthChecks(this IServiceCollection return services; } + private static IServiceCollection AddTimeProvider(this IServiceCollection services, string serviceKey = "shared") + { + // A TimeProvider is already registered as a singleton by default in .NET 8 and later. However, + // to control time during testing, we register our own TimeProvider using .NET Keyed Services, + // so when we inject a fake time provider during testing. The reason for registering a keyed service + // is that it is better to only control time/stop time during testing for services we know work correct when + // done so. A keyed sevice allows us to only override the TimeProvider for specific services during testing.. + services.AddKeyedSingleton(serviceKey, TimeProvider.System); + return services; + } + private static IServiceCollection AddEmailClient(this IServiceCollection services) { if (SharedInfrastructureConfiguration.IsRunningInAzure) diff --git a/application/shared-kernel/SharedKernel/Domain/AudibleEntity.cs b/application/shared-kernel/SharedKernel/Domain/AudibleEntity.cs index ea35b274a..46c78c04c 100644 --- a/application/shared-kernel/SharedKernel/Domain/AudibleEntity.cs +++ b/application/shared-kernel/SharedKernel/Domain/AudibleEntity.cs @@ -8,7 +8,7 @@ namespace PlatformPlatform.SharedKernel.Domain; /// public abstract class AudibleEntity(T id) : Entity(id), IAuditableEntity where T : IComparable { - public DateTimeOffset CreatedAt { get; init; } = TimeProvider.System.GetUtcNow(); + public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; [ConcurrencyCheck] public DateTimeOffset? ModifiedAt { get; private set; } diff --git a/application/shared-kernel/SharedKernel/SharedKernel.csproj b/application/shared-kernel/SharedKernel/SharedKernel.csproj index c1d87707c..ab2402952 100644 --- a/application/shared-kernel/SharedKernel/SharedKernel.csproj +++ b/application/shared-kernel/SharedKernel/SharedKernel.csproj @@ -55,6 +55,7 @@ + From bbad74e354edd4941fd25d17b96aab40beb7453b Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sat, 18 Oct 2025 22:20:54 +0000 Subject: [PATCH 2/5] Refactor from stringly typed [FromKeyedServices("shared")] to [FromPlatformServices] Instead of having a hardcoded string for platform specific keyed services, a custom attribute FromPlatformServicesAttributes is added and used instead. It makes code more readable and less error prone to typing mistakes. --- .cursor/rules/backend/backend.mdc | 2 +- .../Middleware/AuthenticationCookieMiddleware.cs | 2 +- .../Authentication/Commands/CompleteLogin.cs | 2 +- .../Commands/CompleteEmailConfirmation.cs | 2 +- .../Commands/ResendEmailConfirmationCode.cs | 2 +- .../Commands/StartEmailConfirmation.cs | 2 +- .../Features/Users/Commands/DeclineInvitation.cs | 2 +- .../Core/Features/Users/Domain/UserRepository.cs | 2 +- .../account-management/Tests/EndpointBaseTest.cs | 2 +- .../FromPlatformServicesAttribute.cs | 16 ++++++++++++++++ .../SharedDependencyConfiguration.cs | 2 +- 11 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 application/shared-kernel/SharedKernel/Configuration/FromPlatformServicesAttribute.cs diff --git a/.cursor/rules/backend/backend.mdc b/.cursor/rules/backend/backend.mdc index dc95f938b..dd45cf692 100644 --- a/.cursor/rules/backend/backend.mdc +++ b/.cursor/rules/backend/backend.mdc @@ -36,7 +36,7 @@ Carefully follow these instructions for C# backend development, including code s - Don't add comments unless the code is truly not expressing the intent. - Do add comments explains "the why" of code. In other words, never add comments that explains what code does, but explaining why code is there, what non-obvious problem it solves, is expected. - Never add XML comments. -- Inject TimeProvider from keyed service "shared" and `timeProvider.GetUtcNow()` and not `DateTimeOffset.UtcNow()` in all business code. +- Inject TimeProvider from [FromPlatformServices] and use `timeProvider.GetUtcNow()` and not `DateTimeOffset.UtcNow()` in all business code that needs timers, current time, timezone info. - Use `DateTimeOffset.UtcNow()` for code that needs timestamps and does not support/work with fake TimeProvider during testing. ## Implementation diff --git a/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs b/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs index e4df2965a..506fb7597 100644 --- a/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs +++ b/application/AppGateway/Middleware/AuthenticationCookieMiddleware.cs @@ -10,7 +10,7 @@ namespace PlatformPlatform.AppGateway.Middleware; public class AuthenticationCookieMiddleware( ITokenSigningClient tokenSigningClient, IHttpClientFactory httpClientFactory, - [FromKeyedServices("shared")] TimeProvider timeProvider, + [FromPlatformServices] TimeProvider timeProvider, ILogger logger ) : IMiddleware diff --git a/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs b/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs index 285c7eba9..61cfd9a50 100644 --- a/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs +++ b/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs @@ -28,7 +28,7 @@ public sealed class CompleteLoginHandler( AvatarUpdater avatarUpdater, GravatarClient gravatarClient, ITelemetryEventsCollector events, - [FromKeyedServices("shared")] TimeProvider timeProvider, + [FromPlatformServices] TimeProvider timeProvider, ILogger logger ) : IRequestHandler { diff --git a/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs b/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs index b4c56daa2..a5f4027f8 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Commands/CompleteEmailConfirmation.cs @@ -18,7 +18,7 @@ public sealed class CompleteEmailConfirmationHandler( IEmailConfirmationRepository emailConfirmationRepository, OneTimePasswordHelper oneTimePasswordHelper, ITelemetryEventsCollector events, - [FromKeyedServices("shared")] TimeProvider timeProvider, + [FromPlatformServices] TimeProvider timeProvider, ILogger logger ) : IRequestHandler> { diff --git a/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs b/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs index 280e190ac..20a62bdcf 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Commands/ResendEmailConfirmationCode.cs @@ -24,7 +24,7 @@ public sealed class ResendEmailConfirmationCodeHandler( IEmailClient emailClient, IPasswordHasher passwordHasher, ITelemetryEventsCollector events, - [FromKeyedServices("shared")] TimeProvider timeProvider, + [FromPlatformServices] TimeProvider timeProvider, ILogger logger ) : IRequestHandler> { diff --git a/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs b/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs index ed75d5a97..797cd171b 100644 --- a/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs +++ b/application/account-management/Core/Features/EmailConfirmations/Commands/StartEmailConfirmation.cs @@ -31,7 +31,7 @@ public sealed class StartEmailConfirmationHandler( IEmailConfirmationRepository emailConfirmationRepository, IEmailClient emailClient, IPasswordHasher passwordHasher, - [FromKeyedServices("shared")] TimeProvider timeProvider) : IRequestHandler> + [FromPlatformServices] TimeProvider timeProvider) : IRequestHandler> { public async Task> Handle(StartEmailConfirmationCommand command, CancellationToken cancellationToken) { diff --git a/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs b/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs index e705d9a48..6aa3807f4 100644 --- a/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs +++ b/application/account-management/Core/Features/Users/Commands/DeclineInvitation.cs @@ -15,7 +15,7 @@ public sealed class DeclineInvitationHandler( IUserRepository userRepository, IExecutionContext executionContext, ITelemetryEventsCollector events, - [FromKeyedServices("shared")] TimeProvider timeProvider) : IRequestHandler + [FromPlatformServices] TimeProvider timeProvider) : IRequestHandler { public async Task Handle(DeclineInvitationCommand command, CancellationToken cancellationToken) { diff --git a/application/account-management/Core/Features/Users/Domain/UserRepository.cs b/application/account-management/Core/Features/Users/Domain/UserRepository.cs index f4153437f..3d73e2ab8 100644 --- a/application/account-management/Core/Features/Users/Domain/UserRepository.cs +++ b/application/account-management/Core/Features/Users/Domain/UserRepository.cs @@ -44,7 +44,7 @@ CancellationToken cancellationToken internal sealed class UserRepository( AccountManagementDbContext accountManagementDbContext, IExecutionContext executionContext, - [FromKeyedServices("shared")] TimeProvider timeProvider) + [FromPlatformServices] TimeProvider timeProvider) : RepositoryBase(accountManagementDbContext), IUserRepository { /// diff --git a/application/account-management/Tests/EndpointBaseTest.cs b/application/account-management/Tests/EndpointBaseTest.cs index 28e921eb2..d2e4e211b 100644 --- a/application/account-management/Tests/EndpointBaseTest.cs +++ b/application/account-management/Tests/EndpointBaseTest.cs @@ -112,7 +112,7 @@ protected EndpointBaseTest() services.AddTransient(_ => EmailClient); RegisterMockLoggers(services); - services.AddKeyedSingleton("shared", TimeProvider); + services.AddKeyedSingleton(FromPlatformServicesAttribute.PlatformServiceKey, TimeProvider); services.AddScoped(); }); diff --git a/application/shared-kernel/SharedKernel/Configuration/FromPlatformServicesAttribute.cs b/application/shared-kernel/SharedKernel/Configuration/FromPlatformServicesAttribute.cs new file mode 100644 index 000000000..92dd11a17 --- /dev/null +++ b/application/shared-kernel/SharedKernel/Configuration/FromPlatformServicesAttribute.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Indicates that the parameter should be bound using the keyed service registered with the specified key. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class FromPlatformServicesAttribute : FromKeyedServicesAttribute +{ + public const string PlatformServiceKey = "PlatformPlatform"; + /// + /// Creates a new instance. + /// + public FromPlatformServicesAttribute() : base(PlatformServiceKey) + { + } +} diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs index b81f5adf2..e6556b28c 100644 --- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs +++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs @@ -121,7 +121,7 @@ private static IServiceCollection AddTimeProvider(this IServiceCollection servic // so when we inject a fake time provider during testing. The reason for registering a keyed service // is that it is better to only control time/stop time during testing for services we know work correct when // done so. A keyed sevice allows us to only override the TimeProvider for specific services during testing.. - services.AddKeyedSingleton(serviceKey, TimeProvider.System); + services.AddKeyedSingleton(FromPlatformServicesAttribute.PlatformServiceKey, TimeProvider.System); return services; } From 7b3e370bb7bb03f7a6dea1223414a91aff537792 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sat, 18 Oct 2025 22:25:55 +0000 Subject: [PATCH 3/5] Fix spelling mistake Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Egil Hansen --- .../SharedKernel/Configuration/SharedDependencyConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs index e6556b28c..19f45b7fc 100644 --- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs +++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs @@ -120,7 +120,7 @@ private static IServiceCollection AddTimeProvider(this IServiceCollection servic // to control time during testing, we register our own TimeProvider using .NET Keyed Services, // so when we inject a fake time provider during testing. The reason for registering a keyed service // is that it is better to only control time/stop time during testing for services we know work correct when - // done so. A keyed sevice allows us to only override the TimeProvider for specific services during testing.. + // done so. A keyed service allows us to only override the TimeProvider for specific services during testing.. services.AddKeyedSingleton(FromPlatformServicesAttribute.PlatformServiceKey, TimeProvider.System); return services; } From 7c00923a9ce0116a52c45d06848c387650426a98 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sat, 18 Oct 2025 22:26:26 +0000 Subject: [PATCH 4/5] Remove unused parameter Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Egil Hansen --- .../SharedKernel/Configuration/SharedDependencyConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs index 19f45b7fc..b36516b05 100644 --- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs +++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs @@ -114,7 +114,7 @@ private static IServiceCollection AddDefaultHealthChecks(this IServiceCollection return services; } - private static IServiceCollection AddTimeProvider(this IServiceCollection services, string serviceKey = "shared") + private static IServiceCollection AddTimeProvider(this IServiceCollection services) { // A TimeProvider is already registered as a singleton by default in .NET 8 and later. However, // to control time during testing, we register our own TimeProvider using .NET Keyed Services, From 64bdb53f09708eca30ae1840311c7f0ff78206e6 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sat, 18 Oct 2025 22:28:43 +0000 Subject: [PATCH 5/5] Revert line breaks Signed-off-by: Egil Hansen --- .../account-management/Tests/Signups/StartSignupTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/account-management/Tests/Signups/StartSignupTests.cs b/application/account-management/Tests/Signups/StartSignupTests.cs index 8f078ee68..507cea158 100644 --- a/application/account-management/Tests/Signups/StartSignupTests.cs +++ b/application/account-management/Tests/Signups/StartSignupTests.cs @@ -100,8 +100,6 @@ public async Task StartSignup_WhenTooManyAttempts_ShouldReturnTooManyRequests() await response.ShouldHaveErrorStatusCode(HttpStatusCode.TooManyRequests, "Too many attempts to confirm this email address. Please try again later."); TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse(); - await EmailClient - .DidNotReceive() - .SendAsync(Arg.Any(), Arg.Any(), Arg.Any(), CancellationToken.None); + await EmailClient.DidNotReceive().SendAsync(Arg.Any(), Arg.Any(), Arg.Any(), CancellationToken.None); } }