mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
108 lines
4.4 KiB
C#
108 lines
4.4 KiB
C#
using System.Reflection;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Routing;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// Two endpoints that claim the same method + path make ASP.NET throw
|
|
/// <c>AmbiguousMatchException</c> (HTTP 500) for EVERY request to that path, before any
|
|
/// authorization or handler code runs. That is how a club "set minimum level" or "remove member"
|
|
/// click became an unhandled exception in the server log instead of an answer.
|
|
///
|
|
/// The route table is therefore asserted directly: a controller-level test cannot see two
|
|
/// endpoints fighting over one URL, because it calls the handler it wants by name.
|
|
///
|
|
/// The classic way this happens here is an action carrying both the class-relative template
|
|
/// (<c>"{clubId}/minlevel"</c> under <c>[Route("clubs/club")]</c>) and an absolute
|
|
/// <c>"~/clubs/club/{clubId}/minlevel"</c> copy of it: the same path, registered twice.
|
|
/// </summary>
|
|
public class RouteAmbiguityTests
|
|
{
|
|
/// <summary>
|
|
/// <c>{id}</c> and <c>{id:long}</c> both match a numeric segment with the same route
|
|
/// precedence, so ASP.NET reports ambiguity for that pair as well - compare on the
|
|
/// constraint-free form to catch both kinds.
|
|
/// </summary>
|
|
private static string Unconstrained(string template)
|
|
{
|
|
var result = new System.Text.StringBuilder(template.Length);
|
|
var index = 0;
|
|
|
|
while (true)
|
|
{
|
|
var start = template.IndexOf('{', index);
|
|
if (start < 0)
|
|
{
|
|
result.Append(template, index, template.Length - index);
|
|
return result.ToString();
|
|
}
|
|
|
|
var end = template.IndexOf('}', start);
|
|
if (end < 0)
|
|
{
|
|
result.Append(template, index, template.Length - index);
|
|
return result.ToString();
|
|
}
|
|
|
|
var name = template.Substring(start + 1, end - start - 1).TrimStart('*');
|
|
var cut = name.IndexOfAny(new[] { ':', '(' });
|
|
if (cut >= 0) name = name.Substring(0, cut);
|
|
|
|
result.Append(template, index, start - index).Append('{').Append(name).Append('}');
|
|
index = end + 1;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<(string Verb, string Template, string Owner)> Routes()
|
|
{
|
|
var assembly = typeof(RadicadeServer.Clubs.ClubsAPI).Assembly;
|
|
|
|
foreach (var type in assembly.GetTypes())
|
|
{
|
|
if (!typeof(ControllerBase).IsAssignableFrom(type) || type.IsAbstract) continue;
|
|
|
|
var classRoutes = type.GetCustomAttributes<RouteAttribute>(inherit: false)
|
|
.Select(attribute => (attribute.Template ?? string.Empty).Trim('/'))
|
|
.ToArray();
|
|
|
|
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
|
{
|
|
foreach (var attribute in method.GetCustomAttributes<HttpMethodAttribute>(inherit: false))
|
|
{
|
|
var template = attribute.Template ?? string.Empty;
|
|
|
|
if (template.StartsWith("~/", StringComparison.Ordinal))
|
|
{
|
|
foreach (var verb in attribute.HttpMethods)
|
|
yield return (verb, template.Substring(2), $"{type.Name}.{method.Name}");
|
|
continue;
|
|
}
|
|
|
|
var prefixes = classRoutes.Length == 0 ? new[] { string.Empty } : classRoutes;
|
|
foreach (var prefix in prefixes)
|
|
{
|
|
var combined = prefix.Length == 0 ? template : $"{prefix}/{template.TrimStart('/')}";
|
|
foreach (var verb in attribute.HttpMethods)
|
|
yield return (verb, combined, $"{type.Name}.{method.Name}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NoTwoEndpointsClaimTheSameMethodAndPath()
|
|
{
|
|
var duplicates = Routes()
|
|
.GroupBy(route => (route.Verb, Path: Unconstrained(route.Template)))
|
|
.Where(group => group.Select(route => (route.Owner, route.Template)).Distinct().Count() > 1)
|
|
.Select(group => $"{group.Key.Verb} /{group.Key.Path} is claimed by " +
|
|
string.Join(" and ", group.Select(route => $"{route.Owner} [{route.Template}]").Distinct()))
|
|
.ToArray();
|
|
|
|
Assert.Empty(duplicates);
|
|
}
|
|
}
|