Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions lib/core/storage/connection_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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!,
Expand All @@ -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;
}
Expand Down
39 changes: 39 additions & 0 deletions test/core/storage/secrets_failure_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 = <String>[];
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", '
Expand Down
Loading