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."); } } }