mirror of
https://github.com/azaion/missions.git
synced 2026-06-21 06:41:07 +00:00
6b2c2d998e
Adds 26 blackbox tests (FT-P-01..18, FT-N-01..08) covering full AC
matrices for Vehicles/Missions/Waypoints/Health/Errors. Three
spec-vs-code carry-forwards documented in batch_02_report.md and
pinned with [Trait("carry_forward", ...)].
Shared scaffolding: ApiDtos.cs, AssertProblemEnvelopeAsync helper,
Seeds.cs, StubSchema.cs, CascadeF3/F4 fixtures, PostgresStopStart
fixture (gated by COMPOSE_RESTART_ENABLED). Removes the 4 placeholder
Sanity.cs files (now superseded). docker-compose.test.yml gains the
expected_results volume mount + FIXTURE_SQL_DIR for the consumer.
Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
5.4 KiB
C#
135 lines
5.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Owns its own xUnit collection because the DROP corrupts the schema for
|
|
/// every other test class. Teardown uses <see cref="ComposeRestartFixture"/>
|
|
/// (down -v && up -d) which requires <c>COMPOSE_RESTART_ENABLED=1</c>.
|
|
/// When the fixture is disabled (developer inner-loop), the test skips with
|
|
/// a clear reason — silent passing is rejected by the contract.
|
|
/// </remarks>
|
|
[Collection("ErrorEnvelope500")]
|
|
[Trait("Category", "Blackbox")]
|
|
[Trait("db_access", "seed-or-assert-only")]
|
|
public sealed class Error500Tests : TestBase, IClassFixture<ComposeRestartFixture>
|
|
{
|
|
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.");
|
|
}
|
|
}
|
|
}
|