diff --git a/lib/screens/home_edit_screen.dart b/lib/screens/home_edit_screen.dart index 32cdd91..9ff6f61 100644 --- a/lib/screens/home_edit_screen.dart +++ b/lib/screens/home_edit_screen.dart @@ -25,6 +25,7 @@ class _HomeEditScreenState extends ConsumerState { final _lonCtrl = TextEditingController(); bool _saving = false; bool _loadingApiKey = false; + bool _hasStoredApiKey = false; String _originalApiKey = ''; bool get _isEdit => widget.home != null; @@ -59,11 +60,11 @@ class _HomeEditScreenState extends ConsumerState { final apiKey = await ref .read(settingsServiceProvider) .getHomeApiKey(widget.home!.id); - _originalApiKey = apiKey ?? ''; + _originalApiKey = apiKey?.trim() ?? ''; + _hasStoredApiKey = _originalApiKey.isNotEmpty; if (!mounted) { return; } - _keyCtrl.text = _originalApiKey; } finally { if (mounted) { setState(() => _loadingApiKey = false); @@ -144,14 +145,24 @@ class _HomeEditScreenState extends ConsumerState { controller: _keyCtrl, decoration: InputDecoration( labelText: 'API Key', + hintText: _hasStoredApiKey + ? 'Оставьте пустым, чтобы сохранить текущий ключ' + : null, helperText: _loadingApiKey ? 'Загружаем сохранённый ключ...' - : 'Ключ проверяется только при изменении подключения', + : _hasStoredApiKey + ? 'Сохранённый ключ хранится отдельно и не показывается в поле' + : 'Ключ хранится отдельно в защищённом хранилище', prefixIcon: const Icon(Icons.key), ), obscureText: true, - validator: (value) => - (value?.trim().isEmpty ?? true) ? 'Укажите API key' : null, + validator: (value) { + final enteredKey = value?.trim() ?? ''; + if (enteredKey.isNotEmpty || _hasStoredApiKey) { + return null; + } + return 'Укажите API key'; + }, ), const SizedBox(height: 24), const Text( @@ -285,7 +296,8 @@ class _HomeEditScreenState extends ConsumerState { final name = _nameCtrl.text.trim(); final rawUrl = _urlCtrl.text.trim(); - final key = _keyCtrl.text.trim(); + final enteredKey = _keyCtrl.text.trim(); + final key = enteredKey.isNotEmpty ? enteredKey : _originalApiKey; final latText = _latCtrl.text.trim(); final lonText = _lonCtrl.text.trim(); @@ -370,7 +382,7 @@ class _HomeEditScreenState extends ConsumerState { if (_isEdit) { await ref .read(homesProvider.notifier) - .update(home, apiKey: credentialsChanged ? key : null); + .update(home, apiKey: enteredKey.isNotEmpty ? key : null); } else { await ref.read(homesProvider.notifier).add(home, apiKey: key); } diff --git a/test/forms_widget_test.dart b/test/forms_widget_test.dart index 1840257..d56739b 100644 --- a/test/forms_widget_test.dart +++ b/test/forms_widget_test.dart @@ -82,25 +82,25 @@ void main() { expect(api.createdGroupMacs, ['AA:BB']); }); - testWidgets('group edit screen shows backend rescan summary', ( - tester, - ) async { - final api = FakeIgnisApi( - devicesData: { - 'devices': [ - {'mac': 'AA:BB', 'name': 'Лампа 1'}, - ], - }, - groupsData: [], - )..rescanNetworkData = { - 'status': 'ok', - 'found': 3, - 'added': 1, - 'updated': 2, - 'removed_offline': 1, - 'pending_removal': 0, - 'online': 3, - }; + testWidgets('group edit screen shows backend rescan summary', (tester) async { + final api = + FakeIgnisApi( + devicesData: { + 'devices': [ + {'mac': 'AA:BB', 'name': 'Лампа 1'}, + ], + }, + groupsData: [], + ) + ..rescanNetworkData = { + 'status': 'ok', + 'found': 3, + 'added': 1, + 'updated': 2, + 'removed_offline': 1, + 'pending_removal': 0, + 'online': 3, + }; await pumpTestApp(tester, child: const GroupEditScreen(), api: api); await tester.pumpAndSettle(); @@ -109,7 +109,9 @@ void main() { await tester.pumpAndSettle(); expect( - find.text('Сканирование завершено: найдено 3, новых 1, обновлено 2, убрано 1'), + find.text( + 'Сканирование завершено: найдено 3, новых 1, обновлено 2, убрано 1', + ), findsOneWidget, ); expect(api.rescanCalls, 1); @@ -228,4 +230,57 @@ void main() { ); expect(savedApiKey, 'secret-key'); }); + + testWidgets('home edit screen keeps stored api key hidden on edit', ( + tester, + ) async { + SharedPreferences.setMockInitialValues({}); + final settingsService = SettingsService( + credentialsStorage: InMemoryCredentialsStorage(), + ); + final existingHome = HomeConfig( + id: 'home-1', + name: 'Квартира', + url: 'https://ignis.akokos.ru', + ); + await settingsService.upsertHome(existingHome, apiKey: 'stored-secret'); + + final api = FakeIgnisApi(authData: {'is_admin': true, 'name': 'owner'}); + + await pumpTestApp( + tester, + api: api, + settingsService: settingsService, + child: HomeEditScreen(home: existingHome), + ); + await tester.pumpAndSettle(); + + final keyField = tester.widget( + find.byWidgetPredicate( + (widget) => widget is EditableText && widget.obscureText, + ), + ); + expect(keyField.controller.text, isEmpty); + expect( + find.text('Сохранённый ключ хранится отдельно и не показывается в поле'), + findsOneWidget, + ); + + await tester.enterText( + find.widgetWithText(TextFormField, 'Адрес сервера'), + 'ignis.example.com', + ); + await tester.pump(); + + await tester.tap(find.widgetWithText(ElevatedButton, 'СОХРАНИТЬ')); + await tester.pumpAndSettle(); + + expect(api.validateCredentialsCalls, 1); + expect(api.validatedBaseUrl, 'https://ignis.example.com'); + expect(api.validatedApiKey, 'stored-secret'); + expect( + await settingsService.getHomeApiKey(existingHome.id), + 'stored-secret', + ); + }); } diff --git a/test/test_support.dart b/test/test_support.dart index a61f29d..7966984 100644 --- a/test/test_support.dart +++ b/test/test_support.dart @@ -73,6 +73,9 @@ class FakeIgnisApi extends IgnisApi { String? deletedGroupId; Map? scheduledOnceParams; Map? scheduledCronParams; + String? validatedBaseUrl; + String? validatedApiKey; + int validateCredentialsCalls = 0; int rescanCalls = 0; FakeIgnisApi({ @@ -99,6 +102,9 @@ class FakeIgnisApi extends IgnisApi { @override Future validateCredentials(String baseUrl, String apiKey) async { + validateCredentialsCalls += 1; + validatedBaseUrl = baseUrl; + validatedApiKey = apiKey; final error = authError; if (error != null) throw error; }