Files
missions/tests/Azaion.Missions.E2E.Tests/Tests/Vehicles/PositiveTests.cs
T
Oleksandr Bezdieniezhnykh 3398ec49a0
ci/woodpecker/push/build-arm Pipeline was successful
Enhance test infrastructure and configuration for JWKS and Docker setup
- Updated Azaion.Missions.csproj to exclude test sources from service compilation, preventing build failures due to test project dependencies.
- Modified docker-compose.test.yml to preload the pg_stat_statements extension for testing and adjusted JWT refresh intervals for better test execution timing.
- Enhanced Dockerfile to install wget for health checks and ensure proper initialization of the container.
- Introduced a test-only endpoint for JWKS refresh to facilitate end-to-end testing without relying on the default refresh intervals.
- Updated DTOs in ApiDtos.cs to reflect camelCase naming conventions for consistency with service responses.
- Improved test cases to handle JWKS rotation and refresh scenarios effectively, ensuring robust validation of JWT handling.

This commit lays the groundwork for more reliable and efficient testing of the Azaion.Missions project.
2026-05-16 10:20:38 +03:00

269 lines
10 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Azaion.Missions.E2E.Fixtures;
using Azaion.Missions.E2E.Helpers;
using Xunit;
namespace Azaion.Missions.E2E.Tests.Vehicles;
/// <summary>
/// FT-P-01..06 — vehicle happy-path scenarios from
/// <c>_docs/02_document/tests/blackbox-tests.md § Positive</c>.
/// Traces: AC-1.1 / AC-1.2 / AC-1.4 / AC-1.5 / AC-1.6 / AC-1.10.
/// </summary>
[Collection("Vehicles")]
[Trait("Category", "Blackbox")]
[Trait("db_access", "seed-or-assert-only")]
public sealed class PositiveTests : TestBase, IClassFixture<DbResetFixture>
{
[Fact]
[Trait("Traces", "AC-1.1")]
[Trait("max_ms", "5000")]
[Trait("carry_forward", "json-camelcase-vs-pascalcase")]
public async Task FT_P_01_create_non_default_returns_201_with_camel_case_body()
{
// CARRY-FORWARD: results_report.md row 1.1 + AC-8.1 specified
// PascalCase response bodies. The actual SUT relies on ASP.NET Core
// default JsonSerializerOptions (camelCase) — no JsonNamingPolicy
// override is configured in Program.cs. Per /autodev batch 3 we
// pin the CODE shape (camelCase). Flip when the spec/code
// divergence is closed.
// Arrange
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
var token = await Tokens.MintDefaultAsync();
var request = new
{
Type = 0,
Model = "Bayraktar",
Name = "BR-01",
FuelType = 1,
BatteryCapacity = 0,
EngineConsumption = 5,
EngineConsumptionIdle = 1,
IsDefault = false
};
// Act
using var http = new HttpRequestMessage(HttpMethod.Post, "/vehicles")
{
Content = JsonContent.Create(request)
};
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.Created);
var raw = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(raw);
var root = doc.RootElement;
Assert.True(root.TryGetProperty("id", out var idEl), $"body missing camelCase 'id': {raw}");
Assert.True(root.TryGetProperty("name", out var nameEl));
Assert.True(root.TryGetProperty("isDefault", out var defEl));
Assert.False(root.TryGetProperty("Id", out _), "body unexpectedly PascalCase");
var id = idEl.GetGuid();
Assert.Equal("BR-01", nameEl.GetString());
Assert.False(defEl.GetBoolean());
var count = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE id = @id",
("id", id));
Assert.Equal(1, count);
}
[Fact]
[Trait("Traces", "AC-1.2")]
[Trait("max_ms", "5000")]
public async Task FT_P_02_create_default_demotes_prior_default()
{
// Arrange
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
Seeds.Apply(Seeds.OneDefaultVehicle.Sql);
var priorDefaultId = Seeds.OneDefaultVehicle.Id;
var token = await Tokens.MintDefaultAsync();
var request = new
{
Type = 0,
Model = "Bayraktar",
Name = "BR-02-default",
FuelType = 1,
BatteryCapacity = 0,
EngineConsumption = 5,
EngineConsumptionIdle = 1,
IsDefault = true
};
// Act
using var http = new HttpRequestMessage(HttpMethod.Post, "/vehicles")
{
Content = JsonContent.Create(request)
};
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.Created);
var newVehicle = await response.Content.ReadFromJsonAsync<VehicleDto>()
?? throw new InvalidOperationException("response body deserialized to null");
Assert.True(newVehicle.IsDefault, "newly-created vehicle must be default");
var totalDefaults = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE is_default = TRUE");
Assert.Equal(1, totalDefaults);
var priorIsDefault = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE id = @id AND is_default = TRUE",
("id", priorDefaultId));
Assert.Equal(0, priorIsDefault);
var newIsDefault = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE id = @id AND is_default = TRUE",
("id", newVehicle.Id));
Assert.Equal(1, newIsDefault);
}
[Fact]
[Trait("Traces", "AC-1.4")]
[Trait("max_ms", "5000")]
[Trait("carry_forward", "setDefault-route-method-return")]
public async Task FT_P_03_setDefault_promotes_existing_vehicle()
{
// CARRY-FORWARD: the canonical task spec + results_report.md row 1.4 say
// "POST /vehicles/{id}/setDefault" returning "200 with body {Vehicle}",
// but the actual code (Controllers/VehiclesController.cs:48) is
// "[HttpPatch("{id:guid}/default")]" returning "204 NoContent" (no body).
// Per /autodev batch 2 user choice, this test asserts the CODE shape.
// When the spec/code divergence is closed, flip method+status here.
// Arrange
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
Seeds.Apply(Seeds.OneDefaultVehicle.Sql);
var priorDefaultId = Seeds.OneDefaultVehicle.Id;
var p2Id = Guid.NewGuid();
Seeds.Apply($"""
INSERT INTO vehicles
(id, type, model, name, fuel_type, battery_capacity,
engine_consumption, engine_consumption_idle, is_default)
VALUES
('{p2Id}', 0, 'Bayraktar', 'BR-promote', 1, 0, 5, 1, false);
""");
var token = await Tokens.MintDefaultAsync();
// Act
using var http = new HttpRequestMessage(HttpMethod.Patch, $"/vehicles/{p2Id}/default")
{
Content = JsonContent.Create(new { IsDefault = true })
};
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.NoContent);
var promoted = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE id = @id AND is_default = TRUE",
("id", p2Id));
Assert.Equal(1, promoted);
var demoted = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE id = @id AND is_default = TRUE",
("id", priorDefaultId));
Assert.Equal(0, demoted);
DbAssertions.AssertExactlyOneDefaultVehicle();
}
[Fact]
[Trait("Traces", "AC-1.5")]
[Trait("max_ms", "2000")]
public async Task FT_P_04_list_is_unpaginated_array_ordered_by_name()
{
// Arrange
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
Seeds.Apply(Seeds.Three_BR01_BR02_MQ9.Sql);
var token = await Tokens.MintDefaultAsync();
// Act
using var http = new HttpRequestMessage(HttpMethod.Get, "/vehicles");
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.OK);
var raw = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(raw);
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
var vehicles = JsonSerializer.Deserialize<List<VehicleDto>>(raw)
?? throw new InvalidOperationException($"could not deserialize array: {raw}");
Assert.Equal(3, vehicles.Count);
Assert.Equal(new[] { "BR-01", "BR-02", "MQ-9" },
vehicles.Select(v => v.Name).ToArray());
}
[Fact]
[Trait("Traces", "AC-1.6")]
[Trait("max_ms", "2000")]
public async Task FT_P_05_filter_is_case_insensitive_for_both_casings()
{
// Arrange
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
Seeds.Apply(Seeds.Three_BR01_BR02_MQ9.Sql);
var token = await Tokens.MintDefaultAsync();
async Task<List<VehicleDto>> FetchAsync(string query)
{
using var http = new HttpRequestMessage(HttpMethod.Get, "/vehicles?" + query);
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var resp = await Missions.SendAsync(http);
await HttpAssertions.AssertStatusAsync(resp, HttpStatusCode.OK);
return await resp.Content.ReadFromJsonAsync<List<VehicleDto>>() ?? throw new InvalidOperationException("null body for /vehicles filter");
}
// Act
var upper = await FetchAsync("name=BR&isDefault=true");
var lower = await FetchAsync("name=br&isDefault=true");
// Assert
Assert.Single(upper);
Assert.Equal("BR-01", upper[0].Name);
Assert.Single(lower);
Assert.Equal("BR-01", lower[0].Name);
}
[Fact]
[Trait("Traces", "AC-1.10")]
[Trait("max_ms", "2000")]
public async Task FT_P_06_delete_with_no_references_returns_204_and_row_is_gone()
{
// Arrange
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
Seeds.Apply(Seeds.OneDefaultVehicle.Sql);
var id = Seeds.OneDefaultVehicle.Id;
var token = await Tokens.MintDefaultAsync();
// Act
using var http = new HttpRequestMessage(HttpMethod.Delete, $"/vehicles/{id}");
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.NoContent);
var bodyLength = (await response.Content.ReadAsByteArrayAsync()).Length;
Assert.Equal(0, bodyLength);
var remaining = DbAssertions.ScalarCount(
"SELECT COUNT(*) FROM vehicles WHERE id = @id",
("id", id));
Assert.Equal(0, remaining);
}
}