From 811d23f7012dcfcb4b07bf4a139cb99d26bc9afd Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Mon, 13 Jul 2026 16:27:26 -0300 Subject: [PATCH] chore(sonar): hoist constant array arguments to static readonly fields (CA1861) Drives external_roslyn:CA1861 "Avoid constant arrays as arguments" to zero across the test suite (53 SonarCloud-reported instances). Each constant array literal passed directly as an argument is hoisted into a descriptively-named `private static readonly T[]` field so it is allocated once instead of on every call. Identical literals are deduped to a single field. Behavior-preserving: same values, order, and element types; full suite green (5284 tests). Refs thomasluizon/orbit-ui-mobile#243 Co-Authored-By: Claude Opus 4.8 --- ...SetSelectedCalendarsCommandHandlerTests.cs | 16 +++--- .../ProcessUserChatCommandHandlerTests.cs | 11 ++-- .../BulkCreateHabitsCommandHandlerTests.cs | 4 +- .../SuggestHabitSetupCommandHandlerTests.cs | 7 ++- .../GetUserCalendarsQueryHandlerTests.cs | 3 +- .../GetDailySummaryQueryHandlerTests.cs | 3 +- .../GetPublicProfileQueryHandlerTests.cs | 4 +- .../XpAwardLogBackfillServiceTests.cs | 4 +- .../GetFriendProfileQueryHandlerTests.cs | 4 +- .../CreateHabitCommandValidatorTests.cs | 23 +++++--- .../UpdateHabitCommandValidatorTests.cs | 14 +++-- .../Services/AiHabitSuggestionServiceTests.cs | 53 ++++++++++++------- .../Services/AiTagSuggestionServiceTests.cs | 7 ++- .../GoogleCalendarEventFetcherTests.cs | 10 ++-- .../Services/ReminderSchedulerServiceTests.cs | 19 +++---- 15 files changed, 118 insertions(+), 64 deletions(-) diff --git a/tests/Orbit.Application.Tests/Commands/Calendar/SetSelectedCalendarsCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Calendar/SetSelectedCalendarsCommandHandlerTests.cs index cb484d32..ca8c16fa 100644 --- a/tests/Orbit.Application.Tests/Commands/Calendar/SetSelectedCalendarsCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Calendar/SetSelectedCalendarsCommandHandlerTests.cs @@ -10,6 +10,10 @@ namespace Orbit.Application.Tests.Commands.Calendar; public class SetSelectedCalendarsCommandHandlerTests { + private static readonly string[] SingleCalendarSelection = new[] { "cal_a" }; + private static readonly string[] TwoCalendarSelection = new[] { "cal_a", "cal_b" }; + private static readonly string[] SelectionWithEmptyId = new[] { "cal_a", "" }; + private readonly IGenericRepository _userRepo = Substitute.For>(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly SetSelectedCalendarsCommandHandler _handler; @@ -34,7 +38,7 @@ public async Task Handle_UserNotFound_ReturnsFailure() StubUser(null); var result = await _handler.Handle( - new SetSelectedCalendarsCommand(Guid.NewGuid(), new[] { "cal_a" }), CancellationToken.None); + new SetSelectedCalendarsCommand(Guid.NewGuid(), SingleCalendarSelection), CancellationToken.None); result.IsFailure.Should().BeTrue(); await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); @@ -47,10 +51,10 @@ public async Task Handle_SetsSelectionAndPersists() StubUser(user); var result = await _handler.Handle( - new SetSelectedCalendarsCommand(user.Id, new[] { "cal_a", "cal_b" }), CancellationToken.None); + new SetSelectedCalendarsCommand(user.Id, TwoCalendarSelection), CancellationToken.None); result.IsSuccess.Should().BeTrue(); - user.GetSelectedCalendarIds().Should().BeEquivalentTo(new[] { "cal_a", "cal_b" }); + user.GetSelectedCalendarIds().Should().BeEquivalentTo(TwoCalendarSelection); await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); } @@ -58,7 +62,7 @@ public async Task Handle_SetsSelectionAndPersists() public async Task Handle_EmptyList_ClearsSelectionToDefault() { var user = CreateUser(); - user.SetSelectedCalendars(new[] { "cal_a" }); + user.SetSelectedCalendars(SingleCalendarSelection); StubUser(user); var result = await _handler.Handle( @@ -75,7 +79,7 @@ public void Validator_RejectsEmptyId() var validator = new SetSelectedCalendarsCommandValidator(); var result = validator.Validate( - new SetSelectedCalendarsCommand(Guid.NewGuid(), new[] { "cal_a", "" })); + new SetSelectedCalendarsCommand(Guid.NewGuid(), SelectionWithEmptyId)); result.IsValid.Should().BeFalse(); } @@ -98,7 +102,7 @@ public void Validator_AcceptsValidSelection() var validator = new SetSelectedCalendarsCommandValidator(); var result = validator.Validate( - new SetSelectedCalendarsCommand(Guid.NewGuid(), new[] { "cal_a", "cal_b" })); + new SetSelectedCalendarsCommand(Guid.NewGuid(), TwoCalendarSelection)); result.IsValid.Should().BeTrue(); } diff --git a/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs index 69bd998d..88224341 100644 --- a/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs @@ -42,6 +42,9 @@ public class ProcessUserChatCommandHandlerTests private static readonly Guid UserId = Guid.NewGuid(); private static readonly DateOnly Today = new(2026, 4, 3); + private static readonly string[] ExpectedOrderedToolNames = new[] { "assign_tags", "create_habit", "delete_habit" }; + private static readonly string[] GamificationTodaySurfaces = new[] { "gamification", "today" }; + private static readonly string[] HabitsSurfaces = new[] { "habits" }; private static Habit CreateHabit(string title, bool isCompleted = false) { @@ -646,7 +649,7 @@ public async Task Handle_SendsToolsInDeterministicOrdinalOrder() await _aiIntentService.Received(1).SendWithToolsAsync( Arg.Any(), Arg.Any(), Arg.Is>(declarations => - ToolNames(declarations).SequenceEqual(new[] { "assign_tags", "create_habit", "delete_habit" })), + ToolNames(declarations).SequenceEqual(ExpectedOrderedToolNames)), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any?>(), Arg.Any?>(), Arg.Any()); @@ -907,7 +910,7 @@ public async Task Handle_ReadOnlyToolWithRelatedSurfaces_SurfacesThemOnResponse( .Returns(new ToolResult(true, Payload: new { key = "streaks", - related_surfaces = new[] { "gamification", "today" }, + related_surfaces = GamificationTodaySurfaces, markdown = "# Streaks" })); @@ -1697,7 +1700,7 @@ public async Task Handle_MultipleReadOnlyTools_PreserveDeterministicRelatedSurfa .Returns(async _ => { await Task.Delay(40); - return new ToolResult(true, Payload: new { related_surfaces = new[] { "gamification", "today" } }); + return new ToolResult(true, Payload: new { related_surfaces = GamificationTodaySurfaces }); }); var fastSecond = Substitute.For(); @@ -1706,7 +1709,7 @@ public async Task Handle_MultipleReadOnlyTools_PreserveDeterministicRelatedSurfa fastSecond.IsReadOnly.Returns(true); fastSecond.GetParameterSchema().Returns(new { type = "object" }); fastSecond.ExecuteAsync(Arg.Any(), UserId, Arg.Any()) - .Returns(new ToolResult(true, Payload: new { related_surfaces = new[] { "habits" } })); + .Returns(new ToolResult(true, Payload: new { related_surfaces = HabitsSurfaces })); var handler = CreateHandler(slowFirst, fastSecond); diff --git a/tests/Orbit.Application.Tests/Commands/Habits/BulkCreateHabitsCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/BulkCreateHabitsCommandHandlerTests.cs index 64f294e5..a9347725 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/BulkCreateHabitsCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/BulkCreateHabitsCommandHandlerTests.cs @@ -27,6 +27,8 @@ public class BulkCreateHabitsCommandHandlerTests private static readonly Guid UserId = Guid.NewGuid(); private static readonly DateOnly Today = new(2026, 3, 20); + private static readonly string[] ExpectedTagNames = new[] { "Fitness", "Health" }; + public BulkCreateHabitsCommandHandlerTests() { _handler = new BulkCreateHabitsCommandHandler( @@ -281,7 +283,7 @@ public async Task Handle_WithTags_CreatesAndAttachesNewTagsWithDefaultColor() result.IsSuccess.Should().BeTrue(); addedTags.Should().HaveCount(2); - addedTags.Select(t => t.Name).Should().BeEquivalentTo(new[] { "Fitness", "Health" }); + addedTags.Select(t => t.Name).Should().BeEquivalentTo(ExpectedTagNames); addedTags.Should().AllSatisfy(t => t.Color.Should().Be("#7c3aed")); addedHabits.Should().ContainSingle(); addedHabits[0].Tags.Should().HaveCount(2); diff --git a/tests/Orbit.Application.Tests/Commands/Habits/SuggestHabitSetupCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Habits/SuggestHabitSetupCommandHandlerTests.cs index d0e0d214..fd7ead85 100644 --- a/tests/Orbit.Application.Tests/Commands/Habits/SuggestHabitSetupCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Habits/SuggestHabitSetupCommandHandlerTests.cs @@ -25,6 +25,9 @@ public class SuggestHabitSetupCommandHandlerTests private static readonly Guid UserId = Guid.NewGuid(); + private static readonly DayOfWeek[] SuggestionDays = new[] { DayOfWeek.Monday }; + private static readonly string[] SubHabits = new[] { "Warm up" }; + public SuggestHabitSetupCommandHandlerTests() { _handler = new SuggestHabitSetupCommandHandler( @@ -32,9 +35,9 @@ public SuggestHabitSetupCommandHandlerTests() } private static HabitSetupSuggestion SampleSuggestion() => - new("R", FrequencyUnit.Day, 1, new[] { DayOfWeek.Monday }, + new("R", FrequencyUnit.Day, 1, SuggestionDays, IsFlexible: false, FlexibleTarget: null, DueTime: null, - SubHabits: new[] { "Warm up" }, ChecklistItems: Array.Empty()); + SubHabits: SubHabits, ChecklistItems: Array.Empty()); private void SetupTrackedUser() { diff --git a/tests/Orbit.Application.Tests/Queries/Calendar/GetUserCalendarsQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Calendar/GetUserCalendarsQueryHandlerTests.cs index 217356e2..e10661d4 100644 --- a/tests/Orbit.Application.Tests/Queries/Calendar/GetUserCalendarsQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Calendar/GetUserCalendarsQueryHandlerTests.cs @@ -21,6 +21,7 @@ public class GetUserCalendarsQueryHandlerTests private readonly GetUserCalendarsQueryHandler _handler; private static readonly Guid UserId = Guid.NewGuid(); + private static readonly string[] SharedCalendarSelection = new[] { "shared" }; public GetUserCalendarsQueryHandlerTests() { @@ -81,7 +82,7 @@ public async Task Handle_NullSelection_IsSyncedReflectsDefaultOwned() public async Task Handle_ExplicitSelection_IsSyncedReflectsSelectedSet() { var user = CreateUser(); - user.SetSelectedCalendars(new[] { "shared" }); + user.SetSelectedCalendars(SharedCalendarSelection); _userRepo.GetByIdAsync(UserId, Arg.Any()).Returns(user); _googleTokenService.GetValidAccessTokenAsync(user, Arg.Any()).Returns("token"); _eventFetcher.ListCalendarsAsync("token", Arg.Any()).Returns(SampleCalendars()); diff --git a/tests/Orbit.Application.Tests/Queries/Habits/GetDailySummaryQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Habits/GetDailySummaryQueryHandlerTests.cs index 703cb5f7..5c23d311 100644 --- a/tests/Orbit.Application.Tests/Queries/Habits/GetDailySummaryQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Habits/GetDailySummaryQueryHandlerTests.cs @@ -23,6 +23,7 @@ public class GetDailySummaryQueryHandlerTests private static readonly Guid UserId = Guid.NewGuid(); private static readonly DateOnly Today = new(2026, 4, 3); + private static readonly string[] ExpectedHabitTitles = new[] { "Read" }; public GetDailySummaryQueryHandlerTests() { @@ -136,7 +137,7 @@ public async Task Handle_ExcludesHabitsSkippedInRequestedRange() result.IsSuccess.Should().BeTrue(); await _summaryService.Received(1).GenerateSummaryAsync( Arg.Is>(habits => - habits.Select(h => h.Title).SequenceEqual(new[] { "Read" })), + habits.Select(h => h.Title).SequenceEqual(ExpectedHabitTitles)), Today, Today, Arg.Any(), "en", Arg.Any(), Arg.Any(), Arg.Any(), diff --git a/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs index 70254ead..5af97f67 100644 --- a/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Profile/GetPublicProfileQueryHandlerTests.cs @@ -19,6 +19,8 @@ public class GetPublicProfileQueryHandlerTests private const string Slug = "ABCDEFGHJKLMNPQRSTUV12"; + private static readonly string[] ExpectedAchievementKeys = new[] { "first_orbit", "week_warrior" }; + public GetPublicProfileQueryHandlerTests() { _handler = new GetPublicProfileQueryHandler(_userRepo, _achievementRepo, _habitRepo); @@ -88,7 +90,7 @@ public async Task Handle_AllStatFlagsOn_ReturnsPublicFieldsAndOwnerLanguage() view.Level.Should().NotBeNull(); view.LevelTitle.Should().NotBeNullOrWhiteSpace(); view.Achievements.Should().NotBeNull(); - view.Achievements!.Select(a => a.IconKey).Should().Contain(new[] { "first_orbit", "week_warrior" }); + view.Achievements!.Select(a => a.IconKey).Should().Contain(ExpectedAchievementKeys); view.TopHabits.Should().BeNull(); } diff --git a/tests/Orbit.Application.Tests/Services/XpAwardLogBackfillServiceTests.cs b/tests/Orbit.Application.Tests/Services/XpAwardLogBackfillServiceTests.cs index 2cdc76d7..1fa72c42 100644 --- a/tests/Orbit.Application.Tests/Services/XpAwardLogBackfillServiceTests.cs +++ b/tests/Orbit.Application.Tests/Services/XpAwardLogBackfillServiceTests.cs @@ -22,6 +22,8 @@ public class XpAwardLogBackfillServiceTests private static readonly Guid UserId = Guid.NewGuid(); private static readonly DateOnly Today = new(2026, 3, 20); + private static readonly int[] ExpectedHabitXpAmounts = new[] { 11, 12, 13 }; + public XpAwardLogBackfillServiceTests() { _sut = new XpAwardLogBackfillService(_userRepo, _habitRepo, _goalRepo, _achievementRepo, _xpRepo, _unitOfWork); @@ -79,7 +81,7 @@ public async Task BackfillUser_ReplaysHabitXpWithRecomputedPerDateStreak() processed.Should().BeTrue(); _added.Should().HaveCount(3); _added.Should().OnlyContain(r => r.Source == XpAwardSource.HabitLog); - _added.Select(r => r.Amount).Should().BeEquivalentTo(new[] { 11, 12, 13 }); + _added.Select(r => r.Amount).Should().BeEquivalentTo(ExpectedHabitXpAmounts); await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); } diff --git a/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs index 9027e8ed..f7503986 100644 --- a/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Social/GetFriendProfileQueryHandlerTests.cs @@ -29,6 +29,8 @@ public class GetFriendProfileQueryHandlerTests private static readonly DateOnly Today = new(2026, 6, 15); + private static readonly string[] ExpectedAchievementKeys = new[] { "first_orbit", "week_warrior" }; + public GetFriendProfileQueryHandlerTests() { var guard = new SocialAccessGuard(_userRepository); @@ -105,7 +107,7 @@ public async Task Handle_AcceptedFriend_ReturnsProfileStatsAndAchievements() view.Handle.Should().NotBeNullOrWhiteSpace(); view.CurrentStreak.Should().Be(12); view.Level.Should().Be(5); - view.Achievements.Select(a => a.IconKey).Should().Contain(new[] { "first_orbit", "week_warrior" }); + view.Achievements.Select(a => a.IconKey).Should().Contain(ExpectedAchievementKeys); } [Fact] diff --git a/tests/Orbit.Application.Tests/Validators/CreateHabitCommandValidatorTests.cs b/tests/Orbit.Application.Tests/Validators/CreateHabitCommandValidatorTests.cs index c7409651..40ae6911 100644 --- a/tests/Orbit.Application.Tests/Validators/CreateHabitCommandValidatorTests.cs +++ b/tests/Orbit.Application.Tests/Validators/CreateHabitCommandValidatorTests.cs @@ -14,6 +14,13 @@ public class CreateHabitCommandValidatorTests { private readonly CreateHabitCommandValidator _validator = new(); + private static readonly DayOfWeek[] MondayOnly = new[] { DayOfWeek.Monday }; + private static readonly DayOfWeek[] MondayAndWednesday = new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }; + private static readonly int[] ValidReminderTimes = new[] { 0, 15, 60, 1440 }; + private static readonly int[] DuplicateReminderTimes = new[] { 15, 15 }; + private static readonly int[] NegativeReminderTimes = new[] { -1 }; + private static readonly int[] OverMaxReminderTimes = new[] { AppConstants.MaxReminderMinutesBefore + 1 }; + private static CreateHabitCommand ValidCommand() => new( UserId: Guid.NewGuid(), Title: "My Habit", @@ -97,7 +104,7 @@ public void Validate_DaysWithQtyNot1_HasError() var command = ValidCommand() with { FrequencyQuantity = 2, - Options = new HabitCommandOptions(Days: new[] { DayOfWeek.Monday }) + Options = new HabitCommandOptions(Days: MondayOnly) }; var result = _validator.TestValidate(command); @@ -111,7 +118,7 @@ public void Validate_DaysWithQty1_NoError() var command = ValidCommand() with { FrequencyQuantity = 1, - Options = new HabitCommandOptions(Days: new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }) + Options = new HabitCommandOptions(Days: MondayAndWednesday) }; var result = _validator.TestValidate(command); @@ -126,7 +133,7 @@ public void Validate_DaysWithNonDayUnit_HasError() { FrequencyUnit = FrequencyUnit.Week, FrequencyQuantity = 1, - Options = new HabitCommandOptions(Days: new[] { DayOfWeek.Monday }) + Options = new HabitCommandOptions(Days: MondayOnly) }; var result = _validator.TestValidate(command); @@ -141,7 +148,7 @@ public void Validate_DaysWhenFlexible_NoError() { FrequencyUnit = FrequencyUnit.Week, FrequencyQuantity = 1, - Options = new HabitCommandOptions(Days: new[] { DayOfWeek.Monday }, IsFlexible: true) + Options = new HabitCommandOptions(Days: MondayOnly, IsFlexible: true) }; var result = _validator.TestValidate(command); @@ -303,7 +310,7 @@ public void Validate_ValidReminderTimes_NoError() { var command = ValidCommand() with { - Options = new HabitCommandOptions(ReminderTimes: new[] { 0, 15, 60, 1440 }) + Options = new HabitCommandOptions(ReminderTimes: ValidReminderTimes) }; var result = _validator.TestValidate(command); @@ -315,7 +322,7 @@ public void Validate_ReminderTimes_Duplicates_HasError() { var command = ValidCommand() with { - Options = new HabitCommandOptions(ReminderTimes: new[] { 15, 15 }) + Options = new HabitCommandOptions(ReminderTimes: DuplicateReminderTimes) }; var result = _validator.TestValidate(command); @@ -327,7 +334,7 @@ public void Validate_ReminderTimes_Negative_HasError() { var command = ValidCommand() with { - Options = new HabitCommandOptions(ReminderTimes: new[] { -1 }) + Options = new HabitCommandOptions(ReminderTimes: NegativeReminderTimes) }; var result = _validator.TestValidate(command); @@ -339,7 +346,7 @@ public void Validate_ReminderTimes_OverMax_HasError() { var command = ValidCommand() with { - Options = new HabitCommandOptions(ReminderTimes: new[] { AppConstants.MaxReminderMinutesBefore + 1 }) + Options = new HabitCommandOptions(ReminderTimes: OverMaxReminderTimes) }; var result = _validator.TestValidate(command); diff --git a/tests/Orbit.Application.Tests/Validators/UpdateHabitCommandValidatorTests.cs b/tests/Orbit.Application.Tests/Validators/UpdateHabitCommandValidatorTests.cs index 37e23c80..3f4dff0b 100644 --- a/tests/Orbit.Application.Tests/Validators/UpdateHabitCommandValidatorTests.cs +++ b/tests/Orbit.Application.Tests/Validators/UpdateHabitCommandValidatorTests.cs @@ -9,6 +9,10 @@ public class UpdateHabitCommandValidatorTests { private readonly UpdateHabitCommandValidator _validator = new(); + private static readonly DayOfWeek[] MondayOnly = new[] { DayOfWeek.Monday }; + private static readonly int[] DuplicateReminderTimes = new[] { 15, 15 }; + private static readonly int[] OutOfRangeReminderTimes = new[] { -5 }; + private static UpdateHabitCommand ValidCommand() => new( UserId: Guid.NewGuid(), HabitId: Guid.NewGuid(), @@ -83,7 +87,7 @@ public void Validate_DaysWithQtyNot1_HasError() var command = ValidCommand() with { FrequencyQuantity = 2, - Options = new UpdateHabitCommandOptions(Days: new[] { DayOfWeek.Monday }) + Options = new UpdateHabitCommandOptions(Days: MondayOnly) }; var result = _validator.TestValidate(command); @@ -98,7 +102,7 @@ public void Validate_DaysWithNonDayUnit_HasError() { FrequencyUnit = FrequencyUnit.Week, FrequencyQuantity = 1, - Options = new UpdateHabitCommandOptions(Days: new[] { DayOfWeek.Monday }) + Options = new UpdateHabitCommandOptions(Days: MondayOnly) }; var result = _validator.TestValidate(command); @@ -113,7 +117,7 @@ public void Validate_DaysWithDayUnitQty1_NoError() { FrequencyUnit = FrequencyUnit.Day, FrequencyQuantity = 1, - Options = new UpdateHabitCommandOptions(Days: new[] { DayOfWeek.Monday }) + Options = new UpdateHabitCommandOptions(Days: MondayOnly) }; var result = _validator.TestValidate(command); @@ -156,7 +160,7 @@ public void Validate_ReminderTimes_Duplicates_HasError() { var command = ValidCommand() with { - Options = new UpdateHabitCommandOptions(ReminderTimes: new[] { 15, 15 }) + Options = new UpdateHabitCommandOptions(ReminderTimes: DuplicateReminderTimes) }; var result = _validator.TestValidate(command); @@ -169,7 +173,7 @@ public void Validate_ReminderTimes_OutOfRange_HasError() { var command = ValidCommand() with { - Options = new UpdateHabitCommandOptions(ReminderTimes: new[] { -5 }) + Options = new UpdateHabitCommandOptions(ReminderTimes: OutOfRangeReminderTimes) }; var result = _validator.TestValidate(command); diff --git a/tests/Orbit.Infrastructure.Tests/Services/AiHabitSuggestionServiceTests.cs b/tests/Orbit.Infrastructure.Tests/Services/AiHabitSuggestionServiceTests.cs index 33733781..b43894db 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/AiHabitSuggestionServiceTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/AiHabitSuggestionServiceTests.cs @@ -9,6 +9,21 @@ namespace Orbit.Infrastructure.Tests.Services; public class AiHabitSuggestionServiceTests { private static readonly JsonSerializerOptions DeserializeOptions = new() { PropertyNameCaseInsensitive = true }; + private static readonly string[] BrushAndShowerSubHabits = new[] { "Brush teeth", "Shower" }; + private static readonly string[] CheeseAndBreadChecklist = new[] { "Cheese", "Bread" }; + private static readonly string[] BrushAndMakeBedSubHabits = new[] { "Brush teeth", "Make bed" }; + private static readonly string[] MondayAndWednesdayNames = new[] { "Monday", "Wednesday" }; + private static readonly string[] WarmUpAndCoolDownSubHabits = new[] { "Warm up", "Cool down" }; + private static readonly string[] MondayName = new[] { "Monday" }; + private static readonly string[] PlaceholderSubHabits = new[] { "x" }; + private static readonly string[] WeekdayNamesWithInvalids = new[] { "Monday", "Notaday", "monday" }; + private static readonly string[] CheeseBreadEggsChecklist = new[] { "Cheese", "Bread", "Eggs" }; + private static readonly string[] OkSubHabits = new[] { "ok" }; + private static readonly string[] EggsChecklist = new[] { "Eggs" }; + private static readonly string[] BrushTeethSubHabits = new[] { "Brush teeth" }; + private static readonly string[] CheeseChecklist = new[] { "Cheese" }; + private static readonly DayOfWeek[] MondayAndWednesday = new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }; + private static readonly DayOfWeek[] MondayOnly = new[] { DayOfWeek.Monday }; private static Dto Deserialize(string json) => JsonSerializer.Deserialize(json, DeserializeOptions)!; @@ -17,7 +32,7 @@ public void Deserialize_SubHabitsAsStrings_ParsesEachString() { var dto = Deserialize("""{"subHabits":["Brush teeth","Shower"]}"""); - dto.SubHabits.Should().BeEquivalentTo(new[] { "Brush teeth", "Shower" }); + dto.SubHabits.Should().BeEquivalentTo(BrushAndShowerSubHabits); } [Fact] @@ -25,7 +40,7 @@ public void Deserialize_SubHabitsAsObjects_ExtractsTitle() { var dto = Deserialize("""{"subHabits":[{"title":"Brush teeth"},{"title":"Shower"}]}"""); - dto.SubHabits.Should().BeEquivalentTo(new[] { "Brush teeth", "Shower" }); + dto.SubHabits.Should().BeEquivalentTo(BrushAndShowerSubHabits); } [Fact] @@ -33,7 +48,7 @@ public void Deserialize_ChecklistItemsAsObjects_ExtractsName() { var dto = Deserialize("""{"checklistItems":[{"name":"Cheese"},{"name":"Bread"}]}"""); - dto.ChecklistItems.Should().BeEquivalentTo(new[] { "Cheese", "Bread" }); + dto.ChecklistItems.Should().BeEquivalentTo(CheeseAndBreadChecklist); } [Fact] @@ -60,7 +75,7 @@ public void Deserialize_ObjectSubHabits_ThenMapSuggestion_Succeeds() var result = AiHabitSuggestionService.MapSuggestion(dto); result.IsSuccess.Should().BeTrue(); - result.Value.SubHabits.Should().BeEquivalentTo(new[] { "Brush teeth", "Make bed" }); + result.Value.SubHabits.Should().BeEquivalentTo(BrushAndMakeBedSubHabits); } [Fact] @@ -93,7 +108,7 @@ public void MapSuggestion_NullDto_ReturnsFailure() [Fact] public void MapSuggestion_ValidDailyJson_MapsAllFields() { - var dto = new Dto("R", "Day", 1, new[] { "Monday", "Wednesday" }, new[] { "Warm up", "Cool down" }); + var dto = new Dto("R", "Day", 1, MondayAndWednesdayNames, WarmUpAndCoolDownSubHabits); var result = AiHabitSuggestionService.MapSuggestion(dto); @@ -101,8 +116,8 @@ public void MapSuggestion_ValidDailyJson_MapsAllFields() result.Value.Emoji.Should().Be("R"); result.Value.FrequencyUnit.Should().Be(FrequencyUnit.Day); result.Value.FrequencyQuantity.Should().Be(1); - result.Value.Days.Should().BeEquivalentTo(new[] { DayOfWeek.Monday, DayOfWeek.Wednesday }); - result.Value.SubHabits.Should().BeEquivalentTo(new[] { "Warm up", "Cool down" }); + result.Value.Days.Should().BeEquivalentTo(MondayAndWednesday); + result.Value.SubHabits.Should().BeEquivalentTo(WarmUpAndCoolDownSubHabits); result.Value.IsFlexible.Should().BeFalse(); result.Value.FlexibleTarget.Should().BeNull(); result.Value.DueTime.Should().BeNull(); @@ -112,7 +127,7 @@ public void MapSuggestion_ValidDailyJson_MapsAllFields() [Fact] public void MapSuggestion_WeeklyWithDays_StripsDays() { - var dto = new Dto(null, "Week", 1, new[] { "Monday" }, null); + var dto = new Dto(null, "Week", 1, MondayName, null); var result = AiHabitSuggestionService.MapSuggestion(dto); @@ -123,7 +138,7 @@ public void MapSuggestion_WeeklyWithDays_StripsDays() [Fact] public void MapSuggestion_DailyQuantityNotOne_StripsDays() { - var dto = new Dto(null, "Day", 2, new[] { "Monday" }, null); + var dto = new Dto(null, "Day", 2, MondayName, null); var result = AiHabitSuggestionService.MapSuggestion(dto); @@ -166,7 +181,7 @@ public void MapSuggestion_TooManySubHabits_ClampsToCap() [Fact] public void MapSuggestion_InvalidFrequencyUnit_BecomesNullAndDropsQuantity() { - var dto = new Dto(null, "Fortnight", 3, null, new[] { "x" }); + var dto = new Dto(null, "Fortnight", 3, null, PlaceholderSubHabits); var result = AiHabitSuggestionService.MapSuggestion(dto); @@ -191,17 +206,17 @@ public void MapSuggestion_DropsBlankAndOverLongSubHabitTitles() var result = AiHabitSuggestionService.MapSuggestion(dto); - result.Value.SubHabits.Should().BeEquivalentTo(new[] { "ok" }); + result.Value.SubHabits.Should().BeEquivalentTo(OkSubHabits); } [Fact] public void MapSuggestion_InvalidWeekdayNames_DroppedKeepingValidOnes() { - var dto = new Dto(null, "Day", 1, new[] { "Monday", "Notaday", "monday" }, null); + var dto = new Dto(null, "Day", 1, WeekdayNamesWithInvalids, null); var result = AiHabitSuggestionService.MapSuggestion(dto); - result.Value.Days.Should().BeEquivalentTo(new[] { DayOfWeek.Monday }); + result.Value.Days.Should().BeEquivalentTo(MondayOnly); } [Fact] @@ -219,7 +234,7 @@ public void BuildPrompt_DescribesFlexibleChecklistAndTimeFields() [Fact] public void MapSuggestion_Flexible_KeepsTargetForcesQuantityOneAndStripsDays() { - var dto = new Dto(null, "Week", 9, new[] { "Monday" }, null, IsFlexible: true, FlexibleTarget: 4); + var dto = new Dto(null, "Week", 9, MondayName, null, IsFlexible: true, FlexibleTarget: 4); var result = AiHabitSuggestionService.MapSuggestion(dto); @@ -289,11 +304,11 @@ public void MapSuggestion_DueTime_Invalid_Dropped() [Fact] public void MapSuggestion_ChecklistItems_MappedWhenNoSubHabits() { - var dto = new Dto(null, null, null, null, null, ChecklistItems: new[] { "Cheese", "Bread", "Eggs" }); + var dto = new Dto(null, null, null, null, null, ChecklistItems: CheeseBreadEggsChecklist); var result = AiHabitSuggestionService.MapSuggestion(dto); - result.Value.ChecklistItems.Should().BeEquivalentTo(new[] { "Cheese", "Bread", "Eggs" }); + result.Value.ChecklistItems.Should().BeEquivalentTo(CheeseBreadEggsChecklist); result.Value.SubHabits.Should().BeEmpty(); } @@ -315,17 +330,17 @@ public void MapSuggestion_DropsBlankAndOverLongChecklistItems() var result = AiHabitSuggestionService.MapSuggestion(dto); - result.Value.ChecklistItems.Should().BeEquivalentTo(new[] { "Eggs" }); + result.Value.ChecklistItems.Should().BeEquivalentTo(EggsChecklist); } [Fact] public void MapSuggestion_SubHabitsAndChecklist_AreMutuallyExclusive_SubHabitsWin() { - var dto = new Dto(null, null, null, null, new[] { "Brush teeth" }, ChecklistItems: new[] { "Cheese" }); + var dto = new Dto(null, null, null, null, BrushTeethSubHabits, ChecklistItems: CheeseChecklist); var result = AiHabitSuggestionService.MapSuggestion(dto); - result.Value.SubHabits.Should().BeEquivalentTo(new[] { "Brush teeth" }); + result.Value.SubHabits.Should().BeEquivalentTo(BrushTeethSubHabits); result.Value.ChecklistItems.Should().BeEmpty(); } } diff --git a/tests/Orbit.Infrastructure.Tests/Services/AiTagSuggestionServiceTests.cs b/tests/Orbit.Infrastructure.Tests/Services/AiTagSuggestionServiceTests.cs index 378786a0..a1959c24 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/AiTagSuggestionServiceTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/AiTagSuggestionServiceTests.cs @@ -5,13 +5,16 @@ namespace Orbit.Infrastructure.Tests.Services; public class AiTagSuggestionServiceTests { + private static readonly string[] HealthFitnessTags = new[] { "Health", "Fitness" }; + private static readonly string[] LearningTags = new[] { "Learning" }; + [Fact] public void BuildPrompt_IncludesTitleDescriptionAndExistingTags() { var prompt = AiTagSuggestionService.BuildPrompt( "Morning run", "Jog around the park", - new[] { "Health", "Fitness" }, + HealthFitnessTags, "en"); prompt.Should().Contain("Morning run"); @@ -33,7 +36,7 @@ public void BuildPrompt_NoExistingTags_RendersPlaceholder() [Fact] public void BuildPrompt_NullDescription_RendersPlaceholder() { - var prompt = AiTagSuggestionService.BuildPrompt("Read a book", null, new[] { "Learning" }, "en"); + var prompt = AiTagSuggestionService.BuildPrompt("Read a book", null, LearningTags, "en"); prompt.Should().Contain("(no description)"); } diff --git a/tests/Orbit.Infrastructure.Tests/Services/GoogleCalendarEventFetcherTests.cs b/tests/Orbit.Infrastructure.Tests/Services/GoogleCalendarEventFetcherTests.cs index 045eb1d8..7f41188e 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/GoogleCalendarEventFetcherTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/GoogleCalendarEventFetcherTests.cs @@ -16,6 +16,10 @@ public class GoogleCalendarEventFetcherTests private const string Token = "access-token"; + private static readonly string[] SharedCalendarSelection = new[] { "shared" }; + private static readonly string[] ChosenCalendarSelection = new[] { "chosen" }; + private static readonly string[] ExpectedCalendarIds = new[] { "a", "b" }; + public GoogleCalendarEventFetcherTests() { _fetcher = new GoogleCalendarEventFetcher(_api, _logger); @@ -115,7 +119,7 @@ public async Task FetchAsync_ExplicitSelection_FetchesOnlyChosenCalendars() Calendar("shared", "reader")); StubEvents("shared", TimedEvent("s1", "Shared event")); - var result = await _fetcher.FetchAsync(Token, new[] { "shared" }, null, CancellationToken.None); + var result = await _fetcher.FetchAsync(Token, SharedCalendarSelection, null, CancellationToken.None); result.Should().ContainSingle(); result[0].CalendarId.Should().Be("shared"); @@ -128,7 +132,7 @@ public async Task FetchAsync_ExplicitSelection_StillSkipsDeletedAndHidden() { StubCalendars(Calendar("chosen", "owner", deleted: true)); - var result = await _fetcher.FetchAsync(Token, new[] { "chosen" }, null, CancellationToken.None); + var result = await _fetcher.FetchAsync(Token, ChosenCalendarSelection, null, CancellationToken.None); result.Should().BeEmpty(); await _api.DidNotReceive().ListEventsAsync(Token, "chosen", Arg.Any(), Arg.Any()); @@ -147,7 +151,7 @@ public async Task FetchAsync_RecurringMasterDedup_IsPerCalendar() result.Should().HaveCount(2); result.Should().OnlyContain(i => i.Id == "master"); - result.Select(i => i.CalendarId).Should().BeEquivalentTo(new[] { "a", "b" }); + result.Select(i => i.CalendarId).Should().BeEquivalentTo(ExpectedCalendarIds); } [Fact] diff --git a/tests/Orbit.Infrastructure.Tests/Services/ReminderSchedulerServiceTests.cs b/tests/Orbit.Infrastructure.Tests/Services/ReminderSchedulerServiceTests.cs index 41df3c59..91a0414e 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/ReminderSchedulerServiceTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/ReminderSchedulerServiceTests.cs @@ -18,6 +18,7 @@ namespace Orbit.Infrastructure.Tests.Services; public class ReminderSchedulerServiceTests { private static readonly DateOnly UtcToday = DateOnly.FromDateTime(DateTime.UtcNow); + private static readonly int[] ReminderTimes = new[] { 0 }; private static string FormatReminderText(int minutesBefore, string lang) { var method = typeof(ReminderSchedulerService) @@ -428,7 +429,7 @@ public async Task CheckAndSendReminders_RelativeReminderDueButUnsent_FiresAndRec ReminderEnabled: true, DueDate: UtcToday, DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; dbContext.Users.Add(user); dbContext.Habits.Add(habit); @@ -457,7 +458,7 @@ public async Task CheckAndSendReminders_RelativeReminderAlreadySent_DoesNotDoubl ReminderEnabled: true, DueDate: UtcToday, DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; dbContext.Users.Add(user); dbContext.Habits.Add(habit); @@ -491,7 +492,7 @@ public async Task CheckAndSendReminders_RelativeReminderAlreadySentOnUserLocalDa ReminderEnabled: true, DueDate: UtcToday.AddDays(-1), DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; dbContext.Users.Add(user); dbContext.Habits.Add(habit); @@ -525,7 +526,7 @@ public async Task CheckAndSendReminders_RelativeReminderHabitLoggedOnUserLocalDa ReminderEnabled: true, DueDate: UtcToday.AddDays(-1), DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; habit.Log(userToday, advanceDueDate: false); dbContext.Users.Add(user); @@ -552,7 +553,7 @@ public async Task CheckAndSendReminders_RelativeReminderSentYesterday_DoesNotBlo ReminderEnabled: true, DueDate: UtcToday.AddDays(-1), DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; dbContext.Users.Add(user); dbContext.Habits.Add(habit); @@ -625,7 +626,7 @@ public async Task ExecuteAsync_HostedLifecycle_RunsOneReminderPassThenStopsGrace var habit = Habit.Create(new HabitCreateParams( user.Id, "Workout", FrequencyUnit.Day, 1, ReminderEnabled: true, DueDate: UtcToday, DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; dbContext.Users.Add(user); dbContext.Habits.Add(habit); await dbContext.SaveChangesAsync(); @@ -656,12 +657,12 @@ public async Task CheckAndSendReminders_OneHabitRecordFails_StillDeliversRemaini var throwingHabit = Habit.Create(new HabitCreateParams( throwingUser.Id, "Alice workout", FrequencyUnit.Day, 1, ReminderEnabled: true, DueDate: UtcToday, DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; var healthyUser = User.Create("Bob", "bob@test.com").Value; var healthyHabit = Habit.Create(new HabitCreateParams( healthyUser.Id, "Bob workout", FrequencyUnit.Day, 1, ReminderEnabled: true, DueDate: UtcToday, DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; await using var dbContext = CreateInterceptingDbContext( new ThrowForHabitReminderInterceptor(throwingHabit.Id)); @@ -695,7 +696,7 @@ private static List SeedDueRelativeReminders(OrbitDbContext dbContext, int ReminderEnabled: true, DueDate: UtcToday, DueTime: new TimeOnly(0, 0), - ReminderTimes: new[] { 0 })).Value; + ReminderTimes: ReminderTimes)).Value; dbContext.Users.Add(user); dbContext.Habits.Add(habit); users.Add(user);