diff --git a/src/Orbit.Domain/Entities/User.cs b/src/Orbit.Domain/Entities/User.cs index 0b7c0f69..ff9878a8 100644 --- a/src/Orbit.Domain/Entities/User.cs +++ b/src/Orbit.Domain/Entities/User.cs @@ -365,6 +365,9 @@ public void Deactivate(DateTime scheduledDeletion) IsDeactivated = true; DeactivatedAt = DateTime.UtcNow; ScheduledDeletionAt = scheduledDeletion; + GoogleAccessToken = null; + GoogleRefreshToken = null; + GoogleCalendarAutoSyncEnabled = false; } public void CancelDeactivation() diff --git a/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs index 4b0bcee2..c58cd2e6 100644 --- a/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Auth/ConfirmAccountDeletionCommandHandlerTests.cs @@ -41,6 +41,36 @@ public async Task Handle_ValidCode_DeactivatesUserAndReturnsScheduledDate() _cache.TryGetValue($"delete:{TestEmail}", out _).Should().BeFalse(); } + [Fact] + public async Task Handle_ValidCode_ClearsGoogleOAuthTokens() + { + var user = User.Create("Test", TestEmail).Value; + user.SetGoogleTokens("access-token", "refresh-token"); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + + var result = await _handler.Handle(new ConfirmAccountDeletionCommand(UserId, "123456"), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + user.GoogleAccessToken.Should().BeNull(); + user.GoogleRefreshToken.Should().BeNull(); + } + + [Fact] + public async Task Handle_InvalidCode_LeavesGoogleOAuthTokensIntact() + { + var user = User.Create("Test", TestEmail).Value; + user.SetGoogleTokens("access-token", "refresh-token"); + SetupUser(user); + SetupDeletionCode(TestEmail, "123456"); + + var result = await _handler.Handle(new ConfirmAccountDeletionCommand(UserId, "999999"), CancellationToken.None); + + result.IsFailure.Should().BeTrue(); + user.GoogleAccessToken.Should().Be("access-token"); + user.GoogleRefreshToken.Should().Be("refresh-token"); + } + [Fact] public async Task Handle_InvalidCode_ReturnsFailureAndIncrementsAttempts() { diff --git a/tests/Orbit.Domain.Tests/Entities/UserTests.cs b/tests/Orbit.Domain.Tests/Entities/UserTests.cs index cef623af..7ce03bcb 100644 --- a/tests/Orbit.Domain.Tests/Entities/UserTests.cs +++ b/tests/Orbit.Domain.Tests/Entities/UserTests.cs @@ -903,4 +903,34 @@ public void CompleteOnboardingChecklist_SetsFlagAndIsIdempotent() user.HasCompletedOnboardingChecklist.Should().BeTrue(); } + + [Fact] + public void Deactivate_ClearsGoogleCalendarConnection() + { + var user = CreateValidUser(); + typeof(User).GetProperty(nameof(User.IsLifetimePro))!.SetValue(user, true); + user.SetGoogleTokens("access-token", "refresh-token"); + user.EnableCalendarAutoSync().IsSuccess.Should().BeTrue(); + + user.Deactivate(DateTime.UtcNow.AddDays(7)); + + user.IsDeactivated.Should().BeTrue(); + user.GoogleAccessToken.Should().BeNull(); + user.GoogleRefreshToken.Should().BeNull(); + user.GoogleCalendarAutoSyncEnabled.Should().BeFalse(); + } + + [Fact] + public void CancelDeactivation_LeavesGoogleTokensCleared_ReconnectRequired() + { + var user = CreateValidUser(); + user.SetGoogleTokens("access-token", "refresh-token"); + user.Deactivate(DateTime.UtcNow.AddDays(7)); + + user.CancelDeactivation(); + + user.IsDeactivated.Should().BeFalse(); + user.GoogleAccessToken.Should().BeNull(); + user.GoogleRefreshToken.Should().BeNull(); + } } diff --git a/tests/Orbit.Infrastructure.Tests/Persistence/FriendGraphServiceDeactivationTests.cs b/tests/Orbit.Infrastructure.Tests/Persistence/FriendGraphServiceDeactivationTests.cs new file mode 100644 index 00000000..40697028 --- /dev/null +++ b/tests/Orbit.Infrastructure.Tests/Persistence/FriendGraphServiceDeactivationTests.cs @@ -0,0 +1,107 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using NSubstitute; +using Orbit.Application.Social.Services; +using Orbit.Domain.Entities; +using Orbit.Domain.Interfaces; +using Orbit.Infrastructure.Persistence; + +namespace Orbit.Infrastructure.Tests.Persistence; + +public class FriendGraphServiceDeactivationTests +{ + [Fact] + public async Task ResolveTargetAsync_ByHandle_ReturnsNullForDeactivatedUser() + { + var deactivated = DeactivatedUser("Gone", "handle@example.com", "gonehandle"); + var service = await CreateServiceAsync(deactivated); + + var resolved = await service.ResolveTargetAsync("gonehandle", null, CancellationToken.None); + + resolved.Should().BeNull(); + } + + [Fact] + public async Task ResolveTargetAsync_ByReferralCode_ReturnsNullForDeactivatedUser() + { + var deactivated = DeactivatedUser("Gone", "referral@example.com", "gonehandle2"); + deactivated.SetReferralCode("REF12345"); + var service = await CreateServiceAsync(deactivated); + + var resolved = await service.ResolveTargetAsync(null, "REF12345", CancellationToken.None); + + resolved.Should().BeNull(); + } + + [Fact] + public async Task ResolveTargetAsync_ByHandle_ResolvesActiveUser() + { + var active = ActiveUser("Active", "active@example.com", "activehandle"); + var service = await CreateServiceAsync(active); + + var resolved = await service.ResolveTargetAsync("activehandle", null, CancellationToken.None); + + resolved.Should().NotBeNull(); + resolved!.Id.Should().Be(active.Id); + } + + [Fact] + public async Task ResolveTargetAsync_ByReferralCode_ResolvesActiveUser() + { + var active = ActiveUser("Active", "active-ref@example.com", "activehandle2"); + active.SetReferralCode("ACTIVE01"); + var service = await CreateServiceAsync(active); + + var resolved = await service.ResolveTargetAsync(null, "ACTIVE01", CancellationToken.None); + + resolved.Should().NotBeNull(); + resolved!.Id.Should().Be(active.Id); + } + + private static User ActiveUser(string name, string email, string handle) + { + var user = User.Create(name, email).Value; + user.SetHandle(handle).IsSuccess.Should().BeTrue(); + return user; + } + + private static User DeactivatedUser(string name, string email, string handle) + { + var user = ActiveUser(name, email, handle); + user.Deactivate(DateTime.UtcNow.AddDays(7)); + return user; + } + + private static async Task CreateServiceAsync(params User[] users) + { + var context = CreateContext(); + context.Users.AddRange(users); + await context.SaveChangesAsync(); + context.ChangeTracker.Clear(); + + return new FriendGraphService( + new GenericRepository(context), + Substitute.For>(), + Substitute.For>()); + } + + private static OrbitDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ReplaceService() + .Options; + + return new OrbitDbContext(options); + } + + private sealed class EncryptionAwareModelCacheKeyFactory : IModelCacheKeyFactory + { + public object Create(DbContext context, bool designTime) + { + var hasEncryption = context is OrbitDbContext orbit && orbit.HasEncryptionService; + return (context.GetType(), hasEncryption, designTime); + } + } +}