diff --git a/GoogleMapsApi.Test/AssertInconclusiveTests.cs b/GoogleMapsApi.Test/AssertInconclusiveTests.cs index cb43f4d..1ffd7c6 100644 --- a/GoogleMapsApi.Test/AssertInconclusiveTests.cs +++ b/GoogleMapsApi.Test/AssertInconclusiveTests.cs @@ -44,5 +44,35 @@ public void HasTransitStep_OkResponseWithoutTransitStep_IsInconclusive() Assert.Throws(() => AssertInconclusive.HasTransitStep(response)); } + + [Test] + public void EnforcedRouteLengthLimit_RouteRejected_Passes() + { + var response = new DirectionsResponse { Status = DirectionsStatusCodes.MAX_ROUTE_LENGTH_EXCEEDED }; + + Assert.DoesNotThrow(() => AssertInconclusive.EnforcedRouteLengthLimit(response)); + } + + [Test] + public void EnforcedRouteLengthLimit_GoogleAcceptedTheRoute_IsInconclusive() + { + var response = new DirectionsResponse { Status = DirectionsStatusCodes.OK }; + + Assert.Throws(() => AssertInconclusive.EnforcedRouteLengthLimit(response)); + } + + [Test] + public void EnforcedRouteLengthLimit_UnrelatedFailure_Fails() + { + var response = new DirectionsResponse + { + Status = DirectionsStatusCodes.REQUEST_DENIED, + ErrorMessage = "API key rejected", + }; + + var ex = Assert.Throws(() => AssertInconclusive.EnforcedRouteLengthLimit(response)); + + Assert.That(ex!.Message, Does.Contain("API key rejected")); + } } } diff --git a/GoogleMapsApi.Test/DirectionsUnitTests.cs b/GoogleMapsApi.Test/DirectionsUnitTests.cs new file mode 100644 index 0000000..08bfb8c --- /dev/null +++ b/GoogleMapsApi.Test/DirectionsUnitTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GoogleMapsApi.Engine; +using GoogleMapsApi.Entities.Directions.Request; +using GoogleMapsApi.Entities.Directions.Response; +using NUnit.Framework; + +namespace GoogleMapsApi.Test +{ + /// + /// Offline coverage for the Directions status codes. The live + /// DirectionsTests.Directions_ExceedingRouteLength can only observe whatever Google's + /// current server-side route-length threshold happens to be, so the contract we actually own - + /// mapping the wire status onto - is pinned here instead. + /// + [TestFixture] + public class DirectionsUnitTests + { + [Test] + public async Task MaxRouteLengthExceeded_IsDeserialized() + { + const string json = """ + { + "geocoded_waypoints" : [], + "routes" : [], + "status" : "MAX_ROUTE_LENGTH_EXCEEDED" + } + """; + + var response = await QueryAsync(json); + + Assert.That(response.Status, Is.EqualTo(DirectionsStatusCodes.MAX_ROUTE_LENGTH_EXCEEDED)); + Assert.That(response.Routes, Is.Empty); + } + + [Test] + public async Task ErrorStatus_CarriesErrorMessage() + { + const string json = """ + { + "error_message" : "The provided API key is invalid.", + "routes" : [], + "status" : "REQUEST_DENIED" + } + """; + + var response = await QueryAsync(json); + + Assert.That(response.Status, Is.EqualTo(DirectionsStatusCodes.REQUEST_DENIED)); + Assert.That(response.ErrorMessage, Is.EqualTo("The provided API key is invalid.")); + } + + [TestCase("OK", DirectionsStatusCodes.OK)] + [TestCase("NOT_FOUND", DirectionsStatusCodes.NOT_FOUND)] + [TestCase("ZERO_RESULTS", DirectionsStatusCodes.ZERO_RESULTS)] + [TestCase("MAX_WAYPOINTS_EXCEEDED", DirectionsStatusCodes.MAX_WAYPOINTS_EXCEEDED)] + [TestCase("MAX_ROUTE_LENGTH_EXCEEDED", DirectionsStatusCodes.MAX_ROUTE_LENGTH_EXCEEDED)] + [TestCase("INVALID_REQUEST", DirectionsStatusCodes.INVALID_REQUEST)] + [TestCase("OVER_QUERY_LIMIT", DirectionsStatusCodes.OVER_QUERY_LIMIT)] + [TestCase("REQUEST_DENIED", DirectionsStatusCodes.REQUEST_DENIED)] + [TestCase("UNKNOWN_ERROR", DirectionsStatusCodes.UNKNOWN_ERROR)] + public async Task EveryDocumentedStatus_RoundTripsFromTheWire(string wireValue, DirectionsStatusCodes expected) + { + var response = await QueryAsync($$"""{ "routes" : [], "status" : "{{wireValue}}" }"""); + + Assert.That(response.Status, Is.EqualTo(expected)); + } + + private static Task QueryAsync(string responseJson) + { + var request = new DirectionsRequest { Origin = "NYC, USA", Destination = "Miami, USA", ApiKey = "KEY" }; + var handler = new StubHandler(responseJson); + using var http = new HttpClient(handler); + return MapsAPIGenericEngine.QueryGoogleAPIAsync( + http, request, TimeSpan.FromMilliseconds(-1), CancellationToken.None, null, null); + } + + private sealed class StubHandler : HttpMessageHandler + { + private readonly string _responseJson; + public StubHandler(string responseJson) { _responseJson = responseJson; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(_responseJson, Encoding.UTF8, "application/json") + }); + } + } +} diff --git a/GoogleMapsApi.Test/IntegrationTests/DirectionsTests.cs b/GoogleMapsApi.Test/IntegrationTests/DirectionsTests.cs index 15ed2d8..1019aba 100644 --- a/GoogleMapsApi.Test/IntegrationTests/DirectionsTests.cs +++ b/GoogleMapsApi.Test/IntegrationTests/DirectionsTests.cs @@ -88,7 +88,7 @@ public async Task Directions_ExceedingRouteLength() var result = await Maps.Directions.QueryAsync(request); AssertInconclusive.NotExceedQuota(result); - Assert.That(result.Status, Is.EqualTo(DirectionsStatusCodes.MAX_ROUTE_LENGTH_EXCEEDED), result.ErrorMessage); + AssertInconclusive.EnforcedRouteLengthLimit(result); } [Test] diff --git a/GoogleMapsApi.Test/Utils/AssertInconclusive.cs b/GoogleMapsApi.Test/Utils/AssertInconclusive.cs index 16119f6..93861a2 100644 --- a/GoogleMapsApi.Test/Utils/AssertInconclusive.cs +++ b/GoogleMapsApi.Test/Utils/AssertInconclusive.cs @@ -49,6 +49,22 @@ public static void HasTransitStep(DirectionsResponse response) throw new InconclusiveException("Google returned no transit step for this route (likely a walking-only itinerary); cannot assert transit vehicle data."); } + /// + /// Google's route-length ceiling is an undocumented server-side threshold that has moved + /// before: a route it once rejected with MAX_ROUTE_LENGTH_EXCEEDED now routes fine. Treat OK + /// as inconclusive so a shifted threshold surfaces as live drift instead of a red build; any + /// other status still fails. Deterministic coverage of the status itself lives in + /// DirectionsUnitTests. + /// + public static void EnforcedRouteLengthLimit(DirectionsResponse response) + { + if (response?.Status == DirectionsStatusCodes.OK) + throw new InconclusiveException("Google accepted a route that previously exceeded its length limit; the server-side threshold has moved."); + + if (response?.Status != DirectionsStatusCodes.MAX_ROUTE_LENGTH_EXCEEDED) + Assert.Fail($"Directions API returned {response?.Status}. {response?.ErrorMessage}"); + } + /// /// If the response status indicates fail because of quota exceeded - mark test as inconclusive. ///