feat: add device authentication, place gates support, and PiP navigation features
This commit is contained in:
@@ -4,14 +4,11 @@ import 'package:siro_maps/core/env/env.dart';
|
||||
|
||||
void main() {
|
||||
group('Env & Obfuscated Secrets Verification', () {
|
||||
test('MapSaaS API key is decrypted correctly from obfuscated bytecode', () {
|
||||
expect(Env.mapSaasApiKey, equals('in_9478b32836d19cff73db3063'));
|
||||
expect(ApiConstants.mapSaasKey, equals('in_9478b32836d19cff73db3063'));
|
||||
});
|
||||
|
||||
test('Google Map API key is decrypted correctly from obfuscated bytecode', () {
|
||||
expect(Env.googleMapApiKey, equals('AIzaSyAPFR_XbRN0XZ5Iz3AYDjNYHGJG2s2QWwM'));
|
||||
expect(ApiConstants.googleMapApiKey, equals('AIzaSyAPFR_XbRN0XZ5Iz3AYDjNYHGJG2s2QWwM'));
|
||||
test('MapSaaS and Google API keys in Env default to empty (dynamic provisioning via Device Fingerprint)', () {
|
||||
expect(Env.mapSaasApiKey, isEmpty);
|
||||
expect(ApiConstants.mapSaasKey, isEmpty);
|
||||
expect(Env.googleMapApiKey, isEmpty);
|
||||
expect(ApiConstants.googleMapApiKey, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ void main() {
|
||||
expect(headers.containsKey('x-client-version'), isTrue);
|
||||
expect(headers.containsKey('x-device-platform'), isTrue);
|
||||
expect(headers.containsKey('x-api-key'), isTrue);
|
||||
expect(headers['x-api-key'], isNotEmpty);
|
||||
expect(headers['x-api-key'], equals(ApiConstants.mapSaasKey));
|
||||
});
|
||||
|
||||
test('Provisions dedicated consumer API key and switches activeApiKey', () async {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:siro_maps/core/services/device_fingerprint_service.dart';
|
||||
import 'package:siro_maps/core/services/telemetry_tracking_service.dart';
|
||||
import 'package:siro_maps/data/repositories/map_saas_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late TelemetryTrackingService telemetryService;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
telemetryService = TelemetryTrackingService.instance;
|
||||
telemetryService.resetForTesting();
|
||||
DeviceFingerprintService.instance.setMockState(
|
||||
fingerprint: 'siro_test_hw_unit_test_device_123',
|
||||
);
|
||||
});
|
||||
|
||||
group('TelemetryTrackingService Sampling & Stationary Filter Tests', () {
|
||||
test('Samples position only if at least 3 seconds have passed', () {
|
||||
final baseTime = DateTime(2026, 9, 15, 12, 0, 0);
|
||||
|
||||
// First point at T=0
|
||||
final p1 = telemetryService.recordPosition(
|
||||
latitude: 31.9539,
|
||||
longitude: 35.9106,
|
||||
speedKmH: 45.0,
|
||||
heading: 90.0,
|
||||
now: baseTime,
|
||||
);
|
||||
expect(p1, isTrue);
|
||||
expect(telemetryService.bufferedPointCount, equals(1));
|
||||
|
||||
// Attempt second point after only 1 second (T=1s) -> Should be rejected
|
||||
final p2 = telemetryService.recordPosition(
|
||||
latitude: 31.9540,
|
||||
longitude: 35.9107,
|
||||
speedKmH: 46.0,
|
||||
heading: 90.0,
|
||||
now: baseTime.add(const Duration(seconds: 1)),
|
||||
);
|
||||
expect(p2, isFalse);
|
||||
expect(telemetryService.bufferedPointCount, equals(1));
|
||||
|
||||
// Attempt third point after 3 seconds (T=3s) -> Should be accepted
|
||||
final p3 = telemetryService.recordPosition(
|
||||
latitude: 31.9545,
|
||||
longitude: 35.9110,
|
||||
speedKmH: 48.0,
|
||||
heading: 92.0,
|
||||
now: baseTime.add(const Duration(seconds: 3)),
|
||||
);
|
||||
expect(p3, isTrue);
|
||||
expect(telemetryService.bufferedPointCount, equals(2));
|
||||
});
|
||||
|
||||
test('Smart stationary filter suppresses redundant points when vehicle is stopped', () {
|
||||
final baseTime = DateTime(2026, 9, 15, 12, 0, 0);
|
||||
|
||||
// Stop at intersection: initial point (speed = 0)
|
||||
final initial = telemetryService.recordPosition(
|
||||
latitude: 31.9500,
|
||||
longitude: 35.9100,
|
||||
speedKmH: 0.0,
|
||||
heading: 0.0,
|
||||
now: baseTime,
|
||||
);
|
||||
expect(initial, isTrue);
|
||||
expect(telemetryService.bufferedPointCount, equals(1));
|
||||
|
||||
// After 5 seconds: still at same spot (speed = 0, distance = 0) -> filter out
|
||||
final duplicateAfter5s = telemetryService.recordPosition(
|
||||
latitude: 31.9500,
|
||||
longitude: 35.9100,
|
||||
speedKmH: 0.0,
|
||||
heading: 0.0,
|
||||
now: baseTime.add(const Duration(seconds: 5)),
|
||||
);
|
||||
expect(duplicateAfter5s, isFalse);
|
||||
expect(telemetryService.bufferedPointCount, equals(1));
|
||||
|
||||
// After 35 seconds: still at same spot -> 30s heartbeat triggers!
|
||||
final heartbeatAfter35s = telemetryService.recordPosition(
|
||||
latitude: 31.9500,
|
||||
longitude: 35.9100,
|
||||
speedKmH: 0.0,
|
||||
heading: 0.0,
|
||||
now: baseTime.add(const Duration(seconds: 35)),
|
||||
);
|
||||
expect(heartbeatAfter35s, isTrue);
|
||||
expect(telemetryService.bufferedPointCount, equals(2));
|
||||
});
|
||||
});
|
||||
|
||||
group('Telemetry Batch Upload & Offline Buffer Tests', () {
|
||||
test('Flushes batched telemetry to server in a single request', () async {
|
||||
List<dynamic> receivedPayloadPoints = [];
|
||||
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path.contains('/api/telemetry/batch')) {
|
||||
final body = jsonDecode(request.body) as Map<String, dynamic>;
|
||||
receivedPayloadPoints = body['points'] as List<dynamic>;
|
||||
return http.Response(jsonEncode({'success': true, 'count': receivedPayloadPoints.length}), 201);
|
||||
}
|
||||
return http.Response('Not Found', 404);
|
||||
});
|
||||
|
||||
final mockRepo = MapSaasRepository(client: mockClient);
|
||||
|
||||
// Record 3 points spaced by 3 seconds
|
||||
final baseTime = DateTime(2026, 9, 15, 12, 0, 0);
|
||||
telemetryService.recordPosition(
|
||||
latitude: 31.951,
|
||||
longitude: 35.911,
|
||||
speedKmH: 30,
|
||||
heading: 45,
|
||||
now: baseTime,
|
||||
);
|
||||
telemetryService.recordPosition(
|
||||
latitude: 31.952,
|
||||
longitude: 35.912,
|
||||
speedKmH: 35,
|
||||
heading: 45,
|
||||
now: baseTime.add(const Duration(seconds: 3)),
|
||||
);
|
||||
telemetryService.recordPosition(
|
||||
latitude: 31.953,
|
||||
longitude: 35.913,
|
||||
speedKmH: 40,
|
||||
heading: 50,
|
||||
now: baseTime.add(const Duration(seconds: 6)),
|
||||
);
|
||||
|
||||
expect(telemetryService.bufferedPointCount, equals(3));
|
||||
|
||||
// Flush buffer to backend
|
||||
final success = await telemetryService.flush(repository: mockRepo);
|
||||
expect(success, isTrue);
|
||||
expect(receivedPayloadPoints.length, equals(3));
|
||||
// Buffer should be empty after successful flush
|
||||
expect(telemetryService.bufferedPointCount, equals(0));
|
||||
});
|
||||
|
||||
test('Retains points in offline buffer if batch upload encounters error', () async {
|
||||
final offlineClient = MockClient((request) async {
|
||||
return http.Response('Gateway Timeout', 504);
|
||||
});
|
||||
|
||||
final mockRepo = MapSaasRepository(client: offlineClient);
|
||||
|
||||
telemetryService.recordPosition(
|
||||
latitude: 31.951,
|
||||
longitude: 35.911,
|
||||
speedKmH: 20,
|
||||
heading: 10,
|
||||
now: DateTime(2026, 9, 15, 12, 0, 0),
|
||||
);
|
||||
|
||||
expect(telemetryService.bufferedPointCount, equals(1));
|
||||
|
||||
final success = await telemetryService.flush(repository: mockRepo);
|
||||
expect(success, isFalse);
|
||||
// Points must still be retained for next attempt
|
||||
expect(telemetryService.bufferedPointCount, equals(1));
|
||||
});
|
||||
});
|
||||
|
||||
group('10-Day Periodic Updates & Maintenance Tests', () {
|
||||
test('Skips update if less than 10 days since last check', () async {
|
||||
final now = DateTime(2026, 9, 15, 12, 0, 0);
|
||||
final lastCheck = now.subtract(const Duration(days: 4)); // Only 4 days ago
|
||||
|
||||
SharedPreferences.setMockInitialValues({
|
||||
TelemetryTrackingService.prefKeyLastPeriodicCheck: lastCheck.millisecondsSinceEpoch,
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final executed = await telemetryService.checkPeriodicUpdates(
|
||||
now: now,
|
||||
mockPrefs: prefs,
|
||||
);
|
||||
|
||||
expect(executed, isFalse);
|
||||
});
|
||||
|
||||
test('Executes maintenance cycle if 10 or more days elapsed', () async {
|
||||
final now = DateTime(2026, 9, 15, 12, 0, 0);
|
||||
final lastCheck = now.subtract(const Duration(days: 12)); // 12 days ago
|
||||
|
||||
SharedPreferences.setMockInitialValues({
|
||||
TelemetryTrackingService.prefKeyLastPeriodicCheck: lastCheck.millisecondsSinceEpoch,
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final executed = await telemetryService.checkPeriodicUpdates(
|
||||
now: now,
|
||||
mockPrefs: prefs,
|
||||
);
|
||||
|
||||
expect(executed, isTrue);
|
||||
final updatedTimestamp = prefs.getInt(TelemetryTrackingService.prefKeyLastPeriodicCheck);
|
||||
expect(updatedTimestamp, equals(now.millisecondsSinceEpoch));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user