using System.Net;
using System.Net.Http.Headers;
using Azaion.Missions.E2E.Fixtures;
using Azaion.Missions.E2E.Helpers;
using Npgsql;
using Xunit;
namespace Azaion.Missions.E2E.Tests.Missions;
///
/// FT-N-06 — DELETE /missions/{missing_uuid} must short-circuit on the
/// initial existence check (Step 1 of the cascade walk) and emit ZERO
/// DELETE statements against any dependency table. The contract protects
/// downstream consumers from typo'd UUIDs silently corrupting unrelated
/// missions' data (results_report.md row 3.2 / AC-3.2).
///
///
/// The strict assertion uses two independent signals: (1) per-table row
/// counts before and after must match, AND (2) when
/// pg_stat_statements is available, the post-request query stats
/// must contain ZERO DELETE FROM map_objects/waypoints/media/...
/// rows attributable to this request window.
/// Without pg_stat_statements (e.g. extension not preloaded in the
/// postgres image), the test still asserts the row-count invariant and
/// records a warning trait — silent passing is rejected by the
/// row-count check.
///
[Collection("CascadeShortCircuit")]
[Trait("Category", "Blackbox")]
[Trait("db_access", "seed-or-assert-only")]
public sealed class CascadeShortCircuitTests : TestBase
{
[Fact]
[Trait("Traces", "AC-3.2")]
[Trait("max_ms", "5000")]
public async Task FT_N_06_delete_missing_mission_emits_zero_dependency_table_deletes()
{
// Arrange — clean DB, F3 fixture for a populated cascade chain.
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
StubSchema.EnsureCreated();
Seeds.Apply(FixtureSql.Load("fixture_cascade_F3"));
// Try to attach pg_stat_statements; fall back gracefully if the
// extension isn't preloaded.
var pgssAvailable = TryEnablePgStatStatements();
if (pgssAvailable) ResetPgStatStatements();
var token = await Tokens.MintDefaultAsync();
var notInDb = Guid.NewGuid();
// Pre-state row counts — these must equal post-state counts iff the
// cascade short-circuited correctly.
var pre = SnapshotCounts();
// Act
using var http = new HttpRequestMessage(HttpMethod.Delete, $"/missions/{notInDb}");
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert
await HttpAssertions.AssertProblemEnvelopeAsync(response, HttpStatusCode.NotFound)
;
var post = SnapshotCounts();
foreach (var table in pre.Keys)
{
Assert.True(pre[table] == post[table],
$"row count for '{table}' changed after a 404 cascade: " +
$"pre={pre[table]} post={post[table]} — short-circuit failed");
}
if (pgssAvailable)
{
var deleteCount = ScalarCountSql("""
SELECT COUNT(*) FROM pg_stat_statements
WHERE query ILIKE '%DELETE FROM map_objects%'
OR query ILIKE '%DELETE FROM waypoints%'
OR query ILIKE '%DELETE FROM media%'
OR query ILIKE '%DELETE FROM annotations%'
OR query ILIKE '%DELETE FROM detection%'
OR query ILIKE '%DELETE FROM missions%'
""");
Assert.True(deleteCount == 0,
$"pg_stat_statements shows {deleteCount} DELETE statements against " +
"cascade tables after a 404 — short-circuit failed at the SQL layer");
}
}
private static Dictionary SnapshotCounts()
{
var tables = new[] { "missions", "waypoints", "map_objects",
"media", "annotations", "detection" };
return tables.ToDictionary(t => t, DbAssertions.TableRowCount);
}
private static bool TryEnablePgStatStatements()
{
try
{
using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;";
cmd.ExecuteNonQuery();
return true;
}
catch (PostgresException ex)
{
// Most common cause: the extension is not in
// shared_preload_libraries. Surface the reason — skipping
// silently would defeat the purpose of this test.
Console.WriteLine(
$"[FT-N-06] pg_stat_statements unavailable ({ex.SqlState}: {ex.MessageText}); " +
"falling back to row-count short-circuit assertion only.");
return false;
}
}
private static void ResetPgStatStatements()
{
using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT pg_stat_statements_reset();";
cmd.ExecuteScalar();
}
private static long ScalarCountSql(string sql)
{
using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
var result = cmd.ExecuteScalar();
if (result is null || result is DBNull)
throw new InvalidOperationException($"scalar query returned NULL: {sql}");
return Convert.ToInt64(result, System.Globalization.CultureInfo.InvariantCulture);
}
}