diff --git a/src/NExpect.Matchers.AspNetCore.Tests/TestControllerMatchers.cs b/src/NExpect.Matchers.AspNetCore.Tests/TestControllerMatchers.cs index 9c4bd4ca..c1dea3ac 100644 --- a/src/NExpect.Matchers.AspNetCore.Tests/TestControllerMatchers.cs +++ b/src/NExpect.Matchers.AspNetCore.Tests/TestControllerMatchers.cs @@ -1,3 +1,4 @@ +using System; using System.Net.Http; using Microsoft.AspNetCore.Mvc; using NExpect.Exceptions; @@ -199,6 +200,29 @@ public void ShouldBeAbleToTestVerb() // Assert } + [Test] + public void ShouldBeAbleToTestArbitraryMethodAttributes() + { + // Arrange + var type = typeof(TestController); + var sut = new TestController(); + // Act + Assert.That(() => + { + Expect(type) + .To.Have.Method(nameof(TestController.PostOnly)) + .Supporting(HttpMethod.Post) + .With.Attribute(o => o.Template == "post-only"); + }, Throws.Nothing); + Assert.That(() => + { + Expect(sut) + .To.Have.Method(nameof(TestController.PostOnly)) + .With.Attribute(o => o.Template == "post-only"); + }, Throws.Nothing); + // Assert + } + [Test] public void ShouldBeAbleToTestMultipleVerbs() { diff --git a/src/NExpect.Matchers.AspNetCore/ControllerMatchers.cs b/src/NExpect.Matchers.AspNetCore/ControllerMatchers.cs index b7097189..029f620b 100644 --- a/src/NExpect.Matchers.AspNetCore/ControllerMatchers.cs +++ b/src/NExpect.Matchers.AspNetCore/ControllerMatchers.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net.Http; +using System.Reflection; using Imported.PeanutButter.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; @@ -73,7 +74,8 @@ Func customMessageGenerator customMessageGenerator ) ); - }); + } + ); return result; } @@ -116,7 +118,8 @@ Func customMessageGenerator customMessageGenerator ) ); - }); + } + ); return have.More(); } @@ -130,7 +133,8 @@ Func customMessageGenerator public static IMore Route( this IHave have, string member, - string expected) + string expected + ) { have.AddMatcher( actual => @@ -150,7 +154,8 @@ public static IMore Route( passed, () => $"Expected {actual}.{method} {passed.AsNot()}to have route '{expected}'" ); - }); + } + ); return have.More(); } @@ -191,7 +196,8 @@ public AndSupportingExtension Supporting(HttpMethod method) passed, () => $"Expected {controllerType}.{Member} to support HttpMethod {method}" ); - }); + } + ); return Next(); } @@ -205,6 +211,85 @@ public AndSupportingExtension Supporting(HttpMethod method) /// public SupportingExtension And => this; + /// + /// Asserts that the method is decorated with the expected attribute + /// + /// + /// + public SupportingExtension Attribute() + { + return Attribute(a => true); + } + + /// + /// Asserts that the method is decorated with the expected attribute + /// + /// + /// + /// + public SupportingExtension Attribute( + Func matcher + ) + { + Continuation.AddMatcher( + controllerType => + { + var method = controllerType.GetMethods() + .Where(o => o.Name == Member) + .ToArray(); + + return method.Length switch + { + 0 => CreateMissingMethodResult(controllerType), + 1 => VerifyAttribute(controllerType, method[0], matcher), + _ => CreateAmbiguousMethodResult(controllerType, method.Length) + }; + } + ); + return this; + } + + private MatcherResult CreateAmbiguousMethodResult( + Type controllerType, + int howMany + ) + { + return new EnforcedMatcherResult( + false, + () => $"Expected to find one method named '{Member}' on {controllerType}, but found {howMany}" + ); + } + + private MatcherResult VerifyAttribute( + Type controllerType, + MethodInfo methodInfo, + Func matcher + ) + { + var passed = methodInfo.GetCustomAttributes(true) + .OfType() + .Where(matcher) + .Any(); + return new MatcherResult( + passed, + () => $@"Expected { + controllerType + }.{ + Member + } { + passed.AsNot() + }to be decorated with [{typeof(TAttribute).Name.RegexReplace("Attribute$", "")}]" + ); + } + + private MatcherResult CreateMissingMethodResult(Type controllerType) + { + return new EnforcedMatcherResult( + false, + () => $"Expected {controllerType} to have method '{Member}' but no such-named method was found" + ); + } + /// /// Asserts that the controller action being operated on has the specified route /// @@ -236,15 +321,19 @@ public SupportingExtension Route(string expected) var colon = count > 0 ? ":" : ""; - return string.Join("\n", new[] - { - start, - $"Have {no}route{s}{colon}" - } - .Concat(routes.Select(r => $" - {r}"))); + return string.Join( + "\n", + new[] + { + start, + $"Have {no}route{s}{colon}" + } + .Concat(routes.Select(r => $" - {r}")) + ); } ); - }); + } + ); return this; } @@ -303,52 +392,55 @@ public static IMore Area( Func customMessageGenerator ) { - return have.AddMatcher(actual => - { - if (actual is null) + return have.AddMatcher( + actual => { - return new EnforcedMatcherResult( - false, - "Provided controller type is null" - ); - } + if (actual is null) + { + return new EnforcedMatcherResult( + false, + "Provided controller type is null" + ); + } - if (areaName is null) - { - return new EnforcedMatcherResult( - false, - "Provided area name is null" - ); - } + if (areaName is null) + { + return new EnforcedMatcherResult( + false, + "Provided area name is null" + ); + } - var attrib = actual.GetCustomAttributes(inherit: false) - .OfType() - .FirstOrDefault(); + var attrib = actual.GetCustomAttributes(inherit: false) + .OfType() + .FirstOrDefault(); - if (attrib is null) - { + if (attrib is null) + { + return new MatcherResult( + false, + FinalMessageFor( + () => + $"Expected type {actual} {false.AsNot()} to be decorated with [Area(\"{areaName}\")]", + customMessageGenerator + ) + ); + } + + var passed = areaName.Equals(attrib.RouteValue, StringComparison.OrdinalIgnoreCase); + var more = passed + ? $" but found [Area(\"{attrib.RouteValue}\")]" + : " but found exactly that"; return new MatcherResult( - false, + passed, FinalMessageFor( - () => $"Expected type {actual} {false.AsNot()} to be decorated with [Area(\"{areaName}\")]", + () => + $"Expected type {actual} {passed.AsNot()} to be decorated with [Area(\"{areaName}\")]{more}", customMessageGenerator ) ); } - - var passed = areaName.Equals(attrib.RouteValue, StringComparison.OrdinalIgnoreCase); - var more = passed - ? $" but found [Area(\"{attrib.RouteValue}\")]" - : " but found exactly that"; - return new MatcherResult( - passed, - FinalMessageFor( - () => - $"Expected type {actual} {passed.AsNot()} to be decorated with [Area(\"{areaName}\")]{more}", - customMessageGenerator - ) - ); - }); + ); } /// @@ -367,7 +459,8 @@ public class AndSupportingExtension internal AndSupportingExtension( IHave continuation, string member, - SupportingExtension supportingExtension) + SupportingExtension supportingExtension + ) { _continuation = continuation; _member = member; diff --git a/src/NExpect.Tests/ObjectEquality/TestReflectiveMatchers.cs b/src/NExpect.Tests/ObjectEquality/TestReflectiveMatchers.cs index b9d3db5b..b15a8172 100644 --- a/src/NExpect.Tests/ObjectEquality/TestReflectiveMatchers.cs +++ b/src/NExpect.Tests/ObjectEquality/TestReflectiveMatchers.cs @@ -23,6 +23,16 @@ public class Data public bool IsCommented { get; set; } public bool IsNotCommented { get; set; } + + [Comment("moo-cakes")] + public void CommentedMethod() + { + } + + public int Add(int a, int b) + { + return a + b; + } } [Test] @@ -188,6 +198,29 @@ public void ShouldBehaveOnTypeAsType() // Assert } + [Test] + public void ShouldBeAbleToAssertAgainstMethodAttributes() + { + // Arrange + var type = typeof(Data); + var sut = GetRandom(); + // Act + Assert.That(() => + { + Expect(type) + .To.Have.Method(nameof(Data.CommentedMethod)) + .With.Attribute(o => o.Comment == "moo-cakes"); + }, Throws.Nothing); + + Assert.That(() => + { + Expect(sut) + .To.Have.Method(nameof(Data.CommentedMethod)) + .With.Attribute(o => o.Comment == "moo-cakes"); + }, Throws.Nothing); + // Assert + } + [Test] public void ShouldBeAbleToAssertAgainstPropertyAttributes() { @@ -197,7 +230,7 @@ public void ShouldBeAbleToAssertAgainstPropertyAttributes() Assert.That( () => { - var foo = Expect(data) + Expect(data) .To.Have.Property(nameof(data.IsCommented)) .With.Attribute(); }, @@ -206,13 +239,12 @@ public void ShouldBeAbleToAssertAgainstPropertyAttributes() Assert.That( () => { - var foo = Expect(data) + Expect(data) .To.Have.Property(nameof(data.IsCommented)) .With.Type() .And .With - .Attribute(o => o.Comment == "this is commented!") - ; + .Attribute(o => o.Comment == "this is commented!"); }, Throws.Nothing ); @@ -242,7 +274,7 @@ public void ShouldBeAbleToAssertAgainstPropertyAttributes() Assert.That( () => { - var foo = Expect(data) + Expect(data) .To.Have.Property(nameof(data.IsCommented)) .With.Attribute(); },