229 lines
7.8 KiB
Dart
229 lines
7.8 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/alerts/road_alert_models.dart';
|
|
import 'package:motorcycle_dash/services/alerts/waze_osint_road_alert_provider.dart';
|
|
|
|
void main() {
|
|
group('WazeOsintRoadAlertProvider Category Mapping', () {
|
|
test('maps all Waze types and subtypes to 10 native MotorSignal categories', () {
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('POLICE', null),
|
|
RoadAlertCategory.police,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('POLICE', 'POLICE_HIDDEN'),
|
|
RoadAlertCategory.police,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('ACCIDENT', 'ACCIDENT_MAJOR'),
|
|
RoadAlertCategory.accident,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('JAM', 'JAM_HEAVY_TRAFFIC'),
|
|
RoadAlertCategory.traffic,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('ROAD_CLOSED', 'ROAD_CLOSED_HAZARD'),
|
|
RoadAlertCategory.roadClosure,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('WEATHERHAZARD', 'HAZARD_WEATHER_ICE'),
|
|
RoadAlertCategory.weatherHazard,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_ROAD_POT_HOLE'),
|
|
RoadAlertCategory.pothole,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_ROAD_OBJECT'),
|
|
RoadAlertCategory.debris,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_SHOULDER_CAR_STOPPED'),
|
|
RoadAlertCategory.disabledVehicle,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_ON_ROAD_CONSTRUCTION'),
|
|
RoadAlertCategory.construction,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('HAZARD', 'HAZARD_GENERIC'),
|
|
RoadAlertCategory.hazard,
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.mapTypeToCategory('OTHER', null),
|
|
RoadAlertCategory.other,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('WazeOsintRoadAlertProvider JSON Fixture Parsing', () {
|
|
late WazeOsintRoadAlertProvider provider;
|
|
|
|
setUp(() {
|
|
provider = WazeOsintRoadAlertProvider();
|
|
});
|
|
|
|
test('parses realistic LiveMap GeoRSS JSON fixture with multiple alerts', () {
|
|
final fixture = {
|
|
'alerts': [
|
|
{
|
|
'uuid': 'alert_police_001',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_HIDDEN',
|
|
'location': {'x': -83.9261, 'y': 35.3934},
|
|
'street': 'US-129 Deals Gap',
|
|
'city': 'Robbinsville',
|
|
'magvar': 180,
|
|
'reportDescription': 'Trooper on bike hiding behind curve',
|
|
'pubMillis': DateTime.now().millisecondsSinceEpoch,
|
|
'confidence': 5,
|
|
'reliability': 8,
|
|
'nThumbsUp': 4,
|
|
},
|
|
{
|
|
'uuid': 'alert_pothole_002',
|
|
'type': 'HAZARD',
|
|
'subtype': 'HAZARD_ON_ROAD_POT_HOLE',
|
|
'latitude': 35.4000,
|
|
'longitude': -83.9300,
|
|
'street': 'Cherohala Skyway',
|
|
'reportDescription': 'Deep rim-bending hole mid-corner',
|
|
'pubMillis': DateTime.now().millisecondsSinceEpoch,
|
|
'nThumbsUp': 2,
|
|
},
|
|
{
|
|
'uuid': 'alert_debris_003',
|
|
'type': 'HAZARD',
|
|
'subtype': 'HAZARD_ON_ROAD_OBJECT',
|
|
'location': {'x': -83.9200, 'y': 35.3900},
|
|
'street': 'NC-28 Moonshiner',
|
|
'nThumbsUp': 0,
|
|
},
|
|
{
|
|
'uuid': 'alert_invalid_coords',
|
|
'type': 'POLICE',
|
|
'latitude': 999.0, // invalid latitude
|
|
'longitude': -83.90,
|
|
},
|
|
{
|
|
'uuid': 'alert_missing_type',
|
|
'location': {'x': -83.90, 'y': 35.0},
|
|
}
|
|
]
|
|
};
|
|
|
|
final parsed = provider.parseWazeResponse(fixture);
|
|
|
|
expect(parsed.length, 3);
|
|
|
|
final police = parsed.firstWhere((a) => a.id == 'waze_osint_alert_police_001');
|
|
expect(police.category, RoadAlertCategory.police);
|
|
expect(police.latitude, 35.3934);
|
|
expect(police.longitude, -83.9261);
|
|
expect(police.roadName, 'US-129 Deals Gap, Robbinsville');
|
|
expect(police.description, 'Trooper on bike hiding behind curve');
|
|
expect(police.severity, RoadAlertSeverity.high);
|
|
expect(police.confirmations, 5); // 1 base + 4 thumbsUp
|
|
expect(police.isCommunityConfirmed, isTrue);
|
|
|
|
final pothole = parsed.firstWhere((a) => a.id == 'waze_osint_alert_pothole_002');
|
|
expect(pothole.category, RoadAlertCategory.pothole);
|
|
expect(pothole.latitude, 35.4000);
|
|
expect(pothole.longitude, -83.9300);
|
|
expect(pothole.roadName, 'Cherohala Skyway');
|
|
expect(pothole.confirmations, 3); // 1 base + 2 thumbsUp
|
|
expect(pothole.isCommunityConfirmed, isTrue);
|
|
|
|
final debris = parsed.firstWhere((a) => a.id == 'waze_osint_alert_debris_003');
|
|
expect(debris.category, RoadAlertCategory.debris);
|
|
expect(debris.latitude, 35.3900);
|
|
expect(debris.longitude, -83.9200);
|
|
expect(debris.confirmations, 1);
|
|
expect(debris.isCommunityConfirmed, isFalse);
|
|
});
|
|
|
|
test('deduplicates alerts sharing the same uuid', () {
|
|
final duplicateFixture = {
|
|
'alerts': [
|
|
{
|
|
'uuid': 'dup_1',
|
|
'type': 'ACCIDENT',
|
|
'location': {'x': -84.0, 'y': 35.0},
|
|
},
|
|
{
|
|
'uuid': 'dup_1',
|
|
'type': 'ACCIDENT',
|
|
'location': {'x': -84.0, 'y': 35.0},
|
|
},
|
|
]
|
|
};
|
|
|
|
final parsed = provider.parseWazeResponse(duplicateFixture);
|
|
expect(parsed.length, 1);
|
|
});
|
|
});
|
|
|
|
group('WazeOsintRoadAlertProvider HTTP Client & Rate Limiting', () {
|
|
test('getAlerts queries bounding box and caches results', () async {
|
|
int requestCount = 0;
|
|
final mockClient = MockClient((request) async {
|
|
requestCount++;
|
|
expect(request.url.host, 'www.waze.com');
|
|
expect(request.url.path, '/live-map/api/georss');
|
|
expect(request.url.queryParameters['types'], 'alerts');
|
|
expect(request.headers['User-Agent'], 'MotorSignal-Community-Alerts/1.0');
|
|
|
|
final payload = {
|
|
'alerts': [
|
|
{
|
|
'uuid': 'test_waze_live_1',
|
|
'type': 'POLICE',
|
|
'location': {'x': -83.9261, 'y': 35.3934},
|
|
'street': 'US-129 Deals Gap',
|
|
'pubMillis': DateTime.now().millisecondsSinceEpoch,
|
|
'nThumbsUp': 3,
|
|
}
|
|
]
|
|
};
|
|
|
|
return http.Response(json.encode(payload), 200, headers: {
|
|
'content-type': 'application/json',
|
|
});
|
|
});
|
|
|
|
final provider = WazeOsintRoadAlertProvider(
|
|
client: mockClient,
|
|
minQueryInterval: const Duration(seconds: 30),
|
|
);
|
|
|
|
// 1. Initial query executes HTTP request
|
|
final alerts1 = await provider.getAlerts(
|
|
latitude: 35.3934,
|
|
longitude: -83.9261,
|
|
radiusMeters: 5000,
|
|
);
|
|
expect(requestCount, 1);
|
|
expect(alerts1.length, 1);
|
|
expect(alerts1.first.category, RoadAlertCategory.police);
|
|
|
|
// 2. Immediate second query within 30s interval uses cache without extra HTTP call
|
|
final alerts2 = await provider.getAlerts(
|
|
latitude: 35.3934,
|
|
longitude: -83.9261,
|
|
radiusMeters: 5000,
|
|
);
|
|
expect(requestCount, 1); // No new network call
|
|
expect(alerts2.length, 1);
|
|
});
|
|
|
|
test('testConnection returns connected without requiring paid API key', () async {
|
|
final provider = WazeOsintRoadAlertProvider();
|
|
final status = await provider.testConnection('');
|
|
expect(status, RoadAlertProviderStatus.connected);
|
|
});
|
|
});
|
|
}
|