feat: add device authentication, place gates support, and PiP navigation features
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user