using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Azaion.Missions.E2E.Fixtures;
using Azaion.Missions.E2E.Helpers;
using Npgsql;
using Xunit;
namespace Azaion.Missions.E2E.Tests.Errors;
///
/// FT-N-08 — destructive scenario: side-channel DROP TABLE vehicles
/// forces the SUT into the generic catch path; the response must redact
/// internals (statusCode/message envelope), and the unhandled exception
/// must land in the container log within 2s.
///
///
/// Owns its own xUnit collection because the DROP corrupts the schema for
/// every other test class. Teardown uses
/// (down -v && up -d) which requires COMPOSE_RESTART_ENABLED=1.
/// When the fixture is disabled (developer inner-loop), the test skips with
/// a clear reason — silent passing is rejected by the contract.
///
[Collection("ErrorEnvelope500")]
[Trait("Category", "Blackbox")]
[Trait("db_access", "seed-or-assert-only")]
public sealed class Error500Tests : TestBase, IClassFixture
{
private readonly ComposeRestartFixture _restart;
public Error500Tests(ComposeRestartFixture restart) => _restart = restart;
[SkippableFact]
[Trait("Traces", "AC-8.6,AC-10.3")]
[Trait("max_ms", "5000")]
public async Task FT_N_08_generic_500_returns_redacted_body_and_logs_unhandled_exception()
{
Skip.IfNot(_restart.Enabled,
"ComposeRestartFixture disabled (COMPOSE_RESTART_ENABLED!=1). " +
"FT-N-08 is destructive and requires `compose down -v && up -d` " +
"in teardown to restore the schema.");
// Arrange — drop the vehicles table; the migrator that runs at
// missions startup is the only thing that re-creates it.
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
DropVehiclesTable();
var requestStart = DateTime.UtcNow;
var token = await Tokens.MintDefaultAsync();
try
{
// Act
using var http = new HttpRequestMessage(
HttpMethod.Get, $"/vehicles/{Guid.NewGuid()}");
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
using var response = await Missions.SendAsync(http);
// Assert — body redacts internals.
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.InternalServerError)
;
var raw = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(raw);
var root = doc.RootElement;
Assert.Equal(500, root.GetProperty("statusCode").GetInt32());
Assert.Equal("Internal server error", root.GetProperty("message").GetString());
// Reject extra keys (no stack leak via key names like 'exception',
// 'stackTrace', 'inner', etc.).
HttpAssertions.AssertNoStackLeak(root);
// Stacktrace must land in the SUT container log.
var deadline = DateTime.UtcNow.AddSeconds(2);
var logFound = false;
while (DateTime.UtcNow < deadline)
{
if (DockerLogsContain("missions-sut", "Unhandled exception", requestStart))
{
logFound = true;
break;
}
await Task.Delay(100);
}
Assert.True(logFound,
"expected 'Unhandled exception' in missions-sut docker logs within 2s of request");
}
finally
{
// Teardown — full stack restart so subsequent tests start clean.
_restart.RestartStack();
}
}
private static void DropVehiclesTable()
{
using var conn = new NpgsqlConnection(TestEnvironment.DbSideChannel);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "DROP TABLE IF EXISTS vehicles CASCADE;";
cmd.ExecuteNonQuery();
}
private static bool DockerLogsContain(string container, string needle, DateTime sinceUtc)
{
var since = sinceUtc.ToString("yyyy-MM-ddTHH:mm:ssZ",
System.Globalization.CultureInfo.InvariantCulture);
var psi = new ProcessStartInfo("docker", $"logs --since {since} {container}")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
try
{
using var p = Process.Start(psi)
?? throw new InvalidOperationException("docker command not available");
// docker logs interleaves stdout/stderr; ASP.NET Core writes
// exception text to stderr in default config.
var stdout = p.StandardOutput.ReadToEnd();
var stderr = p.StandardError.ReadToEnd();
p.WaitForExit();
return stdout.Contains(needle, StringComparison.Ordinal)
|| stderr.Contains(needle, StringComparison.Ordinal);
}
catch (System.ComponentModel.Win32Exception)
{
// No docker CLI in PATH — surface, do not silently pass.
throw new InvalidOperationException(
"docker CLI not available in test container; cannot assert log content for FT-N-08. " +
"Mount /var/run/docker.sock and install docker-cli in the e2e-consumer image.");
}
}
}