428 lines
14 KiB
Dart
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'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|