mirror of
https://github.com/azaion/admin.git
synced 2026-04-22 21:46:33 +00:00
[AZ-189] [AZ-190] [AZ-191] [AZ-192] [AZ-193] [AZ-194] [AZ-195] Add e2e blackbox test suite
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Azaion.E2E.Helpers;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.E2E.Tests;
|
||||
|
||||
[Collection("E2E")]
|
||||
public sealed class AuthTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions ResponseJsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private sealed record ErrorResponse(int ErrorCode, string Message);
|
||||
private sealed record LoginOkResponse(string Token);
|
||||
|
||||
private readonly TestFixture _fixture;
|
||||
|
||||
public AuthTests(TestFixture fixture) => _fixture = fixture;
|
||||
|
||||
[Fact]
|
||||
public async Task Login_with_valid_admin_credentials_returns_200_and_token()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateApiClient();
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/login",
|
||||
new { email = _fixture.AdminEmail, password = _fixture.AdminPassword });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var body = await response.Content.ReadFromJsonAsync<LoginOkResponse>(ResponseJsonOptions);
|
||||
body.Should().NotBeNull();
|
||||
body!.Token.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Jwt_contains_expected_claims_and_lifetime()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateApiClient();
|
||||
|
||||
// Act
|
||||
using var loginResponse = await client.PostAsync("/login",
|
||||
new { email = _fixture.AdminEmail, password = _fixture.AdminPassword });
|
||||
var loginBody = await loginResponse.Content.ReadFromJsonAsync<LoginOkResponse>(ResponseJsonOptions);
|
||||
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(loginBody!.Token);
|
||||
|
||||
// Assert
|
||||
loginResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
jwt.Issuer.Should().Be("AzaionApi");
|
||||
jwt.Audiences.Should().Contain("Annotators/OrangePi/Admins");
|
||||
var iatSeconds = long.Parse(
|
||||
jwt.Claims.Single(c => c.Type == JwtRegisteredClaimNames.Iat).Value,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
var expSeconds = long.Parse(
|
||||
jwt.Claims.Single(c => c.Type == JwtRegisteredClaimNames.Exp).Value,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
TimeSpan.FromSeconds(expSeconds - iatSeconds)
|
||||
.Should().BeCloseTo(TimeSpan.FromHours(4), TimeSpan.FromSeconds(60));
|
||||
jwt.Claims.Should().Contain(c => c.Type == ClaimTypes.Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_with_unknown_email_returns_409_with_error_code_10()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateApiClient();
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/login",
|
||||
new { email = "nonexistent@example.com", password = "irrelevant" });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
var err = await response.Content.ReadFromJsonAsync<ErrorResponse>(ResponseJsonOptions);
|
||||
err.Should().NotBeNull();
|
||||
err!.ErrorCode.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_with_wrong_password_returns_409_with_error_code_30()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateApiClient();
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/login",
|
||||
new { email = _fixture.AdminEmail, password = "DefinitelyWrongPassword" });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
var err = await response.Content.ReadFromJsonAsync<ErrorResponse>(ResponseJsonOptions);
|
||||
err.Should().NotBeNull();
|
||||
err!.ErrorCode.Should().Be(30);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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 HardwareBindingTests
|
||||
{
|
||||
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 HardwareBindingTests(TestFixture fixture) => _fixture = fixture;
|
||||
|
||||
[Fact]
|
||||
public async Task First_hardware_check_binds_and_returns_200_true()
|
||||
{
|
||||
// Arrange
|
||||
string? email = null;
|
||||
|
||||
try
|
||||
{
|
||||
var candidateEmail = $"hwtest-{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
|
||||
using var response = await userClient.PostAsync("/resources/check", new { Hardware = SampleHardware });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var body = await response.Content.ReadFromJsonAsync<bool>(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 Repeat_hardware_check_with_same_hardware_returns_200_true()
|
||||
{
|
||||
// Arrange
|
||||
string? email = null;
|
||||
|
||||
try
|
||||
{
|
||||
var candidateEmail = $"hwtest-{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);
|
||||
using (var first = await userClient.PostAsync("/resources/check", new { Hardware = SampleHardware }))
|
||||
{
|
||||
first.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// Act
|
||||
using var response = await userClient.PostAsync("/resources/check", new { Hardware = SampleHardware });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var body = await response.Content.ReadFromJsonAsync<bool>(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 Hardware_mismatch_returns_409_with_error_code_40()
|
||||
{
|
||||
// Arrange
|
||||
const string hardwareA = "HARDWARE_A";
|
||||
const string hardwareB = "HARDWARE_B";
|
||||
string? email = null;
|
||||
|
||||
try
|
||||
{
|
||||
var candidateEmail = $"hwtest-{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);
|
||||
using (var bind = await userClient.PostAsync("/resources/check", new { Hardware = hardwareA }))
|
||||
{
|
||||
bind.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// Act
|
||||
using var response = await userClient.PostAsync("/resources/check", new { Hardware = hardwareB });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
var err = await response.Content.ReadFromJsonAsync<ErrorResponse>(ResponseJsonOptions);
|
||||
err.Should().NotBeNull();
|
||||
err!.ErrorCode.Should().Be(40);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
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(Skip = "Requires Docker container control to stop/restart test-db")]
|
||||
public void Db_stop_and_restart_recovery_within_10s()
|
||||
{
|
||||
// Arrange
|
||||
// Would: stop the test-db container (docker stop test-db).
|
||||
// Would: call a health or protected endpoint until API returns errors (e.g. 503/500) or connection failure.
|
||||
// Act
|
||||
// Would: start test-db again (docker start test-db).
|
||||
// Assert
|
||||
// Would: poll API until successful response within 10 seconds after DB is up.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Malformed_authorization_headers_return_401_and_system_remains_operational()
|
||||
{
|
||||
// Arrange
|
||||
var baseUrl = _fixture.Configuration["ApiBaseUrl"]
|
||||
?? throw new InvalidOperationException("ApiBaseUrl is required.");
|
||||
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<bool>(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<double>(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];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
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 = "RoundTrip1!";
|
||||
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<ErrorResponse>(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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Azaion.E2E.Helpers;
|
||||
using FluentAssertions;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.E2E.Tests;
|
||||
|
||||
[Collection("E2E")]
|
||||
public sealed class SecurityTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
private readonly TestFixture _fixture;
|
||||
|
||||
public SecurityTests(TestFixture fixture) => _fixture = fixture;
|
||||
|
||||
[Fact]
|
||||
public async Task Unauthenticated_requests_to_protected_endpoints_return_401()
|
||||
{
|
||||
// Arrange
|
||||
var baseUrl = _fixture.Configuration["ApiBaseUrl"]
|
||||
?? throw new InvalidOperationException("ApiBaseUrl is required.");
|
||||
using var bare = new HttpClient { BaseAddress = new Uri(baseUrl, UriKind.Absolute), Timeout = TimeSpan.FromMinutes(5) };
|
||||
using var client = new ApiClient(bare, disposeClient: false);
|
||||
var probeEmail = "test@x.com";
|
||||
|
||||
// Act & Assert
|
||||
using (var r = await client.GetAsync("/users/current"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
using (var r = await client.GetAsync("/users"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
using (var r = await bare.PostAsync("/users",
|
||||
new StringContent("", Encoding.UTF8, "application/json")))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
using (var r = await client.PutAsync($"/users/{Uri.EscapeDataString(probeEmail)}/enable"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
using (var r = await client.DeleteAsync($"/users/{Uri.EscapeDataString(probeEmail)}"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
using (var r = await client.PostAsync("/resources/get",
|
||||
new { password = "irrelevant1", hardware = "h", fileName = "f.bin" }))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Non_admin_uploader_is_forbidden_on_admin_endpoints()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateApiClient();
|
||||
using var login = await client.PostAsync("/login",
|
||||
new { email = _fixture.UploaderEmail, password = _fixture.UploaderPassword });
|
||||
login.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var loginBody = await login.Content.ReadFromJsonAsync<LoginTokenResponse>(JsonOptions);
|
||||
var token = loginBody?.Token ?? throw new InvalidOperationException("Missing token.");
|
||||
client.SetAuthToken(token);
|
||||
var targetEmail = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
|
||||
// Act & Assert
|
||||
using (var r = await client.PostAsync("/users",
|
||||
new { email = targetEmail, password = "TestPwd1234", role = 10 }))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
|
||||
using (var r = await client.GetAsync("/users"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
|
||||
using (var r = await client.PutAsync($"/users/{Uri.EscapeDataString(targetEmail)}/set-role/10"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
|
||||
using (var r = await client.DeleteAsync($"/users/{Uri.EscapeDataString(targetEmail)}"))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Users_list_must_not_expose_non_empty_password_hash_in_json()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.GetAsync("/users");
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
doc.RootElement.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
foreach (var user in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
if (!user.TryGetProperty("passwordHash", out var ph))
|
||||
continue;
|
||||
if (ph.ValueKind == JsonValueKind.Null)
|
||||
continue;
|
||||
ph.ValueKind.Should().Be(JsonValueKind.String);
|
||||
(ph.GetString() ?? "").Should().BeEmpty("password hash must not be exposed in API responses");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Expired_jwt_is_rejected_for_admin_endpoint()
|
||||
{
|
||||
// Arrange
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_fixture.JwtSecret));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: "AzaionApi",
|
||||
audience: "Annotators/OrangePi/Admins",
|
||||
claims:
|
||||
[
|
||||
new Claim(ClaimTypes.Role, "ApiAdmin"),
|
||||
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "expired@x.com")
|
||||
],
|
||||
notBefore: DateTime.UtcNow.AddHours(-3),
|
||||
expires: DateTime.UtcNow.AddHours(-1),
|
||||
signingCredentials: creds);
|
||||
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||
using var client = _fixture.CreateAuthenticatedClient(jwt);
|
||||
|
||||
// Act
|
||||
using var response = await client.GetAsync("/users");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Per_user_encryption_produces_distinct_ciphertext_for_same_file()
|
||||
{
|
||||
// Arrange
|
||||
var folder = $"sectest-{Guid.NewGuid():N}";
|
||||
var fileName = $"enc-{Guid.NewGuid():N}.bin";
|
||||
var payload = Encoding.UTF8.GetBytes($"secret-{Guid.NewGuid()}");
|
||||
var email1 = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
var email2 = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
const string password = "TestPwd1234";
|
||||
var hw1 = $"hw-{Guid.NewGuid():N}";
|
||||
var hw2 = $"hw-{Guid.NewGuid():N}";
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var email in new[] { email1, email2 })
|
||||
{
|
||||
var reg = JsonSerializer.Serialize(new { email, password, role = 10 }, JsonOptions);
|
||||
using var create = await _fixture.HttpClient.PostAsync("/users",
|
||||
new StringContent(reg, Encoding.UTF8, "application/json"));
|
||||
create.IsSuccessStatusCode.Should().BeTrue();
|
||||
}
|
||||
|
||||
using (var adminUpload = _fixture.CreateAuthenticatedClient(_fixture.AdminToken))
|
||||
{
|
||||
using var up = await adminUpload.UploadFileAsync($"/resources/{folder}", payload, fileName);
|
||||
up.IsSuccessStatusCode.Should().BeTrue();
|
||||
}
|
||||
|
||||
async Task<byte[]> DownloadForAsync(string email, string hardware)
|
||||
{
|
||||
using var api = _fixture.CreateApiClient();
|
||||
var token = await api.LoginAsync(email, password);
|
||||
api.SetAuthToken(token);
|
||||
using var check = await api.PostAsync("/resources/check", new { hardware });
|
||||
check.IsSuccessStatusCode.Should().BeTrue();
|
||||
using var get = await api.PostAsync($"/resources/get/{folder}",
|
||||
new { password, hardware, fileName });
|
||||
get.IsSuccessStatusCode.Should().BeTrue();
|
||||
return await get.Content.ReadAsByteArrayAsync();
|
||||
}
|
||||
|
||||
// Act
|
||||
var bytes1 = await DownloadForAsync(email1, hw1);
|
||||
var bytes2 = await DownloadForAsync(email2, hw2);
|
||||
|
||||
// Assert
|
||||
bytes1.Should().NotBeEquivalentTo(bytes2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var clearResponse = await _fixture.HttpClient.PostAsync($"/resources/clear/{folder}",
|
||||
new StringContent("", Encoding.UTF8, "application/json"));
|
||||
|
||||
foreach (var email in new[] { email1, email2 })
|
||||
{
|
||||
using var _ = await _fixture.HttpClient.DeleteAsync($"/users/{Uri.EscapeDataString(email)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disabled_user_cannot_log_in()
|
||||
{
|
||||
// Arrange
|
||||
var email = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
const string password = "TestPwd1234";
|
||||
try
|
||||
{
|
||||
var reg = JsonSerializer.Serialize(new { email, password, role = 10 }, JsonOptions);
|
||||
using (var create = await _fixture.HttpClient.PostAsync("/users",
|
||||
new StringContent(reg, Encoding.UTF8, "application/json")))
|
||||
create.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
using (var disable = await _fixture.HttpClient.PutAsync(
|
||||
$"/users/{Uri.EscapeDataString(email)}/disable", null))
|
||||
disable.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
using var client = _fixture.CreateApiClient();
|
||||
|
||||
// Act
|
||||
using var login = await client.PostAsync("/login", new { email, password });
|
||||
|
||||
// Assert
|
||||
login.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.Conflict);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var _ = await _fixture.HttpClient.DeleteAsync($"/users/{Uri.EscapeDataString(email)}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoginTokenResponse
|
||||
{
|
||||
public string Token { get; init; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
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 UserManagementTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private sealed record ErrorResponse(int ErrorCode, string Message);
|
||||
private sealed record UserDto(Guid Id, string Email, int Role, bool IsEnabled);
|
||||
|
||||
private readonly TestFixture _fixture;
|
||||
|
||||
public UserManagementTests(TestFixture fixture) => _fixture = fixture;
|
||||
|
||||
private static string UserBasePath(string email) => $"/users/{Uri.EscapeDataString(email)}";
|
||||
|
||||
[Fact]
|
||||
public async Task Registration_with_valid_data_succeeds()
|
||||
{
|
||||
var email = $"testuser-{Guid.NewGuid():N}@azaion.com";
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var body = new { email, password = "SecurePass1", role = 10 };
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users", body);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var deleteResponse = await client.DeleteAsync(UserBasePath(email));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_users_returns_non_empty_array()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.GetAsync("/users");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(JsonOptions);
|
||||
users.Should().NotBeNull();
|
||||
users!.Length.Should().BeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_users_filtered_by_email_contains_only_matches()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.GetAsync("/users?searchEmail=" + Uri.EscapeDataString("admin"));
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(JsonOptions);
|
||||
users.Should().NotBeNull();
|
||||
users!.Should().NotBeEmpty();
|
||||
users.Should().OnlyContain(u => u.Email.Contains("admin", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_user_role_succeeds()
|
||||
{
|
||||
var email = $"testuser-{Guid.NewGuid():N}@azaion.com";
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1", role = 10 }))
|
||||
{
|
||||
createResp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
// Act
|
||||
using var response = await client.PutAsync($"{UserBasePath(email)}/set-role/50");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var deleteResponse = await client.DeleteAsync(UserBasePath(email));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disable_user_succeeds()
|
||||
{
|
||||
var email = $"testuser-{Guid.NewGuid():N}@azaion.com";
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1", role = 10 }))
|
||||
{
|
||||
createResp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
// Act
|
||||
using var response = await client.PutAsync($"{UserBasePath(email)}/disable");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var deleteResponse = await client.DeleteAsync(UserBasePath(email));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_user_succeeds_and_user_not_in_search_results()
|
||||
{
|
||||
var email = $"testuser-{Guid.NewGuid():N}@azaion.com";
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1", role = 10 }))
|
||||
{
|
||||
createResp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
// Act
|
||||
using var deleteResponse = await client.DeleteAsync(UserBasePath(email));
|
||||
|
||||
// Assert
|
||||
deleteResponse.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
|
||||
using var verifyResponse = await client.GetAsync("/users?searchEmail=" + Uri.EscapeDataString(email));
|
||||
verifyResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var users = await verifyResponse.Content.ReadFromJsonAsync<UserDto[]>(JsonOptions);
|
||||
users.Should().NotBeNull();
|
||||
users!.Should().NotContain(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var deleteResponse = await client.DeleteAsync(UserBasePath(email));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registration_rejects_short_email_with_400()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = "ab@c.de", password = "ValidPass1", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registration_rejects_invalid_email_format_with_400()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = "notavalidemail", password = "ValidPass1", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registration_rejects_short_password_with_400()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = "validmail@test.com", password = "short", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registration_rejects_duplicate_admin_email_with_409()
|
||||
{
|
||||
// Arrange
|
||||
using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken);
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = _fixture.AdminEmail, password = "DuplicateP1", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
var err = await response.Content.ReadFromJsonAsync<ErrorResponse>(JsonOptions);
|
||||
err.Should().NotBeNull();
|
||||
err!.ErrorCode.Should().Be(20);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user