MotorSignal-App/test/aws_location_route_provider...

428 lines
14 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:motorcycle_dash/services/map/map_provider_interface.dart';
import 'package:motorcycle_dash/services/routes/aws_location_route_provider.dart';
import 'package:motorcycle_dash/services/routes/route_models.dart';
import 'package:motorcycle_dash/services/routes/route_provider.dart';
void main() {
group('AwsLocationRouteProvider Tests', () {
test('instance properties and custom configuration', () {
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
region: 'us-west-2',
);
expect(provider.name, equals('Amazon AWS Location'));
expect(provider.apiKey, equals('test-key'));
expect(provider.region, equals('us-west-2'));
});
test('searchPlaces returns empty list for queries shorter than 2 characters', () async {
final provider = AwsLocationRouteProvider();
final results = await provider.searchPlaces('a');
expect(results, isEmpty);
});
test('searchPlaces parses combined autocomplete and searchText results', () async {
final mockClient = MockClient((request) async {
if (request.url.path.contains('/v2/autocomplete')) {
expect(request.url.queryParameters['key'], equals('test-key'));
final body = jsonDecode(request.body);
expect(body['QueryText'], equals('Atlanta'));
return http.Response(
jsonEncode({
'ResultItems': [
{
'PlaceId': 'place_auto_1',
'Title': 'Atlanta, GA, USA',
'Address': {'Label': 'Atlanta, Fulton County, Georgia, USA'},
}
]
}),
200,
);
} else if (request.url.path.contains('/v2/search-text')) {
return http.Response(
jsonEncode({
'ResultItems': [
{
'PlaceId': 'place_text_1',
'Title': 'Atlanta City Hall',
'Address': {'Label': '68 Mitchell St SW, Atlanta, GA 30303'},
'Position': [-84.3902, 33.7490],
}
]
}),
200,
);
}
return http.Response('Not Found', 404);
});
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
client: mockClient,
);
final results = await provider.searchPlaces(
'Atlanta',
nearLocation: const AppLatLng(33.7490, -84.3880),
);
expect(results.length, equals(2));
// First is text search result with coordinates
expect(results[0].providerId, equals('place_text_1'));
expect(results[0].primaryText, equals('Atlanta City Hall'));
expect(results[0].latitude, equals(33.7490));
expect(results[0].longitude, equals(-84.3902));
expect(results[0].hasCoordinates, isTrue);
// Second is autocomplete result
expect(results[1].providerId, equals('place_auto_1'));
expect(results[1].primaryText, equals('Atlanta, GA, USA'));
expect(results[1].hasCoordinates, isFalse);
});
test('resolvePlace fetches place details and returns ResolvedPlace', () async {
final mockClient = MockClient((request) async {
expect(request.url.path, contains('/v2/place/place_123'));
expect(request.url.queryParameters['key'], equals('test-key'));
return http.Response(
jsonEncode({
'PlaceId': 'place_123',
'Title': 'Blue Ridge Parkway North End',
'Address': {'Label': 'Afton, VA 22920'},
'Position': [-78.8608, 38.0315],
}),
200,
);
});
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
client: mockClient,
);
final place = await provider.resolvePlace('place_123');
expect(place.providerId, equals('place_123'));
expect(place.displayName, equals('Blue Ridge Parkway North End'));
expect(place.formattedAddress, equals('Afton, VA 22920'));
expect(place.latitude, equals(38.0315));
expect(place.longitude, equals(-78.8608));
});
test('resolvePlace throws on empty placeId or 404', () async {
final mockClient = MockClient((request) async {
return http.Response('Not Found', 404);
});
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
client: mockClient,
);
expect(
() => provider.resolvePlace(' '),
throwsA(isA<RouteProviderException>().having((e) => e.code, 'code', 'invalidRequest')),
);
expect(
() => provider.resolvePlace('missing_id'),
throwsA(isA<RouteProviderException>().having((e) => e.code, 'code', 'placeNotFound')),
);
});
test('calculateRoute requires at least 2 stops', () async {
final provider = AwsLocationRouteProvider();
expect(
() => provider.calculateRoute([
const RouteStop(
id: '1',
order: 0,
displayName: 'Origin',
latitude: 34.0,
longitude: -84.0,
),
]),
throwsA(isA<RouteProviderException>().having((e) => e.code, 'code', 'invalidRequest')),
);
});
test('calculateRoute computes route and returns parsed geometry', () async {
final mockClient = MockClient((request) async {
expect(request.url.path, contains('/v2/routes'));
expect(request.url.queryParameters['key'], equals('test-key'));
final body = jsonDecode(request.body);
expect(body['TravelMode'], equals('Car'));
expect(body['LegGeometryFormat'], equals('Simple'));
expect(body['Origin'], equals([-84.0, 34.0]));
expect(body['Destination'], equals([-84.5, 34.5]));
return http.Response(
jsonEncode({
'Routes': [
{
'Summary': {
'Distance': 65432.1,
'Duration': 2700,
},
'Legs': [
{
'Geometry': {
'LineString': [
[-84.0, 34.0],
[-84.25, 34.25],
[-84.5, 34.5],
],
},
},
],
},
],
}),
200,
);
});
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
client: mockClient,
);
final stops = [
const RouteStop(
id: '1',
order: 0,
displayName: 'Start',
latitude: 34.0,
longitude: -84.0,
),
const RouteStop(
id: '2',
order: 1,
displayName: 'End',
latitude: 34.5,
longitude: -84.5,
),
];
final result = await provider.calculateRoute(stops);
expect(result.estimatedDistanceMeters, equals(65432.1));
expect(result.estimatedDurationSeconds, equals(2700));
expect(result.geometry.length, equals(3));
expect(result.geometry[0].latitude, equals(34.0));
expect(result.geometry[0].longitude, equals(-84.0));
expect(result.geometry[2].latitude, equals(34.5));
expect(result.geometry[2].longitude, equals(-84.5));
expect(result.isStraightLine, isFalse);
expect(result.providerName, equals('Amazon AWS Location'));
// Fallback maneuvers synthesized from geometry
expect(result.maneuvers.isNotEmpty, isTrue);
expect(result.maneuvers.first.type, equals(ManeuverType.depart));
expect(result.maneuvers.last.type, equals(ManeuverType.arrive));
});
test('calculateRoute with RoutingOptions applies avoidHighways and TravelSteps maneuvers', () async {
final mockClient = MockClient((request) async {
final body = jsonDecode(request.body);
expect(body['TravelModeOptions']['Car']['Avoid']['Highways'], isTrue);
expect(body['OptimizeRoutingFor'], equals('ShortestRoute'));
expect(body['LegAdditionalInputs'], contains('TravelSteps'));
return http.Response(
jsonEncode({
'Routes': [
{
'Summary': {
'Distance': 15000.0,
'Duration': 900,
},
'Legs': [
{
'Geometry': {
'LineString': [
[-84.0, 34.0],
[-84.05, 34.05],
[-84.10, 34.10],
],
},
'VehicleLegDetails': {
'TravelSteps': [
{
'Instruction': 'Head north on Main St',
'Type': 'Depart',
'Distance': 5000.0,
'Duration': 300,
'GeometryOffset': 0,
'CurrentRoad': 'Main St',
},
{
'Instruction': 'Turn right onto Canyon Rd',
'Type': 'Turn',
'Turn': 'Right',
'Distance': 10000.0,
'Duration': 600,
'GeometryOffset': 1,
'NextRoad': 'Canyon Rd',
},
{
'Instruction': 'Arrive at destination',
'Type': 'Arrive',
'Distance': 0.0,
'Duration': 0,
'GeometryOffset': 2,
},
],
},
},
],
},
],
}),
200,
);
});
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
client: mockClient,
);
final stops = [
const RouteStop(id: '1', displayName: 'A', latitude: 34.0, longitude: -84.0),
const RouteStop(id: '2', displayName: 'B', latitude: 34.10, longitude: -84.10),
];
final result = await provider.calculateRoute(
stops,
options: const RoutingOptions(
preference: RoutingPreference.shortest,
avoidHighways: true,
),
);
expect(result.maneuvers.length, equals(3));
expect(result.maneuvers[0].instruction, equals('Head north on Main St'));
expect(result.maneuvers[0].type, equals(ManeuverType.depart));
expect(result.maneuvers[1].instruction, equals('Turn right onto Canyon Rd'));
expect(result.maneuvers[1].type, equals(ManeuverType.turnRight));
expect(result.maneuvers[1].streetName, equals('Canyon Rd'));
expect(result.maneuvers[2].type, equals(ManeuverType.arrive));
expect(result.formattedDuration, equals('15 min'));
expect(result.curvatureMetric, isNotNull);
});
test('calculateRoute with twisty preference requests alternatives and selects highest curvature', () async {
final mockClient = MockClient((request) async {
final body = jsonDecode(request.body);
expect(body['MaxAlternatives'], equals(2));
expect(body['TravelModeOptions']['Car']['Avoid']['Highways'], isTrue);
return http.Response(
jsonEncode({
'Routes': [
{
// Route 1: Straight route
'Summary': {'Distance': 10000.0, 'Duration': 600},
'Legs': [
{
'Geometry': {
'LineString': [
[-84.0, 34.0],
[-84.05, 34.05],
[-84.10, 34.10],
],
},
},
],
},
{
// Route 2: Highly twisty mountain alternative with sharp turns
'Summary': {'Distance': 12000.0, 'Duration': 900},
'Legs': [
{
'Geometry': {
'LineString': [
[-84.0, 34.0],
[-84.02, 34.01],
[-84.01, 34.03],
[-84.05, 34.04],
[-84.03, 34.07],
[-84.08, 34.08],
[-84.10, 34.10],
],
},
},
],
},
],
}),
200,
);
});
final provider = AwsLocationRouteProvider(
apiKey: 'test-key',
client: mockClient,
);
final stops = [
const RouteStop(id: '1', displayName: 'A', latitude: 34.0, longitude: -84.0),
const RouteStop(id: '2', displayName: 'B', latitude: 34.10, longitude: -84.10),
];
final result = await provider.calculateRoute(
stops,
options: const RoutingOptions(
preference: RoutingPreference.twisty,
),
);
// Verify that Route 2 was chosen because it has more curvature
expect(result.estimatedDistanceMeters, equals(12000.0));
expect(result.estimatedDurationSeconds, equals(900));
expect(result.geometry.length, equals(7));
expect(result.curvatureMetric, isNotNull);
expect(result.curvatureMetric, greaterThan(0));
});
test('calculateRoute extracts actionable error message on AWS API failure', () async {
final mockClient = MockClient((request) async {
return http.Response(
jsonEncode({
'message': 'AccessDeniedException: The provided API key is unauthorized for routes.geo.us-east-1.',
}),
403,
);
});
final provider = AwsLocationRouteProvider(
apiKey: 'invalid-key',
client: mockClient,
);
final stops = [
const RouteStop(id: '1', displayName: 'A', latitude: 34.0, longitude: -84.0),
const RouteStop(id: '2', displayName: 'B', latitude: 34.10, longitude: -84.10),
];
expect(
() => provider.calculateRoute(stops),
throwsA(
isA<RouteProviderException>()
.having((e) => e.code, 'code', 'routeUnavailable')
.having(
(e) => e.message,
'message',
contains('AccessDeniedException: The provided API key is unauthorized'),
),
),
);
});
});
}