Skip to content

Commit

Permalink
✨ add ability to assert against decorations on controller type methods
Browse files Browse the repository at this point in the history
  • Loading branch information
fluffynuts committed Nov 28, 2023
1 parent 8944c0f commit b308af4
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 54 deletions.
24 changes: 24 additions & 0 deletions src/NExpect.Matchers.AspNetCore.Tests/TestControllerMatchers.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
using NExpect.Exceptions;
Expand Down Expand Up @@ -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<RouteAttribute>(o => o.Template == "post-only");
}, Throws.Nothing);
Assert.That(() =>
{
Expect(sut)
.To.Have.Method(nameof(TestController.PostOnly))
.With.Attribute<RouteAttribute>(o => o.Template == "post-only");
}, Throws.Nothing);
// Assert
}

[Test]
public void ShouldBeAbleToTestMultipleVerbs()
{
Expand Down
191 changes: 142 additions & 49 deletions src/NExpect.Matchers.AspNetCore/ControllerMatchers.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -73,7 +74,8 @@ Func<string> customMessageGenerator
customMessageGenerator
)
);
});
}
);
return result;
}

Expand Down Expand Up @@ -116,7 +118,8 @@ Func<string> customMessageGenerator
customMessageGenerator
)
);
});
}
);
return have.More();
}

Expand All @@ -130,7 +133,8 @@ Func<string> customMessageGenerator
public static IMore<Type> Route(
this IHave<Type> have,
string member,
string expected)
string expected
)
{
have.AddMatcher(
actual =>
Expand All @@ -150,7 +154,8 @@ public static IMore<Type> Route(
passed,
() => $"Expected {actual}.{method} {passed.AsNot()}to have route '{expected}'"
);
});
}
);
return have.More();
}

Expand Down Expand Up @@ -191,7 +196,8 @@ public AndSupportingExtension Supporting(HttpMethod method)
passed,
() => $"Expected {controllerType}.{Member} to support HttpMethod {method}"
);
});
}
);
return Next();
}

Expand All @@ -205,6 +211,85 @@ public AndSupportingExtension Supporting(HttpMethod method)
/// </summary>
public SupportingExtension And => this;

/// <summary>
/// Asserts that the method is decorated with the expected attribute
/// </summary>
/// <typeparam name="TAttribute"></typeparam>
/// <returns></returns>
public SupportingExtension Attribute<TAttribute>()
{
return Attribute<TAttribute>(a => true);
}

/// <summary>
/// Asserts that the method is decorated with the expected attribute
/// </summary>
/// <param name="matcher"></param>
/// <typeparam name="TAttribute"></typeparam>
/// <returns></returns>
public SupportingExtension Attribute<TAttribute>(
Func<TAttribute, bool> matcher
)
{
Continuation.AddMatcher(
controllerType =>
{
var method = controllerType.GetMethods()
.Where(o => o.Name == Member)
.ToArray();

return method.Length switch
{
0 => CreateMissingMethodResult(controllerType),
1 => VerifyAttribute<TAttribute>(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<TAttribute>(
Type controllerType,
MethodInfo methodInfo,
Func<TAttribute, bool> matcher
)
{
var passed = methodInfo.GetCustomAttributes(true)
.OfType<TAttribute>()
.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"
);
}

/// <summary>
/// Asserts that the controller action being operated on has the specified route
/// </summary>
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -303,52 +392,55 @@ public static IMore<Type> Area(
Func<string> 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<AreaAttribute>()
.FirstOrDefault();
var attrib = actual.GetCustomAttributes(inherit: false)
.OfType<AreaAttribute>()
.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
)
);
});
);
}

/// <summary>
Expand All @@ -367,7 +459,8 @@ public class AndSupportingExtension
internal AndSupportingExtension(
IHave<Type> continuation,
string member,
SupportingExtension supportingExtension)
SupportingExtension supportingExtension
)
{
_continuation = continuation;
_member = member;
Expand Down
42 changes: 37 additions & 5 deletions src/NExpect.Tests/ObjectEquality/TestReflectiveMatchers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -188,6 +198,29 @@ public void ShouldBehaveOnTypeAsType()
// Assert
}

[Test]
public void ShouldBeAbleToAssertAgainstMethodAttributes()
{
// Arrange
var type = typeof(Data);
var sut = GetRandom<Data>();
// Act
Assert.That(() =>
{
Expect(type)
.To.Have.Method(nameof(Data.CommentedMethod))
.With.Attribute<CommentAttribute>(o => o.Comment == "moo-cakes");
}, Throws.Nothing);

Assert.That(() =>
{
Expect(sut)
.To.Have.Method(nameof(Data.CommentedMethod))
.With.Attribute<CommentAttribute>(o => o.Comment == "moo-cakes");
}, Throws.Nothing);
// Assert
}

[Test]
public void ShouldBeAbleToAssertAgainstPropertyAttributes()
{
Expand All @@ -197,7 +230,7 @@ public void ShouldBeAbleToAssertAgainstPropertyAttributes()
Assert.That(
() =>
{
var foo = Expect(data)
Expect(data)
.To.Have.Property(nameof(data.IsCommented))
.With.Attribute<CommentAttribute>();
},
Expand All @@ -206,13 +239,12 @@ public void ShouldBeAbleToAssertAgainstPropertyAttributes()
Assert.That(
() =>
{
var foo = Expect(data)
Expect(data)
.To.Have.Property(nameof(data.IsCommented))
.With.Type<bool>()
.And
.With
.Attribute<CommentAttribute>(o => o.Comment == "this is commented!")
;
.Attribute<CommentAttribute>(o => o.Comment == "this is commented!");
},
Throws.Nothing
);
Expand Down Expand Up @@ -242,7 +274,7 @@ public void ShouldBeAbleToAssertAgainstPropertyAttributes()
Assert.That(
() =>
{
var foo = Expect(data)
Expect(data)
.To.Have.Property(nameof(data.IsCommented))
.With.Attribute<RequiredAttribute>();
},
Expand Down

0 comments on commit b308af4

Please sign in to comment.