feat: add device authentication, place gates support, and PiP navigation features

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:26 +03:00
parent 55830beee8
commit ce896df30a
86 changed files with 3414 additions and 341 deletions
@@ -0,0 +1,158 @@
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/constants/api_constants.dart';
import 'package:siro_maps/core/services/device_fingerprint_service.dart';
import 'package:siro_maps/data/models/place_gate.dart';
import 'package:siro_maps/data/models/place_model.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('PlaceGate & PlaceModel Tests', () {
test('PlaceGate parses json correctly', () {
final json = {
'name_ar': 'بوابة رقم 1 - كارفور',
'name_en': 'Gate 1 - Carrefour',
'latitude': 31.9712,
'longitude': 35.8456,
'is_main_gate': true,
};
final gate = PlaceGate.fromJson(json);
expect(gate.nameAr, equals('بوابة رقم 1 - كارفور'));
expect(gate.nameEn, equals('Gate 1 - Carrefour'));
expect(gate.latitude, equals(31.9712));
expect(gate.longitude, equals(35.8456));
expect(gate.isMainGate, isTrue);
final outJson = gate.toJson();
expect(outJson['name_ar'], equals('بوابة رقم 1 - كارفور'));
expect(outJson['is_main_gate'], isTrue);
});
test('PlaceModel parses gates array from API payload', () {
final rawPlace = {
'id': 'poi_city_mall',
'name': 'سيتي مول عمان',
'category': 'mall',
'latitude': 31.9823,
'longitude': 35.8344,
'elevation_meters': 980.5,
'gates': [
{
'name_ar': 'بوابة 1 - الطوارئ والخدمات',
'latitude': 31.9820,
'longitude': 35.8340,
'is_main_gate': false,
},
{
'name_ar': 'بوابة 2 - المدخل الرئيسي والزوار',
'latitude': 31.9825,
'longitude': 35.8346,
'is_main_gate': true,
}
]
};
final place = PlaceModel.fromJson(rawPlace);
expect(place.id, equals('poi_city_mall'));
expect(place.name, equals('سيتي مول عمان'));
expect(place.hasGates, isTrue);
expect(place.gates.length, equals(2));
expect(place.gates.first.nameAr, equals('بوابة 1 - الطوارئ والخدمات'));
expect(place.gates.last.isMainGate, isTrue);
});
test('PlaceModel handles empty or null gates gracefully', () {
final rawPlace = {
'name': 'كافيه عادي',
'category': 'cafe',
'latitude': 31.95,
'longitude': 35.91,
};
final place = PlaceModel.fromJson(rawPlace);
expect(place.hasGates, isFalse);
expect(place.gates, isEmpty);
});
});
group('DeviceFingerprintService Tests', () {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('Generates deterministic hardware fingerprint and initializes headers', () async {
final service = DeviceFingerprintService.instance;
await service.init();
expect(service.fingerprintId, isNotEmpty);
expect(service.fingerprintId.startsWith('siro_'), isTrue);
expect(service.shortFingerprint.isNotEmpty, isTrue);
final headers = service.headers;
expect(headers.containsKey('x-device-fingerprint'), isTrue);
expect(headers['x-device-fingerprint'], equals(service.fingerprintId));
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);
});
test('Provisions dedicated consumer API key and switches activeApiKey', () async {
final service = DeviceFingerprintService.instance;
final mockClient = MockClient((request) async {
if (request.url.path.contains('/auth/device-provision')) {
return http.Response(
jsonEncode({
'apiKey': 'in_mob_unit_test_dedicated_key_998877',
'keyName': 'Siro Mobile - Unit Test Device',
'rateLimit': 60,
'plan': 'FREE',
'deviceFingerprint': service.fingerprintId,
'isNew': true,
}),
200,
headers: {'content-type': 'application/json'},
);
}
return http.Response('Not Found', 404);
});
final success = await service.provisionDedicatedApiKey(httpClient: mockClient);
expect(success, isTrue);
expect(service.activeApiKey, equals('in_mob_unit_test_dedicated_key_998877'));
expect(service.isDedicatedKeyActive, isTrue);
expect(service.rateLimit, equals(60));
expect(service.headers['x-api-key'], equals('in_mob_unit_test_dedicated_key_998877'));
});
test('Falls back gracefully to embedded key if provisioning fails or is offline', () async {
final service = DeviceFingerprintService.instance;
final offlineClient = MockClient((request) async {
return http.Response('Internal Server Error', 500);
});
service.setMockState(
fingerprint: 'siro_test_hw_offline_device',
clearApiKey: true,
);
final success = await service.provisionDedicatedApiKey(httpClient: offlineClient);
expect(success, isFalse);
// Falls back to master MapSaaS key without crashing
expect(service.activeApiKey, equals(ApiConstants.mapSaasKey));
expect(service.isDedicatedKeyActive, isFalse);
});
});
}
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:siro_maps/core/services/deep_link_service.dart';
import 'package:siro_maps/core/services/pip_service.dart';
import 'package:siro_maps/core/services/vehicle_icon_generator.dart';
import 'package:siro_maps/core/utils/arabic_search_normalizer.dart';
@@ -43,6 +45,23 @@ void main() {
isTrue,
);
});
test('Correctly identifies category queries vs specific landmarks', () {
// General categories -> true
expect(ArabicSearchNormalizer.isCategoryQuery('مسجد'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('مطعم'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('مخبز'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('سوبرماركت'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('دكان'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('كافيه'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('صيدلية'), isTrue);
expect(ArabicSearchNormalizer.isCategoryQuery('محطة بنزين'), isTrue);
// Specific landmarks / names -> false
expect(ArabicSearchNormalizer.isCategoryQuery('المدينة الطبية'), isFalse);
expect(ArabicSearchNormalizer.isCategoryQuery('سيتي مول'), isFalse);
expect(ArabicSearchNormalizer.isCategoryQuery('دوار الداخلية'), isFalse);
});
});
group('VehicleIconGenerator Tests', () {
@@ -85,4 +104,106 @@ void main() {
expect(VehicleIconGenerator.availableColors.any((c) => c.colorValue == 0xFF007AFF), isTrue);
});
});
group('DeepLinkService Tests', () {
test('Parses basic geo URI', () {
final target = DeepLinkService.instance.parseUri('geo:31.97869,35.83426');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.97869, 0.0001));
expect(target.destination.longitude, closeTo(35.83426, 0.0001));
expect(target.autoStartNavigation, isFalse);
});
test('Parses geo URI with label in parenthesis', () {
final target = DeepLinkService.instance.parseUri('geo:31.97869,35.83426?q=31.97869,35.83426(المدينة%20الطبية)');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.97869, 0.0001));
expect(target.destination.longitude, closeTo(35.83426, 0.0001));
expect(target.title, equals('المدينة الطبية'));
});
test('Parses geo:0,0 query format used by Android apps', () {
final target = DeepLinkService.instance.parseUri('geo:0,0?q=31.9539,35.9106(سيتي%20مول)');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.9539, 0.0001));
expect(target.destination.longitude, closeTo(35.9106, 0.0001));
expect(target.title, equals('سيتي مول'));
});
test('Parses custom siromaps://navigate with autoStartNavigation', () {
final target = DeepLinkService.instance.parseUri('siromaps://navigate?lat=31.97869&lng=35.83426&title=مدينة_الحسين_الطبية');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.97869, 0.0001));
expect(target.destination.longitude, closeTo(35.83426, 0.0001));
expect(target.title, equals('مدينة_الحسين_الطبية'));
expect(target.autoStartNavigation, isTrue);
});
test('Parses custom siromaps://route with preview mode', () {
final target = DeepLinkService.instance.parseUri('siromaps://route?dlat=32.01&dlng=35.86&dname=جامعة_اليرموك');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(32.01, 0.0001));
expect(target.destination.longitude, closeTo(35.86, 0.0001));
expect(target.title, equals('جامعة_اليرموك'));
expect(target.autoStartNavigation, isFalse);
});
test('Parses google.navigation URI intent', () {
final target = DeepLinkService.instance.parseUri('google.navigation:q=31.97869,35.83426&mode=d');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.97869, 0.0001));
expect(target.destination.longitude, closeTo(35.83426, 0.0001));
expect(target.autoStartNavigation, isTrue);
});
test('Parses Google Maps URL', () {
final target = DeepLinkService.instance.parseUri('https://www.google.com/maps/dir/?api=1&destination=31.97869,35.83426');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.97869, 0.0001));
expect(target.destination.longitude, closeTo(35.83426, 0.0001));
});
test('Parses Universal Siro Web Link', () {
final target = DeepLinkService.instance.parseUri('https://maps.siro.app/navigate?lat=31.97869&lng=35.83426&title=المدينة%20الطبية');
expect(target, isNotNull);
expect(target!.destination.latitude, closeTo(31.97869, 0.0001));
expect(target.destination.longitude, closeTo(35.83426, 0.0001));
expect(target.title, equals('المدينة الطبية'));
expect(target.autoStartNavigation, isTrue);
});
test('Returns null on invalid or empty URIs', () {
expect(DeepLinkService.instance.parseUri(''), isNull);
expect(DeepLinkService.instance.parseUri('invalid_link_without_coords'), isNull);
expect(DeepLinkService.instance.parseUri('geo:invalid,coords'), isNull);
});
});
group('PipService Tests', () {
test('PipService initializes and reports supported', () async {
final pip = PipService.instance;
pip.init();
final supported = await pip.isPipSupported();
expect(supported, isTrue);
});
test('PipService toggles and enters/exits PiP mode correctly', () async {
final pip = PipService.instance;
await pip.exitPictureInPicture();
expect(pip.isInPipMode.value, isFalse);
await pip.enterPictureInPicture();
expect(pip.isInPipMode.value, isTrue);
await pip.exitPictureInPicture();
expect(pip.isInPipMode.value, isFalse);
await pip.togglePip();
expect(pip.isInPipMode.value, isTrue);
await pip.togglePip();
expect(pip.isInPipMode.value, isFalse);
});
});
}
+1 -1
View File
@@ -27,7 +27,7 @@ void main() {
),
);
expect(find.text('خرائط سيرو'), findsOneWidget);
expect(find.text('خرائط أوروك'), findsOneWidget);
expect(find.textContaining('v2.4 PRO'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 3000));