MotorSignal-App/test/waze_road_alert_provider_te...

400 lines
16 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(apiHost: 'waze-proxy.internal');
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('waze-proxy.internal'));
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, apiHost: 'waze-proxy.internal');
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, apiHost: 'waze-proxy.internal');
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, apiHost: 'waze-proxy.internal');
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, apiHost: 'waze-proxy.internal');
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, apiHost: 'waze-proxy.internal');
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, apiHost: 'waze-proxy.internal');
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('waze-proxy.internal'));
expect(request.url.path, equals('/waze/alerts-and-jams'));
expect(request.headers['x-api-key'], equals('custom_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, apiHost: 'waze-proxy.internal');
final alerts = await provider.getAlerts(
latitude: 37.7749,
longitude: -122.4194,
radiusMeters: 2500,
apiKey: 'custom_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('waze-proxy.internal'));
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, apiHost: 'waze-proxy.internal');
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 empty default and does not expose bundled key', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
final initialKey = container.read(wazeApiKeyProvider);
expect(initialKey, isEmpty, reason: 'Bundled API key must not be present by default');
expect(initialKey, isNot(contains('ak_dud2s1987x5k7amfokrddptn41ov0iyf8kfdu3ljpv1fgvq')));
await container.read(wazeApiKeyProvider.notifier).setApiKey('custom_key_999');
expect(container.read(wazeApiKeyProvider), equals('custom_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 excludes Waze when key is empty, includes when key is set', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// Initially no key, so Waze should not be active
var activeList = container.read(activeRoadAlertProviders);
expect(activeList.any((p) => p.id == 'waze'), isFalse);
// After user sets an API key, Waze should be active
await container.read(wazeApiKeyProvider.notifier).setApiKey('valid_test_key_123');
activeList = container.read(activeRoadAlertProviders);
expect(activeList.any((p) => p.id == 'waze'), isTrue);
});
test('WazeRoadAlertProvider uses custom apiHost if provided', () async {
final mockClient = MockClient((request) async {
expect(request.url.host, equals('custom-proxy.motorsignal.app'));
return http.Response('{"status":"success","data":{"alerts":[]}}', 200);
});
final provider = WazeRoadAlertProvider(
client: mockClient,
apiHost: 'custom-proxy.motorsignal.app',
);
final status = await provider.testConnection('test_key');
expect(status, equals(RoadAlertProviderStatus.connected));
});
});
}