using System.Reflection; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Xunit; namespace JobTrackerApi.Tests; public sealed class RouteUniquenessTests { [Fact] public void Controller_actions_have_one_handler_per_http_method_and_route() { var actions = typeof(JobTrackerApi.Controllers.JobApplicationsController).Assembly.GetTypes() .Where(type => !type.IsAbstract && typeof(ControllerBase).IsAssignableFrom(type)) .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) .SelectMany(method => method.GetCustomAttributes() .Select(attribute => new { Action = $"{type.Name}.{method.Name}", Methods = string.Join(",", attribute.HttpMethods.OrderBy(x => x)), Route = Normalize(type, attribute.Template), }))) .ToList(); var duplicates = actions .GroupBy(x => $"{x.Methods} {x.Route}", StringComparer.OrdinalIgnoreCase) .Where(group => group.Count() > 1) .Select(group => $"{group.Key}: {string.Join(", ", group.Select(x => x.Action))}") .ToList(); Assert.Empty(duplicates); } private static string Normalize(Type controller, string? actionTemplate) { var prefix = controller.GetCustomAttribute()?.Template ?? string.Empty; prefix = prefix.Replace("[controller]", controller.Name[..^"Controller".Length], StringComparison.OrdinalIgnoreCase); var route = $"{prefix.TrimEnd('/')}/{(actionTemplate ?? string.Empty).TrimStart('/')}".ToLowerInvariant(); return Regex.Replace(route, @"\{[^}:]+(?:[^}]+)?\}", match => $"{{parameter{match.Groups["constraint"].Value}}}"); } }