From 327c4504a6cfce603ee8e473b815b16490ddbce5 Mon Sep 17 00:00:00 2001 From: thomasluizon Date: Fri, 7 Aug 2026 11:56:07 -0300 Subject: [PATCH 1/2] chore: start ORB-97 From cd6c560f561223db0005d21d4ef7fb6458999b1a Mon Sep 17 00:00:00 2001 From: thomasluizon Date: Fri, 7 Aug 2026 12:02:53 -0300 Subject: [PATCH 2/2] fix(auth): cap deletion grace at 30 days --- .../Commands/ConfirmAccountDeletionCommand.cs | 2 + src/Orbit.Application/Common/AppConstants.cs | 1 + ...nfirmAccountDeletionCommandHandlerTests.cs | 105 ++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/src/Orbit.Application/Auth/Commands/ConfirmAccountDeletionCommand.cs b/src/Orbit.Application/Auth/Commands/ConfirmAccountDeletionCommand.cs index c660960d..6d27a963 100644 --- a/src/Orbit.Application/Auth/Commands/ConfirmAccountDeletionCommand.cs +++ b/src/Orbit.Application/Auth/Commands/ConfirmAccountDeletionCommand.cs @@ -44,6 +44,8 @@ public async Task> Handle(ConfirmAccountDeletionCommand request var scheduledDate = user.HasProAccess && user.PlanExpiresAt.HasValue && user.PlanExpiresAt.Value > nowAtUtc ? user.PlanExpiresAt.Value.AddDays(7) : nowAtUtc.AddDays(7); + var maximumScheduledDate = nowAtUtc.AddDays(AppConstants.MaxDeletionGraceDays); + scheduledDate = scheduledDate < maximumScheduledDate ? scheduledDate : maximumScheduledDate; user.Deactivate(scheduledDate); await unitOfWork.SaveChangesAsync(cancellationToken); diff --git a/src/Orbit.Application/Common/AppConstants.cs b/src/Orbit.Application/Common/AppConstants.cs index 6c290909..493d6da1 100644 --- a/src/Orbit.Application/Common/AppConstants.cs +++ b/src/Orbit.Application/Common/AppConstants.cs @@ -41,6 +41,7 @@ public static class AppConstants public const int MaxLanguageLength = 10; public const int MaxVerificationAttempts = 3; public const int VerificationAttemptWindowMinutes = 15; + public const int MaxDeletionGraceDays = 30; public const int MaxChatMessageLength = 4000; public const int MaxClarificationValueLength = 2048; public const int MaxClarificationArgsLength = 16384; diff --git a/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs index c58cd2e6..3187bff1 100644 --- a/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs @@ -41,6 +41,110 @@ public async Task Handle_ValidCode_DeactivatesUserAndReturnsScheduledDate() _cache.TryGetValue($"delete:{TestEmail}", out _).Should().BeFalse(); } + [Fact] + public async Task Handle_ProPlanBeyondGraceCap_SchedulesDeletionAtThirtyDays() + { + var user = User.Create("Test", TestEmail).Value; + user.SetStripeSubscription("sub_123", DateTime.UtcNow.AddDays(340)); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + var earliestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(30); + + var result = await _handler.Handle( + new ConfirmAccountDeletionCommand(UserId, "123456"), + CancellationToken.None); + + var latestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(30); + result.Value.Should().BeOnOrAfter(earliestScheduledDeletionAtUtc) + .And.BeOnOrBefore(latestScheduledDeletionAtUtc); + } + + [Fact] + public async Task Handle_ProPlanWithinGraceCap_SchedulesDeletionSevenDaysAfterPlanExpiry() + { + var user = User.Create("Test", TestEmail).Value; + var planExpiresAtUtc = DateTime.UtcNow.AddDays(3); + user.SetStripeSubscription("sub_123", planExpiresAtUtc); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + + var result = await _handler.Handle( + new ConfirmAccountDeletionCommand(UserId, "123456"), + CancellationToken.None); + + result.Value.Should().Be(planExpiresAtUtc.AddDays(7)); + } + + [Fact] + public async Task Handle_ProPlanAtGraceCapBoundary_SchedulesDeletionSevenDaysAfterPlanExpiry() + { + var user = User.Create("Test", TestEmail).Value; + var planExpiresAtUtc = DateTime.UtcNow.AddDays(23); + user.SetStripeSubscription("sub_123", planExpiresAtUtc); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + + var result = await _handler.Handle( + new ConfirmAccountDeletionCommand(UserId, "123456"), + CancellationToken.None); + + result.Value.Should().Be(planExpiresAtUtc.AddDays(7)); + } + + [Fact] + public async Task Handle_FreeUser_SchedulesDeletionInSevenDays() + { + var user = User.Create("Test", TestEmail).Value; + user.StartTrial(DateTime.UtcNow.AddDays(-1)); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + var earliestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(7); + + var result = await _handler.Handle( + new ConfirmAccountDeletionCommand(UserId, "123456"), + CancellationToken.None); + + var latestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(7); + result.Value.Should().BeOnOrAfter(earliestScheduledDeletionAtUtc) + .And.BeOnOrBefore(latestScheduledDeletionAtUtc); + } + + [Fact] + public async Task Handle_TrialUserWithoutPlanExpiry_SchedulesDeletionInSevenDays() + { + var user = User.Create("Test", TestEmail).Value; + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + var earliestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(7); + + var result = await _handler.Handle( + new ConfirmAccountDeletionCommand(UserId, "123456"), + CancellationToken.None); + + var latestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(7); + result.Value.Should().BeOnOrAfter(earliestScheduledDeletionAtUtc) + .And.BeOnOrBefore(latestScheduledDeletionAtUtc); + } + + [Fact] + public async Task Handle_ExpiredProPlan_SchedulesDeletionInSevenDays() + { + var user = User.Create("Test", TestEmail).Value; + user.SetStripeSubscription("sub_123", DateTime.UtcNow.AddDays(-1)); + user.StartTrial(DateTime.UtcNow.AddDays(-1)); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + var earliestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(7); + + var result = await _handler.Handle( + new ConfirmAccountDeletionCommand(UserId, "123456"), + CancellationToken.None); + + var latestScheduledDeletionAtUtc = DateTime.UtcNow.AddDays(7); + result.Value.Should().BeOnOrAfter(earliestScheduledDeletionAtUtc) + .And.BeOnOrBefore(latestScheduledDeletionAtUtc); + } + [Fact] public async Task Handle_ValidCode_ClearsGoogleOAuthTokens() { @@ -85,6 +189,7 @@ public async Task Handle_InvalidCode_ReturnsFailureAndIncrementsAttempts() result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Invalid"); user.IsDeactivated.Should().BeFalse(); + user.ScheduledDeletionAt.Should().BeNull(); _cache.TryGetValue($"delete-attempts:{TestEmail}", out int attempts).Should().BeTrue(); attempts.Should().Be(1); }