Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Orbit.Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
30 changes: 30 additions & 0 deletions tests/Orbit.Domain.Tests/Entities/UserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<FriendGraphService> CreateServiceAsync(params User[] users)
{
var context = CreateContext();
context.Users.AddRange(users);
await context.SaveChangesAsync();
context.ChangeTracker.Clear();

return new FriendGraphService(
new GenericRepository<User>(context),
Substitute.For<IGenericRepository<Friendship>>(),
Substitute.For<IGenericRepository<BlockedUser>>());
}

private static OrbitDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<OrbitDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.ReplaceService<IModelCacheKeyFactory, EncryptionAwareModelCacheKeyFactory>()
.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);
}
}
}
Loading