using System.Net.Http.Headers;
using System.Net.Http.Json;
using Azaion.Missions.E2E.Fixtures;
using Azaion.Missions.E2E.Helpers;
using Npgsql;
using Xunit;
namespace Azaion.Missions.E2E.Tests.Resilience;
///
/// NFT-RES-08 — TOCTOU race on vehicles.is_default.
///
///
///
/// Spec AC-1.4 expects the race to be OBSERVABLE — i.e. at least one of 100
/// concurrent iterations leaves two rows with is_default=true. The
/// current migrator ships
/// ux_vehicles_one_default ON vehicles (is_default) WHERE is_default = TRUE,
/// which closes the race at the storage layer: the second writer always
/// fails with 23505.
///
///
/// Following CascadeF4Tests precedent we pin the CURRENT behaviour
/// (max-one default after the race) and mark the divergence with the
/// carry_forward trait. If the index is ever removed without an
/// application-level guard replacing it, this test fails loudly — that
/// failure is the signal to revisit the AC-1.4 carry-forward in the
/// traceability matrix.
///
///
[Collection("MigratorRestart")]
[Trait("Category", "Res")]
[Trait("carry_forward", "AC-1.4/index-closes-race")]
[Trait("db_access", "seed-or-assert-only")]
public sealed class DefaultVehicleRaceTests : TestBase, IClassFixture
{
private const int Iterations = 100;
[Fact]
[Trait("Traces", "AC-1.4")]
[Trait("max_ms", "30000")]
public async Task NFT_RES_08_concurrent_default_writes_converge_on_one_default_today()
{
// Arrange — fresh DB and a valid token reused across iterations.
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
var token = await Tokens.MintDefaultAsync();
Missions.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token.Jwt);
var observations = new int[Iterations];
for (int i = 0; i < Iterations; i++)
{
ResetVehiclesAndSeedOneDefault();
// Each writer carries a unique id so PK collisions never mask
// the race that AC-1.4 is interested in.
var postTask = TryPostVehicleAsync(Guid.NewGuid());
var insertTask = TrySideChannelInsertAsync(Guid.NewGuid());
await Task.WhenAll(postTask, insertTask);
observations[i] = CountDefaultVehicles();
}
var maxObserved = observations.Max();
// Assert — CURRENT behaviour: the partial unique index forces
// every iteration to converge on a single default vehicle.
// If this assertion ever fails (max >= 2), the index has been
// removed/relaxed and AC-1.4 carry-forward should be revisited.
Assert.True(maxObserved <= 1,
$"observed >= 2 defaults in some iteration (max={maxObserved}). " +
"Index ux_vehicles_one_default appears removed/relaxed — revisit " +
"AC-1.4 carry-forward in traceability_matrix.csv.");
}
private async Task TryPostVehicleAsync(Guid vehicleId)
{
try
{
var body = new
{
Id = vehicleId,
Name = $"race-api-{vehicleId:N}",
IsDefault = true,
};
using var resp = await Missions.PostAsJsonAsync("/vehicles", body);
return new HttpRequestState((int)resp.StatusCode, null);
}
catch (Exception ex)
{
return new HttpRequestState(-1, ex);
}
}
private static async Task TrySideChannelInsertAsync(Guid vehicleId)
{
try
{
await using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO vehicles (id, name, is_default, created_at, updated_at)
VALUES (@id, @name, TRUE, NOW(), NOW());
""";
cmd.Parameters.AddWithValue("id", vehicleId);
cmd.Parameters.AddWithValue("name", $"race-side-{vehicleId:N}");
await cmd.ExecuteNonQueryAsync();
return new SideChannelState(true, null);
}
catch (PostgresException ex)
{
return new SideChannelState(false, ex);
}
}
private static void ResetVehiclesAndSeedOneDefault()
{
using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
TRUNCATE vehicles RESTART IDENTITY CASCADE;
INSERT INTO vehicles (id, name, is_default, created_at, updated_at)
VALUES (gen_random_uuid(), 'seed-default', TRUE, NOW(), NOW());
""";
cmd.ExecuteNonQuery();
}
private static int CountDefaultVehicles()
{
using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM vehicles WHERE is_default = TRUE;";
return Convert.ToInt32(cmd.ExecuteScalar());
}
private sealed record HttpRequestState(int StatusCode, Exception? Error);
private sealed record SideChannelState(bool Inserted, Exception? Error);
}