using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using Azaion.Missions.E2E.Fixtures; using Azaion.Missions.E2E.Helpers; using Xunit; namespace Azaion.Missions.E2E.Tests.Waypoints; /// /// FT-P-13..15 โ€” waypoint happy-path scenarios. FT-P-18 (cascade delete) is /// in , FT-P-16/17 (health) are in /// Tests/Health/HealthTests.cs. /// Traces: AC-4.3 / AC-4 (data_parameters ยง 2.3) / AC-4.4. /// [Collection("Waypoints")] [Trait("Category", "Blackbox")] [Trait("db_access", "seed-or-assert-only")] public sealed class PositiveTests : TestBase, IClassFixture { [Fact] [Trait("Traces", "AC-4.3")] [Trait("max_ms", "2000")] public async Task FT_P_13_waypoint_list_is_ordered_by_order_num_asc() { // Arrange DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel); Seeds.Apply(Seeds.FiveWaypointsUnordered.Sql); var token = await Tokens.MintDefaultAsync(); // Act using var http = new HttpRequestMessage( HttpMethod.Get, $"/missions/{Seeds.FiveWaypointsUnordered.MissionId}/waypoints"); http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt); using var response = await Missions.SendAsync(http); // Assert await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.OK); var raw = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(raw); Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); var waypoints = JsonSerializer.Deserialize>(raw) ?? throw new InvalidOperationException($"could not deserialize array: {raw}"); Assert.Equal(5, waypoints.Count); Assert.Equal(new[] { 1, 2, 3, 4, 5 }, waypoints.Select(w => w.OrderNum).ToArray()); } [Fact] [Trait("Traces", "AC-4")] [Trait("max_ms", "2000")] [Trait("carry_forward", "waypoint-response-flat-vs-nested-geo")] public async Task FT_P_14_create_waypoint_echoes_lat_lon_and_does_not_auto_convert_to_mgrs() { // CARRY-FORWARD: the canonical task spec (AZ-579 AC-2) says the // response body has nested "GeoPoint:{Lat,Lon,Mgrs}". The actual SUT // (Database/Entities/Waypoint.cs + Controllers/MissionsController.cs) // returns the LinqToDB entity directly, which has flat Lat/Lon/Mgrs // columns โ€” there is no GeoPoint object in the response. Per /autodev // batch 2 user choice we assert the CODE shape (flat) here. Flip when // the spec/code divergence is closed. // Arrange DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel); Seeds.Apply(Seeds.FiveWaypointsUnordered.Sql); var missionId = Seeds.FiveWaypointsUnordered.MissionId; var token = await Tokens.MintDefaultAsync(); // Act using var http = new HttpRequestMessage(HttpMethod.Post, $"/missions/{missionId}/waypoints") { Content = JsonContent.Create(new { GeoPoint = new { Lat = 50.45m, Lon = 30.52m, Mgrs = (string?)null }, WaypointSource = 0, WaypointObjective = 0, OrderNum = 99, Height = 120m }) }; http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt); using var response = await Missions.SendAsync(http); // Assert await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.Created); var waypoint = await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("waypoint body deserialized to null"); Assert.Equal(50.45m, waypoint.Lat); Assert.Equal(30.52m, waypoint.Lon); Assert.Null(waypoint.Mgrs); } [Fact] [Trait("Traces", "AC-4.4")] [Trait("max_ms", "2000")] [Trait("carry_forward", "waypoint-response-flat-vs-nested-geo")] public async Task FT_P_15_waypoint_update_is_full_overwrite_height_zero_geofields_cleared() { // CARRY-FORWARD: same flat-vs-nested divergence as FT-P-14. The "full // overwrite" semantic IS pinned: send Height:0 and assert the prior // Height:120 is replaced; send geo nullable fields and assert they // become null. // Arrange DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel); Seeds.Apply(Seeds.FiveWaypointsUnordered.Sql); var missionId = Seeds.FiveWaypointsUnordered.MissionId; var targetWaypoint = await GetSeededWaypointAsync(missionId); // Sanity check the seed shape โ€” the original Height for a seed row // is 100/110/120/130/140; pick whichever waypoint has Height==120. var token = await Tokens.MintDefaultAsync(); // Act using var http = new HttpRequestMessage( HttpMethod.Put, $"/missions/{missionId}/waypoints/{targetWaypoint.Id}") { Content = JsonContent.Create(new { GeoPoint = (object?)null, WaypointSource = 1, WaypointObjective = 1, OrderNum = targetWaypoint.OrderNum + 100, Height = 0m }) }; http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt); using var response = await Missions.SendAsync(http); // Assert await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.OK); var updated = await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("waypoint body deserialized to null"); Assert.Equal(0m, updated.Height); Assert.Equal(targetWaypoint.OrderNum + 100, updated.OrderNum); Assert.Null(updated.Lat); Assert.Null(updated.Lon); Assert.Null(updated.Mgrs); Assert.Equal(1, updated.WaypointSource); Assert.Equal(1, updated.WaypointObjective); } private async Task GetSeededWaypointAsync(Guid missionId) { var token = await Tokens.MintDefaultAsync(); using var http = new HttpRequestMessage(HttpMethod.Get, $"/missions/{missionId}/waypoints"); http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt); using var resp = await Missions.SendAsync(http); resp.EnsureSuccessStatusCode(); var list = await resp.Content.ReadFromJsonAsync>() ?? throw new InvalidOperationException("waypoints list deserialized to null"); return list.First(w => w.OrderNum == 1); } }