223 lines
9.2 KiB
Dart
223 lines
9.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_models.dart';
|
|
import 'package:motorcycle_dash/services/alerts/community_traffic_parser.dart';
|
|
import 'package:motorcycle_dash/services/alerts/tomtom_road_alert_provider.dart';
|
|
import 'package:motorcycle_dash/services/alerts/mock_road_alert_provider.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
group('Production Hardening Tests', () {
|
|
// ─── Distance Calculation ────────────────────────────────────────────────
|
|
test('Distance calculation meters vs miles - no unit confusion', () {
|
|
// 1 mile is ~1609 meters
|
|
final distMeters = Geolocator.distanceBetween(
|
|
37.7749, -122.4194, // SF
|
|
37.7891, -122.4194, // ~1 mile north
|
|
);
|
|
expect(distMeters, greaterThan(1500.0)); // must be in meters
|
|
expect(distMeters, lessThan(1800.0));
|
|
|
|
// Convert to miles - must NOT be near raw meter value
|
|
final distMiles = distMeters * 0.000621371;
|
|
expect(distMiles, greaterThan(0.9));
|
|
expect(distMiles, lessThan(1.2));
|
|
expect(distMiles, isNot(closeTo(distMeters, 1.0)));
|
|
});
|
|
|
|
// ─── Bounding Box Clamping ────────────────────────────────────────────────
|
|
test('TomTom provider does not generate invalid bounding box near poles', () {
|
|
final provider = TomTomRoadAlertProvider();
|
|
// Near north pole - must not throw, must clamp latitudes
|
|
expect(() async => await provider.getAlerts(
|
|
latitude: 84.9,
|
|
longitude: 0.0,
|
|
radiusMeters: 500.0,
|
|
), returnsNormally);
|
|
});
|
|
|
|
// ─── Alert Expiration ─────────────────────────────────────────────────────
|
|
test('Expired alerts are not shown as current', () {
|
|
final now = DateTime.now();
|
|
|
|
final expiredAlert = RoadAlert(
|
|
id: 'exp_1',
|
|
providerId: 'test',
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
description: 'This alert has expired',
|
|
reportedAt: now.subtract(const Duration(hours: 2)),
|
|
expiresAt: now.subtract(const Duration(minutes: 30)),
|
|
lastUpdated: now.subtract(const Duration(hours: 2)),
|
|
source: 'Test',
|
|
);
|
|
|
|
final liveAlert = RoadAlert(
|
|
id: 'live_1',
|
|
providerId: 'test',
|
|
category: RoadAlertCategory.police,
|
|
latitude: 37.7750,
|
|
longitude: -122.4195,
|
|
description: 'This alert is live',
|
|
reportedAt: now.subtract(const Duration(minutes: 5)),
|
|
expiresAt: now.add(const Duration(minutes: 25)),
|
|
lastUpdated: now,
|
|
source: 'Test',
|
|
);
|
|
|
|
final alerts = [expiredAlert, liveAlert];
|
|
final activeAlerts = alerts.where((a) =>
|
|
a.expiresAt == null || a.expiresAt!.isAfter(now)).toList();
|
|
|
|
expect(activeAlerts.length, equals(1));
|
|
expect(activeAlerts.first.id, equals('live_1'));
|
|
});
|
|
|
|
// ─── Deduplication ────────────────────────────────────────────────────────
|
|
test('Alerts are deduplicated by providerId + id', () {
|
|
final now = DateTime.now();
|
|
|
|
RoadAlert makeAlert(String id) => RoadAlert(
|
|
id: id,
|
|
providerId: 'test_provider',
|
|
category: RoadAlertCategory.accident,
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
description: 'Accident',
|
|
reportedAt: now,
|
|
expiresAt: now.add(const Duration(hours: 1)),
|
|
lastUpdated: now,
|
|
source: 'Test',
|
|
);
|
|
|
|
final alerts = [
|
|
makeAlert('acc_1'),
|
|
makeAlert('acc_1'), // duplicate
|
|
makeAlert('acc_2'),
|
|
];
|
|
|
|
final deduplicated = <String, RoadAlert>{};
|
|
for (var a in alerts) {
|
|
final key = '${a.providerId}_${a.id}';
|
|
deduplicated[key] = a;
|
|
}
|
|
|
|
expect(deduplicated.length, equals(2));
|
|
});
|
|
|
|
// ─── Category Parsing ─────────────────────────────────────────────────────
|
|
test('CommunityTrafficParser maps all police subtypes correctly', () {
|
|
final inputs = ['POLICE', 'POLICE_VISIBLE', 'POLICE_HIDDEN', 'POLICE_TRAP'];
|
|
for (final subtype in inputs) {
|
|
final cat = CommunityTrafficParser.mapTypeToCategory('POLICE', subtype);
|
|
expect(cat, equals(RoadAlertCategory.police),
|
|
reason: 'Expected police for subtype $subtype');
|
|
}
|
|
});
|
|
|
|
test('CommunityTrafficParser maps speed camera types correctly', () {
|
|
expect(CommunityTrafficParser.mapTypeToCategory('SPEED_CAM', ''),
|
|
equals(RoadAlertCategory.speedCamera));
|
|
expect(CommunityTrafficParser.mapTypeToCategory('', 'RED_LIGHT_CAMERA'),
|
|
equals(RoadAlertCategory.speedCamera));
|
|
});
|
|
|
|
test('CommunityTrafficParser handles unknown type as other', () {
|
|
final cat = CommunityTrafficParser.mapTypeToCategory('UNKNOWN_FUTURE_TYPE', '');
|
|
// Should not throw - should fall back to 'other'
|
|
expect(cat, isNotNull);
|
|
});
|
|
|
|
test('CommunityTrafficParser skips alerts with missing coordinates', () {
|
|
final jsonMissingCoords = {
|
|
'alerts': [
|
|
{'id': 'no_coords', 'type': 'HAZARD', 'subtype': ''},
|
|
{'id': 'valid', 'type': 'ACCIDENT', 'subtype': '', 'latitude': 37.77, 'longitude': -122.41},
|
|
]
|
|
};
|
|
|
|
final alerts = CommunityTrafficParser.parseAlertsJson(
|
|
jsonMissingCoords,
|
|
providerId: 'test',
|
|
providerName: 'Test',
|
|
);
|
|
|
|
expect(alerts.length, equals(1));
|
|
expect(alerts.first.id, equals('valid'));
|
|
});
|
|
|
|
// ─── Provider Capabilities ────────────────────────────────────────────────
|
|
test('TomTom does NOT advertise police or speed camera categories', () {
|
|
final provider = TomTomRoadAlertProvider();
|
|
expect(provider.supportedCategories, isNot(contains(RoadAlertCategory.police)));
|
|
expect(provider.supportedCategories, isNot(contains(RoadAlertCategory.speedCamera)));
|
|
});
|
|
|
|
test('Mock provider returns empty in non-debug mode', () async {
|
|
// This test can only verify in the current build mode
|
|
final mockProvider = MockRoadAlertProvider();
|
|
if (!kDebugMode) {
|
|
final alerts = await mockProvider.getAlerts(
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
radiusMeters: 1000.0,
|
|
);
|
|
expect(alerts.isEmpty, isTrue,
|
|
reason: 'Mock provider must not expose alerts in release mode');
|
|
}
|
|
});
|
|
|
|
// ─── Riverpod Settings Defaults ───────────────────────────────────────────
|
|
test('Road alerts settings have safe defaults', () {
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
expect(container.read(roadAlertsEnabledProvider), isTrue);
|
|
expect(container.read(audioAlertsEnabledProvider), isTrue);
|
|
expect(container.read(alertRadiusMetersProvider), equals(804.7));
|
|
expect(container.read(tomtomApiKeyProvider), isEmpty);
|
|
expect(container.read(communityApiKeyProvider), anyOf(isEmpty, contains('wz_live')));
|
|
expect(container.read(tomtomStatusProvider),
|
|
equals(RoadAlertProviderStatus.notConfigured));
|
|
expect(container.read(communityStatusProvider),
|
|
anyOf(equals(RoadAlertProviderStatus.notConfigured), equals(RoadAlertProviderStatus.configured)));
|
|
});
|
|
|
|
test('Active providers list type is correct', () {
|
|
// Just verify the provider definition is sane without reading SharedPreferences.
|
|
// Full integration is verified on-device.
|
|
expect(activeRoadAlertProviders, isNotNull);
|
|
});
|
|
|
|
// ─── Alert Radius Conversions ─────────────────────────────────────────────
|
|
test('Alert radius values match documented distances', () {
|
|
const ft500 = 152.4; // 500 feet in meters
|
|
const ft1000 = 304.8;
|
|
const quarterMile = 402.3;
|
|
const halfMile = 804.7;
|
|
const oneMile = 1609.3;
|
|
|
|
// Verify none are equal (distinct values)
|
|
final radii = [ft500, ft1000, quarterMile, halfMile, oneMile];
|
|
final unique = radii.toSet();
|
|
expect(unique.length, equals(5));
|
|
|
|
// Verify ordering
|
|
expect(ft500, lessThan(ft1000));
|
|
expect(ft1000, lessThan(quarterMile));
|
|
expect(quarterMile, lessThan(halfMile));
|
|
expect(halfMile, lessThan(oneMile));
|
|
});
|
|
});
|
|
}
|