mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
f1583fc586
The /api/admin/* prefix collides with the Azure Functions runtime's built-in
admin paths, which sit behind mTLS client-certificate auth. Browser preflights
to /api/admin/* trigger TLS renegotiation against the Function App's mTLS
boundary and die before our PolicyEnforcementMiddleware ever runs — the
operator sees CORS-shaped errors that aren't actually CORS-shaped.
Repo convention all along has been /api/global/* for cross-tenant GA-only
HTTP endpoints (e.g. global/session-deletions, global/session-reports). The
cascade-delete endpoints landed under /api/admin/* in the original 7-PR
backend branch and went undetected until the admin UI started calling them
from the browser.
Routes renamed:
admin/sessions/{sessionId}/delete/preview → global/sessions/{sessionId}/delete/preview
admin/sessions/{sessionId}/restore → global/sessions/{sessionId}/restore
admin/sessions/{sessionId}/deletion-manifest → global/sessions/{sessionId}/deletion-manifest
admin/tenants/{tenantId}/deletion-manifests → global/tenants/{tenantId}/deletion-manifests
Touched:
- 4 Function classes: Route attribute + XML-doc header
- EndpointAccessPolicyCatalog: 4 entries (TenantScoping.RouteParam preserved
on the tenants route), with a comment explaining why /api/global/* is the
load-bearing prefix for new GA-only HTTP routes
- DeleteSessionFunction: 3 user-facing "Cascade is poisoned; recover via
POST /api/.../restore" error messages updated
- SessionDeletion.cs: XML-doc reference to the restore route
- lib/api.ts: 4 frontend URL builders (storedManifest, preview, restore,
tenantManifests)
- RestoreSessionFunctionTests: 3 EndpointAccessPolicyCatalog.FindPolicy
lookups
- deleteSessionResponse.test.ts: test fixture message string
No semantic change — only the URL prefix moves. CORS config + JWT middleware
+ policy gating all work the same. The Restore Browser tab + the per-row
Preview / Restore buttons in the Session Cleanup admin page now actually
reach the worker from a browser.
Tests: backend xUnit 1538/1538 pass, web vitest 347/347 pass, dotnet build +
tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
2.6 KiB
C#
56 lines
2.6 KiB
C#
using System.Linq;
|
|
using System.Net;
|
|
using AutopilotMonitor.Functions.Functions.Sessions;
|
|
using AutopilotMonitor.Functions.Security;
|
|
using AutopilotMonitor.Functions.Services.Deletion;
|
|
using Xunit;
|
|
|
|
namespace AutopilotMonitor.Functions.Tests;
|
|
|
|
/// <summary>
|
|
/// Thin HTTP-layer tests for <see cref="RestoreSessionFunction"/> (PR4b). The bulk of behaviour
|
|
/// lives in <see cref="SessionRestoreService"/> (covered by <c>SessionRestoreServiceTests</c>);
|
|
/// this file only verifies the layer-specific pieces: status mapping, route registration in
|
|
/// the policy catalog (GA-only enforcement), and body shape.
|
|
/// </summary>
|
|
public class RestoreSessionFunctionTests
|
|
{
|
|
[Theory]
|
|
[InlineData(SessionRestoreOutcome.Restored, HttpStatusCode.OK)]
|
|
[InlineData(SessionRestoreOutcome.DryRunOk, HttpStatusCode.OK)]
|
|
[InlineData(SessionRestoreOutcome.RejectManifestNotFound, HttpStatusCode.NotFound)]
|
|
[InlineData(SessionRestoreOutcome.RejectAlreadyAtOriginalState, HttpStatusCode.Conflict)]
|
|
[InlineData(SessionRestoreOutcome.RejectActiveCascade, HttpStatusCode.Conflict)]
|
|
[InlineData(SessionRestoreOutcome.RejectManifestIdMismatch, HttpStatusCode.Conflict)]
|
|
[InlineData(SessionRestoreOutcome.RejectCorruptState, HttpStatusCode.Conflict)]
|
|
[InlineData(SessionRestoreOutcome.RejectManifestCorruption, HttpStatusCode.Conflict)]
|
|
[InlineData(SessionRestoreOutcome.RejectCasConflictOnClear, HttpStatusCode.Conflict)]
|
|
public void MapOutcomeToStatus_returns_expected_status(SessionRestoreOutcome outcome, HttpStatusCode expected)
|
|
{
|
|
Assert.Equal(expected, RestoreSessionFunction.MapOutcomeToStatus(outcome));
|
|
}
|
|
|
|
[Fact]
|
|
public void Restore_route_is_registered_in_policy_catalog_as_GlobalAdminOnly()
|
|
{
|
|
// Per memory feedback_route_policy_catalog — every HTTP route MUST be registered in
|
|
// EndpointAccessPolicyCatalog. An unregistered route fail-closes to 403. This test
|
|
// guards the restore endpoint's GA-only enforcement.
|
|
var entry = EndpointAccessPolicyCatalog.FindPolicy("POST", "global/sessions/some-id/restore");
|
|
|
|
Assert.NotNull(entry);
|
|
Assert.Equal(EndpointPolicy.GlobalAdminOnly, entry!.Policy);
|
|
}
|
|
|
|
[Fact]
|
|
public void Restore_route_is_NOT_registered_for_non_POST_methods()
|
|
{
|
|
// GET to the restore route should not match the POST registration.
|
|
var getEntry = EndpointAccessPolicyCatalog.FindPolicy("GET", "global/sessions/some-id/restore");
|
|
Assert.Null(getEntry);
|
|
|
|
var putEntry = EndpointAccessPolicyCatalog.FindPolicy("PUT", "global/sessions/some-id/restore");
|
|
Assert.Null(putEntry);
|
|
}
|
|
}
|