Make the ChicoryTV SPA the default UI: GET/HEAD requests to / and to legacy Blazor routes with SPA equivalents (channels, schedules, playouts, libraries, all settings pages, channel add) now 302 to their /app counterparts, preserving query strings and the ETV_BASE_URL path base. 302 not 301: the map will grow as parity lands and permanent- redirect caching would make rollback painful. Blazor-only functionality (collections, media browse/search, trakt, filler presets, watermarks, ffmpeg profiles, blocks/decos/templates, playout detail editors, logs, troubleshooting, channel edit) keeps serving Blazor; the Blazor home stays reachable at /system/health. Parity gaps are tracked in #140-#146; Blazor removal is phase (b). Also adds an /app smoke assertion to the docker build workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.4 KiB
C#
113 lines
4.4 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests;
|
|
|
|
[TestFixture]
|
|
public class LegacyUiRedirectsTests
|
|
{
|
|
private static readonly string StartupSource = File.ReadAllText(FindStartupPath());
|
|
|
|
[Test]
|
|
public void Every_Mapping_Should_Resolve()
|
|
{
|
|
foreach ((string from, string to) in LegacyUiRedirects.Map)
|
|
{
|
|
LegacyUiRedirects.TryGetRedirect(new PathString(from), out string target).ShouldBeTrue();
|
|
target.ShouldBe(to);
|
|
}
|
|
}
|
|
|
|
[TestCase("/", "/app")]
|
|
[TestCase("/channels", "/app/channels")]
|
|
[TestCase("/channels/add", "/app/new-channel")]
|
|
[TestCase("/schedules", "/app/schedules")]
|
|
[TestCase("/playouts", "/app/playouts")]
|
|
[TestCase("/media/libraries", "/app/libraries")]
|
|
[TestCase("/settings/ffmpeg", "/app/settings/streaming")]
|
|
[TestCase("/settings/hdhr", "/app/settings/system")]
|
|
[TestCase("/settings/logging", "/app/settings/logging")]
|
|
[TestCase("/settings/playout", "/app/settings/playout")]
|
|
[TestCase("/settings/scanner", "/app/settings/scanner")]
|
|
[TestCase("/settings/ui", "/app/settings/general")]
|
|
[TestCase("/settings/xmltv", "/app/settings/xmltv")]
|
|
public void Known_Route_Should_Redirect(string path, string expected)
|
|
{
|
|
LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
|
|
target.ShouldBe(expected);
|
|
}
|
|
|
|
[TestCase("/channels/", "/app/channels")]
|
|
[TestCase("/schedules/", "/app/schedules")]
|
|
[TestCase("/settings/ffmpeg/", "/app/settings/streaming")]
|
|
public void Trailing_Slash_Should_Match(string path, string expected)
|
|
{
|
|
LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
|
|
target.ShouldBe(expected);
|
|
}
|
|
|
|
[Test]
|
|
public void Lookup_Should_Be_Case_Insensitive()
|
|
{
|
|
LegacyUiRedirects.TryGetRedirect(new PathString("/Channels"), out string target).ShouldBeTrue();
|
|
target.ShouldBe("/app/channels");
|
|
}
|
|
|
|
[TestCase("/channels/5")] // channel edit (Blazor-only)
|
|
[TestCase("/channels/numbers")] // Blazor-only
|
|
[TestCase("/system/health")] // Blazor home escape hatch
|
|
[TestCase("/media/collections")] // Blazor-only media page
|
|
[TestCase("/ffmpeg")] // Blazor-only
|
|
[TestCase("/watermarks")] // Blazor-only
|
|
[TestCase("/app")] // already the SPA
|
|
[TestCase("/app/channels")] // already the SPA
|
|
[TestCase("/iptv/channels.m3u")] // IPTV surface
|
|
[TestCase("/api/health")] // API surface
|
|
[TestCase("")] // empty
|
|
[TestCase("//")] // all-slash path must not collapse to root "/"
|
|
[TestCase("/channels//")] // double trailing slash is not normalized to a match
|
|
public void Non_Migrated_Route_Should_Not_Redirect(string path)
|
|
{
|
|
LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeFalse();
|
|
target.ShouldBe(string.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void Startup_Should_Redirect_Legacy_Routes_In_Blazor_Branch_Before_Routing()
|
|
{
|
|
// The redirect middleware must be wired inside the blazor branch and run
|
|
// before UseRouting so migrated routes never reach the Blazor fallback.
|
|
int redirectIndex = StartupSource.IndexOf("LegacyUiRedirects.TryGetRedirect", StringComparison.Ordinal);
|
|
redirectIndex.ShouldBeGreaterThan(-1);
|
|
|
|
// The Blazor branch's UseRouting call that follows the redirect middleware.
|
|
int routingIndex = StartupSource.IndexOf("blazor.UseRouting()", StringComparison.Ordinal);
|
|
routingIndex.ShouldBeGreaterThan(-1);
|
|
|
|
redirectIndex.ShouldBeLessThan(routingIndex);
|
|
|
|
// 302 (temporary), not a permanent redirect.
|
|
StartupSource.ShouldContain("context.Request.PathBase + target");
|
|
StartupSource.ShouldNotContain("RedirectPermanent(target");
|
|
}
|
|
|
|
private static string FindStartupPath()
|
|
{
|
|
DirectoryInfo? directory = new(TestContext.CurrentContext.TestDirectory);
|
|
|
|
while (directory is not null)
|
|
{
|
|
string candidate = Path.Combine(directory.FullName, "ErsatzTV", "Startup.cs");
|
|
if (File.Exists(candidate))
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
|
|
throw new FileNotFoundException("Could not find ErsatzTV/Startup.cs");
|
|
}
|
|
}
|