pin_service_test.dart 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import 'package:cardash_launcher/data/models/slot_assignment.dart';
  2. import 'package:cardash_launcher/data/models/slot_type.dart';
  3. import 'package:cardash_launcher/data/repositories/settings_repository.dart';
  4. import 'package:cardash_launcher/data/services/pin_service.dart';
  5. import 'package:cardash_launcher/data/services/secure_store.dart';
  6. import 'package:flutter_test/flutter_test.dart';
  7. import 'package:shared_preferences/shared_preferences.dart';
  8. class MemorySecureStore implements SecureStore {
  9. final Map<String, String> values = {};
  10. @override
  11. Future<String?> read(String key) async => values[key];
  12. @override
  13. Future<void> write(String key, String value) async => values[key] = value;
  14. @override
  15. Future<void> delete(String key) async => values.remove(key);
  16. }
  17. void main() {
  18. TestWidgetsFlutterBinding.ensureInitialized();
  19. group('settings repository', () {
  20. test('persists and clears a shortcut assignment', () async {
  21. SharedPreferences.setMockInitialValues({});
  22. final prefs = await SharedPreferences.getInstance();
  23. final repository = SettingsRepository(prefs);
  24. const assignment = SlotAssignment(
  25. packageName: 'com.example.waze',
  26. activityName: 'com.example.waze.Main',
  27. label: 'Waze',
  28. );
  29. expect(repository.slot(SlotType.navigation), isNull);
  30. await repository.setSlot(SlotType.navigation, assignment);
  31. final restored = repository.slot(SlotType.navigation);
  32. expect(restored, isNotNull);
  33. expect(restored!.packageName, 'com.example.waze');
  34. expect(restored.label, 'Waze');
  35. await repository.clearSlot(SlotType.navigation);
  36. expect(repository.slot(SlotType.navigation), isNull);
  37. });
  38. test('stores a custom night window', () async {
  39. SharedPreferences.setMockInitialValues({});
  40. final prefs = await SharedPreferences.getInstance();
  41. final repository = SettingsRepository(prefs);
  42. await repository.setNightWindow(
  43. startMinutes: 20 * 60,
  44. endMinutes: 6 * 60,
  45. );
  46. expect(repository.nightStartMinutes, 20 * 60);
  47. expect(repository.nightEndMinutes, 6 * 60);
  48. });
  49. test('the first-run launcher hint is shown once', () async {
  50. SharedPreferences.setMockInitialValues({});
  51. final prefs = await SharedPreferences.getInstance();
  52. final repository = SettingsRepository(prefs);
  53. expect(repository.homeRoleHintShown, isFalse);
  54. await repository.setHomeRoleHintShown(true);
  55. expect(repository.homeRoleHintShown, isTrue);
  56. });
  57. test('records the first launch once for a new installation', () async {
  58. SharedPreferences.setMockInitialValues({});
  59. final prefs = await SharedPreferences.getInstance();
  60. final repository = SettingsRepository(prefs);
  61. final now = DateTime(2026, 1, 1, 12);
  62. expect(repository.firstLaunchAt, isNull);
  63. expect(await repository.ensureFirstLaunchAt(now), now);
  64. expect(repository.firstLaunchAt, now);
  65. expect(await repository.ensureFirstLaunchAt(DateTime(2026, 1, 2)), now);
  66. });
  67. test(
  68. 'an upgraded installation never reopens the onboarding window',
  69. () async {
  70. SharedPreferences.setMockInitialValues({'settings.theme': 'dark'});
  71. final prefs = await SharedPreferences.getInstance();
  72. final repository = SettingsRepository(prefs);
  73. final first = await repository.ensureFirstLaunchAt(
  74. DateTime(2026, 1, 1),
  75. );
  76. expect(first.millisecondsSinceEpoch, 0);
  77. },
  78. );
  79. test('speed and temperature preferences persist', () async {
  80. SharedPreferences.setMockInitialValues({});
  81. final prefs = await SharedPreferences.getInstance();
  82. final repository = SettingsRepository(prefs);
  83. expect(repository.speedEnabled, isTrue);
  84. expect(repository.speedUseObd, isTrue);
  85. expect(repository.temperatureEnabled, isFalse);
  86. expect(repository.temperatureUseObd, isTrue);
  87. await repository.ensureTelemetryDefaults();
  88. expect(repository.temperatureEnabled, isFalse);
  89. await repository.setTemperatureEnabled(true);
  90. expect(repository.temperatureEnabled, isTrue);
  91. await repository.setSpeedEnabled(false);
  92. await repository.setSpeedUseObd(false);
  93. await repository.setTemperatureEnabled(false);
  94. await repository.setTemperatureUseObd(false);
  95. expect(repository.speedEnabled, isFalse);
  96. expect(repository.speedUseObd, isFalse);
  97. expect(repository.temperatureEnabled, isFalse);
  98. expect(repository.temperatureUseObd, isFalse);
  99. });
  100. test('temperature migrates on for an existing OBD install', () async {
  101. SharedPreferences.setMockInitialValues({
  102. 'obd.adapterAddress': '00:11:22:33:44:55',
  103. });
  104. final prefs = await SharedPreferences.getInstance();
  105. final repository = SettingsRepository(prefs);
  106. await repository.ensureTelemetryDefaults();
  107. expect(repository.temperatureEnabled, isTrue);
  108. await repository.setTemperatureEnabled(false);
  109. await repository.ensureTelemetryDefaults();
  110. expect(repository.temperatureEnabled, isFalse);
  111. });
  112. test('temperature migrates on for an existing weather install', () async {
  113. SharedPreferences.setMockInitialValues({
  114. 'weather.locationTemperature': true,
  115. });
  116. final prefs = await SharedPreferences.getInstance();
  117. final repository = SettingsRepository(prefs);
  118. await repository.ensureTelemetryDefaults();
  119. expect(repository.temperatureEnabled, isTrue);
  120. });
  121. test('OBD adapter and weather preferences persist', () async {
  122. SharedPreferences.setMockInitialValues({});
  123. final prefs = await SharedPreferences.getInstance();
  124. final repository = SettingsRepository(prefs);
  125. expect(repository.obdAdapterAddress, isNull);
  126. expect(repository.keepScreenOn, isTrue);
  127. expect(repository.weatherLocationTemperature, isFalse);
  128. await repository.setObdAdapterAddress('00:11:22:33:44:55');
  129. await repository.setWeatherLocationTemperature(true);
  130. await repository.setKeepScreenOn(false);
  131. expect(repository.obdAdapterAddress, '00:11:22:33:44:55');
  132. expect(repository.weatherLocationTemperature, isTrue);
  133. expect(repository.keepScreenOn, isFalse);
  134. await repository.setObdAdapterAddress(null);
  135. expect(repository.obdAdapterAddress, isNull);
  136. });
  137. test('home buttons default to enabled and persist a toggle', () async {
  138. SharedPreferences.setMockInitialValues({});
  139. final prefs = await SharedPreferences.getInstance();
  140. final repository = SettingsRepository(prefs);
  141. expect(repository.slotEnabled(SlotType.navigation), isTrue);
  142. expect(repository.slotEnabled(SlotType.music), isTrue);
  143. expect(repository.slotEnabled(SlotType.radio), isTrue);
  144. await repository.setSlotEnabled(SlotType.music, false);
  145. expect(repository.slotEnabled(SlotType.music), isFalse);
  146. expect(repository.slotEnabled(SlotType.navigation), isTrue);
  147. });
  148. test('pinned apps round-trip through the repository', () async {
  149. SharedPreferences.setMockInitialValues({});
  150. final prefs = await SharedPreferences.getInstance();
  151. final repository = SettingsRepository(prefs);
  152. const pinned = [
  153. SlotAssignment(
  154. packageName: 'com.example.one',
  155. activityName: 'com.example.one.Main',
  156. label: 'One',
  157. iconAsset: 'assets/icons/star.svg',
  158. ),
  159. SlotAssignment(
  160. packageName: 'com.example.two',
  161. activityName: 'com.example.two.Main',
  162. label: 'Two',
  163. ),
  164. ];
  165. await repository.setPinnedApps(pinned);
  166. final restored = repository.pinnedApps;
  167. expect(restored.length, 2);
  168. expect(restored.first.label, 'One');
  169. expect(restored.first.hasCustomIcon, isTrue);
  170. expect(restored.first.iconAsset, 'assets/icons/star.svg');
  171. expect(restored.last.packageName, 'com.example.two');
  172. expect(restored.last.hasCustomIcon, isFalse);
  173. await repository.setPinnedApps(const []);
  174. expect(repository.pinnedApps, isEmpty);
  175. });
  176. });
  177. group('pin service', () {
  178. test('sets, verifies and clears a four-digit PIN', () async {
  179. final store = MemorySecureStore();
  180. final service = PinService(store, iterations: 50);
  181. await service.setPin('1234');
  182. expect(await service.isPinSet(), isTrue);
  183. expect(await service.verify('1234'), isTrue);
  184. expect(await service.verify('9999'), isFalse);
  185. expect(store.values['cardash.pin.hash'], isNot(contains('1234')));
  186. await service.clearPin();
  187. expect(await service.isPinSet(), isFalse);
  188. });
  189. test('rejects PINs that are not four digits', () {
  190. final service = PinService(MemorySecureStore(), iterations: 10);
  191. expect(() => service.setPin('12'), throwsArgumentError);
  192. expect(() => service.setPin('abcd'), throwsArgumentError);
  193. });
  194. });
  195. }