Files
ersatztv/ErsatzTV.Tests/LegacyUiRedirectsTests.cs
T
timothyandClaude Fable 5 439272b405
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m31s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
feat(web): SPA cutover — root route + legacy redirects (#91)
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>
2026-07-07 11:07:01 +02:00

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");
}
}