using System.Diagnostics; using System.Net; using System.Net.Http.Json; using System.Text.Json; using Azaion.E2E.Helpers; using FluentAssertions; using Xunit; namespace Azaion.E2E.Tests; [Collection("E2E")] public sealed class ResilienceTests { private static readonly JsonSerializerOptions ResponseJsonOptions = new() { PropertyNameCaseInsensitive = true }; private const string TestUserPassword = "TestPass1234"; private const string MalformedJwtUnsigned = "eyJhbGciOiJub25lIn0.eyJ0ZXN0IjoiMSJ9."; private readonly TestFixture _fixture; public ResilienceTests(TestFixture fixture) => _fixture = fixture; [Fact] public async Task Malformed_authorization_headers_return_401_and_system_remains_operational() { // Arrange var baseUrl = _fixture.Settings.ApiBaseUrl; var headers = new[] { "Bearer invalidtoken123", $"Bearer {MalformedJwtUnsigned}", "NotBearer somevalue", "Bearer " }; using var http = new HttpClient { BaseAddress = new Uri(baseUrl, UriKind.Absolute), Timeout = TimeSpan.FromMinutes(5) }; // Act foreach (var h in headers) { using var request = new HttpRequestMessage(HttpMethod.Get, "/users/current"); request.Headers.TryAddWithoutValidation("Authorization", h); using var response = await http.SendAsync(request); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } // Arrange using var client = _fixture.CreateApiClient(); // Act var token = await client.LoginAsync(_fixture.AdminEmail, _fixture.AdminPassword); // Assert token.Should().NotBeNullOrWhiteSpace(); } [Fact] public async Task Concurrent_hardware_binding_same_hardware_has_no_500_and_state_consistent() { // Arrange string? email = null; var hardware = $"CPU: ConcCPU. GPU: ConcGPU. Memory: 8192. DriveSerial: {Guid.NewGuid():N}."; try { var candidateEmail = $"resilience-hw-{Guid.NewGuid()}@azaion.com"; using (var adminClient = _fixture.CreateAuthenticatedClient(_fixture.AdminToken)) { using var createResp = await adminClient.PostAsync("/users", new { Email = candidateEmail, Password = TestUserPassword, Role = 10 }); createResp.EnsureSuccessStatusCode(); } email = candidateEmail; using var loginClient = _fixture.CreateApiClient(); var userToken = await loginClient.LoginAsync(email, TestUserPassword); using var userClient = _fixture.CreateAuthenticatedClient(userToken); // Act var concurrentTasks = Enumerable.Range(0, 5) .Select(_ => userClient.PostAsync("/resources/check", new { Hardware = hardware })) .ToArray(); var concurrentResponses = await Task.WhenAll(concurrentTasks); // Assert foreach (var r in concurrentResponses) { using (r) { r.StatusCode.Should().NotBe(HttpStatusCode.InternalServerError); } } // Act using var followUp = await userClient.PostAsync("/resources/check", new { Hardware = hardware }); // Assert followUp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await followUp.Content.ReadFromJsonAsync(ResponseJsonOptions); body.Should().BeTrue(); } finally { if (email is not null) { using var adminCleanup = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); using var del = await adminCleanup.DeleteAsync($"/users/{Uri.EscapeDataString(email)}"); del.EnsureSuccessStatusCode(); } } } [Fact] public async Task Login_p95_latency_under_500ms_after_warmup() { // Arrange using var client = _fixture.CreateApiClient(); for (var i = 0; i < 5; i++) { using var w = await client.PostAsync("/login", new { email = _fixture.UploaderEmail, password = _fixture.UploaderPassword }); w.EnsureSuccessStatusCode(); } var samples = new List(100); // Act for (var i = 0; i < 100; i++) { var sw = Stopwatch.StartNew(); using var resp = await client.PostAsync("/login", new { email = _fixture.UploaderEmail, password = _fixture.UploaderPassword }); sw.Stop(); resp.EnsureSuccessStatusCode(); samples.Add(sw.Elapsed.TotalMilliseconds); } var sorted = samples.OrderBy(x => x).ToArray(); var p95Index = (int)Math.Ceiling(0.95 * sorted.Length) - 1; if (p95Index < 0) p95Index = 0; var p95 = sorted[p95Index]; // Assert p95.Should().BeLessThan(500); } [Fact] [Trait("Category", "ResourceLimit")] public async Task Max_file_upload_200_mb_accepted() { // Arrange const string folder = "testfolder"; const string fileName = "max.bin"; var payload = new byte[200 * 1024 * 1024 - 4096]; try { using var adminClient = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); // Act using var response = await adminClient.UploadFileAsync($"/resources/{folder}", payload, fileName); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } finally { using var adminCleanup = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); using var clear = await adminCleanup.PostAsync($"/resources/clear/{folder}", new { }); clear.EnsureSuccessStatusCode(); } } [Fact] [Trait("Category", "ResourceLimit")] public async Task Over_max_upload_201_mb_rejected_or_connection_aborted() { // Arrange const string folder = "testfolder"; const string fileName = "over.bin"; var payload = new byte[201 * 1024 * 1024]; using var adminClient = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); // Act var outcome = await TryUploadAsync(adminClient, $"/resources/{folder}", payload, fileName); // Assert outcome.Acceptable.Should().BeTrue(); using (var adminCleanup = _fixture.CreateAuthenticatedClient(_fixture.AdminToken)) { using var clear = await adminCleanup.PostAsync($"/resources/clear/{folder}", new { }); clear.EnsureSuccessStatusCode(); } } private static async Task<(bool Acceptable, HttpStatusCode? Status)> TryUploadAsync( ApiClient adminClient, string url, byte[] payload, string fileName) { try { using var response = await adminClient.UploadFileAsync(url, payload, fileName); if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge) return (true, response.StatusCode); return (false, response.StatusCode); } catch (Exception ex) when (IsConnectionRelated(ex)) { return (true, null); } } private static bool IsConnectionRelated(Exception ex) { if (ex is HttpRequestException or TaskCanceledException or IOException) return true; return ex.InnerException is not null && IsConnectionRelated(ex.InnerException); } }