379 lines
14 KiB
Dart
379 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_models.dart';
|
|
import 'package:motorcycle_dash/services/alerts/waze_road_alert_provider.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
group('WazeRoadAlertProvider Metadata Tests', () {
|
|
test('provider has correct id, name, and supported categories', () {
|
|
final provider = WazeRoadAlertProvider();
|
|
expect(provider.id, equals('waze'));
|
|
expect(provider.name, equals('Waze Police Alerts'));
|
|
expect(
|
|
provider.supportedCategories,
|
|
equals({RoadAlertCategory.police}),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('WazeRoadAlertProvider Connection Tests', () {
|
|
test('testConnection returns notConfigured for empty or whitespace key', () async {
|
|
final provider = WazeRoadAlertProvider();
|
|
expect(await provider.testConnection(''), equals(RoadAlertProviderStatus.notConfigured));
|
|
expect(await provider.testConnection(' '), equals(RoadAlertProviderStatus.notConfigured));
|
|
});
|
|
|
|
test('testConnection returns connected on HTTP 200 with required headers and params', () async {
|
|
final mockClient = MockClient((request) async {
|
|
expect(request.url.host, equals('api.openwebninja.com'));
|
|
expect(request.url.path, equals('/waze/alerts-and-jams'));
|
|
expect(request.headers['x-api-key'], equals('test_waze_key'));
|
|
expect(request.url.queryParameters['center'], equals('34.0522,-118.2437'));
|
|
expect(request.url.queryParameters['radius_units'], equals('KM'));
|
|
expect(request.url.queryParameters['max_alerts'], equals('1'));
|
|
expect(request.url.queryParameters['max_jams'], equals('0'));
|
|
expect(request.url.queryParameters['alert_types'], equals('POLICE'));
|
|
return http.Response(json.encode({'status': 'success', 'data': {'alerts': []}}), 200);
|
|
});
|
|
|
|
final provider = WazeRoadAlertProvider(client: mockClient);
|
|
final status = await provider.testConnection('test_waze_key');
|
|
expect(status, equals(RoadAlertProviderStatus.connected));
|
|
});
|
|
|
|
test('testConnection returns authError on HTTP 401 or 403', () async {
|
|
final mockClient401 = MockClient((request) async {
|
|
return http.Response('{"message": "Unauthorized"}', 401);
|
|
});
|
|
final provider401 = WazeRoadAlertProvider(client: mockClient401);
|
|
expect(await provider401.testConnection('bad_key'), equals(RoadAlertProviderStatus.authError));
|
|
|
|
final mockClient403 = MockClient((request) async {
|
|
return http.Response('{"message": "Forbidden"}', 403);
|
|
});
|
|
final provider403 = WazeRoadAlertProvider(client: mockClient403);
|
|
expect(await provider403.testConnection('forbidden_key'), equals(RoadAlertProviderStatus.authError));
|
|
});
|
|
|
|
test('testConnection returns rateLimited on HTTP 429', () async {
|
|
final mockClient = MockClient((request) async {
|
|
return http.Response('{"message": "Too Many Requests"}', 429);
|
|
});
|
|
final provider = WazeRoadAlertProvider(client: mockClient);
|
|
expect(await provider.testConnection('busy_key'), equals(RoadAlertProviderStatus.rateLimited));
|
|
});
|
|
|
|
test('testConnection returns networkError on HTTP 500 or network exception', () async {
|
|
final mockClient500 = MockClient((request) async {
|
|
return http.Response('{"message": "Internal Server Error"}', 500);
|
|
});
|
|
final provider500 = WazeRoadAlertProvider(client: mockClient500);
|
|
expect(await provider500.testConnection('key'), equals(RoadAlertProviderStatus.networkError));
|
|
|
|
final mockClientException = MockClient((request) async {
|
|
throw http.ClientException('Connection reset by peer');
|
|
});
|
|
final providerEx = WazeRoadAlertProvider(client: mockClientException);
|
|
expect(await providerEx.testConnection('key'), equals(RoadAlertProviderStatus.networkError));
|
|
});
|
|
});
|
|
|
|
group('WazeRoadAlertProvider Query & Parsing Tests', () {
|
|
test('getAlerts returns empty list if apiKey is null or empty', () async {
|
|
final provider = WazeRoadAlertProvider();
|
|
final r1 = await provider.getAlerts(
|
|
latitude: 34.05,
|
|
longitude: -118.25,
|
|
radiusMeters: 1000,
|
|
apiKey: null,
|
|
);
|
|
expect(r1, isEmpty);
|
|
|
|
final r2 = await provider.getAlerts(
|
|
latitude: 34.05,
|
|
longitude: -118.25,
|
|
radiusMeters: 1000,
|
|
apiKey: ' ',
|
|
);
|
|
expect(r2, isEmpty);
|
|
});
|
|
|
|
test('getAlerts enforces alert_types=POLICE, max_jams=0, and radius_units=KM', () async {
|
|
final mockClient = MockClient((request) async {
|
|
expect(request.url.host, equals('api.openwebninja.com'));
|
|
expect(request.url.path, equals('/waze/alerts-and-jams'));
|
|
expect(request.headers['x-api-key'], equals('ninja_key_123'));
|
|
expect(request.url.queryParameters['center'], equals('37.7749,-122.4194'));
|
|
expect(request.url.queryParameters['radius'], equals('2.5'));
|
|
expect(request.url.queryParameters['radius_units'], equals('KM'));
|
|
expect(request.url.queryParameters['max_alerts'], equals('25'));
|
|
expect(request.url.queryParameters['max_jams'], equals('0'));
|
|
expect(request.url.queryParameters['alert_types'], equals('POLICE'));
|
|
|
|
return http.Response(
|
|
json.encode({
|
|
'status': 'success',
|
|
'data': {
|
|
'alerts': [
|
|
{
|
|
'alert_id': '101',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_VISIBLE',
|
|
'latitude': 37.775,
|
|
'longitude': -122.419,
|
|
'street': 'Market St',
|
|
'city': 'San Francisco, CA',
|
|
'publish_datetime_utc': '2026-09-26T18:00:00.000Z',
|
|
'alert_confidence': 5,
|
|
'alert_reliability': 8,
|
|
'num_thumbs_up': 4,
|
|
},
|
|
{
|
|
'alert_id': '102',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_HIDDEN',
|
|
'latitude': 37.778,
|
|
'longitude': -122.421,
|
|
'street': 'Mission St',
|
|
'city': 'San Francisco, CA',
|
|
'publish_datetime_utc': '2026-09-26T18:05:00.000Z',
|
|
'alert_confidence': 4,
|
|
'alert_reliability': 7,
|
|
'num_thumbs_up': 1,
|
|
},
|
|
// Unrelated category that must be strictly discarded
|
|
{
|
|
'alert_id': '999',
|
|
'type': 'HAZARD',
|
|
'subtype': 'HAZARD_ON_SHOULDER',
|
|
'latitude': 37.780,
|
|
'longitude': -122.425,
|
|
}
|
|
]
|
|
}
|
|
}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
final provider = WazeRoadAlertProvider(client: mockClient);
|
|
final alerts = await provider.getAlerts(
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
radiusMeters: 2500,
|
|
apiKey: 'ninja_key_123',
|
|
);
|
|
|
|
// Only the 2 police alerts are parsed; hazard is strictly dropped
|
|
expect(alerts.length, equals(2));
|
|
|
|
final visibleTrap = alerts[0];
|
|
expect(visibleTrap.id, equals('waze_101'));
|
|
expect(visibleTrap.category, equals(RoadAlertCategory.police));
|
|
expect(visibleTrap.severity, equals(RoadAlertSeverity.medium));
|
|
expect(visibleTrap.description, contains('Police speed trap reported on Market St'));
|
|
expect(visibleTrap.roadName, equals('Market St, San Francisco, CA'));
|
|
expect(visibleTrap.source, equals('Waze'));
|
|
|
|
final hiddenTrap = alerts[1];
|
|
expect(hiddenTrap.id, equals('waze_102'));
|
|
expect(hiddenTrap.category, equals(RoadAlertCategory.police));
|
|
expect(hiddenTrap.severity, equals(RoadAlertSeverity.high));
|
|
expect(hiddenTrap.description, contains('Hidden police speed trap reported on Mission St'));
|
|
});
|
|
|
|
test('parseWazeItem strictly rejects non-POLICE alerts', () {
|
|
final provider = WazeRoadAlertProvider();
|
|
expect(
|
|
provider.parseWazeItem({
|
|
'alert_id': '1',
|
|
'type': 'ACCIDENT',
|
|
'latitude': 34.0,
|
|
'longitude': -118.0,
|
|
}),
|
|
isNull,
|
|
);
|
|
expect(
|
|
provider.parseWazeItem({
|
|
'alert_id': '2',
|
|
'type': 'JAM',
|
|
'latitude': 34.0,
|
|
'longitude': -118.0,
|
|
}),
|
|
isNull,
|
|
);
|
|
expect(
|
|
provider.parseWazeItem({
|
|
'alert_id': '3',
|
|
'type': 'HAZARD',
|
|
'latitude': 34.0,
|
|
'longitude': -118.0,
|
|
}),
|
|
isNull,
|
|
);
|
|
expect(
|
|
provider.parseWazeItem({
|
|
'alert_id': '4',
|
|
'type': 'ROAD_CLOSED',
|
|
'latitude': 34.0,
|
|
'longitude': -118.0,
|
|
}),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('parseWazeItem handles missing street or generic subtype smoothly', () {
|
|
final provider = WazeRoadAlertProvider();
|
|
final alert = provider.parseWazeItem({
|
|
'alert_id': '555',
|
|
'type': 'POLICE',
|
|
'latitude': 34.0522,
|
|
'longitude': -118.2437,
|
|
});
|
|
|
|
expect(alert, isNotNull);
|
|
expect(alert!.id, equals('waze_555'));
|
|
expect(alert.category, equals(RoadAlertCategory.police));
|
|
expect(alert.description, equals('Police reported in area'));
|
|
});
|
|
});
|
|
|
|
group('Riverpod Wiring for Waze Police Alerts Tests', () {
|
|
test('parseWazeItem handles POLICE_WITH_MOBILE_CAMERA subtype', () {
|
|
final provider = WazeRoadAlertProvider();
|
|
final alert = provider.parseWazeItem({
|
|
'alert_id': 'camera_123',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_WITH_MOBILE_CAMERA',
|
|
'latitude': 34.078,
|
|
'longitude': -118.266,
|
|
'street': 'Sunset Blvd',
|
|
'city': 'Los Angeles',
|
|
'num_thumbs_up': 25,
|
|
});
|
|
|
|
expect(alert, isNotNull);
|
|
expect(alert!.id, equals('waze_camera_123'));
|
|
expect(alert.category, equals(RoadAlertCategory.police));
|
|
expect(alert.severity, equals(RoadAlertSeverity.high));
|
|
expect(alert.description, equals('Police speed trap with camera reported on Sunset Blvd'));
|
|
expect(alert.roadName, equals('Sunset Blvd, Los Angeles'));
|
|
});
|
|
|
|
test('parseWazeItem handles POLICE_HIDING and POLICE_ON_OTHER_SIDE subtypes', () {
|
|
final provider = WazeRoadAlertProvider();
|
|
final hidingAlert = provider.parseWazeItem({
|
|
'alert_id': 'hiding_1',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_HIDING',
|
|
'latitude': 40.656,
|
|
'longitude': -74.009,
|
|
'street': '2nd Ave',
|
|
'city': 'New York',
|
|
});
|
|
expect(hidingAlert, isNotNull);
|
|
expect(hidingAlert!.severity, equals(RoadAlertSeverity.high));
|
|
expect(hidingAlert.description, contains('Hidden police speed trap reported on 2nd Ave'));
|
|
|
|
final otherSideAlert = provider.parseWazeItem({
|
|
'alert_id': 'side_2',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_ON_OTHER_SIDE',
|
|
'latitude': 40.658,
|
|
'longitude': -74.011,
|
|
'street': '3rd Ave',
|
|
});
|
|
expect(otherSideAlert, isNotNull);
|
|
expect(otherSideAlert!.severity, equals(RoadAlertSeverity.medium));
|
|
expect(otherSideAlert.description, contains('Police reported on opposite side of 3rd Ave'));
|
|
});
|
|
|
|
test('getAlertsInBoundingBox sends bottom_left and top_right with POLICE filter', () async {
|
|
final mockClient = MockClient((request) async {
|
|
expect(request.url.host, equals('api.openwebninja.com'));
|
|
expect(request.url.path, equals('/waze/alerts-and-jams'));
|
|
expect(request.headers['x-api-key'], equals('bbox_key'));
|
|
expect(request.url.queryParameters['bottom_left'], equals('40.6661,-74.1373'));
|
|
expect(request.url.queryParameters['top_right'], equals('40.7727,-73.7681'));
|
|
expect(request.url.queryParameters['max_alerts'], equals('15'));
|
|
expect(request.url.queryParameters['max_jams'], equals('0'));
|
|
expect(request.url.queryParameters['alert_types'], equals('POLICE'));
|
|
|
|
return http.Response(
|
|
json.encode({
|
|
'status': 'OK',
|
|
'data': {
|
|
'alerts': [
|
|
{
|
|
'alert_id': 'ny_trap_1',
|
|
'type': 'POLICE',
|
|
'subtype': 'POLICE_HIDING',
|
|
'latitude': 40.700,
|
|
'longitude': -73.950,
|
|
'street': 'FDR Drive',
|
|
}
|
|
]
|
|
}
|
|
}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
final provider = WazeRoadAlertProvider(client: mockClient);
|
|
final alerts = await provider.getAlertsInBoundingBox(
|
|
bottomLat: 40.6661,
|
|
bottomLng: -74.1373,
|
|
topLat: 40.7727,
|
|
topLng: -73.7681,
|
|
apiKey: 'bbox_key',
|
|
maxAlerts: 15,
|
|
);
|
|
|
|
expect(alerts.length, equals(1));
|
|
expect(alerts.first.id, equals('waze_ny_trap_1'));
|
|
expect(alerts.first.category, equals(RoadAlertCategory.police));
|
|
expect(alerts.first.severity, equals(RoadAlertSeverity.high));
|
|
});
|
|
|
|
test('wazeApiKeyProvider initializes with default key and can update', () async {
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
final initialKey = container.read(wazeApiKeyProvider);
|
|
expect(initialKey, equals('ak_dud2s1987x5k7amfokrddptn41ov0iyf8kfdu3ljpv1fgvq'));
|
|
|
|
await container.read(wazeApiKeyProvider.notifier).setApiKey('new_ninja_key_999');
|
|
expect(container.read(wazeApiKeyProvider), equals('new_ninja_key_999'));
|
|
});
|
|
|
|
test('wazeRoadAlertsEnabledProvider defaults to true and toggles', () async {
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
expect(container.read(wazeRoadAlertsEnabledProvider), isTrue);
|
|
|
|
await container.read(wazeRoadAlertsEnabledProvider.notifier).setEnabled(false);
|
|
expect(container.read(wazeRoadAlertsEnabledProvider), isFalse);
|
|
});
|
|
|
|
test('activeRoadAlertProviders includes Waze provider when enabled and key present', () {
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
final activeList = container.read(activeRoadAlertProviders);
|
|
expect(activeList.any((p) => p.id == 'waze'), isTrue);
|
|
});
|
|
});
|
|
}
|