using System.Net; using System.Net.Http; using Azaion.E2E.Helpers; using FluentAssertions; using Xunit; namespace Azaion.E2E.Tests; // AZ-538 — CORS http-origin removal + HSTS + HTTPS redirection. // // The test environment runs ASPNETCORE_ENVIRONMENT=Development (matches the // docker-compose.test.yml setting), so AC-3/AC-4 (production-only behavior) are // verified by direct inspection of the production-side code path: // `if (!app.Environment.IsDevelopment()) { app.UseHsts(); app.UseHttpsRedirection(); }` // — the same gate ASP.NET Core's standard project template uses. // // AC-1, AC-2, and AC-5 are exercised here against the running container. [Collection("E2E")] public sealed class CorsHttpsTests { private readonly TestFixture _fixture; public CorsHttpsTests(TestFixture fixture) => _fixture = fixture; [Fact] public async Task AC1_Http_origin_is_rejected_by_cors_preflight() { // Arrange using var bare = new HttpClient { BaseAddress = new Uri(_fixture.Settings.ApiBaseUrl), Timeout = TimeSpan.FromSeconds(30) }; var preflight = new HttpRequestMessage(HttpMethod.Options, "/login"); preflight.Headers.Add("Origin", "http://admin.azaion.com"); preflight.Headers.Add("Access-Control-Request-Method", "POST"); preflight.Headers.Add("Access-Control-Request-Headers", "content-type"); // Act using var response = await bare.SendAsync(preflight); // Assert response.Headers.Contains("Access-Control-Allow-Origin").Should().BeFalse( "the http origin is no longer allow-listed"); } [Fact] public async Task AC2_Https_origin_is_accepted_with_credentials() { // Arrange using var bare = new HttpClient { BaseAddress = new Uri(_fixture.Settings.ApiBaseUrl), Timeout = TimeSpan.FromSeconds(30) }; var preflight = new HttpRequestMessage(HttpMethod.Options, "/login"); preflight.Headers.Add("Origin", "https://admin.azaion.com"); preflight.Headers.Add("Access-Control-Request-Method", "POST"); preflight.Headers.Add("Access-Control-Request-Headers", "content-type"); // Act using var response = await bare.SendAsync(preflight); // Assert response.Headers.GetValues("Access-Control-Allow-Origin") .Should().ContainSingle().Which.Should().Be("https://admin.azaion.com"); response.Headers.GetValues("Access-Control-Allow-Credentials") .Should().ContainSingle().Which.Should().Be("true"); } // AZ-538 AC-3 — HSTS is gated to non-Development envs in Program.cs: // if (!app.Environment.IsDevelopment()) { app.UseHsts(); ... } // The test container runs ASPNETCORE_ENVIRONMENT=Development, so the production // path can't be exercised here without spinning a second Production-mode SUT. // Tracked here as a skipped Fact so the AC stays linked to a test rather than a // file-level comment; promote to a runnable test when a prod-mode harness exists. [Fact(Skip = "AZ-538 AC-3 requires ASPNETCORE_ENVIRONMENT=Production; test harness runs Development. Verified by code inspection of Program.cs UseHsts gate.")] public void AC3_Hsts_header_present_in_production() { } // AZ-538 AC-4 — same gate as AC-3: UseHttpsRedirection only fires when not // Development. Skipped for the same prod-only reason. [Fact(Skip = "AZ-538 AC-4 requires ASPNETCORE_ENVIRONMENT=Production; test harness runs Development. Verified by code inspection of Program.cs UseHttpsRedirection gate.")] public void AC4_Http_request_redirects_to_https_in_production() { } [Fact] public async Task AC5_Development_env_does_not_redirect_or_send_hsts() { // Arrange using var bare = new HttpClient { BaseAddress = new Uri(_fixture.Settings.ApiBaseUrl), Timeout = TimeSpan.FromSeconds(30) }; // Act using var response = await bare.GetAsync("/health/live"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); response.Headers.Contains("Strict-Transport-Security").Should().BeFalse( "Development env must not emit HSTS so http://localhost workflows still work"); } }