Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions.Tests/RestoreSessionFunctionTests.cs
T
Oliver Kieselbach f1583fc586 fix(backend,web): move cascade-delete admin routes from /api/admin/* to /api/global/*
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>
2026-05-14 11:38:34 +02:00

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