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
23 changes: 20 additions & 3 deletions Areas/Api/Controllers/TripEditorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.EntityFrameworkCore;
using Wayfarer.Models;
using Wayfarer.Models.Dtos.Editor;
Expand All @@ -20,6 +21,8 @@ namespace Wayfarer.Areas.Api.Controllers;
[Route("api/trips/{tripId:guid}/editor")]
public sealed partial class TripEditorController : ControllerBase
{
private const string PublicTripViewRouteName = "PublicTripView";

private readonly ApplicationDbContext _dbContext;
private readonly IWebHostEnvironment _environment;
private readonly IIconColorProvider _iconColorProvider;
Expand Down Expand Up @@ -362,11 +365,25 @@ private EditorOptionsDto BuildOptions()
new EditorLimitsDto(6, 1));
}

private string? GeneratePublicTripUrl(Guid tripId) =>
Url.Action("View", "TripViewer", new { area = "Public", id = tripId }, Request.Scheme);
/// <summary>
/// Generates absolute public trip links through the named attribute route to avoid conventional area fallback URLs.
/// </summary>
private string? GeneratePublicTripUrl(Guid tripId, int? progress = null)
{
object values = progress.HasValue
? new { id = tripId, progress = progress.Value }
: new { id = tripId };

return Url.RouteUrl(new UrlRouteContext
{
RouteName = PublicTripViewRouteName,
Values = values,
Protocol = Request.Scheme
});
}

private string? GenerateProgressPublicTripUrl(Guid tripId) =>
Url.Action("View", "TripViewer", new { area = "Public", id = tripId, progress = 1 }, Request.Scheme);
GeneratePublicTripUrl(tripId, progress: 1);

private IActionResult? RequireEditorUser(out string? userId)
{
Expand Down
4 changes: 2 additions & 2 deletions Areas/Public/Controllers/TripViewerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ public IActionResult ByTag(string slug, string? view, string? sort, int page = 1
return RedirectToRoute("PublicTripsIndex", new { tags = slug, view, sort, page });
}

// GET: /Public/Trips/View/{id}?embed=true
// GET: /Public/Trips/{id}?embed=true
[HttpGet]
[Route("/Public/Trips/{id}", Order = 2)]
[Route("/Public/Trips/{id}", Name = "PublicTripView", Order = 2)]
public async Task<IActionResult> View(Guid id, bool embed = false)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
Expand Down
31 changes: 27 additions & 4 deletions tests/Wayfarer.Tests/Controllers/TripEditorControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -12,6 +13,7 @@
using Wayfarer.Parsers;
using Wayfarer.Services;
using Wayfarer.Tests.Infrastructure;
using PublicTripViewerController = Wayfarer.Areas.Public.Controllers.TripViewerController;
using Xunit;

namespace Wayfarer.Tests.Controllers;
Expand All @@ -21,6 +23,8 @@ namespace Wayfarer.Tests.Controllers;
/// </summary>
public sealed class TripEditorControllerTests : TestBase
{
private const string PublicTripViewRouteName = "PublicTripView";

[Fact]
public async Task GetEditorStateWithoutUserReturnsUnauthorized()
{
Expand Down Expand Up @@ -152,6 +156,19 @@ public async Task GetEditorStateForPublicTripWithProgressEnabledReturnsBothPubli
Assert.Equal("https://example.test/Public/Trips/" + trip.Id + "?progress=1", metadata.ProgressPublicUrl);
}

[Fact]
public void PublicTripViewRouteUsesCanonicalPublicTripsTemplate()
{
var route = typeof(PublicTripViewerController)
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
.Single(method => method.Name == nameof(PublicTripViewerController.View))
.GetCustomAttributes(typeof(RouteAttribute), inherit: false)
.Cast<RouteAttribute>()
.Single(attribute => attribute.Name == PublicTripViewRouteName);

Assert.Equal("/Public/Trips/{id}", route.Template);
}

[Fact]
public async Task GetEditorStateMapsCoordinatesAndGeoJsonWithExpectedShapes()
{
Expand Down Expand Up @@ -265,9 +282,13 @@ public async Task GetEditorStateWithMissingAreaGeometryReturnsProblemDetails()

private static void ConfigureControllerWithUserRole(ControllerBase controller, string userId, string role = "User")
{
var httpContext = BuildHttpContextWithUser(userId, role);
httpContext.Request.Scheme = "https";
httpContext.Request.Host = new HostString("example.test");

controller.ControllerContext = new ControllerContext
{
HttpContext = BuildHttpContextWithUser(userId, role)
HttpContext = httpContext
};
}

Expand Down Expand Up @@ -297,12 +318,14 @@ private static TripEditorController BuildController(
new TripEditorSegmentMutationService(db),
Mock.Of<ILogger<TripEditorController>>());

var url = new Mock<IUrlHelper>();
url.Setup(u => u.Action(It.IsAny<UrlActionContext>()))
.Returns((UrlActionContext context) =>
var url = new Mock<IUrlHelper>(MockBehavior.Strict);
url.Setup(u => u.RouteUrl(It.IsAny<UrlRouteContext>()))
.Returns((UrlRouteContext context) =>
{
var id = context.Values?.GetType().GetProperty("id")?.GetValue(context.Values);
var progress = context.Values?.GetType().GetProperty("progress")?.GetValue(context.Values);
Assert.Equal(PublicTripViewRouteName, context.RouteName);
Assert.Equal("https", context.Protocol);
return progress == null
? $"https://example.test/Public/Trips/{id}"
: $"https://example.test/Public/Trips/{id}?progress={progress}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace Wayfarer.Tests.Controllers;
/// </summary>
public sealed class TripEditorMetadataControllerTests : TestBase
{
private const string PublicTripViewRouteName = "PublicTripView";

[Fact]
public async Task PatchMetadataForOwnerUpdatesMetadataAndReturnsMetadataOnlyEnvelope()
{
Expand Down Expand Up @@ -210,9 +212,13 @@ await PatchMetadata(

private static void ConfigureControllerWithUserRole(ControllerBase controller, string userId, string role = "User")
{
var httpContext = BuildHttpContextWithUser(userId, role);
httpContext.Request.Scheme = "https";
httpContext.Request.Host = new HostString("example.test");

controller.ControllerContext = new ControllerContext
{
HttpContext = BuildHttpContextWithUser(userId, role)
HttpContext = httpContext
};
}

Expand All @@ -235,12 +241,14 @@ private static TripEditorController BuildController(
new TripEditorSegmentMutationService(db),
Mock.Of<ILogger<TripEditorController>>());

var url = new Mock<IUrlHelper>();
url.Setup(u => u.Action(It.IsAny<UrlActionContext>()))
.Returns((UrlActionContext context) =>
var url = new Mock<IUrlHelper>(MockBehavior.Strict);
url.Setup(u => u.RouteUrl(It.IsAny<UrlRouteContext>()))
.Returns((UrlRouteContext context) =>
{
var id = context.Values?.GetType().GetProperty("id")?.GetValue(context.Values);
var progress = context.Values?.GetType().GetProperty("progress")?.GetValue(context.Values);
Assert.Equal(PublicTripViewRouteName, context.RouteName);
Assert.Equal("https", context.Protocol);
return progress == null
? $"https://example.test/Public/Trips/{id}"
: $"https://example.test/Public/Trips/{id}?progress={progress}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace Wayfarer.Tests.Controllers;
/// </summary>
public sealed class TripEditorTagsShareProgressControllerTests : TestBase
{
private const string PublicTripViewRouteName = "PublicTripView";

[Fact]
public async Task PutTagsReplacesCompleteSetAndReturnsAffectedTags()
{
Expand Down Expand Up @@ -233,12 +235,14 @@ private static TripEditorController BuildController(ApplicationDbContext db)
new TripEditorSegmentMutationService(db),
Mock.Of<ILogger<TripEditorController>>());

var url = new Mock<IUrlHelper>();
url.Setup(u => u.Action(It.IsAny<UrlActionContext>()))
.Returns((UrlActionContext context) =>
var url = new Mock<IUrlHelper>(MockBehavior.Strict);
url.Setup(u => u.RouteUrl(It.IsAny<UrlRouteContext>()))
.Returns((UrlRouteContext context) =>
{
var id = context.Values?.GetType().GetProperty("id")?.GetValue(context.Values);
var progress = context.Values?.GetType().GetProperty("progress")?.GetValue(context.Values);
Assert.Equal(PublicTripViewRouteName, context.RouteName);
Assert.Equal("https", context.Protocol);
return progress == null
? $"https://example.test/Public/Trips/{id}"
: $"https://example.test/Public/Trips/{id}?progress={progress}";
Expand All @@ -263,9 +267,13 @@ private static async Task<IActionResult> SendJson(

private static void ConfigureControllerWithUserRole(ControllerBase controller, string userId, string role = "User")
{
var httpContext = BuildHttpContextWithUser(userId, role);
httpContext.Request.Scheme = "https";
httpContext.Request.Host = new HostString("example.test");

controller.ControllerContext = new ControllerContext
{
HttpContext = BuildHttpContextWithUser(userId, role)
HttpContext = httpContext
};
}

Expand Down
Loading