diff --git a/.cursor/rules/backend/backend.mdc b/.cursor/rules/backend/backend.mdc index a555934ac..dd45cf692 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 [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 80a5064f0..506fb7597 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, + [FromPlatformServices] 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..61cfd9a50 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, + [FromPlatformServices] 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..a5f4027f8 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, + [FromPlatformServices] 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..20a62bdcf 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, + [FromPlatformServices] 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..797cd171b 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, + [FromPlatformServices] 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..6aa3807f4 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, + [FromPlatformServices] 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..3d73e2ab8 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, + [FromPlatformServices] 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..d2e4e211b 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(FromPlatformServicesAttribute.PlatformServiceKey, 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..507cea158 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) 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/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 2b8bfa50e..b36516b05 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) + { + // 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 service allows us to only override the TimeProvider for specific services during testing.. + services.AddKeyedSingleton(FromPlatformServicesAttribute.PlatformServiceKey, 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 @@ +