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;
///
/// FT-P-01..06 — vehicle happy-path scenarios from
/// _docs/02_document/tests/blackbox-tests.md § Positive.
/// Traces: AC-1.1 / AC-1.2 / AC-1.4 / AC-1.5 / AC-1.6 / AC-1.10.
///
[Collection("Vehicles")]
[Trait("Category", "Blackbox")]
[Trait("db_access", "seed-or-assert-only")]
public sealed class PositiveTests : TestBase, IClassFixture
{
[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()
?? 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>(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> 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>() ?? 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);
}
}