Skip to content

Commit

Permalink
✨ add assert against ModelStateDictionary instances having errors
Browse files Browse the repository at this point in the history
- Expect(modelState){.Not}.To.Have.Errors();
  • Loading branch information
fluffynuts committed Apr 15, 2024
1 parent df5953c commit ac7302a
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 8 deletions.
83 changes: 83 additions & 0 deletions src/NExpect.Matchers.AspNetCore.Tests/TestModelStateMatchers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NExpect.Exceptions;

namespace NExpect.Matchers.AspNet.Tests;

[TestFixture]
public class TestModelStateMatchers
{
[TestFixture]
public class Errors
{
[TestFixture]
public class WhenModelStateHasErrors
{
[Test]
public void ShouldReturnAssertErrorsExist()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError(GetRandomString(), GetRandomWords());
// Act
Assert.That(
() =>
{
Expect(modelState)
.To.Have.Errors();
},
Throws.Nothing
);

// Assert
}

[Test]
public void ShouldFailIfNegatedAndErrorsExist()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError(GetRandomString(), GetRandomWords());

// Act
Assert.That(
() =>
{
Expect(modelState)
.Not.To.Have.Errors();
},
Throws.Exception.InstanceOf<UnmetExpectationException>()
);
// Assert
}
}

[TestFixture]
public class WHenModelStateHasNoErrors
{
[Test]
public void ShouldAssertNoErrors()
{
// Arrange
var modelState = new ModelStateDictionary();
// Act
Assert.That(
() =>
{
Expect(modelState)
.Not.To.Have.Errors();
},
Throws.Nothing
);
Assert.That(
() =>
{
Expect(modelState)
.To.Have.Errors();
},
Throws.Exception.InstanceOf<UnmetExpectationException>()
);
// Assert
}
}
}
}
8 changes: 7 additions & 1 deletion src/NExpect.Matchers.AspNetCore/AspNetCoreExpectations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,14 @@ public static ICollectionExpectation<KeyValuePair<string, ModelStateEntry>> Expe
ModelStateDictionary dict
)
{
var collection = dict?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (collection is not null)
{
collection.SetMetadata("__actual__", dict);
}

return Expectations.Expect(
dict?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
collection
);
}

Expand Down
82 changes: 82 additions & 0 deletions src/NExpect.Matchers.AspNetCore/ModelStateMatchers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using Imported.PeanutButter.Utils;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NExpect.Implementations;
using NExpect.Interfaces;
using NExpect.MatcherLogic;
using static NExpect.Implementations.MessageHelpers;

namespace NExpect;

/// <summary>
/// Provides matchers for ModelStateDictionary objects
/// </summary>
public static class ModelStateMatchers
{
/// <summary>
/// Asserts that the model state dictionary has errors
/// </summary>
/// <param name="have"></param>
/// <returns></returns>
public static ICollectionMore<KeyValuePair<string, ModelStateEntry>> Errors(
this ICollectionHave<KeyValuePair<string, ModelStateEntry>> have
)
{
return have.Errors(NULL_STRING);
}

/// <summary>
/// Asserts that the model state dictionary has errors
/// </summary>
/// <param name="have"></param>
/// <param name="customMessage"></param>
/// <returns></returns>
public static ICollectionMore<KeyValuePair<string, ModelStateEntry>> Errors(
this ICollectionHave<KeyValuePair<string, ModelStateEntry>> have,
string customMessage
)
{
return have.Errors(() => customMessage);
}

/// <summary>
/// Asserts that the model state dictionary has errors
/// </summary>
/// <param name="have"></param>
/// <param name="customMessageGenerator"></param>
/// <returns></returns>
public static ICollectionMore<KeyValuePair<string, ModelStateEntry>> Errors(
this ICollectionHave<KeyValuePair<string, ModelStateEntry>> have,
Func<string> customMessageGenerator
)
{
return have.AddMatcher(
collection =>
{
if (
!collection.TryGetMetadata<ModelStateDictionary>("__actual__", out var actual) ||
actual is null
)
{
return new EnforcedMatcherResult(
false,
FinalMessageFor(
() => "Unable to assert model state errors on a null model state dictionary",
customMessageGenerator
)
);
}

var passed = actual.ErrorCount > 0;
return new MatcherResult(
passed,
FinalMessageFor(
() => $"Expected {passed.AsNot()}to find errors (found {actual.ErrorCount})",
customMessageGenerator
)
);
}
);
}
}
3 changes: 3 additions & 0 deletions src/NExpect.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=236f7aa5_002D7b06_002D43ca_002Dbf2a_002D9b31bfcff09a/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=669e5282_002Dfb4b_002D4e90_002D91e7_002D07d269d04b60/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMIXED_005FENUM/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=agenumber/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Comparers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Homogenous/@EntryIndexedValue">True</s:Boolean>
Expand Down
9 changes: 4 additions & 5 deletions src/NExpect/Implementations/Collections/CollectionHave.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
using System;
using System.Collections.Generic;
using NExpect.Implementations.Strings;
using NExpect.Interfaces;

// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable MemberCanBePrivate.Global

namespace NExpect.Implementations.Collections;

internal class CollectionHave<T> :
ExpectationContextWithLazyActual<IEnumerable<T>>,
IHasActual<IEnumerable<T>>,
ICollectionHave<T>
internal class CollectionHave<T>
: ExpectationContextWithLazyActual<IEnumerable<T>>,
IHasActual<IEnumerable<T>>,
ICollectionHave<T>
{
public ICollectionUnique<T> Unique => Next<CollectionUnique<T>>();

Expand Down
20 changes: 18 additions & 2 deletions src/NExpect/MatcherLogic/AddMatcherExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ public static IMore<T> AddMatcher<T>(
return continuation.More();
}

/// <summary>
/// Adds a matcher for a collection 'have' invocation
/// </summary>
/// <param name="continuation"></param>
/// <param name="matcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static ICollectionMore<T> AddMatcher<T>(
this ICollectionHave<T> continuation,
Func<IEnumerable<T>, IMatcherResult> matcher
)
{
AddMatcherPrivate(continuation, matcher);
return continuation.More();
}

/// <summary>
/// Most general matcher add - onto ICanAddMatcher&lt;T&gt;
/// </summary>
Expand Down Expand Up @@ -118,7 +134,7 @@ public static IMore<T> Compose<T>(
)
{
return continuation.Compose(expectationsRunner,
(a, b) => $"Expectation \"{callingMethod}\" should {(!b).AsNot()}have failed.");
(_, b) => $"Expectation \"{callingMethod}\" should {(!b).AsNot()}have failed.");
}

/// <summary>
Expand Down Expand Up @@ -173,7 +189,7 @@ public static IMore<IEnumerable<T>> Compose<T>(
)
{
return continuation.Compose(expectationsRunner,
(a, b) => $"{callingMethod} should {b.AsNot()}have passed.");
(_, b) => $"{callingMethod} should {b.AsNot()}have passed.");
}

/// <summary>
Expand Down

0 comments on commit ac7302a

Please sign in to comment.