diff --git a/lib/core/storage/connection_repository.dart b/lib/core/storage/connection_repository.dart index 6d49ead5..0567bcae 100644 --- a/lib/core/storage/connection_repository.dart +++ b/lib/core/storage/connection_repository.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:querya_desktop/core/storage/connection_secrets_store.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:querya_desktop/core/storage/local_db_models.dart'; +import 'package:querya_desktop/core/util/log_ignored.dart'; /// Saved connections: rows, secrets and SSH configuration kept in step. /// Part of [LocalDb], which keeps the same methods and delegates here. @@ -154,12 +155,21 @@ class ConnectionRepository { await ConnectionSecretsStore.writeSshSecretsForConnection(row.id!); } } catch (e) { - await db.update( - 'connections', - previousRow.toPersistenceMap(), - where: 'id = ?', - whereArgs: [row.id], - ); + // Best effort, but never silent: when the restore fails as well, the row + // and the keyring can disagree (old row, new password), and this line is + // what explains it. The original error is the one reported. + final where = 'ConnectionRepository.updateConnection restore of ' + 'connection ${row.id}'; + try { + await db.update( + 'connections', + previousRow.toPersistenceMap(), + where: 'id = ?', + whereArgs: [row.id], + ); + } catch (restoreError, stack) { + logIgnored('$where (row)', restoreError, stack); + } try { await ConnectionSecretsStore.writeForConnection( row.id!, @@ -174,8 +184,8 @@ class ConnectionRepository { passphrase: previousSsh.passphrase, jumpPassword: previousSsh.jumpPassword, ); - } catch (_) { - // Best-effort restore of previous secrets; surface the original error. + } catch (restoreError, stack) { + logIgnored('$where (secrets)', restoreError, stack); } rethrow; } diff --git a/test/core/storage/secrets_failure_test.dart b/test/core/storage/secrets_failure_test.dart index f2d06528..3e71a340 100644 --- a/test/core/storage/secrets_failure_test.dart +++ b/test/core/storage/secrets_failure_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:querya_desktop/core/security/ssh_tunnel_config.dart'; @@ -158,6 +159,44 @@ void main() { }); }); + test( + 'updateConnection: when the restore is refused as well the first error ' + 'is reported and the failed restore is logged (#1778)', () async { + final id = await LocalDb.instance.addConnection(row('Before')); + testMemorySecrets.resetCounts(); + final error = StateError('connection string refused'); + testMemorySecrets.failOnCall(SecretsOp.write, 2, error); + // The restore writes the old password first (call 3). + testMemorySecrets.failOnCall( + SecretsOp.write, 3, StateError('keyring went away')); + final printed = []; + final original = debugPrint; + debugPrint = (message, {wrapWidth}) => printed.add(message ?? ''); + addTearDown(() => debugPrint = original); + + await expectLater( + LocalDb.instance.updateConnection(ConnectionRow( + id: id, + type: 'postgresql', + name: 'After', + host: 'other-host', + port: 5433, + username: 'root', + password: 'new-password', + connectionString: 'postgres://root@other-host/db', + createdAt: '2026-01-01T00:00:00Z', + )), + throwsA(same(error)), + ); + + expect( + printed.where((l) => l.contains('restore of connection $id')), + isNotEmpty, + ); + expect(printed.join('\n'), contains('keyring went away')); + expect((await rowsNamed('Before')), hasLength(1)); + }); + group('a read that fails', () { test( 'a refusal on the second key is "could not read", not "no password", '