diff --git a/TASVideos/TagHelpers/WikiLinkTagHelpers.cs b/TASVideos/TagHelpers/WikiLinkTagHelpers.cs index 59475486e..6973d1ec4 100644 --- a/TASVideos/TagHelpers/WikiLinkTagHelpers.cs +++ b/TASVideos/TagHelpers/WikiLinkTagHelpers.cs @@ -5,36 +5,42 @@ namespace TASVideos.TagHelpers; -public class PubLinkTagHelper : TagHelper +public class PubLinkTagHelper(IHtmlGenerator generator) : AnchorTagHelper(generator) { public int Id { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; - output.Attributes.Add("href", $"/{Id}M"); + Page = "/Publications/View"; + RouteValues.Add(nameof(Pages.Publications.ViewModel.Id), Id.ToString()); + base.Process(context, output); } } -public class SubLinkTagHelper : TagHelper +public class SubLinkTagHelper(IHtmlGenerator generator) : AnchorTagHelper(generator) { public int Id { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; - output.Attributes.Add("href", $"/{Id}S"); + Page = "/Submissions/View"; + RouteValues.Add(nameof(Pages.Submissions.ViewModel.Id), Id.ToString()); + base.Process(context, output); } } -public class GameLinkTagHelper : TagHelper +public class GameLinkTagHelper(IHtmlGenerator generator) : AnchorTagHelper(generator) { public int Id { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; - output.Attributes.Add("href", $"/{Id}G"); + Page = "/Games/Index"; + RouteValues.Add(nameof(Pages.Games.IndexModel.Id), Id.ToString()); + base.Process(context, output); } } @@ -85,7 +91,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu output.TagName = "a"; Page = "/Users/Profile"; - RouteValues.Add("UserName", Username); + RouteValues.Add(nameof(Pages.Users.ProfileModel.UserName), Username); await base.ProcessAsync(context, output); } } diff --git a/tests/TASVideos.RazorPages.Tests/TagHelpers/GameLinkTagHelperTests.cs b/tests/TASVideos.RazorPages.Tests/TagHelpers/GameLinkTagHelperTests.cs new file mode 100644 index 000000000..3a6153fde --- /dev/null +++ b/tests/TASVideos.RazorPages.Tests/TagHelpers/GameLinkTagHelperTests.cs @@ -0,0 +1,19 @@ +using TASVideos.Extensions; +using TASVideos.TagHelpers; + +namespace TASVideos.RazorPages.Tests.TagHelpers; + +[TestClass] +public sealed class GameLinkTagHelperTests : LinkTagHelperTestsBase +{ + [DataRow(123, "some game", """some game""")] + [TestMethod] + public async Task TestGameLinkHelper(int id, string label, string expected) + { + var generator = TestableHtmlGenerator.Create(out var viewCtx, ServiceCollectionExtensions.Aliases.First(kvp => kvp.Key is "/Games/Index")); + GameLinkTagHelper gameLinkHelper = new(generator) { Id = id, ViewContext = viewCtx }; + var output = GetOutputObj(contentsUnencoded: label, tagName: "game-link"); + await gameLinkHelper.ProcessAsync(GetHelperContext(), output); + Assert.AreEqual(expected, GetHtmlString(output)); + } +} diff --git a/tests/TASVideos.RazorPages.Tests/TagHelpers/LinkTagHelperTestsBase.cs b/tests/TASVideos.RazorPages.Tests/TagHelpers/LinkTagHelperTestsBase.cs new file mode 100644 index 000000000..e82a3aeac --- /dev/null +++ b/tests/TASVideos.RazorPages.Tests/TagHelpers/LinkTagHelperTestsBase.cs @@ -0,0 +1,31 @@ +using System.Collections.Immutable; + +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace TASVideos.RazorPages.Tests.TagHelpers; + +public abstract class LinkTagHelperTestsBase +{ + public static TagHelperContext GetHelperContext() + => new( + [], + ImmutableDictionary.Empty, + Guid.NewGuid().ToString("N")); + + public static string GetHtmlString(TagHelperOutput output) + { + using var writer = new StringWriter(); + output.WriteTo(writer, NullHtmlEncoder.Default); + return writer.ToString(); + } + + public static TagHelperOutput GetOutputObj(string contentsUnencoded, string tagName = "") + { + TagHelperOutput output = new( + tagName, + [], + (_, _) => Task.FromResult(new DefaultTagHelperContent())); + output.Content.SetContent(contentsUnencoded); + return output; + } +} diff --git a/tests/TASVideos.RazorPages.Tests/TagHelpers/MovieLinkTagHelperTests.cs b/tests/TASVideos.RazorPages.Tests/TagHelpers/MovieLinkTagHelperTests.cs new file mode 100644 index 000000000..ecb5204c9 --- /dev/null +++ b/tests/TASVideos.RazorPages.Tests/TagHelpers/MovieLinkTagHelperTests.cs @@ -0,0 +1,30 @@ +using TASVideos.Extensions; +using TASVideos.TagHelpers; + +namespace TASVideos.RazorPages.Tests.TagHelpers; + +[TestClass] +public class MovieLinkTagHelperTests : LinkTagHelperTestsBase +{ + [DataRow(1234, "some movie", """some movie""")] + [TestMethod] + public async Task TestPubLinkHelper(int id, string label, string expected) + { + var generator = TestableHtmlGenerator.Create(out var viewCtx, ServiceCollectionExtensions.Aliases.First(kvp => kvp.Key is "/Publications/View")); + PubLinkTagHelper pubLinkHelper = new(generator) { Id = id, ViewContext = viewCtx }; + var output = GetOutputObj(contentsUnencoded: label, tagName: "pub-link"); + await pubLinkHelper.ProcessAsync(GetHelperContext(), output); + Assert.AreEqual(expected, GetHtmlString(output)); + } + + [DataRow(1234, "some movie", """some movie""")] + [TestMethod] + public async Task TestSubLinkHelper(int id, string label, string expected) + { + var generator = TestableHtmlGenerator.Create(out var viewCtx, ServiceCollectionExtensions.Aliases.First(kvp => kvp.Key is "/Submissions/View")); + SubLinkTagHelper subLinkHelper = new(generator) { Id = id, ViewContext = viewCtx }; + var output = GetOutputObj(contentsUnencoded: label, tagName: "sub-link"); + await subLinkHelper.ProcessAsync(GetHelperContext(), output); + Assert.AreEqual(expected, GetHtmlString(output)); + } +} diff --git a/tests/TASVideos.RazorPages.Tests/TagHelpers/ProfileLinkTagHelperTests.cs b/tests/TASVideos.RazorPages.Tests/TagHelpers/ProfileLinkTagHelperTests.cs new file mode 100644 index 000000000..38fdd8f0c --- /dev/null +++ b/tests/TASVideos.RazorPages.Tests/TagHelpers/ProfileLinkTagHelperTests.cs @@ -0,0 +1,18 @@ +using TASVideos.TagHelpers; + +namespace TASVideos.RazorPages.Tests.TagHelpers; + +[TestClass] +public class ProfileLinkTagHelperTests : LinkTagHelperTestsBase +{ + [DataRow("YoshiRulz", "unused", """YoshiRulz""")] + [TestMethod] + public async Task TestProfileLinkHelper(string username, string label, string expected) + { + var generator = TestableHtmlGenerator.Create(out var viewCtx, [new("/Users/Profile", "Users/Profile/{Username}")]); + ProfileLinkTagHelper profileLinkHelper = new(generator) { Username = username, ViewContext = viewCtx }; + var output = GetOutputObj(contentsUnencoded: label, tagName: "profile-link"); + await profileLinkHelper.ProcessAsync(GetHelperContext(), output); + Assert.AreEqual(expected, GetHtmlString(output)); + } +} diff --git a/tests/TASVideos.RazorPages.Tests/TagHelpers/WikiLinkTagHelperTests.cs b/tests/TASVideos.RazorPages.Tests/TagHelpers/WikiLinkTagHelperTests.cs index e01917a3f..2edaf22f7 100644 --- a/tests/TASVideos.RazorPages.Tests/TagHelpers/WikiLinkTagHelperTests.cs +++ b/tests/TASVideos.RazorPages.Tests/TagHelpers/WikiLinkTagHelperTests.cs @@ -1,35 +1,22 @@ -using Microsoft.AspNetCore.Razor.TagHelpers; using TASVideos.TagHelpers; namespace TASVideos.RazorPages.Tests.TagHelpers; [TestClass] -public class WikiLinkTagHelperTests +public sealed class WikiLinkTagHelperTests : LinkTagHelperTestsBase { + [DataRow("GameResources/NES/SuperMarioBros", "unused", """GameResources/NES/SuperMarioBros""")] + [DataRow("WelcomeToTASVideos", "unused", """WelcomeToTASVideos""")] [TestMethod] - public void WikiLinkTagHelper_Process_RendersCorrectHtml() + public async Task WikiLinkTagHelper_Process_RendersCorrectHtml(string wikiPageName, string label, string expected) { - var tagHelper = new WikiLinkTagHelper { PageName = "GameResources/NES/SuperMarioBros" }; - var context = new TagHelperContext( - allAttributes: [], - items: new Dictionary(), - uniqueId: "test"); - var output = new TagHelperOutput( - tagName: "wiki-link", - attributes: [], - getChildContentAsync: (_, _) => - Task.FromResult(new DefaultTagHelperContent())); + var tagHelper = new WikiLinkTagHelper { PageName = wikiPageName }; + var context = GetHelperContext(); + var output = GetOutputObj(contentsUnencoded: label, tagName: "wiki-link"); - tagHelper.Process(context, output); + await tagHelper.ProcessAsync(context, output); var htmlString = GetHtmlString(output); - Assert.AreEqual("GameResources/NES/SuperMarioBros", htmlString); - } - - private static string GetHtmlString(TagHelperOutput output) - { - using var writer = new StringWriter(); - output.WriteTo(writer, NullHtmlEncoder.Default); - return writer.ToString(); + Assert.AreEqual(expected, htmlString); } } diff --git a/tests/TASVideos.RazorPages.Tests/TestableHtmlGenerator.cs b/tests/TASVideos.RazorPages.Tests/TestableHtmlGenerator.cs new file mode 100644 index 000000000..a7da23a3b --- /dev/null +++ b/tests/TASVideos.RazorPages.Tests/TestableHtmlGenerator.cs @@ -0,0 +1,134 @@ +/* + * taken from .NET 8 source, MIT-licensed + * specifically https://github.com/dotnet/aspnetcore/blob/v8.0.0/src/Mvc/Mvc.TagHelpers/test/TestableHtmlGenerator.cs + * but also I cut out most of it anyway + */ + +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.WebEncoders.Testing; + +namespace TASVideos.RazorPages.Tests; + +// ReSharper disable EmptyConstructor +internal class TestableHtmlGenerator( + IOptions options, + IModelMetadataProvider metadataProvider) : DefaultHtmlGenerator( + Substitute.For(), + options, + metadataProvider, + CreateUrlHelperFactory(), + new HtmlTestEncoder(), + new DefaultValidationHtmlAttributeProvider(options, metadataProvider, new())) +{ + private static IUrlHelperFactory CreateUrlHelperFactory() + { + var urlHelperFactory = Substitute.For(); + urlHelperFactory.GetUrlHelper(Arg.Any()) + .Returns(callInfo => new FakeUrlHelper(callInfo.ArgAt(0))); + return urlHelperFactory; + } + + private sealed class FakeUrlHelper(ActionContext context) : UrlHelperBase(context) + { + public override string Action(UrlActionContext actionContext) + => throw new NotSupportedException(); + + public override string? RouteUrl(UrlRouteContext routeContext) + { + var virtualPath = ActionContext.RouteData.Routers[0].GetVirtualPath(new( + ActionContext.HttpContext, + AmbientValues, + GetValuesDictionary(routeContext.Values), + routeContext.RouteName))?.VirtualPath; + + return GenerateUrl( + protocol: routeContext.Protocol, + host: routeContext.Host, + virtualPath: virtualPath, + fragment: routeContext.Fragment); + } + } + + public static TestableHtmlGenerator Create(out ViewContext viewCtx, params KeyValuePair[] routeStrs) + { + ServiceCollection services = []; + services.AddSingleton(); + + var routeOptionsWrapper = Substitute.For>(); + routeOptionsWrapper.Value.Returns(new RouteOptions()); + services.AddSingleton(routeOptionsWrapper); + + // equivalent to: + // ObjectPool objPool = new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()); + // DefaultTemplateBinderFactory defaultFactoryImpl = new(Substitute.For(), objPool); + var aspNetRoutingAsm = typeof(TemplateBinderFactory).Assembly; + var objPool = typeof(ObjectPoolProvider).GetMethods() + .First(mi => mi.Name is "Create" && mi.GetParameters().Length is 1) + .MakeGenericMethod(aspNetRoutingAsm.GetType("Microsoft.AspNetCore.Routing.UriBuildingContext")!) + .Invoke(new DefaultObjectPoolProvider(), parameters: [ + Activator.CreateInstance(aspNetRoutingAsm.GetType("Microsoft.AspNetCore.Routing.UriBuilderContextPooledObjectPolicy")!), + ]); + var defaultFactoryImpl = (TemplateBinderFactory)Activator.CreateInstance( + aspNetRoutingAsm.GetType("Microsoft.AspNetCore.Routing.Template.DefaultTemplateBinderFactory")!, + Substitute.For(), + objPool)!; + services.AddSingleton(defaultFactoryImpl); + var serviceProvider = services.BuildServiceProvider(); + + RouteHandler routeHandler = new(_ => Task.CompletedTask); + DefaultInlineConstraintResolver constraintResolver = new(routeOptionsWrapper, serviceProvider); + RouteCollection router = new(); + foreach (var (name, template) in routeStrs) + { + router.Add(new Route( + routeHandler, + routeName: name, + routeTemplate: template, + defaults: new([new KeyValuePair("page", name)]), + constraints: null, + dataTokens: null, + constraintResolver)); + } + + ModelStateDictionary modelState = new(); + EmptyModelMetadataProvider metadataProvider = new(); + viewCtx = new( + new( + new DefaultHttpContext { RequestServices = serviceProvider }, + new() { Routers = { router } }, + new(), + modelState), + Substitute.For(), + new(metadataProvider, modelState) { Model = null }, + Substitute.For(), + TextWriter.Null, + new()); + + IOptions mvcViewOptions = Substitute.For>(); + mvcViewOptions.Value.Returns(new MvcViewOptions()); + return new(mvcViewOptions, metadataProvider); + } + + protected override void AddValidationAttributes( + ViewContext viewContext, + TagBuilder tagBuilder, + ModelExplorer modelExplorer, + string expression) + => throw new NotSupportedException(); + + public override TagBuilder GenerateAntiforgery(ViewContext viewContext) + => throw new NotSupportedException(); +}