diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index d13b932..fd99eb2 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Net; +using System.Runtime.CompilerServices; using InertiaCore.Props; using InertiaCore.Utils; using Microsoft.AspNetCore.Html; @@ -27,6 +28,9 @@ public static class Inertia public static LocationResult Location(string url) => _factory.Location(url); + public static BackResult Back(string? fallbackUrl = null, HttpStatusCode statusCode = HttpStatusCode.SeeOther) => + _factory.Back(fallbackUrl, statusCode); + public static void Share(string key, object? value) => _factory.Share(key, value); public static void Share(IDictionary data) => _factory.Share(data); diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index ad57af9..69bff94 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -20,6 +20,7 @@ internal interface IResponseFactory public void Version(Func version); public string? GetVersion(); public LocationResult Location(string url); + public BackResult Back(string? fallbackUrl = null, HttpStatusCode statusCode = HttpStatusCode.SeeOther); public void Share(string key, object? value); public void Share(IDictionary data); public AlwaysProp Always(object? value); @@ -109,6 +110,9 @@ public async Task Html(dynamic model) public LocationResult Location(string url) => new(url); + public BackResult Back(string? fallbackUrl = null, HttpStatusCode statusCode = HttpStatusCode.SeeOther) => + new(fallbackUrl, statusCode); + public void Share(string key, object? value) { var context = _contextAccessor.HttpContext!; diff --git a/InertiaCore/Utils/BackResult.cs b/InertiaCore/Utils/BackResult.cs new file mode 100644 index 0000000..d3bd70d --- /dev/null +++ b/InertiaCore/Utils/BackResult.cs @@ -0,0 +1,25 @@ +using System.Net; +using InertiaCore.Extensions; +using Microsoft.AspNetCore.Mvc; + +namespace InertiaCore.Utils; + +public class BackResult : IActionResult +{ + private readonly string _fallbackUrl; + private readonly HttpStatusCode _statusCode; + + public BackResult(string? fallbackUrl = null, HttpStatusCode statusCode = HttpStatusCode.SeeOther) => + (_fallbackUrl, _statusCode) = (fallbackUrl ?? "/", statusCode); + + public Task ExecuteResultAsync(ActionContext context) + { + var referrer = context.HttpContext.Request.Headers.Referer.ToString(); + var redirectUrl = !string.IsNullOrEmpty(referrer) ? referrer : _fallbackUrl; + + context.HttpContext.Response.StatusCode = (int)_statusCode; + context.HttpContext.Response.Headers.Location = redirectUrl; + + return Task.CompletedTask; + } +} diff --git a/InertiaCoreTests/UnitTestBack.cs b/InertiaCoreTests/UnitTestBack.cs new file mode 100644 index 0000000..f3d0edc --- /dev/null +++ b/InertiaCoreTests/UnitTestBack.cs @@ -0,0 +1,174 @@ +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Moq; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test Back function with Inertia request returns redirect status with location header.")] + public async Task TestBackWithInertiaRequest() + { + var backResult = _factory.Back("/fallback"); + + var headers = new HeaderDictionary + { + { "X-Inertia", "true" } + }; + + var responseHeaders = new HeaderDictionary(); + var response = new Mock(); + response.SetupGet(r => r.Headers).Returns(responseHeaders); + response.SetupProperty(r => r.StatusCode); + + var request = new Mock(); + request.SetupGet(r => r.Headers).Returns(headers); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.Response).Returns(response.Object); + + var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + + await backResult.ExecuteResultAsync(context); + + // Should set status code to 303 (SeeOther) and location header to fallback URL + Assert.That(response.Object.StatusCode, Is.EqualTo(303)); + Assert.That(responseHeaders["Location"].ToString(), Is.EqualTo("/fallback")); + } + + [Test] + [Description("Test Back function with regular request and referrer header redirects to referrer.")] + public async Task TestBackWithReferrerHeader() + { + var backResult = _factory.Back("/fallback"); + + var headers = new HeaderDictionary + { + { "Referer", "https://example.com/previous-page" } + }; + + var responseHeaders = new HeaderDictionary(); + var response = new Mock(); + response.SetupGet(r => r.Headers).Returns(responseHeaders); + response.SetupProperty(r => r.StatusCode); + + var request = new Mock(); + request.SetupGet(r => r.Headers).Returns(headers); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.Response).Returns(response.Object); + + var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + + var result = backResult as IActionResult; + Assert.That(result, Is.Not.Null); + + await result.ExecuteResultAsync(context); + + // Should set status code to 303 (SeeOther) and location header to referrer + Assert.That(response.Object.StatusCode, Is.EqualTo(303)); + Assert.That(responseHeaders["Location"].ToString(), Is.EqualTo("https://example.com/previous-page")); + } + + [Test] + [Description("Test Back function without referrer uses fallback URL.")] + public async Task TestBackWithFallbackUrl() + { + var backResult = _factory.Back("/custom-fallback"); + + var headers = new HeaderDictionary(); + + var responseHeaders = new HeaderDictionary(); + var response = new Mock(); + response.SetupGet(r => r.Headers).Returns(responseHeaders); + response.SetupProperty(r => r.StatusCode); + + var request = new Mock(); + request.SetupGet(r => r.Headers).Returns(headers); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.Response).Returns(response.Object); + + var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + + var result = backResult as IActionResult; + Assert.That(result, Is.Not.Null); + + await result.ExecuteResultAsync(context); + + // Should set status code to 303 (SeeOther) and location header to custom fallback + Assert.That(response.Object.StatusCode, Is.EqualTo(303)); + Assert.That(responseHeaders["Location"].ToString(), Is.EqualTo("/custom-fallback")); + } + + [Test] + [Description("Test Back function without fallback URL uses default root path.")] + public async Task TestBackWithDefaultFallback() + { + var backResult = _factory.Back(); + + var headers = new HeaderDictionary(); + + var responseHeaders = new HeaderDictionary(); + var response = new Mock(); + response.SetupGet(r => r.Headers).Returns(responseHeaders); + response.SetupProperty(r => r.StatusCode); + + var request = new Mock(); + request.SetupGet(r => r.Headers).Returns(headers); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.Response).Returns(response.Object); + + var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + + var result = backResult as IActionResult; + Assert.That(result, Is.Not.Null); + + await result.ExecuteResultAsync(context); + + // Should set status code to 303 (SeeOther) and location header to default fallback + Assert.That(response.Object.StatusCode, Is.EqualTo(303)); + Assert.That(responseHeaders["Location"].ToString(), Is.EqualTo("/")); + } + + [Test] + [Description("Test Back function with permanent redirect.")] + public async Task TestBackWithPermanentRedirect() + { + var backResult = _factory.Back("/fallback", HttpStatusCode.MovedPermanently); + + var headers = new HeaderDictionary(); + + var responseHeaders = new HeaderDictionary(); + var response = new Mock(); + response.SetupGet(r => r.Headers).Returns(responseHeaders); + response.SetupProperty(r => r.StatusCode); + + var request = new Mock(); + request.SetupGet(r => r.Headers).Returns(headers); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.Response).Returns(response.Object); + + var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + + var result = backResult as IActionResult; + Assert.That(result, Is.Not.Null); + + await result.ExecuteResultAsync(context); + + // Should set status code to 301 (MovedPermanently) and location header to fallback + Assert.That(response.Object.StatusCode, Is.EqualTo(301)); + Assert.That(responseHeaders["Location"].ToString(), Is.EqualTo("/fallback")); + } +}