609 lines
21 KiB
Dart
609 lines
21 KiB
Dart
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_models.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
|
|
import 'package:motorcycle_dash/services/alerts/swarm_road_alert_models.dart';
|
|
import 'package:motorcycle_dash/services/alerts/swarm_road_alert_provider.dart';
|
|
import 'package:motorcycle_dash/services/alerts/swarm_road_alert_service.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
group('SwarmReport Model Tests', () {
|
|
test('roundtrip serialization to/from JSON', () {
|
|
final now = DateTime.now();
|
|
final expires = now.add(const Duration(hours: 1));
|
|
|
|
final report = SwarmReport(
|
|
id: 'swarm_test_1',
|
|
category: RoadAlertCategory.police,
|
|
latitude: 35.3934,
|
|
longitude: -83.9261,
|
|
heading: 180.0,
|
|
roadName: 'US-129 Deals Gap',
|
|
description: 'Tennessee Highway Patrol stationary radar',
|
|
reporterUid: 'rider_abc',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 3,
|
|
dismissals: 1,
|
|
groupRideId: 'group_ride_123',
|
|
);
|
|
|
|
final json = report.toJson();
|
|
expect(json['id'], 'swarm_test_1');
|
|
expect(json['category'], 'police');
|
|
expect(json['latitude'], 35.3934);
|
|
expect(json['longitude'], -83.9261);
|
|
expect(json['confirmations'], 3);
|
|
expect(json['dismissals'], 1);
|
|
expect(json['groupRideId'], 'group_ride_123');
|
|
|
|
final fromJson = SwarmReport.fromJson(json);
|
|
expect(fromJson.id, report.id);
|
|
expect(fromJson.category, RoadAlertCategory.police);
|
|
expect(fromJson.latitude, 35.3934);
|
|
expect(fromJson.longitude, -83.9261);
|
|
expect(fromJson.confirmations, 3);
|
|
expect(fromJson.dismissals, 1);
|
|
expect(fromJson.groupRideId, 'group_ride_123');
|
|
});
|
|
|
|
test('confidence scoring based on confirmations and dismissals', () {
|
|
final now = DateTime.now();
|
|
final expires = now.add(const Duration(hours: 1));
|
|
|
|
// 3 confirmations, 1 dismissal -> 3/4 = 0.75
|
|
final r1 = SwarmReport(
|
|
id: '1',
|
|
category: RoadAlertCategory.police,
|
|
latitude: 35.0,
|
|
longitude: -83.0,
|
|
description: 'Test',
|
|
reporterUid: 'u1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 3,
|
|
dismissals: 1,
|
|
);
|
|
expect(r1.confidence, closeTo(0.75, 0.001));
|
|
|
|
// 1 confirmation, 0 dismissals -> 1/1 = 1.0
|
|
final r2 = SwarmReport(
|
|
id: '2',
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 35.0,
|
|
longitude: -83.0,
|
|
description: 'Test',
|
|
reporterUid: 'u1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 1,
|
|
dismissals: 0,
|
|
);
|
|
expect(r2.confidence, 1.0);
|
|
|
|
// Clamp minimum
|
|
final r3 = SwarmReport(
|
|
id: '3',
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 35.0,
|
|
longitude: -83.0,
|
|
description: 'Test',
|
|
reporterUid: 'u1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 0,
|
|
dismissals: 10,
|
|
);
|
|
expect(r3.confidence, 0.1);
|
|
});
|
|
|
|
test('toRoadAlert maps severity and source attributes properly', () {
|
|
final now = DateTime.now();
|
|
final expires = now.add(const Duration(hours: 1));
|
|
|
|
final policeReport = SwarmReport(
|
|
id: 'police_1',
|
|
category: RoadAlertCategory.police,
|
|
latitude: 35.39,
|
|
longitude: -83.92,
|
|
description: 'Speed trap',
|
|
reporterUid: 'uid1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
);
|
|
|
|
final policeAlert = policeReport.toRoadAlert(distanceFromUser: 250.0);
|
|
expect(policeAlert.providerId, 'swarm_alerts');
|
|
expect(policeAlert.severity, RoadAlertSeverity.high);
|
|
expect(policeAlert.source, 'MotorSignal Swarm');
|
|
expect(policeAlert.sourceType, RoadAlertSourceType.userReported);
|
|
expect(policeAlert.distanceFromUser, 250.0);
|
|
|
|
final packReport = SwarmReport(
|
|
id: 'hazard_1',
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 35.39,
|
|
longitude: -83.92,
|
|
description: 'Gravel in corner',
|
|
reporterUid: 'uid2',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
groupRideId: 'pack_77',
|
|
);
|
|
|
|
final packAlert = packReport.toRoadAlert();
|
|
expect(packAlert.severity, RoadAlertSeverity.medium);
|
|
expect(packAlert.source, 'Pack Rider Report');
|
|
});
|
|
|
|
test('isExpired detects when TTL has lapsed', () {
|
|
final past = DateTime.now().subtract(const Duration(minutes: 5));
|
|
final report = SwarmReport(
|
|
id: 'expired_1',
|
|
category: RoadAlertCategory.police,
|
|
latitude: 35.0,
|
|
longitude: -83.0,
|
|
description: 'Old trap',
|
|
reporterUid: 'u1',
|
|
reportedAt: past.subtract(const Duration(hours: 1)),
|
|
expiresAt: past,
|
|
);
|
|
expect(report.isExpired, isTrue);
|
|
});
|
|
});
|
|
|
|
group('SwarmRoadAlertService Cloud & Cache Tests', () {
|
|
late FakeFirebaseFirestore fakeFirestore;
|
|
late SwarmRoadAlertService service;
|
|
|
|
setUp(() {
|
|
fakeFirestore = FakeFirebaseFirestore();
|
|
service = SwarmRoadAlertService(firestore: fakeFirestore);
|
|
});
|
|
|
|
test('reportAlert saves to Cloud Firestore and caches in memory', () async {
|
|
final report = await service.reportAlert(
|
|
category: RoadAlertCategory.police,
|
|
latitude: 34.0522,
|
|
longitude: -118.2437,
|
|
roadName: 'Mulholland Drive',
|
|
description: 'Radar trap near lookout',
|
|
);
|
|
|
|
expect(report.id, startsWith('swarm_'));
|
|
expect(report.category, RoadAlertCategory.police);
|
|
expect(report.confirmations, 1);
|
|
expect(report.dismissals, 0);
|
|
|
|
// Check Firestore doc
|
|
final doc = await fakeFirestore.collection('road_alerts').doc(report.id).get();
|
|
expect(doc.exists, isTrue);
|
|
expect(doc.data()!['category'], 'police');
|
|
expect(doc.data()!['latitude'], 34.0522);
|
|
expect(doc.data()!['confirmations'], 1);
|
|
|
|
// Default TTL for police is 60 minutes
|
|
final reportedAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['reportedAt'] as int);
|
|
final expiresAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['expiresAt'] as int);
|
|
expect(expiresAt.difference(reportedAt).inMinutes, 60);
|
|
});
|
|
|
|
test('reportAlert hazard defaults to 120 minutes TTL', () async {
|
|
final report = await service.reportAlert(
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 35.0,
|
|
longitude: -83.0,
|
|
description: 'Gravel patch',
|
|
);
|
|
|
|
final doc = await fakeFirestore.collection('road_alerts').doc(report.id).get();
|
|
final reportedAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['reportedAt'] as int);
|
|
final expiresAt = DateTime.fromMillisecondsSinceEpoch(doc.data()!['expiresAt'] as int);
|
|
expect(expiresAt.difference(reportedAt).inMinutes, 120);
|
|
});
|
|
|
|
test('confirmAlert writes vote to subcollection and increments confirmations in memory', () async {
|
|
final report = await service.reportAlert(
|
|
category: RoadAlertCategory.police,
|
|
latitude: 34.0,
|
|
longitude: -118.0,
|
|
);
|
|
|
|
final result = await service.confirmAlert(report.id, voterUid: 'rider_trailing');
|
|
expect(result, isTrue);
|
|
|
|
final voteDoc = await fakeFirestore
|
|
.collection('road_alerts')
|
|
.doc(report.id)
|
|
.collection('votes')
|
|
.doc('rider_trailing')
|
|
.get();
|
|
expect(voteDoc.exists, isTrue);
|
|
expect(voteDoc.data()!['type'], 'confirm');
|
|
expect(voteDoc.data()!['voterUid'], 'rider_trailing');
|
|
expect(service.getCachedReport(report.id)?.confirmations, 2);
|
|
});
|
|
|
|
test('clearAlert writes dismissal vote to subcollection and auto-expires after 2 dismissals', () async {
|
|
final report = await service.reportAlert(
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 34.0,
|
|
longitude: -118.0,
|
|
);
|
|
|
|
// 1st clear: increments dismissals to 1 and stores vote in subcollection
|
|
final res1 = await service.clearAlert(report.id, voterUid: 'rider_a');
|
|
expect(res1, isTrue);
|
|
final voteA = await fakeFirestore
|
|
.collection('road_alerts')
|
|
.doc(report.id)
|
|
.collection('votes')
|
|
.doc('rider_a')
|
|
.get();
|
|
expect(voteA.exists, isTrue);
|
|
expect(voteA.data()!['type'], 'dismiss');
|
|
expect(service.getCachedReport(report.id)?.dismissals, 1);
|
|
|
|
// 2nd clear: dismissals becomes 2, which is >= confirmations (1) -> expires immediately
|
|
final res2 = await service.clearAlert(report.id, voterUid: 'rider_b');
|
|
expect(res2, isTrue);
|
|
final voteB = await fakeFirestore
|
|
.collection('road_alerts')
|
|
.doc(report.id)
|
|
.collection('votes')
|
|
.doc('rider_b')
|
|
.get();
|
|
expect(voteB.exists, isTrue);
|
|
expect(voteB.data()!['type'], 'dismiss');
|
|
expect(service.getCachedReport(report.id)?.dismissals, 2);
|
|
expect(service.getCachedReport(report.id)?.status, RoadAlertStatus.cleared);
|
|
});
|
|
|
|
test('getAlertsNear finds alerts within radius and excludes distant ones', () async {
|
|
// 1. Report alert near Downtown LA (lat: 34.05, lng: -118.25)
|
|
await service.reportAlert(
|
|
category: RoadAlertCategory.police,
|
|
latitude: 34.0500,
|
|
longitude: -118.2500,
|
|
description: 'Downtown trap',
|
|
);
|
|
|
|
// 2. Report alert in San Francisco (distant: > 500 km)
|
|
await service.reportAlert(
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
description: 'SF hazard',
|
|
);
|
|
|
|
// Query around Downtown LA with 2000 meter radius
|
|
final nearbyAlerts = await service.getAlertsNear(
|
|
latitude: 34.0505,
|
|
longitude: -118.2505,
|
|
radiusMeters: 2000,
|
|
);
|
|
|
|
expect(nearbyAlerts.length, 1);
|
|
expect(nearbyAlerts.first.category, RoadAlertCategory.police);
|
|
expect(nearbyAlerts.first.description, 'Downtown trap');
|
|
expect(nearbyAlerts.first.distanceFromUser, isNotNull);
|
|
expect(nearbyAlerts.first.distanceFromUser!, lessThan(2000));
|
|
});
|
|
|
|
test('offline fallback works seamlessly without Cloud Firestore', () async {
|
|
final offlineService = SwarmRoadAlertService(firestore: null, auth: null);
|
|
expect(offlineService.isCloudAvailable, isFalse);
|
|
|
|
final report = await offlineService.reportAlert(
|
|
category: RoadAlertCategory.accident,
|
|
latitude: 35.39,
|
|
longitude: -83.92,
|
|
description: 'Offline crash report',
|
|
);
|
|
|
|
expect(report.id, startsWith('swarm_'));
|
|
|
|
final alerts = await offlineService.getAlertsNear(
|
|
latitude: 35.39,
|
|
longitude: -83.92,
|
|
radiusMeters: 1000,
|
|
);
|
|
|
|
expect(alerts.length, 1);
|
|
expect(alerts.first.category, RoadAlertCategory.accident);
|
|
});
|
|
});
|
|
|
|
group('SwarmRoadAlertProvider Tests', () {
|
|
test('implements RoadAlertProvider contract correctly', () async {
|
|
final service = SwarmRoadAlertService(firestore: null, auth: null);
|
|
final provider = SwarmRoadAlertProvider(service);
|
|
|
|
expect(provider.id, 'swarm_alerts');
|
|
expect(provider.name, 'MotorSignal Rider Swarm');
|
|
expect(provider.supportedCategories.contains(RoadAlertCategory.police), isTrue);
|
|
expect(provider.supportedCategories.contains(RoadAlertCategory.hazard), isTrue);
|
|
|
|
final status = await provider.testConnection('');
|
|
expect(status, RoadAlertProviderStatus.connected);
|
|
});
|
|
});
|
|
|
|
group('Riverpod Swarm Provider Wiring Tests', () {
|
|
test('activeRoadAlertProviders respects swarmAlertsEnabledProvider toggle', () async {
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
// Default is enabled
|
|
final initialEnabled = container.read(swarmAlertsEnabledProvider);
|
|
expect(initialEnabled, isTrue);
|
|
|
|
var activeProviders = container.read(activeRoadAlertProviders);
|
|
expect(activeProviders.any((p) => p.id == 'swarm_alerts'), isTrue);
|
|
|
|
// Disable swarm alerts
|
|
await container.read(swarmAlertsEnabledProvider.notifier).setEnabled(false);
|
|
expect(container.read(swarmAlertsEnabledProvider), isFalse);
|
|
|
|
activeProviders = container.read(activeRoadAlertProviders);
|
|
expect(activeProviders.any((p) => p.id == 'swarm_alerts'), isFalse);
|
|
|
|
// Re-enable
|
|
await container.read(swarmAlertsEnabledProvider.notifier).setEnabled(true);
|
|
expect(container.read(swarmAlertsEnabledProvider), isTrue);
|
|
|
|
activeProviders = container.read(activeRoadAlertProviders);
|
|
expect(activeProviders.any((p) => p.id == 'swarm_alerts'), isTrue);
|
|
});
|
|
});
|
|
|
|
group('Native Road Alert Categories & Verification Trust Tests', () {
|
|
test('all 10 native categories map properly to RoadAlert', () {
|
|
final now = DateTime.now();
|
|
final expires = now.add(const Duration(hours: 2));
|
|
|
|
final categories = [
|
|
RoadAlertCategory.police,
|
|
RoadAlertCategory.accident,
|
|
RoadAlertCategory.hazard,
|
|
RoadAlertCategory.pothole,
|
|
RoadAlertCategory.debris,
|
|
RoadAlertCategory.disabledVehicle,
|
|
RoadAlertCategory.roadClosure,
|
|
RoadAlertCategory.construction,
|
|
RoadAlertCategory.traffic,
|
|
RoadAlertCategory.weatherHazard,
|
|
];
|
|
|
|
for (final cat in categories) {
|
|
final report = SwarmReport(
|
|
id: 'report_${cat.name}',
|
|
category: cat,
|
|
subtype: 'severe_${cat.name}',
|
|
latitude: 35.0,
|
|
longitude: -84.0,
|
|
description: 'Rider alert for ${cat.name}',
|
|
reporterUid: 'rider_1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 1,
|
|
dismissals: 0,
|
|
source: 'MotorSignal Swarm',
|
|
status: RoadAlertStatus.active,
|
|
);
|
|
|
|
final alert = report.toRoadAlert();
|
|
expect(alert.category, cat);
|
|
expect(alert.subtype, 'severe_${cat.name}');
|
|
expect(alert.isCommunityConfirmed, isFalse);
|
|
expect(alert.confirmations, 1);
|
|
expect(alert.dismissals, 0);
|
|
expect(alert.status, RoadAlertStatus.active);
|
|
}
|
|
});
|
|
|
|
test('isCommunityConfirmed requires at least 2 confirmations', () {
|
|
final now = DateTime.now();
|
|
final expires = now.add(const Duration(hours: 2));
|
|
|
|
final unconfirmed = SwarmReport(
|
|
id: 'rep_1',
|
|
category: RoadAlertCategory.pothole,
|
|
latitude: 35.0,
|
|
longitude: -84.0,
|
|
description: 'Pot hole test',
|
|
reporterUid: 'rider_1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 1,
|
|
dismissals: 0,
|
|
).toRoadAlert();
|
|
expect(unconfirmed.isCommunityConfirmed, isFalse);
|
|
|
|
final confirmed = SwarmReport(
|
|
id: 'rep_2',
|
|
category: RoadAlertCategory.pothole,
|
|
latitude: 35.0,
|
|
longitude: -84.0,
|
|
description: 'Pot hole test',
|
|
reporterUid: 'rider_1',
|
|
reportedAt: now,
|
|
expiresAt: expires,
|
|
confirmations: 2,
|
|
dismissals: 0,
|
|
).toRoadAlert();
|
|
expect(confirmed.isCommunityConfirmed, isTrue);
|
|
});
|
|
});
|
|
|
|
group('Rate Limiting & Duplicate Detection Tests', () {
|
|
late FakeFirebaseFirestore fakeFirestore;
|
|
|
|
setUp(() {
|
|
fakeFirestore = FakeFirebaseFirestore();
|
|
});
|
|
|
|
test('enforces rate limit interval and throws RoadAlertRateLimitException', () async {
|
|
final rateLimitedService = SwarmRoadAlertService(
|
|
firestore: fakeFirestore,
|
|
minReportInterval: const Duration(seconds: 15),
|
|
);
|
|
|
|
// First report succeeds
|
|
final first = await rateLimitedService.reportAlert(
|
|
category: RoadAlertCategory.police,
|
|
latitude: 35.0,
|
|
longitude: -84.0,
|
|
);
|
|
expect(first.id, startsWith('swarm_'));
|
|
|
|
// Second immediate report at distant location throws rate limit exception
|
|
expect(
|
|
() => rateLimitedService.reportAlert(
|
|
category: RoadAlertCategory.accident,
|
|
latitude: 36.0,
|
|
longitude: -85.0,
|
|
),
|
|
throwsA(isA<RoadAlertRateLimitException>().having(
|
|
(e) => e.retryAfterSeconds,
|
|
'retryAfterSeconds',
|
|
inInclusiveRange(1, 15),
|
|
)),
|
|
);
|
|
|
|
// With ignoreRateLimit: true, report succeeds
|
|
final bypassReport = await rateLimitedService.reportAlert(
|
|
category: RoadAlertCategory.accident,
|
|
latitude: 36.0,
|
|
longitude: -85.0,
|
|
ignoreRateLimit: true,
|
|
);
|
|
expect(bypassReport.id, startsWith('swarm_'));
|
|
});
|
|
|
|
test('duplicate reports within 200m and 15min auto-confirm existing alert', () async {
|
|
final service = SwarmRoadAlertService(
|
|
firestore: fakeFirestore,
|
|
minReportInterval: Duration.zero,
|
|
duplicateDistanceMeters: 200.0,
|
|
duplicateWindow: const Duration(minutes: 15),
|
|
);
|
|
|
|
// Rider 1 reports a pothole at lat 35.0000, lng -84.0000
|
|
final first = await service.reportAlert(
|
|
category: RoadAlertCategory.pothole,
|
|
latitude: 35.0000,
|
|
longitude: -84.0000,
|
|
description: 'Deep rim bender',
|
|
reporterUid: 'rider_1',
|
|
);
|
|
expect(first.confirmations, 1);
|
|
|
|
// Rider 2 reports a pothole 50m away (lat 35.0004, lng -84.0000)
|
|
final duplicateMerged = await service.reportAlert(
|
|
category: RoadAlertCategory.pothole,
|
|
latitude: 35.0004,
|
|
longitude: -84.0000,
|
|
description: 'Bad crater in right lane',
|
|
reporterUid: 'rider_2',
|
|
);
|
|
|
|
// Auto-confirmed: ID matches original, confirmations incremented to 2
|
|
expect(duplicateMerged.id, first.id);
|
|
expect(duplicateMerged.confirmations, 2);
|
|
|
|
// Check vote subcollection has confirmation vote from rider_2
|
|
final voteDoc = await fakeFirestore
|
|
.collection('road_alerts')
|
|
.doc(first.id)
|
|
.collection('votes')
|
|
.doc('rider_2')
|
|
.get();
|
|
expect(voteDoc.exists, isTrue);
|
|
expect(voteDoc.data()!['type'], 'confirm');
|
|
expect(service.getCachedReport(first.id)?.confirmations, 2);
|
|
|
|
// Distant report (500m away: lat 35.0050) creates a separate alert
|
|
final separate = await service.reportAlert(
|
|
category: RoadAlertCategory.pothole,
|
|
latitude: 35.0050,
|
|
longitude: -84.0000,
|
|
);
|
|
expect(separate.id, isNot(first.id));
|
|
expect(separate.confirmations, 1);
|
|
});
|
|
|
|
test('per-user voting restrictions prevent self-voting and repeat voting', () async {
|
|
final service = SwarmRoadAlertService(firestore: fakeFirestore);
|
|
|
|
final report = await service.reportAlert(
|
|
category: RoadAlertCategory.debris,
|
|
latitude: 35.0,
|
|
longitude: -84.0,
|
|
);
|
|
|
|
// Reporter cannot vote on their own alert
|
|
expect(service.canUserVote(report.id, report.reporterUid), isFalse);
|
|
final selfVote = await service.confirmAlert(report.id, voterUid: report.reporterUid);
|
|
expect(selfVote, isFalse);
|
|
|
|
// Another user can vote once
|
|
const otherUser = 'rider_trail_42';
|
|
expect(service.canUserVote(report.id, otherUser), isTrue);
|
|
expect(service.hasUserVoted(report.id, otherUser), isFalse);
|
|
|
|
final firstVote = await service.confirmAlert(report.id, voterUid: otherUser);
|
|
expect(firstVote, isTrue);
|
|
expect(service.hasUserVoted(report.id, otherUser), isTrue);
|
|
expect(service.canUserVote(report.id, otherUser), isFalse);
|
|
|
|
// Repeat vote is rejected
|
|
final repeatVote = await service.confirmAlert(report.id, voterUid: otherUser);
|
|
expect(repeatVote, isFalse);
|
|
});
|
|
|
|
test('cleared alerts are marked cleared and pruned from nearby queries', () async {
|
|
final service = SwarmRoadAlertService(firestore: fakeFirestore);
|
|
|
|
final report = await service.reportAlert(
|
|
category: RoadAlertCategory.hazard,
|
|
latitude: 35.1,
|
|
longitude: -84.1,
|
|
);
|
|
|
|
// Query finds it while active
|
|
var nearby = await service.getAlertsNear(
|
|
latitude: 35.1,
|
|
longitude: -84.1,
|
|
radiusMeters: 1000,
|
|
);
|
|
expect(nearby.length, 1);
|
|
|
|
// Two dismissals from different riders
|
|
await service.clearAlert(report.id, voterUid: 'voter_a');
|
|
await service.clearAlert(report.id, voterUid: 'voter_b');
|
|
|
|
// Now marked cleared / expired
|
|
final cached = service.getCachedReport(report.id);
|
|
expect(cached, isNotNull);
|
|
expect(cached!.status, RoadAlertStatus.cleared);
|
|
|
|
// Query prunes cleared alerts
|
|
nearby = await service.getAlertsNear(
|
|
latitude: 35.1,
|
|
longitude: -84.1,
|
|
radiusMeters: 1000,
|
|
);
|
|
expect(nearby, isEmpty);
|
|
});
|
|
});
|
|
}
|