Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion InertiaCore/Inertia.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<string, object?> data) => _factory.Share(data);
Expand Down
4 changes: 4 additions & 0 deletions InertiaCore/ResponseFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal interface IResponseFactory
public void Version(Func<string?> 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<string, object?> data);
public AlwaysProp Always(object? value);
Expand Down Expand Up @@ -109,6 +110,9 @@ public async Task<IHtmlContent> 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!;
Expand Down
25 changes: 25 additions & 0 deletions InertiaCore/Utils/BackResult.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
174 changes: 174 additions & 0 deletions InertiaCoreTests/UnitTestBack.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponse>();
response.SetupGet(r => r.Headers).Returns(responseHeaders);
response.SetupProperty(r => r.StatusCode);

var request = new Mock<HttpRequest>();
request.SetupGet(r => r.Headers).Returns(headers);

var httpContext = new Mock<HttpContext>();
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<HttpResponse>();
response.SetupGet(r => r.Headers).Returns(responseHeaders);
response.SetupProperty(r => r.StatusCode);

var request = new Mock<HttpRequest>();
request.SetupGet(r => r.Headers).Returns(headers);

var httpContext = new Mock<HttpContext>();
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<HttpResponse>();
response.SetupGet(r => r.Headers).Returns(responseHeaders);
response.SetupProperty(r => r.StatusCode);

var request = new Mock<HttpRequest>();
request.SetupGet(r => r.Headers).Returns(headers);

var httpContext = new Mock<HttpContext>();
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<HttpResponse>();
response.SetupGet(r => r.Headers).Returns(responseHeaders);
response.SetupProperty(r => r.StatusCode);

var request = new Mock<HttpRequest>();
request.SetupGet(r => r.Headers).Returns(headers);

var httpContext = new Mock<HttpContext>();
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<HttpResponse>();
response.SetupGet(r => r.Headers).Returns(responseHeaders);
response.SetupProperty(r => r.StatusCode);

var request = new Mock<HttpRequest>();
request.SetupGet(r => r.Headers).Returns(headers);

var httpContext = new Mock<HttpContext>();
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"));
}
}