MotorSignal-App/test/here_road_alert_provider_te...

264 lines
11 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/here_road_alert_provider.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
SharedPreferences.setMockInitialValues({});
});
group('HereRoadAlertProvider Metadata Tests', () {
test('provider has correct id, name, and supported categories', () {
final provider = HereRoadAlertProvider();
expect(provider.id, equals('here_safety_cameras'));
expect(provider.name, equals('HERE Safety Cameras'));
expect(
provider.supportedCategories,
containsAll([
RoadAlertCategory.police,
RoadAlertCategory.speedCamera,
RoadAlertCategory.hazard,
]),
);
});
});
group('HereRoadAlertProvider Connection Tests', () {
test('testConnection returns notConfigured for empty or whitespace key', () async {
final provider = HereRoadAlertProvider();
expect(await provider.testConnection(''), equals(RoadAlertProviderStatus.notConfigured));
expect(await provider.testConnection(' '), equals(RoadAlertProviderStatus.notConfigured));
});
test('testConnection returns connected on HTTP 200', () async {
final mockClient = MockClient((request) async {
expect(request.url.host, equals('safetycameras.hereapi.com'));
expect(request.url.queryParameters['apiKey'], equals('valid_test_key'));
return http.Response(json.encode({'items': []}), 200);
});
final provider = HereRoadAlertProvider(client: mockClient);
final status = await provider.testConnection('valid_test_key');
expect(status, equals(RoadAlertProviderStatus.connected));
});
test('testConnection returns authError on HTTP 401 or 403', () async {
final mockClient401 = MockClient((request) async {
return http.Response('{"error": "Unauthorized"}', 401);
});
final provider401 = HereRoadAlertProvider(client: mockClient401);
expect(await provider401.testConnection('bad_key'), equals(RoadAlertProviderStatus.authError));
final mockClient403 = MockClient((request) async {
return http.Response('{"error": "Forbidden"}', 403);
});
final provider403 = HereRoadAlertProvider(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('{"error": "Rate limit exceeded"}', 429);
});
final provider = HereRoadAlertProvider(client: mockClient);
expect(await provider.testConnection('busy_key'), equals(RoadAlertProviderStatus.rateLimited));
});
test('testConnection returns networkError on 500 or network exception', () async {
final mockClient500 = MockClient((request) async {
return http.Response('{"error": "Internal Error"}', 500);
});
final provider500 = HereRoadAlertProvider(client: mockClient500);
expect(await provider500.testConnection('key'), equals(RoadAlertProviderStatus.networkError));
final mockClientException = MockClient((request) async {
throw http.ClientException('Connection refused');
});
final providerEx = HereRoadAlertProvider(client: mockClientException);
expect(await providerEx.testConnection('key'), equals(RoadAlertProviderStatus.networkError));
});
});
group('HereRoadAlertProvider Parsing & Query Tests', () {
test('getAlerts returns empty list if apiKey is null or empty', () async {
final provider = HereRoadAlertProvider();
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 correctly formats request and parses fixed & mobile camera items', () async {
final sampleResponse = {
'items': [
{
'id': 'here_cam_001',
'type': 'FIXED_SPEED_CAMERA',
'position': {'lat': 34.0522, 'lng': -118.2437},
'road': 'US-101 Northbound',
'speedLimit': 65,
'speedLimitUnit': 'MPH',
'heading': 350.0,
'description': 'Speed enforcement camera',
},
{
'id': 'here_cam_002',
'type': 'MOBILE_SPEED_CAMERA',
'coordinate': {'latitude': 34.0600, 'longitude': -118.2500},
'road': 'I-10 Westbound',
'speedLimit': 55,
'bearing': 270.0,
},
{
'id': 'here_cam_003',
'type': 'RED_LIGHT_CAMERA',
'position': {'lat': 34.0450, 'lng': -118.2400},
'road': 'Grand Ave at 7th St',
},
{
'id': 'corrupt_item',
'type': 'FIXED_SPEED_CAMERA',
'position': {}, // Missing lat/lng
},
]
};
final mockClient = MockClient((request) async {
expect(request.url.queryParameters['apiKey'], equals('test_key_123'));
expect(request.url.queryParameters['in'], contains('circle:34.0522,-118.2437;r=1500'));
return http.Response(json.encode(sampleResponse), 200);
});
final provider = HereRoadAlertProvider(client: mockClient);
final alerts = await provider.getAlerts(
latitude: 34.0522,
longitude: -118.2437,
radiusMeters: 1500,
apiKey: 'test_key_123',
);
expect(alerts.length, equals(3)); // Corrupt item skipped
// Alert 1: Fixed speed camera
final alert1 = alerts[0];
expect(alert1.id, equals('here_cam_001'));
expect(alert1.providerId, equals('here_safety_cameras'));
expect(alert1.category, equals(RoadAlertCategory.speedCamera));
expect(alert1.severity, equals(RoadAlertSeverity.medium));
expect(alert1.latitude, equals(34.0522));
expect(alert1.longitude, equals(-118.2437));
expect(alert1.roadName, equals('US-101 Northbound'));
expect(alert1.heading, equals(350.0));
expect(alert1.description, equals('Speed enforcement camera'));
expect(alert1.source, equals('HERE Technologies'));
expect(alert1.sourceType, equals(RoadAlertSourceType.providerReported));
// Alert 2: Mobile speed camera (mapped to Police)
final alert2 = alerts[1];
expect(alert2.id, equals('here_cam_002'));
expect(alert2.category, equals(RoadAlertCategory.police));
expect(alert2.severity, equals(RoadAlertSeverity.high));
expect(alert2.latitude, equals(34.0600));
expect(alert2.longitude, equals(-118.2500));
expect(alert2.roadName, equals('I-10 Westbound'));
expect(alert2.heading, equals(270.0));
expect(alert2.description, contains('Mobile Speed Trap (55 MPH zone)'));
// Alert 3: Red light camera
final alert3 = alerts[2];
expect(alert3.id, equals('here_cam_003'));
expect(alert3.category, equals(RoadAlertCategory.speedCamera));
expect(alert3.severity, equals(RoadAlertSeverity.medium));
expect(alert3.latitude, equals(34.0450));
expect(alert3.longitude, equals(-118.2400));
expect(alert3.description, contains('Enforcement Camera'));
});
test('getAlerts handles server failure response gracefully', () async {
final mockClient = MockClient((request) async {
return http.Response('{"error": "Internal Server Error"}', 500);
});
final provider = HereRoadAlertProvider(client: mockClient);
final alerts = await provider.getAlerts(
latitude: 34.0522,
longitude: -118.2437,
radiusMeters: 1000,
apiKey: 'test_key',
);
expect(alerts, isEmpty);
});
});
group('HERE Road Alerts Riverpod Integration Tests', () {
test('hereApiKeyProvider and hereRoadAlertsEnabledProvider update properly', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(hereApiKeyProvider), isEmpty);
expect(container.read(hereRoadAlertsEnabledProvider), isTrue);
await container.read(hereApiKeyProvider.notifier).setApiKey('here_new_token');
expect(container.read(hereApiKeyProvider), equals('here_new_token'));
await container.read(hereRoadAlertsEnabledProvider.notifier).setEnabled(false);
expect(container.read(hereRoadAlertsEnabledProvider), isFalse);
});
test('hereStatusProvider reports configured only when key present and enabled', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// Initially no key -> notConfigured
expect(container.read(hereStatusProvider), equals(RoadAlertProviderStatus.notConfigured));
// Set key -> configured
await container.read(hereApiKeyProvider.notifier).setApiKey('test_key');
expect(container.read(hereStatusProvider), equals(RoadAlertProviderStatus.configured));
// Disable provider -> notConfigured
await container.read(hereRoadAlertsEnabledProvider.notifier).setEnabled(false);
expect(container.read(hereStatusProvider), equals(RoadAlertProviderStatus.notConfigured));
});
test('activeRoadAlertProviders dynamically includes HERE provider when enabled with key', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// Without key, here_safety_cameras should not be in activeRoadAlertProviders
var active = container.read(activeRoadAlertProviders);
expect(active.any((p) => p.id == 'here_safety_cameras'), isFalse);
// Add key -> should be included
await container.read(hereApiKeyProvider.notifier).setApiKey('my_here_key');
active = container.read(activeRoadAlertProviders);
expect(active.any((p) => p.id == 'here_safety_cameras'), isTrue);
// Disable HERE -> should not be included
await container.read(hereRoadAlertsEnabledProvider.notifier).setEnabled(false);
active = container.read(activeRoadAlertProviders);
expect(active.any((p) => p.id == 'here_safety_cameras'), isFalse);
});
});
}