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 DetectionClassesTests { private static readonly JsonSerializerOptions ResponseJsonOptions = new() { PropertyNameCaseInsensitive = true }; private sealed record DetectionClassDto(int Id, string Name, string ShortName, string Color, double MaxSizeM, string? PhotoMode); private readonly TestFixture _fixture; public DetectionClassesTests(TestFixture fixture) => _fixture = fixture; private static object NewClassBody(string nameSuffix) => new { name = $"Tank-{nameSuffix}", shortName = "T", color = "#FF0000", maxSizeM = 5.0 }; private async Task CreateClassAsync(ApiClient client, object body) { using var resp = await client.PostAsync("/classes", body); resp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Created); var dto = await resp.Content.ReadFromJsonAsync(ResponseJsonOptions); dto.Should().NotBeNull(); dto!.Id.Should().BeGreaterThan(0); return dto.Id; } [Fact] public async Task AC1_Post_classes_creates_class_with_assigned_id() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); var suffix = Guid.NewGuid().ToString("N")[..8]; var body = NewClassBody(suffix); int? createdId = null; try { // Act using var response = await client.PostAsync("/classes", body); // Assert response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Created); var dto = await response.Content.ReadFromJsonAsync(ResponseJsonOptions); dto.Should().NotBeNull(); dto!.Id.Should().BeGreaterThan(0); dto.Name.Should().Be($"Tank-{suffix}"); dto.ShortName.Should().Be("T"); dto.Color.Should().Be("#FF0000"); dto.MaxSizeM.Should().Be(5.0); createdId = dto.Id; } finally { if (createdId.HasValue) using (await client.DeleteAsync($"/classes/{createdId.Value}")) { } } } [Fact] public async Task AC2_Post_classes_without_jwt_returns_401() { // Arrange using var client = _fixture.CreateApiClient(); var body = NewClassBody(Guid.NewGuid().ToString("N")[..8]); // Act using var response = await client.PostAsync("/classes", body); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task AC2_Post_classes_with_non_admin_jwt_returns_403() { // Arrange var loginClient = _fixture.CreateApiClient(); var uploaderToken = await loginClient.LoginAsync(_fixture.UploaderEmail, _fixture.UploaderPassword); loginClient.Dispose(); using var client = _fixture.CreateAuthenticatedClient(uploaderToken); var body = NewClassBody(Guid.NewGuid().ToString("N")[..8]); // Act using var response = await client.PostAsync("/classes", body); // Assert response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } [Fact] public async Task AC3_Patch_classes_full_body_updates_class() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); var suffix = Guid.NewGuid().ToString("N")[..8]; var id = await CreateClassAsync(client, NewClassBody(suffix)); try { var patchBody = new { name = $"Heavy Tank-{suffix}", shortName = "T", color = "#FF0000", maxSizeM = 5.0 }; // Act using var response = await client.PatchAsync($"/classes/{id}", patchBody); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var dto = await response.Content.ReadFromJsonAsync(ResponseJsonOptions); dto.Should().NotBeNull(); dto!.Id.Should().Be(id); dto.Name.Should().Be($"Heavy Tank-{suffix}"); } finally { using (await client.DeleteAsync($"/classes/{id}")) { } } } [Fact] public async Task AC4_Patch_classes_partial_body_only_updates_specified_field() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); var suffix = Guid.NewGuid().ToString("N")[..8]; var id = await CreateClassAsync(client, NewClassBody(suffix)); try { var patchBody = new { color = "#00FF00" }; // Act using var response = await client.PatchAsync($"/classes/{id}", patchBody); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var dto = await response.Content.ReadFromJsonAsync(ResponseJsonOptions); dto.Should().NotBeNull(); dto!.Color.Should().Be("#00FF00"); dto.Name.Should().Be($"Tank-{suffix}"); dto.ShortName.Should().Be("T"); dto.MaxSizeM.Should().Be(5.0); } finally { using (await client.DeleteAsync($"/classes/{id}")) { } } } [Fact] public async Task AC5_Patch_classes_unknown_id_returns_404() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); var patchBody = new { name = "Anything" }; // Act using var response = await client.PatchAsync("/classes/2147483600", patchBody); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task AC6_Patch_classes_without_jwt_returns_401() { // Arrange using var client = _fixture.CreateApiClient(); var patchBody = new { name = "Anything" }; // Act using var response = await client.PatchAsync("/classes/1", patchBody); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task AC7_Delete_classes_removes_class() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); var suffix = Guid.NewGuid().ToString("N")[..8]; var id = await CreateClassAsync(client, NewClassBody(suffix)); // Act using var response = await client.DeleteAsync($"/classes/{id}"); // Assert response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent); using var followup = await client.DeleteAsync($"/classes/{id}"); followup.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task AC8_Delete_classes_unknown_id_returns_404() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); // Act using var response = await client.DeleteAsync("/classes/2147483600"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task AC9_Delete_classes_without_jwt_returns_401() { // Arrange using var client = _fixture.CreateApiClient(); // Act using var response = await client.DeleteAsync("/classes/1"); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } }