using System.Net; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Azaion.E2E.Helpers; using FluentAssertions; using Xunit; namespace Azaion.E2E.Tests; [Collection("E2E")] public sealed class ResourceTests { private static readonly JsonSerializerOptions ResponseJsonOptions = new() { PropertyNameCaseInsensitive = true }; private const string TestUserPassword = "TestPass1234"; private const string SampleHardware = "CPU: TestCPU. GPU: TestGPU. Memory: 16384. DriveSerial: TESTDRIVE001."; private sealed record ErrorResponse(int ErrorCode, string Message); private readonly TestFixture _fixture; public ResourceTests(TestFixture fixture) => _fixture = fixture; [Fact] public async Task File_upload_succeeds() { // Arrange var folder = $"restest-{Guid.NewGuid():N}"; var fileBytes = Encoding.UTF8.GetBytes(new string('a', 100)); try { using var admin = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); // Act using var response = await admin.UploadFileAsync($"/resources/{folder}", fileBytes, "upload.txt"); // 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] public async Task Encrypted_download_returns_octet_stream_and_non_empty_body() { // Arrange var folder = $"restest-{Guid.NewGuid():N}"; const string fileName = "secure.bin"; var fileBytes = Encoding.UTF8.GetBytes("download-test-payload"); string? email = null; try { using (var admin = _fixture.CreateAuthenticatedClient(_fixture.AdminToken)) { using var upload = await admin.UploadFileAsync($"/resources/{folder}", fileBytes, fileName); upload.EnsureSuccessStatusCode(); } var candidateEmail = $"restest-{Guid.NewGuid():N}@azaion.com"; using (var admin = _fixture.CreateAuthenticatedClient(_fixture.AdminToken)) { using var createResp = await admin.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); using (var bind = await userClient.PostAsync("/resources/check", new { Hardware = SampleHardware })) { bind.EnsureSuccessStatusCode(); } // Act using var response = await userClient.PostAsync($"/resources/get/{folder}", new { Password = TestUserPassword, Hardware = SampleHardware, FileName = fileName }); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); response.Content.Headers.ContentType?.MediaType.Should().Be("application/octet-stream"); var body = await response.Content.ReadAsByteArrayAsync(); body.Should().NotBeEmpty(); } 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(); } using var adminClear = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); using var clear = await adminClear.PostAsync($"/resources/clear/{folder}", new { }); clear.EnsureSuccessStatusCode(); } } [Fact] public async Task Encryption_round_trip_decrypt_matches_original_bytes() { // Arrange var folder = $"restest-{Guid.NewGuid():N}"; const string fileName = "roundtrip.bin"; var original = Enumerable.Range(0, 128).Select(i => (byte)i).ToArray(); const string password = "RoundTrip123"; const string hardware = "RT-HW-CPU-001-GPU-002"; string? email = null; try { using (var admin = _fixture.CreateAuthenticatedClient(_fixture.AdminToken)) { using var upload = await admin.UploadFileAsync($"/resources/{folder}", original, fileName); upload.EnsureSuccessStatusCode(); } var candidateEmail = $"roundtrip-{Guid.NewGuid():N}@azaion.com"; using (var admin = _fixture.CreateAuthenticatedClient(_fixture.AdminToken)) { using var createResp = await admin.PostAsync("/users", new { Email = candidateEmail, Password = password, Role = 10 }); createResp.EnsureSuccessStatusCode(); } email = candidateEmail; using var loginClient = _fixture.CreateApiClient(); var userToken = await loginClient.LoginAsync(email, password); using var userClient = _fixture.CreateAuthenticatedClient(userToken); using (var bind = await userClient.PostAsync("/resources/check", new { Hardware = hardware })) { bind.EnsureSuccessStatusCode(); } // Act using var download = await userClient.PostAsync($"/resources/get/{folder}", new { Password = password, Hardware = hardware, FileName = fileName }); download.EnsureSuccessStatusCode(); var encrypted = await download.Content.ReadAsByteArrayAsync(); var decrypted = DecryptResourcePayload(encrypted, email!, password, hardware); // Assert decrypted.Should().Equal(original); } 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(); } using var adminClear = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); using var clear = await adminClear.PostAsync($"/resources/clear/{folder}", new { }); clear.EnsureSuccessStatusCode(); } } [Fact] public async Task Upload_without_file_is_rejected_with_400_or_409_and_60_on_conflict() { // Arrange var folder = $"restest-{Guid.NewGuid():N}"; using var content = new MultipartFormDataContent(); // Act using var response = await _fixture.HttpClient.PostAsync($"/resources/{folder}", content); // Assert response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.Conflict); if (response.StatusCode == HttpStatusCode.Conflict) { var err = await response.Content.ReadFromJsonAsync(ResponseJsonOptions); err.Should().NotBeNull(); err!.ErrorCode.Should().Be(60); } } private static byte[] DecryptResourcePayload(byte[] encrypted, string email, string password, string hardware) { var hwHash = Convert.ToBase64String(SHA384.HashData( Encoding.UTF8.GetBytes($"Azaion_{hardware}_%$$$)0_"))); var apiKey = Convert.ToBase64String(SHA384.HashData( Encoding.UTF8.GetBytes($"{email}-{password}-{hwHash}-#%@AzaionKey@%#---"))); var aesKey = SHA256.HashData(Encoding.UTF8.GetBytes(apiKey)); if (encrypted.Length <= 16) throw new InvalidOperationException("Encrypted payload too short."); using var aes = Aes.Create(); aes.Key = aesKey; aes.IV = encrypted.AsSpan(0, 16).ToArray(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; using var decryptor = aes.CreateDecryptor(); return decryptor.TransformFinalBlock(encrypted, 16, encrypted.Length - 16); } }