mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 18:31:07 +00:00
[AZ-577] [AZ-578] [AZ-579] [AZ-580] Implement E2E test batch 2
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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Azaion.Missions.E2E.Fixtures;
|
||||
using Azaion.Missions.E2E.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Missions.E2E.Tests.Vehicles;
|
||||
|
||||
/// <summary>
|
||||
/// FT-N-01..03 — vehicle negative scenarios from
|
||||
/// <c>_docs/02_document/tests/blackbox-tests.md § Negative</c>.
|
||||
/// FT-N-08 (generic 500 redacted body) lives in Tests/Errors because it
|
||||
/// owns its own destructive xUnit collection.
|
||||
/// Traces: AC-1.6 (no-match) / AC-1.7 (404) / AC-1.8 (409 in-use).
|
||||
/// </summary>
|
||||
[Collection("Vehicles")]
|
||||
[Trait("Category", "Blackbox")]
|
||||
[Trait("db_access", "seed-or-assert-only")]
|
||||
public sealed class NegativeTests : TestBase, IClassFixture<DbResetFixture>
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-1.6")]
|
||||
[Trait("max_ms", "2000")]
|
||||
public async Task FT_N_01_filter_no_match_returns_empty_array_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("body deserialized to null");
|
||||
}
|
||||
|
||||
// Act
|
||||
var upper = await FetchAsync("name=ZZ");
|
||||
var lower = await FetchAsync("name=zz");
|
||||
|
||||
// Assert
|
||||
Assert.Empty(upper);
|
||||
Assert.Empty(lower);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-1.7,AC-8.2")]
|
||||
[Trait("max_ms", "2000")]
|
||||
public async Task FT_N_02_get_vehicle_returns_404_with_problem_envelope()
|
||||
{
|
||||
// Arrange
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
var randomId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(HttpMethod.Get, $"/vehicles/{randomId}");
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert
|
||||
await HttpAssertions.AssertProblemEnvelopeAsync(response, HttpStatusCode.NotFound)
|
||||
;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-1.8,AC-8.5")]
|
||||
[Trait("max_ms", "2000")]
|
||||
public async Task FT_N_03_delete_in_use_vehicle_returns_409_and_row_remains()
|
||||
{
|
||||
// Arrange
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
Seeds.Apply(Seeds.OneDefaultVehicle.Sql);
|
||||
var vehicleId = Seeds.OneDefaultVehicle.Id;
|
||||
var missionId = Guid.NewGuid();
|
||||
Seeds.Apply($"""
|
||||
INSERT INTO missions (id, created_date, name, vehicle_id)
|
||||
VALUES ('{missionId}', '2026-05-14T00:00:00Z', 'in-use', '{vehicleId}');
|
||||
""");
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(HttpMethod.Delete, $"/vehicles/{vehicleId}");
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert
|
||||
await HttpAssertions.AssertProblemEnvelopeAsync(response, HttpStatusCode.Conflict)
|
||||
;
|
||||
var remaining = DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM vehicles WHERE id = @id",
|
||||
("id", vehicleId));
|
||||
Assert.Equal(1L, remaining);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Missions.E2E.Tests.Vehicles;
|
||||
|
||||
/// <summary>
|
||||
/// Discovery-only smoke test for the Vehicles category. AC-3 of AZ-576
|
||||
/// requires every test folder to expose ≥ 1 test so the runner can confirm
|
||||
/// the test harness is wired correctly. The real Vehicles scenarios
|
||||
/// (FT-P-01..06, FT-N-01..03) land in AZ-577.
|
||||
/// </summary>
|
||||
public sealed class Sanity
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", "Blackbox")]
|
||||
[Trait("Traces", "AC-3")]
|
||||
public void Discovery_smoke_test_runs()
|
||||
{
|
||||
// Arrange
|
||||
const int sentinel = 1;
|
||||
// Act
|
||||
var result = sentinel + 0;
|
||||
// Assert
|
||||
Assert.Equal(1, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user