diff --git a/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs b/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs index f782b706..1dea7e5c 100644 --- a/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs +++ b/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs @@ -11,6 +11,7 @@ namespace Orbit.Application.Profile.Queries; public record ProfileResponse( + Guid UserId, string Name, string Email, string? TimeZone, @@ -99,6 +100,7 @@ public async Task> Handle(GetProfileQuery request, Cance user.PublicProfileShowTopHabits); return Result.Success(new ProfileResponse( + user.Id, user.Name, user.Email, user.TimeZone, diff --git a/tests/Orbit.Application.Tests/Queries/Profile/GetProfileQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Profile/GetProfileQueryHandlerTests.cs index 1a84a8c8..0eb9e3db 100644 --- a/tests/Orbit.Application.Tests/Queries/Profile/GetProfileQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Profile/GetProfileQueryHandlerTests.cs @@ -6,6 +6,7 @@ using Orbit.Domain.Entities; using Orbit.Domain.Interfaces; using System.Linq.Expressions; +using System.Text.Json; namespace Orbit.Application.Tests.Queries.Profile; @@ -77,9 +78,101 @@ public async Task Handle_UserFound_ReturnsProfile() var result = await _handler.Handle(query, CancellationToken.None); result.IsSuccess.Should().BeTrue(); - result.Value.Name.Should().Be("John Doe"); - result.Value.Email.Should().Be("test@example.com"); - result.Value.AiMessagesLimit.Should().Be(20); + result.Value.Should().BeEquivalentTo(new + { + Name = "John Doe", + Email = "test@example.com", + user.TimeZone, + user.AiMemoryEnabled, + user.AiSummaryEnabled, + user.HasCompletedOnboarding, + user.HasCompletedTour, + user.HasCreatedFirstHabit, + user.HasLoggedFirstHabit, + user.HasTriedAstra, + user.HasCompletedOnboardingChecklist, + user.Language, + Plan = "pro", + user.HasProAccess, + user.IsTrialActive, + user.TrialEndsAt, + user.PlanExpiresAt, + AiMessagesUsed = user.AiMessagesUsedThisMonth, + AiMessagesLimit = 20, + user.HasImportedCalendar, + user.HasSeenImportPrompt, + HasGoogleConnection = false, + SubscriptionInterval = (string?)null, + SubscriptionSource = (string?)null, + user.IsLifetimePro, + user.WeekStartDay, + user.TotalXp, + Level = 1, + LevelTitle = "Starter", + AdRewardsClaimedToday = 0, + user.CurrentStreak, + user.LongestStreak, + StreakFreezesAvailable = 3, + user.ThemePreference, + user.ColorScheme, + user.GoogleCalendarAutoSyncEnabled, + GoogleCalendarAutoSyncStatus = Orbit.Domain.Enums.GoogleCalendarAutoSyncStatus.Idle, + user.GoogleCalendarLastSyncedAt, + CanViewGamification = true, + user.Handle, + user.SocialOptIn, + Uses24HourClock = true, + PublicProfile = new + { + Enabled = false, + Slug = (string?)null, + ShareUrl = (string?)null, + ShowStreak = true, + ShowLevel = true, + ShowAchievements = true, + ShowTopHabits = false + }, + user.ProactiveAstraEnabled, + user.MarketingEmailConsent + }); + } + + [Fact] + public async Task Handle_UserFound_ReturnsCallerIdInDistinctIdFormat() + { + var user = CreateTestUser(); + var callerId = user.Id; + _userRepo.GetByIdAsync(callerId, Arg.Any()).Returns(user); + _payGate.GetAiMessageLimit(callerId, Arg.Any()).Returns(20); + _userDateService.GetUserTodayAsync(callerId, Arg.Any()).Returns(Today); + StubFreezeRepoEmpty(); + + var result = await _handler.Handle(new GetProfileQuery(callerId), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.UserId.Should().Be(callerId); + result.Value.UserId.ToString().Should().Be(callerId.ToString("D").ToLowerInvariant()); + result.Value.UserId.ToString().Should().MatchRegex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); + } + + [Fact] + public async Task ProfileResponse_LegacyConsumer_IgnoresUserId() + { + var user = CreateTestUser("Legacy User"); + var callerId = user.Id; + _userRepo.GetByIdAsync(callerId, Arg.Any()).Returns(user); + _payGate.GetAiMessageLimit(callerId, Arg.Any()).Returns(20); + _userDateService.GetUserTodayAsync(callerId, Arg.Any()).Returns(Today); + StubFreezeRepoEmpty(); + var result = await _handler.Handle(new GetProfileQuery(callerId), CancellationToken.None); + + var json = JsonSerializer.Serialize(result.Value, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var legacyProfile = JsonSerializer.Deserialize( + json, + new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + json.Should().Contain($"\"userId\":\"{callerId:D}\""); + legacyProfile.Should().Be(new LegacyProfileResponse("Legacy User", "test@example.com")); } [Fact] @@ -134,6 +227,8 @@ public async Task Handle_UserNotFound_ReturnsFailure() result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("User not found"); result.ErrorCode.Should().Be("USER_NOT_FOUND"); + var readValue = () => result.Value; + readValue.Should().Throw(); } [Fact] @@ -314,4 +409,6 @@ public async Task Handle_Past10Xp_ComputesLevelFromXp() result.Value.Level.Should().Be(12); result.Value.LevelTitle.Should().Be("Legend"); } + + private sealed record LegacyProfileResponse(string Name, string Email); } diff --git a/tests/Orbit.Infrastructure.Tests/Mcp/ProfileToolsTests.cs b/tests/Orbit.Infrastructure.Tests/Mcp/ProfileToolsTests.cs index 25dd940e..f956f8f9 100644 --- a/tests/Orbit.Infrastructure.Tests/Mcp/ProfileToolsTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Mcp/ProfileToolsTests.cs @@ -50,6 +50,7 @@ private async Task CapturedRequestAsync(Func public async Task GetProfile_Success_ReturnsFormattedProfile() { var profile = new ProfileResponse( + Guid.NewGuid(), "Thomas", "thomas@example.com", "America/Sao_Paulo", true, true, true, true, true, true, true, true, "pt-BR", "Pro", true, false, null, null, 5, 100, false, false, false, null, null, false, 1, 500, 5, "Achiever",