From f7263c33a835aaa2497a57baf6d542c7528726fb Mon Sep 17 00:00:00 2001 From: Louis Seubert Date: Sun, 12 Jul 2026 18:50:06 +0200 Subject: [PATCH] fix(validation): make Severity meaningful in Validation.IsValid Validation.IsValid previously counted every problem regardless of severity, leaving Severity inert. Now IsValid only fails on Error problems, so Warning and Info no longer invalidate the result, and IsValidFor(Severity) lets callers set the failing-severity threshold. --- CHANGELOG.md | 1 + .../ValidationSeverityTests.cs | 49 +++++++++++++++++++ .../ValidatorTests.cs | 6 ++- src/request.validation/Validation.cs | 22 ++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/request.validation.tests/ValidationSeverityTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 491f224..3de75fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ To have a consistent experience across all packages, some public interfaces have - **request.result:** Fold `IsSuccess` into `Result.GetHashCode` to avoid collisions between a failure and a success value whose hash is `0` - **request.validation:** Guard null comparison bounds in `GreaterThan`/`LessThan`/`Between` etc. so a `null` bound no longer throws (`NullReferenceException`) - **request.validation:** Return no problems when validating a `null` instance of a reference type instead of dereferencing it (`NullReferenceException`) +- **request.validation:** Make `Severity` meaningful — `Validation.IsValid` now only fails on `Error` problems; `Warning`/`Info` no longer invalidate. Added `Validation.IsValidFor(Severity)` to set the failing-severity threshold (see PLAN.md Inconsistencies#B) ### Removed diff --git a/src/request.validation.tests/ValidationSeverityTests.cs b/src/request.validation.tests/ValidationSeverityTests.cs new file mode 100644 index 0000000..6f24c8d --- /dev/null +++ b/src/request.validation.tests/ValidationSeverityTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) The Geekeey Authors +// SPDX-License-Identifier: EUPL-1.2 + +namespace Geekeey.Request.Validation.Tests; + +internal sealed class ValidationSeverityTests +{ + [Test] + public async Task I_can_have_warning_problems_not_invalidate_by_default() + { + var validator = new PropertyValidator(person + => person.Name, rule => rule.Must(value => !string.IsNullOrWhiteSpace(value), "Name is required.") + .WithSeverity(Severity.Warning)); + + var result = validator.Validate(new Person { Name = "" }); + + await Assert.That(result.Problems).Count().IsEqualTo(1); + await Assert.That(result.IsValid).IsTrue(); + await Assert.That(result.IsValidFor(Severity.Warning)).IsFalse(); + await Assert.That(result.IsValidFor(Severity.Info)).IsFalse(); + } + + [Test] + public async Task I_can_have_info_problems_only_invalidate_at_info_threshold() + { + var validator = new PropertyValidator(person + => person.Name, rule => rule.Must(value => !string.IsNullOrWhiteSpace(value), "Name is required.") + .WithSeverity(Severity.Info)); + + var result = validator.Validate(new Person { Name = "" }); + + await Assert.That(result.Problems).Count().IsEqualTo(1); + await Assert.That(result.IsValid).IsTrue(); + await Assert.That(result.IsValidFor(Severity.Warning)).IsTrue(); + await Assert.That(result.IsValidFor(Severity.Info)).IsFalse(); + } + + [Test] + public async Task I_can_have_error_problems_invalidate_by_default() + { + var validator = new PropertyValidator(person + => person.Name, rule => rule.Must(value => !string.IsNullOrWhiteSpace(value), "Name is required.")); + + var result = validator.Validate(new Person { Name = "" }); + + await Assert.That(result.IsValid).IsFalse(); + await Assert.That(result.IsValidFor(Severity.Error)).IsFalse(); + } +} diff --git a/src/request.validation.tests/ValidatorTests.cs b/src/request.validation.tests/ValidatorTests.cs index 6613720..820edc4 100644 --- a/src/request.validation.tests/ValidatorTests.cs +++ b/src/request.validation.tests/ValidatorTests.cs @@ -14,7 +14,8 @@ internal sealed class ValidatorTests var result = validator.Validate(new Person { Name = "" }); - await Assert.That(result.IsValid).IsFalse(); + await Assert.That(result.IsValid).IsTrue(); + await Assert.That(result.IsValidFor(Severity.Warning)).IsFalse(); await Assert.That(result.Problems).Count().IsEqualTo(1); var problem = result.Problems.Single(); @@ -39,7 +40,8 @@ internal sealed class ValidatorTests using (Assert.Multiple()) { - await Assert.That(invalid.IsValid).IsFalse(); + await Assert.That(invalid.IsValid).IsTrue(); + await Assert.That(invalid.IsValidFor(Severity.Warning)).IsFalse(); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(valid.Problems).IsEmpty(); } diff --git a/src/request.validation/Validation.cs b/src/request.validation/Validation.cs index fadadff..8e9c7c7 100644 --- a/src/request.validation/Validation.cs +++ b/src/request.validation/Validation.cs @@ -23,7 +23,27 @@ public sealed class Validation /// /// Whether the validation was successful. /// - public bool IsValid => Problems.Count is 0; + /// + /// A validation is considered successful unless it contains at least one problem with + /// . Problems of lower severity ( or + /// ) do not invalidate the result. Use + /// to control which severities are treated as failures. + /// + public bool IsValid => IsValidFor(Severity.Error); + + /// + /// Whether the validation was successful, treating problems at or above the given severity as failures. + /// + /// The minimum severity that should be considered a failure. + /// + /// A problem invalidates the validation when its is at least as severe as + /// . For example, IsValidFor(Severity.Warning) fails on errors and + /// warnings but not on informational problems. + /// + public bool IsValidFor(Severity minimum) + { + return !Problems.Any(problem => problem.Severity <= minimum); + } /// /// The problems that were found during validation.