diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index d13b932..e1b6dff 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -2,6 +2,7 @@ using InertiaCore.Props; using InertiaCore.Utils; using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc; [assembly: InternalsVisibleTo("InertiaCoreTests")] @@ -31,6 +32,8 @@ public static class Inertia public static void Share(IDictionary data) => _factory.Share(data); + public static void ResolveUrlUsing(Func urlResolver) => _factory.ResolveUrlUsing(urlResolver); + public static AlwaysProp Always(string value) => _factory.Always(value); public static AlwaysProp Always(Func callback) => _factory.Always(callback); diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 7bdd2cb..9155799 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -18,13 +18,14 @@ public class Response : IActionResult private readonly string? _version; private readonly bool _encryptHistory; private readonly bool _clearHistory; + private readonly Func? _urlResolver; private ActionContext? _context; private Page? _page; private IDictionary? _viewData; - internal Response(string component, Dictionary props, string rootView, string? version, bool encryptHistory, bool clearHistory) - => (_component, _props, _rootView, _version, _encryptHistory, _clearHistory) = (component, props, rootView, version, encryptHistory, clearHistory); + internal Response(string component, Dictionary props, string rootView, string? version, bool encryptHistory, bool clearHistory, Func? urlResolver = null) + => (_component, _props, _rootView, _version, _encryptHistory, _clearHistory, _urlResolver) = (component, props, rootView, version, encryptHistory, clearHistory, urlResolver); public async Task ExecuteResultAsync(ActionContext context) { @@ -41,7 +42,7 @@ protected internal async Task ProcessResponse() { Component = _component, Version = _version, - Url = _context!.RequestedUri(), + Url = _urlResolver?.Invoke(_context!) ?? _context!.RequestedUri(), Props = props, EncryptHistory = _encryptHistory, ClearHistory = _clearHistory, diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 534b7ba..a3db966 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -7,6 +7,7 @@ using InertiaCore.Utils; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace InertiaCore; @@ -24,6 +25,7 @@ internal interface IResponseFactory public void Share(IDictionary data); public void ClearHistory(bool clear = true); public void EncryptHistory(bool encrypt = true); + public void ResolveUrlUsing(Func urlResolver); public AlwaysProp Always(object? value); public AlwaysProp Always(Func callback); public AlwaysProp Always(Func> callback); @@ -40,6 +42,7 @@ internal class ResponseFactory : IResponseFactory private object? _version; private bool _clearHistory; private bool? _encryptHistory; + private Func? _urlResolver; public ResponseFactory(IHttpContextAccessor contextAccessor, IGateway gateway, IOptions options) => (_contextAccessor, _gateway, _options) = (contextAccessor, gateway, options); @@ -54,7 +57,7 @@ public Response Render(string component, object? props = null) .ToDictionary(o => o.Name, o => o.GetValue(props)) }; - return new Response(component, dictProps, _options.Value.RootView, GetVersion(), _encryptHistory ?? _options.Value.EncryptHistory, _clearHistory); + return new Response(component, dictProps, _options.Value.RootView, GetVersion(), _encryptHistory ?? _options.Value.EncryptHistory, _clearHistory, _urlResolver); } public async Task Head(dynamic model) @@ -139,6 +142,8 @@ public void Share(IDictionary data) public void EncryptHistory(bool encrypt = true) => _encryptHistory = encrypt; + public void ResolveUrlUsing(Func urlResolver) => _urlResolver = urlResolver; + public LazyProp Lazy(Func callback) => new(callback); public LazyProp Lazy(Func> callback) => new(callback); public AlwaysProp Always(object? value) => new(value); diff --git a/InertiaCoreTests/UnitTestUrlResolver.cs b/InertiaCoreTests/UnitTestUrlResolver.cs new file mode 100644 index 0000000..2d31294 --- /dev/null +++ b/InertiaCoreTests/UnitTestUrlResolver.cs @@ -0,0 +1,147 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if custom URL resolver is used when provided.")] + public async Task TestCustomUrlResolver() + { + // Set up a custom URL resolver + _factory.ResolveUrlUsing(context => "/custom/url"); + + var response = _factory.Render("Test/Page", new + { + Test = "Test" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Url, Is.EqualTo("/custom/url")); + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if default URL resolver is used when no custom resolver is provided.")] + public async Task TestDefaultUrlResolver() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + // Should use the default RequestedUri() method + Assert.That(page?.Url, Is.Not.Null); + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if custom URL resolver receives correct ActionContext.")] + public async Task TestUrlResolverReceivesContext() + { + ActionContext? receivedContext = null; + + // Set up a custom URL resolver that captures the context + _factory.ResolveUrlUsing(context => + { + receivedContext = context; + return "/captured/context/url"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Url, Is.EqualTo("/captured/context/url")); + Assert.That(receivedContext, Is.Not.Null); + Assert.That(receivedContext, Is.EqualTo(context)); + } + + [Test] + [Description("Test if custom URL resolver can access request information.")] + public async Task TestUrlResolverAccessesRequest() + { + // Set up a custom URL resolver that uses request path + _factory.ResolveUrlUsing(context => + { + var path = context.HttpContext.Request.Path; + return $"/custom{path}"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Url, Is.Not.Null); + Assert.That(page?.Url, Does.StartWith("/custom")); + } + + [Test] + [Description("Test if URL resolver can be changed between requests.")] + public async Task TestUrlResolverCanBeChanged() + { + // First resolver + _factory.ResolveUrlUsing(context => "/first/url"); + + var response1 = _factory.Render("Test/Page", new { Test = "Test1" }); + var context1 = PrepareContext(); + + response1.SetContext(context1); + await response1.ProcessResponse(); + + var page1 = response1.GetJson().Value as Page; + Assert.That(page1?.Url, Is.EqualTo("/first/url")); + + // Change resolver + _factory.ResolveUrlUsing(context => "/second/url"); + + var response2 = _factory.Render("Test/Page", new { Test = "Test2" }); + var context2 = PrepareContext(); + + response2.SetContext(context2); + await response2.ProcessResponse(); + + var page2 = response2.GetJson().Value as Page; + Assert.That(page2?.Url, Is.EqualTo("/second/url")); + } +} \ No newline at end of file