From a88e8d89a672815a62c13f03c2ac3822e5f16ccc Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Sun, 12 Jul 2026 21:56:16 -0300 Subject: [PATCH] chore(app): add missing FluentValidation query validators (#243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds validators for 31 read-model queries flagged by the prod-readiness audit as lacking a validator. Each guards UserId (non-empty Guid) and, where present, the entity id, date range (from<=to, <=366 days), Period, Language, and public-profile Slug — mirroring the existing query-validator pattern (GetHabitSchedule/GetAllHabitLogs/GetDailySummary). Purely additive and behavior-preserving: valid requests are unchanged; malformed input is now rejected at the trust boundary via the existing ValidationBehavior pipeline instead of failing deeper in the handler. Representative validator tests cover every added file (UserId/entity-id/ slug guards) plus the range/period/language logic on GetRetrospective and GetGoalReview. Refs thomasluizon/orbit-ui-mobile#243 Co-Authored-By: Claude Opus 4.8 --- .../Validators/GetApiKeysQueryValidator.cs | 12 + .../GetCalendarAutoSyncStateQueryValidator.cs | 12 + .../GetCalendarEventsQueryValidator.cs | 12 + ...etCalendarSyncSuggestionsQueryValidator.cs | 12 + .../GetUserCalendarsQueryValidator.cs | 12 + .../GetChecklistTemplatesQueryValidator.cs | 12 + .../GetAchievementsQueryValidator.cs | 12 + .../GetGamificationProfileQueryValidator.cs | 12 + .../Validators/GetStreakInfoQueryValidator.cs | 12 + .../Validators/GetGoalByIdQueryValidator.cs | 13 + .../Validators/GetGoalDetailQueryValidator.cs | 13 + .../GetGoalMetricsQueryValidator.cs | 13 + .../Validators/GetGoalReviewQueryValidator.cs | 18 ++ .../Validators/GetHabitByIdQueryValidator.cs | 13 + .../Validators/GetHabitCountQueryValidator.cs | 12 + .../GetHabitFullDetailQueryValidator.cs | 13 + .../Validators/GetHabitLogsQueryValidator.cs | 13 + .../GetHabitMetricsQueryValidator.cs | 13 + .../GetHabitWidgetQueryValidator.cs | 12 + .../GetRetrospectiveQueryValidator.cs | 32 ++ .../GetNotificationsQueryValidator.cs | 12 + .../ExportUserDataQueryValidator.cs | 12 + .../Validators/GetProfileQueryValidator.cs | 12 + .../GetPublicProfileQueryValidator.cs | 12 + .../GetReferralDashboardQueryValidator.cs | 12 + .../GetReferralStatsQueryValidator.cs | 12 + .../GetBillingDetailsQueryValidator.cs | 12 + .../Validators/GetPlansQueryValidator.cs | 12 + .../GetSubscriptionStatusQueryValidator.cs | 12 + .../Tags/Validators/GetTagsQueryValidator.cs | 12 + .../Validators/GetUserFactsQueryValidator.cs | 12 + .../GetGoalReviewQueryValidatorTests.cs | 52 ++++ .../GetRetrospectiveQueryValidatorTests.cs | 94 ++++++ .../Validators/QueryValidatorGuardTests.cs | 276 ++++++++++++++++++ 34 files changed, 827 insertions(+) create mode 100644 src/Orbit.Application/ApiKeys/Validators/GetApiKeysQueryValidator.cs create mode 100644 src/Orbit.Application/Calendar/Validators/GetCalendarAutoSyncStateQueryValidator.cs create mode 100644 src/Orbit.Application/Calendar/Validators/GetCalendarEventsQueryValidator.cs create mode 100644 src/Orbit.Application/Calendar/Validators/GetCalendarSyncSuggestionsQueryValidator.cs create mode 100644 src/Orbit.Application/Calendar/Validators/GetUserCalendarsQueryValidator.cs create mode 100644 src/Orbit.Application/ChecklistTemplates/Validators/GetChecklistTemplatesQueryValidator.cs create mode 100644 src/Orbit.Application/Gamification/Validators/GetAchievementsQueryValidator.cs create mode 100644 src/Orbit.Application/Gamification/Validators/GetGamificationProfileQueryValidator.cs create mode 100644 src/Orbit.Application/Gamification/Validators/GetStreakInfoQueryValidator.cs create mode 100644 src/Orbit.Application/Goals/Validators/GetGoalByIdQueryValidator.cs create mode 100644 src/Orbit.Application/Goals/Validators/GetGoalDetailQueryValidator.cs create mode 100644 src/Orbit.Application/Goals/Validators/GetGoalMetricsQueryValidator.cs create mode 100644 src/Orbit.Application/Goals/Validators/GetGoalReviewQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetHabitByIdQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetHabitCountQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetHabitFullDetailQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetHabitLogsQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetHabitMetricsQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetHabitWidgetQueryValidator.cs create mode 100644 src/Orbit.Application/Habits/Validators/GetRetrospectiveQueryValidator.cs create mode 100644 src/Orbit.Application/Notifications/Validators/GetNotificationsQueryValidator.cs create mode 100644 src/Orbit.Application/Profile/Validators/ExportUserDataQueryValidator.cs create mode 100644 src/Orbit.Application/Profile/Validators/GetProfileQueryValidator.cs create mode 100644 src/Orbit.Application/Profile/Validators/GetPublicProfileQueryValidator.cs create mode 100644 src/Orbit.Application/Referrals/Validators/GetReferralDashboardQueryValidator.cs create mode 100644 src/Orbit.Application/Referrals/Validators/GetReferralStatsQueryValidator.cs create mode 100644 src/Orbit.Application/Subscriptions/Validators/GetBillingDetailsQueryValidator.cs create mode 100644 src/Orbit.Application/Subscriptions/Validators/GetPlansQueryValidator.cs create mode 100644 src/Orbit.Application/Subscriptions/Validators/GetSubscriptionStatusQueryValidator.cs create mode 100644 src/Orbit.Application/Tags/Validators/GetTagsQueryValidator.cs create mode 100644 src/Orbit.Application/UserFacts/Validators/GetUserFactsQueryValidator.cs create mode 100644 tests/Orbit.Application.Tests/Validators/GetGoalReviewQueryValidatorTests.cs create mode 100644 tests/Orbit.Application.Tests/Validators/GetRetrospectiveQueryValidatorTests.cs create mode 100644 tests/Orbit.Application.Tests/Validators/QueryValidatorGuardTests.cs diff --git a/src/Orbit.Application/ApiKeys/Validators/GetApiKeysQueryValidator.cs b/src/Orbit.Application/ApiKeys/Validators/GetApiKeysQueryValidator.cs new file mode 100644 index 00000000..97de2583 --- /dev/null +++ b/src/Orbit.Application/ApiKeys/Validators/GetApiKeysQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.ApiKeys.Queries; + +namespace Orbit.Application.ApiKeys.Validators; + +public class GetApiKeysQueryValidator : AbstractValidator +{ + public GetApiKeysQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Calendar/Validators/GetCalendarAutoSyncStateQueryValidator.cs b/src/Orbit.Application/Calendar/Validators/GetCalendarAutoSyncStateQueryValidator.cs new file mode 100644 index 00000000..9c07a43b --- /dev/null +++ b/src/Orbit.Application/Calendar/Validators/GetCalendarAutoSyncStateQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Calendar.Queries; + +namespace Orbit.Application.Calendar.Validators; + +public class GetCalendarAutoSyncStateQueryValidator : AbstractValidator +{ + public GetCalendarAutoSyncStateQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Calendar/Validators/GetCalendarEventsQueryValidator.cs b/src/Orbit.Application/Calendar/Validators/GetCalendarEventsQueryValidator.cs new file mode 100644 index 00000000..0beefb48 --- /dev/null +++ b/src/Orbit.Application/Calendar/Validators/GetCalendarEventsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Calendar.Queries; + +namespace Orbit.Application.Calendar.Validators; + +public class GetCalendarEventsQueryValidator : AbstractValidator +{ + public GetCalendarEventsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Calendar/Validators/GetCalendarSyncSuggestionsQueryValidator.cs b/src/Orbit.Application/Calendar/Validators/GetCalendarSyncSuggestionsQueryValidator.cs new file mode 100644 index 00000000..7de9321c --- /dev/null +++ b/src/Orbit.Application/Calendar/Validators/GetCalendarSyncSuggestionsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Calendar.Queries; + +namespace Orbit.Application.Calendar.Validators; + +public class GetCalendarSyncSuggestionsQueryValidator : AbstractValidator +{ + public GetCalendarSyncSuggestionsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Calendar/Validators/GetUserCalendarsQueryValidator.cs b/src/Orbit.Application/Calendar/Validators/GetUserCalendarsQueryValidator.cs new file mode 100644 index 00000000..a8199822 --- /dev/null +++ b/src/Orbit.Application/Calendar/Validators/GetUserCalendarsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Calendar.Queries; + +namespace Orbit.Application.Calendar.Validators; + +public class GetUserCalendarsQueryValidator : AbstractValidator +{ + public GetUserCalendarsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/ChecklistTemplates/Validators/GetChecklistTemplatesQueryValidator.cs b/src/Orbit.Application/ChecklistTemplates/Validators/GetChecklistTemplatesQueryValidator.cs new file mode 100644 index 00000000..f82c8df8 --- /dev/null +++ b/src/Orbit.Application/ChecklistTemplates/Validators/GetChecklistTemplatesQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.ChecklistTemplates.Queries; + +namespace Orbit.Application.ChecklistTemplates.Validators; + +public class GetChecklistTemplatesQueryValidator : AbstractValidator +{ + public GetChecklistTemplatesQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Gamification/Validators/GetAchievementsQueryValidator.cs b/src/Orbit.Application/Gamification/Validators/GetAchievementsQueryValidator.cs new file mode 100644 index 00000000..716a8a65 --- /dev/null +++ b/src/Orbit.Application/Gamification/Validators/GetAchievementsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Gamification.Queries; + +namespace Orbit.Application.Gamification.Validators; + +public class GetAchievementsQueryValidator : AbstractValidator +{ + public GetAchievementsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Gamification/Validators/GetGamificationProfileQueryValidator.cs b/src/Orbit.Application/Gamification/Validators/GetGamificationProfileQueryValidator.cs new file mode 100644 index 00000000..5d103265 --- /dev/null +++ b/src/Orbit.Application/Gamification/Validators/GetGamificationProfileQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Gamification.Queries; + +namespace Orbit.Application.Gamification.Validators; + +public class GetGamificationProfileQueryValidator : AbstractValidator +{ + public GetGamificationProfileQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Gamification/Validators/GetStreakInfoQueryValidator.cs b/src/Orbit.Application/Gamification/Validators/GetStreakInfoQueryValidator.cs new file mode 100644 index 00000000..12cd9465 --- /dev/null +++ b/src/Orbit.Application/Gamification/Validators/GetStreakInfoQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Gamification.Queries; + +namespace Orbit.Application.Gamification.Validators; + +public class GetStreakInfoQueryValidator : AbstractValidator +{ + public GetStreakInfoQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Goals/Validators/GetGoalByIdQueryValidator.cs b/src/Orbit.Application/Goals/Validators/GetGoalByIdQueryValidator.cs new file mode 100644 index 00000000..bba0bea0 --- /dev/null +++ b/src/Orbit.Application/Goals/Validators/GetGoalByIdQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Goals.Queries; + +namespace Orbit.Application.Goals.Validators; + +public class GetGoalByIdQueryValidator : AbstractValidator +{ + public GetGoalByIdQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.GoalId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Goals/Validators/GetGoalDetailQueryValidator.cs b/src/Orbit.Application/Goals/Validators/GetGoalDetailQueryValidator.cs new file mode 100644 index 00000000..afde89a4 --- /dev/null +++ b/src/Orbit.Application/Goals/Validators/GetGoalDetailQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Goals.Queries; + +namespace Orbit.Application.Goals.Validators; + +public class GetGoalDetailQueryValidator : AbstractValidator +{ + public GetGoalDetailQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.GoalId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Goals/Validators/GetGoalMetricsQueryValidator.cs b/src/Orbit.Application/Goals/Validators/GetGoalMetricsQueryValidator.cs new file mode 100644 index 00000000..2519a345 --- /dev/null +++ b/src/Orbit.Application/Goals/Validators/GetGoalMetricsQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Goals.Queries; + +namespace Orbit.Application.Goals.Validators; + +public class GetGoalMetricsQueryValidator : AbstractValidator +{ + public GetGoalMetricsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.GoalId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Goals/Validators/GetGoalReviewQueryValidator.cs b/src/Orbit.Application/Goals/Validators/GetGoalReviewQueryValidator.cs new file mode 100644 index 00000000..8fb60751 --- /dev/null +++ b/src/Orbit.Application/Goals/Validators/GetGoalReviewQueryValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using Orbit.Application.Common; +using Orbit.Application.Goals.Queries; + +namespace Orbit.Application.Goals.Validators; + +public class GetGoalReviewQueryValidator : AbstractValidator +{ + public GetGoalReviewQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + + RuleFor(x => x.Language) + .MaximumLength(AppConstants.MaxLanguageLength) + .Must(lang => string.IsNullOrEmpty(lang) || AppConstants.SupportedLanguages.Contains(lang)) + .WithMessage($"Language must be one of: {string.Join(", ", AppConstants.SupportedLanguages)}"); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetHabitByIdQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetHabitByIdQueryValidator.cs new file mode 100644 index 00000000..2d98b18a --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetHabitByIdQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetHabitByIdQueryValidator : AbstractValidator +{ + public GetHabitByIdQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.HabitId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetHabitCountQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetHabitCountQueryValidator.cs new file mode 100644 index 00000000..4c0ab6bf --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetHabitCountQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetHabitCountQueryValidator : AbstractValidator +{ + public GetHabitCountQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetHabitFullDetailQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetHabitFullDetailQueryValidator.cs new file mode 100644 index 00000000..98d53223 --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetHabitFullDetailQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetHabitFullDetailQueryValidator : AbstractValidator +{ + public GetHabitFullDetailQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.HabitId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetHabitLogsQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetHabitLogsQueryValidator.cs new file mode 100644 index 00000000..9fc51598 --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetHabitLogsQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetHabitLogsQueryValidator : AbstractValidator +{ + public GetHabitLogsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.HabitId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetHabitMetricsQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetHabitMetricsQueryValidator.cs new file mode 100644 index 00000000..2d211c8f --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetHabitMetricsQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetHabitMetricsQueryValidator : AbstractValidator +{ + public GetHabitMetricsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.HabitId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetHabitWidgetQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetHabitWidgetQueryValidator.cs new file mode 100644 index 00000000..cbca15ba --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetHabitWidgetQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetHabitWidgetQueryValidator : AbstractValidator +{ + public GetHabitWidgetQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Habits/Validators/GetRetrospectiveQueryValidator.cs b/src/Orbit.Application/Habits/Validators/GetRetrospectiveQueryValidator.cs new file mode 100644 index 00000000..1f80fb3e --- /dev/null +++ b/src/Orbit.Application/Habits/Validators/GetRetrospectiveQueryValidator.cs @@ -0,0 +1,32 @@ +using FluentValidation; +using Orbit.Application.Common; +using Orbit.Application.Habits.Queries; + +namespace Orbit.Application.Habits.Validators; + +public class GetRetrospectiveQueryValidator : AbstractValidator +{ + public GetRetrospectiveQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + + RuleFor(x => x.Period) + .NotEmpty() + .Must(period => RetrospectivePeriodRange.IsKnownPeriod(period)) + .WithMessage("Period must be one of: week, month, quarter, semester, year."); + + RuleFor(x => x.DateFrom) + .LessThanOrEqualTo(x => x.DateTo) + .WithMessage("DateFrom must be on or before DateTo."); + + RuleFor(x => x) + .Must(x => x.DateTo.DayNumber - x.DateFrom.DayNumber <= AppConstants.MaxRangeDays) + .WithMessage($"Date range must not exceed {AppConstants.MaxRangeDays} days.") + .When(x => x.DateFrom <= x.DateTo); + + RuleFor(x => x.Language) + .MaximumLength(AppConstants.MaxLanguageLength) + .Must(lang => string.IsNullOrEmpty(lang) || AppConstants.SupportedLanguages.Contains(lang)) + .WithMessage($"Language must be one of: {string.Join(", ", AppConstants.SupportedLanguages)}"); + } +} diff --git a/src/Orbit.Application/Notifications/Validators/GetNotificationsQueryValidator.cs b/src/Orbit.Application/Notifications/Validators/GetNotificationsQueryValidator.cs new file mode 100644 index 00000000..c0db55e5 --- /dev/null +++ b/src/Orbit.Application/Notifications/Validators/GetNotificationsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Notifications.Queries; + +namespace Orbit.Application.Notifications.Validators; + +public class GetNotificationsQueryValidator : AbstractValidator +{ + public GetNotificationsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Profile/Validators/ExportUserDataQueryValidator.cs b/src/Orbit.Application/Profile/Validators/ExportUserDataQueryValidator.cs new file mode 100644 index 00000000..20c0e930 --- /dev/null +++ b/src/Orbit.Application/Profile/Validators/ExportUserDataQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Profile.Queries; + +namespace Orbit.Application.Profile.Validators; + +public class ExportUserDataQueryValidator : AbstractValidator +{ + public ExportUserDataQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Profile/Validators/GetProfileQueryValidator.cs b/src/Orbit.Application/Profile/Validators/GetProfileQueryValidator.cs new file mode 100644 index 00000000..43ed96fb --- /dev/null +++ b/src/Orbit.Application/Profile/Validators/GetProfileQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Profile.Queries; + +namespace Orbit.Application.Profile.Validators; + +public class GetProfileQueryValidator : AbstractValidator +{ + public GetProfileQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Profile/Validators/GetPublicProfileQueryValidator.cs b/src/Orbit.Application/Profile/Validators/GetPublicProfileQueryValidator.cs new file mode 100644 index 00000000..cbdf6ddc --- /dev/null +++ b/src/Orbit.Application/Profile/Validators/GetPublicProfileQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Profile.Queries; + +namespace Orbit.Application.Profile.Validators; + +public class GetPublicProfileQueryValidator : AbstractValidator +{ + public GetPublicProfileQueryValidator() + { + RuleFor(x => x.Slug).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Referrals/Validators/GetReferralDashboardQueryValidator.cs b/src/Orbit.Application/Referrals/Validators/GetReferralDashboardQueryValidator.cs new file mode 100644 index 00000000..95cb3885 --- /dev/null +++ b/src/Orbit.Application/Referrals/Validators/GetReferralDashboardQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Referrals.Queries; + +namespace Orbit.Application.Referrals.Validators; + +public class GetReferralDashboardQueryValidator : AbstractValidator +{ + public GetReferralDashboardQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Referrals/Validators/GetReferralStatsQueryValidator.cs b/src/Orbit.Application/Referrals/Validators/GetReferralStatsQueryValidator.cs new file mode 100644 index 00000000..193da284 --- /dev/null +++ b/src/Orbit.Application/Referrals/Validators/GetReferralStatsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Referrals.Queries; + +namespace Orbit.Application.Referrals.Validators; + +public class GetReferralStatsQueryValidator : AbstractValidator +{ + public GetReferralStatsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Subscriptions/Validators/GetBillingDetailsQueryValidator.cs b/src/Orbit.Application/Subscriptions/Validators/GetBillingDetailsQueryValidator.cs new file mode 100644 index 00000000..677613e8 --- /dev/null +++ b/src/Orbit.Application/Subscriptions/Validators/GetBillingDetailsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Subscriptions.Queries; + +namespace Orbit.Application.Subscriptions.Validators; + +public class GetBillingDetailsQueryValidator : AbstractValidator +{ + public GetBillingDetailsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Subscriptions/Validators/GetPlansQueryValidator.cs b/src/Orbit.Application/Subscriptions/Validators/GetPlansQueryValidator.cs new file mode 100644 index 00000000..2e772d4f --- /dev/null +++ b/src/Orbit.Application/Subscriptions/Validators/GetPlansQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Subscriptions.Queries; + +namespace Orbit.Application.Subscriptions.Validators; + +public class GetPlansQueryValidator : AbstractValidator +{ + public GetPlansQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Subscriptions/Validators/GetSubscriptionStatusQueryValidator.cs b/src/Orbit.Application/Subscriptions/Validators/GetSubscriptionStatusQueryValidator.cs new file mode 100644 index 00000000..f4663caf --- /dev/null +++ b/src/Orbit.Application/Subscriptions/Validators/GetSubscriptionStatusQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Subscriptions.Queries; + +namespace Orbit.Application.Subscriptions.Validators; + +public class GetSubscriptionStatusQueryValidator : AbstractValidator +{ + public GetSubscriptionStatusQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/Tags/Validators/GetTagsQueryValidator.cs b/src/Orbit.Application/Tags/Validators/GetTagsQueryValidator.cs new file mode 100644 index 00000000..c0970eee --- /dev/null +++ b/src/Orbit.Application/Tags/Validators/GetTagsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.Tags.Queries; + +namespace Orbit.Application.Tags.Validators; + +public class GetTagsQueryValidator : AbstractValidator +{ + public GetTagsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/src/Orbit.Application/UserFacts/Validators/GetUserFactsQueryValidator.cs b/src/Orbit.Application/UserFacts/Validators/GetUserFactsQueryValidator.cs new file mode 100644 index 00000000..f7b6962b --- /dev/null +++ b/src/Orbit.Application/UserFacts/Validators/GetUserFactsQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Orbit.Application.UserFacts.Queries; + +namespace Orbit.Application.UserFacts.Validators; + +public class GetUserFactsQueryValidator : AbstractValidator +{ + public GetUserFactsQueryValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + } +} diff --git a/tests/Orbit.Application.Tests/Validators/GetGoalReviewQueryValidatorTests.cs b/tests/Orbit.Application.Tests/Validators/GetGoalReviewQueryValidatorTests.cs new file mode 100644 index 00000000..656422ab --- /dev/null +++ b/tests/Orbit.Application.Tests/Validators/GetGoalReviewQueryValidatorTests.cs @@ -0,0 +1,52 @@ +using FluentValidation.TestHelper; +using Orbit.Application.Goals.Queries; +using Orbit.Application.Goals.Validators; + +namespace Orbit.Application.Tests.Validators; + +public class GetGoalReviewQueryValidatorTests +{ + private readonly GetGoalReviewQueryValidator _validator = new(); + + private static GetGoalReviewQuery ValidQuery() => new( + UserId: Guid.NewGuid(), + Language: "pt-BR"); + + [Fact] + public void Validate_ValidQuery_NoErrors() + { + var result = _validator.TestValidate(ValidQuery()); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Validate_EmptyUserId_HasError() + { + var query = ValidQuery() with { UserId = Guid.Empty }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(x => x.UserId); + } + + [Fact] + public void Validate_UnsupportedLanguage_HasError() + { + var query = ValidQuery() with { Language = "xx" }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(x => x.Language); + } + + [Fact] + public void Validate_EmptyLanguage_NoLanguageError() + { + var query = ValidQuery() with { Language = string.Empty }; + + var result = _validator.TestValidate(query); + + result.ShouldNotHaveValidationErrorFor(x => x.Language); + } +} diff --git a/tests/Orbit.Application.Tests/Validators/GetRetrospectiveQueryValidatorTests.cs b/tests/Orbit.Application.Tests/Validators/GetRetrospectiveQueryValidatorTests.cs new file mode 100644 index 00000000..7ba358a0 --- /dev/null +++ b/tests/Orbit.Application.Tests/Validators/GetRetrospectiveQueryValidatorTests.cs @@ -0,0 +1,94 @@ +using FluentAssertions; +using FluentValidation.TestHelper; +using Orbit.Application.Habits.Queries; +using Orbit.Application.Habits.Validators; + +namespace Orbit.Application.Tests.Validators; + +public class GetRetrospectiveQueryValidatorTests +{ + private readonly GetRetrospectiveQueryValidator _validator = new(); + + private static GetRetrospectiveQuery ValidQuery() => new( + UserId: Guid.NewGuid(), + DateFrom: new DateOnly(2026, 4, 1), + DateTo: new DateOnly(2026, 4, 30), + Period: "month", + Language: "en"); + + [Fact] + public void Validate_ValidQuery_NoErrors() + { + var result = _validator.TestValidate(ValidQuery()); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Validate_EmptyUserId_HasError() + { + var query = ValidQuery() with { UserId = Guid.Empty }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(x => x.UserId); + } + + [Fact] + public void Validate_UnknownPeriod_HasError() + { + var query = ValidQuery() with { Period = "decade" }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(x => x.Period); + } + + [Fact] + public void Validate_DateFromAfterDateTo_HasError() + { + var query = ValidQuery() with + { + DateFrom = new DateOnly(2026, 5, 1), + DateTo = new DateOnly(2026, 4, 1) + }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(x => x.DateFrom); + } + + [Fact] + public void Validate_RangeOver366Days_HasError() + { + var query = ValidQuery() with + { + DateFrom = new DateOnly(2026, 1, 1), + DateTo = new DateOnly(2027, 1, 3) + }; + + var result = _validator.TestValidate(query); + + result.Errors.Should().Contain(e => e.ErrorMessage.Contains("366")); + } + + [Fact] + public void Validate_UnsupportedLanguage_HasError() + { + var query = ValidQuery() with { Language = "fr" }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(x => x.Language); + } + + [Fact] + public void Validate_EmptyLanguage_NoLanguageError() + { + var query = ValidQuery() with { Language = string.Empty }; + + var result = _validator.TestValidate(query); + + result.ShouldNotHaveValidationErrorFor(x => x.Language); + } +} diff --git a/tests/Orbit.Application.Tests/Validators/QueryValidatorGuardTests.cs b/tests/Orbit.Application.Tests/Validators/QueryValidatorGuardTests.cs new file mode 100644 index 00000000..692c1131 --- /dev/null +++ b/tests/Orbit.Application.Tests/Validators/QueryValidatorGuardTests.cs @@ -0,0 +1,276 @@ +using FluentValidation; +using FluentValidation.TestHelper; +using Orbit.Application.ApiKeys.Queries; +using Orbit.Application.ApiKeys.Validators; +using Orbit.Application.Calendar.Queries; +using Orbit.Application.Calendar.Validators; +using Orbit.Application.ChecklistTemplates.Queries; +using Orbit.Application.ChecklistTemplates.Validators; +using Orbit.Application.Gamification.Queries; +using Orbit.Application.Gamification.Validators; +using Orbit.Application.Goals.Queries; +using Orbit.Application.Goals.Validators; +using Orbit.Application.Habits.Queries; +using Orbit.Application.Habits.Validators; +using Orbit.Application.Notifications.Queries; +using Orbit.Application.Notifications.Validators; +using Orbit.Application.Profile.Queries; +using Orbit.Application.Profile.Validators; +using Orbit.Application.Referrals.Queries; +using Orbit.Application.Referrals.Validators; +using Orbit.Application.Subscriptions.Queries; +using Orbit.Application.Subscriptions.Validators; +using Orbit.Application.Tags.Queries; +using Orbit.Application.Tags.Validators; +using Orbit.Application.UserFacts.Queries; +using Orbit.Application.UserFacts.Validators; + +namespace Orbit.Application.Tests.Validators; + +public class QueryValidatorGuardTests +{ + private static void RejectsProperty(AbstractValidator validator, T invalid, string property) => + validator.TestValidate(invalid).ShouldHaveValidationErrorFor(property); + + private static void Accepts(AbstractValidator validator, T valid) => + validator.TestValidate(valid).ShouldNotHaveAnyValidationErrors(); + + [Fact] + public void GetApiKeys_GuardsUserId() + { + var validator = new GetApiKeysQueryValidator(); + RejectsProperty(validator, new GetApiKeysQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetApiKeysQuery(Guid.NewGuid())); + } + + [Fact] + public void GetCalendarAutoSyncState_GuardsUserId() + { + var validator = new GetCalendarAutoSyncStateQueryValidator(); + RejectsProperty(validator, new GetCalendarAutoSyncStateQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetCalendarAutoSyncStateQuery(Guid.NewGuid())); + } + + [Fact] + public void GetCalendarEvents_GuardsUserId() + { + var validator = new GetCalendarEventsQueryValidator(); + RejectsProperty(validator, new GetCalendarEventsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetCalendarEventsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetCalendarSyncSuggestions_GuardsUserId() + { + var validator = new GetCalendarSyncSuggestionsQueryValidator(); + RejectsProperty(validator, new GetCalendarSyncSuggestionsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetCalendarSyncSuggestionsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetUserCalendars_GuardsUserId() + { + var validator = new GetUserCalendarsQueryValidator(); + RejectsProperty(validator, new GetUserCalendarsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetUserCalendarsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetChecklistTemplates_GuardsUserId() + { + var validator = new GetChecklistTemplatesQueryValidator(); + RejectsProperty(validator, new GetChecklistTemplatesQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetChecklistTemplatesQuery(Guid.NewGuid())); + } + + [Fact] + public void GetAchievements_GuardsUserId() + { + var validator = new GetAchievementsQueryValidator(); + RejectsProperty(validator, new GetAchievementsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetAchievementsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetGamificationProfile_GuardsUserId() + { + var validator = new GetGamificationProfileQueryValidator(); + RejectsProperty(validator, new GetGamificationProfileQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetGamificationProfileQuery(Guid.NewGuid())); + } + + [Fact] + public void GetStreakInfo_GuardsUserId() + { + var validator = new GetStreakInfoQueryValidator(); + RejectsProperty(validator, new GetStreakInfoQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetStreakInfoQuery(Guid.NewGuid())); + } + + [Fact] + public void GetHabitCount_GuardsUserId() + { + var validator = new GetHabitCountQueryValidator(); + RejectsProperty(validator, new GetHabitCountQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetHabitCountQuery(Guid.NewGuid())); + } + + [Fact] + public void GetHabitWidget_GuardsUserId() + { + var validator = new GetHabitWidgetQueryValidator(); + RejectsProperty(validator, new GetHabitWidgetQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetHabitWidgetQuery(Guid.NewGuid())); + } + + [Fact] + public void GetNotifications_GuardsUserId() + { + var validator = new GetNotificationsQueryValidator(); + RejectsProperty(validator, new GetNotificationsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetNotificationsQuery(Guid.NewGuid())); + } + + [Fact] + public void ExportUserData_GuardsUserId() + { + var validator = new ExportUserDataQueryValidator(); + RejectsProperty(validator, new ExportUserDataQuery(Guid.Empty), "UserId"); + Accepts(validator, new ExportUserDataQuery(Guid.NewGuid())); + } + + [Fact] + public void GetProfile_GuardsUserId() + { + var validator = new GetProfileQueryValidator(); + RejectsProperty(validator, new GetProfileQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetProfileQuery(Guid.NewGuid())); + } + + [Fact] + public void GetReferralDashboard_GuardsUserId() + { + var validator = new GetReferralDashboardQueryValidator(); + RejectsProperty(validator, new GetReferralDashboardQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetReferralDashboardQuery(Guid.NewGuid())); + } + + [Fact] + public void GetReferralStats_GuardsUserId() + { + var validator = new GetReferralStatsQueryValidator(); + RejectsProperty(validator, new GetReferralStatsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetReferralStatsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetBillingDetails_GuardsUserId() + { + var validator = new GetBillingDetailsQueryValidator(); + RejectsProperty(validator, new GetBillingDetailsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetBillingDetailsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetSubscriptionStatus_GuardsUserId() + { + var validator = new GetSubscriptionStatusQueryValidator(); + RejectsProperty(validator, new GetSubscriptionStatusQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetSubscriptionStatusQuery(Guid.NewGuid())); + } + + [Fact] + public void GetTags_GuardsUserId() + { + var validator = new GetTagsQueryValidator(); + RejectsProperty(validator, new GetTagsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetTagsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetUserFacts_GuardsUserId() + { + var validator = new GetUserFactsQueryValidator(); + RejectsProperty(validator, new GetUserFactsQuery(Guid.Empty), "UserId"); + Accepts(validator, new GetUserFactsQuery(Guid.NewGuid())); + } + + [Fact] + public void GetPlans_GuardsUserId() + { + var validator = new GetPlansQueryValidator(); + RejectsProperty(validator, new GetPlansQuery(Guid.Empty, null, null), "UserId"); + Accepts(validator, new GetPlansQuery(Guid.NewGuid(), "BR", null)); + } + + [Fact] + public void GetPublicProfile_GuardsSlug() + { + var validator = new GetPublicProfileQueryValidator(); + RejectsProperty(validator, new GetPublicProfileQuery(string.Empty), "Slug"); + Accepts(validator, new GetPublicProfileQuery("astra")); + } + + [Fact] + public void GetGoalById_GuardsUserIdAndGoalId() + { + var validator = new GetGoalByIdQueryValidator(); + RejectsProperty(validator, new GetGoalByIdQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetGoalByIdQuery(Guid.NewGuid(), Guid.Empty), "GoalId"); + Accepts(validator, new GetGoalByIdQuery(Guid.NewGuid(), Guid.NewGuid())); + } + + [Fact] + public void GetGoalDetail_GuardsUserIdAndGoalId() + { + var validator = new GetGoalDetailQueryValidator(); + RejectsProperty(validator, new GetGoalDetailQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetGoalDetailQuery(Guid.NewGuid(), Guid.Empty), "GoalId"); + Accepts(validator, new GetGoalDetailQuery(Guid.NewGuid(), Guid.NewGuid())); + } + + [Fact] + public void GetGoalMetrics_GuardsUserIdAndGoalId() + { + var validator = new GetGoalMetricsQueryValidator(); + RejectsProperty(validator, new GetGoalMetricsQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetGoalMetricsQuery(Guid.NewGuid(), Guid.Empty), "GoalId"); + Accepts(validator, new GetGoalMetricsQuery(Guid.NewGuid(), Guid.NewGuid())); + } + + [Fact] + public void GetHabitById_GuardsUserIdAndHabitId() + { + var validator = new GetHabitByIdQueryValidator(); + RejectsProperty(validator, new GetHabitByIdQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetHabitByIdQuery(Guid.NewGuid(), Guid.Empty), "HabitId"); + Accepts(validator, new GetHabitByIdQuery(Guid.NewGuid(), Guid.NewGuid())); + } + + [Fact] + public void GetHabitFullDetail_GuardsUserIdAndHabitId() + { + var validator = new GetHabitFullDetailQueryValidator(); + RejectsProperty(validator, new GetHabitFullDetailQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetHabitFullDetailQuery(Guid.NewGuid(), Guid.Empty), "HabitId"); + Accepts(validator, new GetHabitFullDetailQuery(Guid.NewGuid(), Guid.NewGuid())); + } + + [Fact] + public void GetHabitLogs_GuardsUserIdAndHabitId() + { + var validator = new GetHabitLogsQueryValidator(); + RejectsProperty(validator, new GetHabitLogsQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetHabitLogsQuery(Guid.NewGuid(), Guid.Empty), "HabitId"); + Accepts(validator, new GetHabitLogsQuery(Guid.NewGuid(), Guid.NewGuid())); + } + + [Fact] + public void GetHabitMetrics_GuardsUserIdAndHabitId() + { + var validator = new GetHabitMetricsQueryValidator(); + RejectsProperty(validator, new GetHabitMetricsQuery(Guid.Empty, Guid.NewGuid()), "UserId"); + RejectsProperty(validator, new GetHabitMetricsQuery(Guid.NewGuid(), Guid.Empty), "HabitId"); + Accepts(validator, new GetHabitMetricsQuery(Guid.NewGuid(), Guid.NewGuid())); + } +}