376 lines
13 KiB
Dart
376 lines
13 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 with injected client returns connected', () async {
|
|
final mockClient = MockClient((request) async {
|
|
return http.Response(json.encode({'alerts': []}), 200);
|
|
});
|
|
final provider = WazeOsintRoadAlertProvider(client: mockClient);
|
|
final status = await provider.testConnection('');
|
|
expect(status, RoadAlertProviderStatus.connected);
|
|
});
|
|
});
|
|
|
|
group('WazeOsintRoadAlertProvider Environment / Region Selection', () {
|
|
test('resolves North America (na) for US, Canada, Mexico, Puerto Rico, Alaska, Hawaii', () {
|
|
// Deals Gap, NC (US-129)
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 35.3934, longitude: -83.9261),
|
|
'na',
|
|
);
|
|
// Los Angeles, CA
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 34.0522, longitude: -118.2437),
|
|
'na',
|
|
);
|
|
// Toronto, ON, Canada
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 43.6532, longitude: -79.3832),
|
|
'na',
|
|
);
|
|
// Mexico City, Mexico
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 19.4326, longitude: -99.1332),
|
|
'na',
|
|
);
|
|
// Honolulu, HI
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 21.3069, longitude: -157.8583),
|
|
'na',
|
|
);
|
|
// Anchorage, AK
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 61.2181, longitude: -149.9003),
|
|
'na',
|
|
);
|
|
// San Juan, Puerto Rico
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 18.4655, longitude: -66.1057),
|
|
'na',
|
|
);
|
|
});
|
|
|
|
test('resolves Israel (il) for Israeli coordinates', () {
|
|
// Tel Aviv
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 32.0853, longitude: 34.7818),
|
|
'il',
|
|
);
|
|
// Jerusalem
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 31.7683, longitude: 35.2137),
|
|
'il',
|
|
);
|
|
// Haifa
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 32.7940, longitude: 34.9896),
|
|
'il',
|
|
);
|
|
});
|
|
|
|
test('resolves Rest of World (row) for Europe, Asia, Australia, South America', () {
|
|
// London, UK
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 51.5074, longitude: -0.1278),
|
|
'row',
|
|
);
|
|
// Paris, France
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 48.8566, longitude: 2.3522),
|
|
'row',
|
|
);
|
|
// Tokyo, Japan
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 35.6762, longitude: 139.6503),
|
|
'row',
|
|
);
|
|
// Sydney, Australia
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: -33.8688, longitude: 151.2093),
|
|
'row',
|
|
);
|
|
// Sao Paulo, Brazil
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: -23.5505, longitude: -46.6333),
|
|
'row',
|
|
);
|
|
// Cape Town, South Africa
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: -33.9249, longitude: 18.4241),
|
|
'row',
|
|
);
|
|
});
|
|
|
|
test('handles coordinate edge cases, NaN and antimeridian crossing safely', () {
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: double.nan, longitude: double.nan),
|
|
'row',
|
|
);
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 95.0, longitude: -100.0),
|
|
'row',
|
|
);
|
|
// Western Aleutians crossing antimeridian (e.g. Attu Island at 52.9°N, 173.2°E)
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 52.9, longitude: 173.2),
|
|
'na',
|
|
);
|
|
// Normalized longitude across antimeridian (-186.8° wraps to 173.2°E)
|
|
expect(
|
|
WazeOsintRoadAlertProvider.determineEnvironment(latitude: 52.9, longitude: -186.8),
|
|
'na',
|
|
);
|
|
});
|
|
});
|
|
|
|
group('WazeOsintRoadAlertProvider Capability Gate & Network Isolation', () {
|
|
test('isLiveNetworkAllowed is false by default in standard builds', () {
|
|
expect(WazeOsintRoadAlertProvider.isLiveNetworkAllowed, isFalse);
|
|
});
|
|
|
|
test('uninjected provider without capability gate fails closed (notConfigured & 0 requests)', () async {
|
|
final defaultProvider = WazeOsintRoadAlertProvider();
|
|
|
|
// testConnection fails closed
|
|
final status = await defaultProvider.testConnection('');
|
|
expect(status, RoadAlertProviderStatus.notConfigured);
|
|
|
|
// getAlerts fails closed and returns empty without throwing
|
|
final alerts = await defaultProvider.getAlerts(
|
|
latitude: 35.3934,
|
|
longitude: -83.9261,
|
|
radiusMeters: 5000,
|
|
);
|
|
expect(alerts, isEmpty);
|
|
});
|
|
|
|
test('injected client allows testing and fixtures without live network', () async {
|
|
final mockClient = MockClient((request) async {
|
|
return http.Response(json.encode({'alerts': []}), 200);
|
|
});
|
|
final testProvider = WazeOsintRoadAlertProvider(client: mockClient);
|
|
|
|
final status = await testProvider.testConnection('');
|
|
expect(status, RoadAlertProviderStatus.connected);
|
|
});
|
|
});
|
|
}
|