fix(api): #286 review — allowlist non-/api routes in the versioning test; base-url-aware deprecation Link
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m5s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 2m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m2s
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 / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m49s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m43s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m5s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 2m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m2s
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 / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m49s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m43s
Cold-fork + Codex review of PR #326: - ApiRouteVersioningTests: iterate IRouteTemplateProvider (covers a template-less [HttpGet] paired with an action-level [Route]) and assert any non-/api route against an explicit KnownNonApiRoutes allowlist instead of silently skipping — an accidental absolute non-/api route (which would also escape ApiAuthorizationFilter's /api-scoped gate) now fails the test. - ApiVersionRewriteMiddleware: root the deprecation Link at Request.PathBase so it stays correct under ETV_BASE_URL (</etv/docs>, not host-root </docs>). refs #286 #197 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #326.
This commit is contained in:
@@ -21,6 +21,13 @@ public class ApiRouteVersioningTests
|
||||
{
|
||||
private static readonly Regex VersionedAbsolute = new(@"^/api/v\d+/", RegexOptions.Compiled);
|
||||
|
||||
// Routes on an API controller that deliberately live OUTSIDE /api because they are browser-navigation
|
||||
// endpoints, not part of the JSON API surface (api-conventions §9). This is an explicit allowlist, NOT
|
||||
// a blanket skip: an accidental non-/api absolute route (e.g. a stray [HttpPost("/channels")]) must fail
|
||||
// here, because it would also escape ApiAuthorizationFilter's /api-scoped gate → an unauthenticated
|
||||
// mutation. The only intentional entry today is AuthController's OIDC challenge.
|
||||
private static readonly string[] KnownNonApiRoutes = ["/auth/oidc/login"];
|
||||
|
||||
[Test]
|
||||
public void Every_Api_Controller_Action_Route_Should_Be_Versioned_And_Absolute()
|
||||
{
|
||||
@@ -49,24 +56,31 @@ public class ApiRouteVersioningTests
|
||||
foreach (MethodInfo action in controllerType
|
||||
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
foreach (HttpMethodAttribute httpAttribute in action
|
||||
.GetCustomAttributes<HttpMethodAttribute>(inherit: true))
|
||||
// IRouteTemplateProvider covers BOTH [HttpGet("...")] (HttpMethodAttribute) and a bare
|
||||
// action-level [Route("...")] — so a template-less [HttpGet] paired with [Route("...")]
|
||||
// can't slip an unversioned route past this net (cold-review nit, #326).
|
||||
foreach (IRouteTemplateProvider routeProvider in action
|
||||
.GetCustomAttributes(inherit: true)
|
||||
.OfType<IRouteTemplateProvider>())
|
||||
{
|
||||
string? effective = CombineRoute(controllerTemplate, httpAttribute.Template);
|
||||
string? effective = CombineRoute(controllerTemplate, routeProvider.Template);
|
||||
if (effective is null)
|
||||
{
|
||||
// No route on the controller or the action → not a routable API endpoint; skip.
|
||||
continue;
|
||||
}
|
||||
|
||||
// A few actions on API controllers deliberately live OUTSIDE /api because they are
|
||||
// browser-navigation endpoints, not part of the JSON API surface — e.g.
|
||||
// AuthController's GET /auth/oidc/login OIDC challenge (api-conventions §9). The
|
||||
// versioning convention only governs the /api surface; a non-/api nav route is a
|
||||
// different surface and must NOT be forced to /api/v1.
|
||||
// A route outside /api must be a KNOWN, intentional browser-nav endpoint — never a
|
||||
// silent skip (see KnownNonApiRoutes above for why: it would also escape the /api-scoped
|
||||
// auth filter). A new one fails here until it's explicitly allowlisted or versioned.
|
||||
if (!effective.StartsWith("/api/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!effective.Equals("/api", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
KnownNonApiRoutes.ShouldContain(
|
||||
effective,
|
||||
$"{controllerType.Name}.{action.Name} route '{effective}' is neither versioned " +
|
||||
"(/api/v1/…) nor a known non-/api browser-nav endpoint — version it, or add it to " +
|
||||
"KnownNonApiRoutes if it is intentionally outside the JSON API surface.");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ public sealed class ApiVersionRewriteMiddleware
|
||||
{
|
||||
context.Request.Path = rewritten;
|
||||
context.Response.Headers["Deprecation"] = "true";
|
||||
context.Response.Headers["Link"] = "</docs>; rel=\"deprecation\"";
|
||||
// Root the docs link at the request PathBase so it stays correct under a reverse-proxy
|
||||
// base URL (ETV_BASE_URL): empty PathBase → </docs>, PathBase "/etv" → </etv/docs>.
|
||||
context.Response.Headers["Link"] = $"<{context.Request.PathBase}/docs>; rel=\"deprecation\"";
|
||||
if (!string.IsNullOrWhiteSpace(_sunset))
|
||||
{
|
||||
context.Response.Headers["Sunset"] = _sunset;
|
||||
|
||||
Reference in New Issue
Block a user