test(ui): extend redirect guard meta-test to Tier-2 pattern templates (#204 review)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m41s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m33s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m11s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m48s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m42s

Fork adversarial review nit: Map_Keys_Should_Not_Begin_With_Forbidden_Prefix
covered only Tier-1 Map keys, not the Tier-2 PatternRule templates. That guard
invariant is the load-bearing protection for the un-prefix-guarded /api|/artwork|
/docs|/openapi surface, so make it self-enforcing over ALL rules — a future
prefix-violating template now fails the test instead of slipping through.

Exposes internal LegacyUiRedirects.PatternTemplates (InternalsVisibleTo already
set for ErsatzTV.Tests); stores the raw template on PatternRule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #260.
This commit is contained in:
2026-07-11 16:45:03 +02:00
co-authored by Claude Opus 4.8
parent 8a2238b62e
commit 495e450e2c
2 changed files with 21 additions and 0 deletions
+12
View File
@@ -178,6 +178,18 @@ public class LegacyUiRedirectsTests
.ShouldBeFalse($"Map key '{key}' must not begin with forbidden prefix '{prefix}'"); .ShouldBeFalse($"Map key '{key}' must not begin with forbidden prefix '{prefix}'");
} }
} }
// The guard invariant is load-bearing for the un-prefix-guarded /api|/artwork|
// /docs|/openapi surface, so it must cover the Tier-2 pattern templates too —
// not only the Tier-1 Map keys — or a future prefix-violating rule would slip in.
foreach (string template in LegacyUiRedirects.PatternTemplates)
{
foreach (string prefix in forbidden)
{
template.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
.ShouldBeFalse($"Pattern template '{template}' must not begin with forbidden prefix '{prefix}'");
}
}
} }
[Test] [Test]
+9
View File
@@ -280,14 +280,23 @@ public static class LegacyUiRedirects
private static bool IsPositiveInteger(string segment) => private static bool IsPositiveInteger(string segment) =>
int.TryParse(segment, NumberStyles.None, CultureInfo.InvariantCulture, out int id) && id > 0; int.TryParse(segment, NumberStyles.None, CultureInfo.InvariantCulture, out int id) && id > 0;
// Exposed for the guard-invariant meta-test (InternalsVisibleTo ErsatzTV.Tests):
// the source-side template of every Tier-2 rule, so the test can assert no
// template — not just no Tier-1 Map key — begins with a forbidden prefix.
internal static IEnumerable<string> PatternTemplates => Patterns.Select(rule => rule.Template);
private sealed class PatternRule private sealed class PatternRule
{ {
public PatternRule(string template, string target) public PatternRule(string template, string target)
{ {
Template = template;
Segments = template.Split('/', StringSplitOptions.RemoveEmptyEntries); Segments = template.Split('/', StringSplitOptions.RemoveEmptyEntries);
Target = target; Target = target;
} }
// The raw legacy route template, e.g. "/channels/{id}" (source side).
public string Template { get; }
// e.g. ["channels", "{id}"] // e.g. ["channels", "{id}"]
public string[] Segments { get; } public string[] Segments { get; }