mirror of
https://github.com/azaion/missions.git
synced 2026-06-21 08:41:07 +00:00
6b2c2d998e
Adds 26 blackbox tests (FT-P-01..18, FT-N-01..08) covering full AC
matrices for Vehicles/Missions/Waypoints/Health/Errors. Three
spec-vs-code carry-forwards documented in batch_02_report.md and
pinned with [Trait("carry_forward", ...)].
Shared scaffolding: ApiDtos.cs, AssertProblemEnvelopeAsync helper,
Seeds.cs, StubSchema.cs, CascadeF3/F4 fixtures, PostgresStopStart
fixture (gated by COMPOSE_RESTART_ENABLED). Removes the 4 placeholder
Sanity.cs files (now superseded). docker-compose.test.yml gains the
expected_results volume mount + FIXTURE_SQL_DIR for the consumer.
Co-authored-by: Cursor <cursoragent@cursor.com>
263 lines
10 KiB
C#
263 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")]
|
|
public async Task FT_P_01_create_non_default_returns_201_with_pascal_case_body()
|
|
{
|
|
// 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;
|
|
// Pin PascalCase contract — a future global camelCase migration must
|
|
// break this test (results_report.md row 1.1 + AC-8.1).
|
|
Assert.True(root.TryGetProperty("Id", out var idEl), $"body missing PascalCase '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 camelCase");
|
|
|
|
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);
|
|
}
|
|
}
|