Skip to content

Commit

Permalink
Merge pull request #16 from /issues/10
Browse files Browse the repository at this point in the history
Add more unit tests and code refactors
  • Loading branch information
mr5z authored Dec 24, 2023
2 parents 7dfc696 + 1a4a01f commit 12ae1a3
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 55 deletions.
24 changes: 12 additions & 12 deletions src/Models/MultiValidationRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public sealed override bool IsValid(T? value)
if (this.validationRules != null)
{
return ValidationService.ValidateRuleCollection(
GetValidationRules(),
value
);
value,
GetValidationRules()
).HasError == false;
}

var actionCollection = new ActionCollection<T>();
Expand All @@ -38,41 +38,41 @@ public sealed override bool IsValid(T? value)
if (value is not INotifyPropertyChanged inpc)
{
return ValidationService.ValidateRuleCollection(
GetValidationRules(),
value
);
value,
GetValidationRules()
).HasError == false;
}

inpc.PropertyChanged -= Target_PropertyChanged;
inpc.PropertyChanged += Target_PropertyChanged;

return ValidationService.ValidateRuleCollection(
GetValidationRules(),
value
);
value,
GetValidationRules()
).HasError == false;
}

private void Target_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var dictionaryRules = GetValidationRules().Where(r => r.Key == e.PropertyName);
var listRules = dictionaryRules.SelectMany(it => it.Value);

var resultArgs = ValidationService.GetValidationResultArgs(e.PropertyName, null, listRules!);
var resultArgs = ValidationService.ValidateRuleCollection(e.PropertyName, null, listRules!);

this.lastErrorMessage = resultArgs.HasError ? $"{e.PropertyName}: {resultArgs.FirstError}" : null;
}

private IDictionary<string, IEnumerable<IValidationRule>> GetValidationRules()
{
return this.validationRules.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return this.validationRules!.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}

// TODO convert to List<string>
public sealed override string ErrorMessage => this.lastErrorMessage ?? $"{{{string.Join(", ", ErrorsAsJsonString())}}}";

private IEnumerable<string?> ErrorsAsJsonString()
{
return this.validationRules
return this.validationRules!
.SelectMany(it => it.Value)
.Select(e => $"\"{e.PropertyName}\":\"{e.Error}\"");
}
Expand Down
54 changes: 20 additions & 34 deletions src/Services/ValidationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private async Task ValidateByProperty<TValue>(string propertyName, TValue? value
if (!validationRules.TryGetValue(propertyName, out var propertyRules))
throw new InvalidOperationException($"'{propertyName}' is not registered to validation rules.");

var resultArgs = GetValidationResultArgs(propertyName, value, propertyRules);
var resultArgs = ValidateRuleCollection(propertyName, value, propertyRules);

this.recentErrors[propertyName] = null;

Expand All @@ -90,7 +90,7 @@ private async Task ValidateByProperty<TValue>(string propertyName, TValue? value
PropertyInvalid?.Invoke(this, resultArgs);
}

internal static ValidationResultArgs GetValidationResultArgs(
internal static ValidationResultArgs ValidateRuleCollection(
string propertyName,
object? propertyValue,
IEnumerable<IValidationRule> validatedRules)
Expand All @@ -107,37 +107,25 @@ internal static ValidationResultArgs GetValidationResultArgs(
return new ValidationResultArgs(errorDictionary);
}

private static ValidationResultArgs GetValidationResultArgs(
internal static ValidationResultArgs ValidateRuleCollection(
object target,
IDictionary<string, IEnumerable<IValidationRule>> validationRules)
{
var type = target.GetType();
var errorDictionary = new Dictionary<string, IEnumerable<string?>>();

foreach (var entry in validationRules)
foreach (var (propertyName, rules) in validationRules)
{
var (propertyName, rules) = entry;
var property = type.GetProperty(propertyName);

if (property == null)
throw new InvalidOperationException($"Invalid state: '{propertyName}' cannot be access.");

var value = property.GetValue(target);
var property = target.GetType().GetProperty(propertyName)
?? throw new InvalidOperationException($"'{target.GetType().Name}.{propertyName}' cannot be accessed.");

var value = property.GetValue(target);
var errorMessages = ValidatePropertyValue(rules, value);

if (!errorMessages.Any())
{
continue;
}

if (errorDictionary.TryGetValue(propertyName, out var oldList))
if (errorMessages.Any())
{
errorDictionary[propertyName] = oldList.Concat(errorMessages);
}
else
{
errorDictionary[propertyName] = errorMessages;
errorDictionary[propertyName] = errorDictionary.TryGetValue(propertyName, out var oldList)
? oldList.Concat(errorMessages)
: errorMessages;
}
}

Expand Down Expand Up @@ -186,7 +174,9 @@ private void UpdateRecentErrors(ValidationResultArgs resultArgs)
{
foreach (var entry in resultArgs.ErrorDictionary)
{
var formattedMessage = (this.errorFormatter != null) ? this.errorFormatter.Invoke(entry.Value) : string.Join(", ", entry.Value);
var formattedMessage = (this.errorFormatter != null)
? this.errorFormatter.Invoke(entry.Value)
: string.Join(", ", entry.Value);
this.recentErrors[entry.Key] = formattedMessage;
}
}
Expand All @@ -195,7 +185,7 @@ public void EnsurePropertiesAreValid()
{
EnsureEntryMethodInvoked();

var resultArgs = GetValidationResultArgs(this.notifiableModel!, GetRules());
var resultArgs = ValidateRuleCollection(this.notifiableModel!, GetRules());
this.recentErrors.Clear();

UpdateRecentErrors(resultArgs);
Expand All @@ -207,15 +197,11 @@ public void EnsurePropertiesAreValid()
public bool Validate()
{
EnsureEntryMethodInvoked();
return ValidateRuleCollection(GetRules(), this.notifiableModel!);
}

internal static bool ValidateRuleCollection(
IDictionary<string, IEnumerable<IValidationRule>> ruleCollection,
object target)
{
var resultArgs = GetValidationResultArgs(target, ruleCollection);
return !resultArgs.HasError;

var result = ValidateRuleCollection(this.notifiableModel!, GetRules());
UpdateRecentErrors(result);

return !result.HasError;
}

public IDictionary<string, string?> GetErrors()
Expand Down
86 changes: 77 additions & 9 deletions test/PropertyValidator.Test/ValidationService.test.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CrossUtility.Extensions;
using PropertyValidator.Exceptions;
using PropertyValidator.Models;
using PropertyValidator.Services;
Expand All @@ -22,7 +23,7 @@ public void Setup()
validationService = new ValidationService();
}

[Test, Description("ValidationService.Validate() must throw Exception if not setup properly.")]
[Test, Description("Validate() must throw Exception if not setup properly.")]
public void MustThrowWhenNotConfigured()
{
var ex1 = new Exception("This shouldn't be thrown.");
Expand All @@ -38,7 +39,7 @@ public void MustThrowWhenNotConfigured()
}
}

[Test, Description("Calling ValidationService.For() multiple times should throw an Exception.")]
[Test, Description("Calling For() multiple times should throw an Exception.")]
public void MustThrowForMultipleCalls()
{
var ex1 = new Exception("This shouldn't be thrown.");
Expand All @@ -56,7 +57,7 @@ public void MustThrowForMultipleCalls()
}
}

[Test, Description("ValidationService.Validate() must not throw Exception if setup properly.")]
[Test, Description("Validate() must not throw Exception if setup properly.")]
public void MustNotThrowWhenConfigured()
{
var ex1 = new Exception("This shouldn't be thrown.");
Expand All @@ -72,7 +73,7 @@ public void MustNotThrowWhenConfigured()
}
}

[Test, Description("ValidationService.Validate() must return false if property rule is violated.")]
[Test, Description("Validate() must return false if property rule is violated.")]
public void MustValidatePropertyRuleToFalse()
{
var vm = new DummyViewModel();
Expand All @@ -86,7 +87,7 @@ public void MustValidatePropertyRuleToFalse()
Assert.That(result, Is.EqualTo(false));
}

[Test, Description("ValidationService.Validate() must return true if property rule is not violated.")]
[Test, Description("Validate() must return true if property rule is not violated.")]
public void MustValidatePropertyRuleToTrue()
{
var vm = new DummyViewModel();
Expand All @@ -101,7 +102,7 @@ public void MustValidatePropertyRuleToTrue()
Assert.That(result, Is.EqualTo(true));
}

[Test, Description("ValidationService.Validate() must validate for StringRequired() rule.")]
[Test, Description("Validate() must validate for StringRequired() rule.")]
[TestCaseSource(nameof(DummyViewModelFixturesStringRequiredRule))]
public void MustValidateStringRequiredRule(DummyViewModel vm)
{
Expand All @@ -114,7 +115,7 @@ public void MustValidateStringRequiredRule(DummyViewModel vm)
Assert.That(result, Is.EqualTo(true));
}

[Test, Description("ValidationService.Validate() must validate for RangeLengthRule() rule.")]
[Test, Description("Validate() must validate for RangeLengthRule() rule.")]
[TestCaseSource(nameof(DummyViewModelFixturesRangeLengthRule))]
public void MustValidateRangeLengthRule(DummyViewModel vm, RangeLengthRule rule)
{
Expand All @@ -127,7 +128,7 @@ public void MustValidateRangeLengthRule(DummyViewModel vm, RangeLengthRule rule)
Assert.That(result, Is.EqualTo(true), $"Didn't succeed because vb.Value: '{vm.Value}', length: {vm.Value?.Length}");
}

[Test, Description("ValidationService.PropertyInvalid must be invoked upon violation of property rules.")]
[Test, Description("PropertyInvalid must be invoked upon violation of property rules.")]
public async Task MustInvokePropertyInvalidEvent()
{
var vm = new DummyViewModel { Value = "something" };
Expand All @@ -154,7 +155,7 @@ void PropertyInvalid(object? sender, ValidationResultArgs e)
Assert.That(result, Is.EqualTo(true), $"Didn't succeed because result from PropertyInvalid is not expected.");
}

[Test, Description("ValidationService.EnsurePropertiesAreValid() must throw Exception.")]
[Test, Description("EnsurePropertiesAreValid() must throw Exception.")]
[TestCaseSource(nameof(DummyViewModelFixturesXRule))]
public void MustEnsurePropertiesAreInvalid(DummyViewModel vm, IValidationRule rule)
{
Expand All @@ -177,6 +178,73 @@ public void MustEnsurePropertiesAreInvalid(DummyViewModel vm, IValidationRule ru
Assert.That(result, Is.EqualTo(true), $"Didn't succeed because EnsurePropertiesAreValid() didn't throw.");
}

[Test, Description("GetErrors() must include error messages from registered property rule.")]
public void MustFetchCorrectErrorMessages()
{
var vm = new DummyViewModel();
var rule = new StringRequiredRule();

validationService.For(vm)
.AddRule(e => vm.Value, rule);

_ = validationService.Validate();

var errorMessages = validationService.GetErrors()
.Where(e => !string.IsNullOrEmpty(e.Value))
.Select(kvp => kvp.Value);

Assert.That(errorMessages, Contains.Item(rule.ErrorMessage));
}

[Test, Description("EnsurePropertiesAreValid() must throw and contains expected ValidationResultArgs.")]
public void MustContainExpectedValidationResultArgs()
{
var vm = new DummyViewModel();
var rule = new StringRequiredRule();

validationService.For(vm)
.AddRule(e => vm.Value, rule);

var notEx = new Exception("this shouldn't be thrown");
try
{
validationService.EnsurePropertiesAreValid();
throw notEx;
}
catch (Exception ex)
{
Assert.That(ex, Is.Not.EqualTo(notEx));
Assert.That(ex, Is.TypeOf<PropertyException>());

var pEx = (PropertyException)ex;
var result = pEx.ValidationResultArgs;
Assert.Multiple(() =>
{
Assert.That(result.HasError, Is.EqualTo(true));
Assert.That(result.FirstError, Is.Not.Null);
Assert.That(result.ErrorMessages, Contains.Item(rule.ErrorMessage));
Assert.That(result.ErrorDictionary, Contains.Key(nameof(DummyViewModel.Value)));
Assert.That(result.ErrorDictionary, Contains.Value(new [] { rule.ErrorMessage }));
});
}
}

[Test, Description("SetErrorFormatter() must modify the GetErrors() format.")]
public void MustFollowErrorMessageFormat()
{
var vm = new DummyViewModel();
var rule = new StringRequiredRule();
validationService.For(vm).AddRule(e => e.Value, rule);
validationService.SetErrorFormatter(errorMessages
=> "<error>" + errorMessages.FirstOrDefault() + "</error>"
);

_ = validationService.Validate();
var errorMessages = validationService.GetErrors();

Assert.That(errorMessages, Contains.Value("<error>" + rule.ErrorMessage + "</error>"));
}

private static IEnumerable<TestCaseData> DummyViewModelFixturesStringRequiredRule
{
get
Expand Down

0 comments on commit 12ae1a3

Please sign in to comment.