From 77956d04672d1c0d01ab9fae1065ccd09b2037bc Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:19:06 +0300 Subject: [PATCH 01/17] refactor: move connection management files to features/connections/ Move connections_panel.dart, new_connection_dialog.dart, new_folder_dialog.dart, and driver_manager_dialog.dart from features/main_screen/ to features/connections/ for better project organization. Each connector type now has its own dedicated folder under lib/features/. --- .../{main_screen => connections}/connections_panel.dart | 4 ++-- .../{main_screen => connections}/driver_manager_dialog.dart | 0 .../{main_screen => connections}/new_connection_dialog.dart | 0 .../{main_screen => connections}/new_folder_dialog.dart | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename lib/features/{main_screen => connections}/connections_panel.dart (99%) rename lib/features/{main_screen => connections}/driver_manager_dialog.dart (100%) rename lib/features/{main_screen => connections}/new_connection_dialog.dart (100%) rename lib/features/{main_screen => connections}/new_folder_dialog.dart (100%) diff --git a/lib/features/main_screen/connections_panel.dart b/lib/features/connections/connections_panel.dart similarity index 99% rename from lib/features/main_screen/connections_panel.dart rename to lib/features/connections/connections_panel.dart index 9bda35b8..dbe9a573 100644 --- a/lib/features/main_screen/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -3,10 +3,10 @@ import 'package:querya_desktop/core/storage/folders_storage.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; -import 'mongodb_connection_form.dart'; +import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart'; +import 'package:querya_desktop/features/redis/redis_connection_form.dart'; import 'new_connection_dialog.dart'; import 'new_folder_dialog.dart'; -import 'redis_connection_form.dart'; /// Left panel: Browser tree (pgAdmin-style). Uses shadcn layout widgets. class ConnectionsPanel extends StatefulWidget { diff --git a/lib/features/main_screen/driver_manager_dialog.dart b/lib/features/connections/driver_manager_dialog.dart similarity index 100% rename from lib/features/main_screen/driver_manager_dialog.dart rename to lib/features/connections/driver_manager_dialog.dart diff --git a/lib/features/main_screen/new_connection_dialog.dart b/lib/features/connections/new_connection_dialog.dart similarity index 100% rename from lib/features/main_screen/new_connection_dialog.dart rename to lib/features/connections/new_connection_dialog.dart diff --git a/lib/features/main_screen/new_folder_dialog.dart b/lib/features/connections/new_folder_dialog.dart similarity index 100% rename from lib/features/main_screen/new_folder_dialog.dart rename to lib/features/connections/new_folder_dialog.dart From 7b5a9292611cfc0a4e5e29fad3164dc5498151a6 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:19:23 +0300 Subject: [PATCH 02/17] refactor: move MongoDB files to features/mongodb/ Move mongodb_connection_form.dart and mongo_databases_view.dart from features/main_screen/ to features/mongodb/. Keeps all MongoDB-specific UI (connection form, databases list, server status view) in a dedicated directory. --- lib/features/{main_screen => mongodb}/mongo_databases_view.dart | 0 .../{main_screen => mongodb}/mongodb_connection_form.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/features/{main_screen => mongodb}/mongo_databases_view.dart (100%) rename lib/features/{main_screen => mongodb}/mongodb_connection_form.dart (100%) diff --git a/lib/features/main_screen/mongo_databases_view.dart b/lib/features/mongodb/mongo_databases_view.dart similarity index 100% rename from lib/features/main_screen/mongo_databases_view.dart rename to lib/features/mongodb/mongo_databases_view.dart diff --git a/lib/features/main_screen/mongodb_connection_form.dart b/lib/features/mongodb/mongodb_connection_form.dart similarity index 100% rename from lib/features/main_screen/mongodb_connection_form.dart rename to lib/features/mongodb/mongodb_connection_form.dart From 5aa01541918d0dd61190311e2abaa820ad6fccc2 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:19:40 +0300 Subject: [PATCH 03/17] refactor: move Redis files to features/redis/ Move redis_connection_form.dart and redis_view.dart from features/main_screen/ to features/redis/. All Redis-specific UI (connection form, server info dashboard) is now grouped in its own directory. --- lib/features/{main_screen => redis}/redis_connection_form.dart | 0 lib/features/{main_screen => redis}/redis_view.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/features/{main_screen => redis}/redis_connection_form.dart (100%) rename lib/features/{main_screen => redis}/redis_view.dart (100%) diff --git a/lib/features/main_screen/redis_connection_form.dart b/lib/features/redis/redis_connection_form.dart similarity index 100% rename from lib/features/main_screen/redis_connection_form.dart rename to lib/features/redis/redis_connection_form.dart diff --git a/lib/features/main_screen/redis_view.dart b/lib/features/redis/redis_view.dart similarity index 100% rename from lib/features/main_screen/redis_view.dart rename to lib/features/redis/redis_view.dart From d8c9ef921153d2326f0ecfaacbe41a02632886c2 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:19:55 +0300 Subject: [PATCH 04/17] refactor: update imports in main_screen and workspace_panel Update import paths in main_screen.dart and workspace_panel.dart to reference the new feature directory locations: - connections_panel, new_connection_dialog, driver_manager_dialog -> features/connections/ - mongodb_connection_form, mongo_databases_view -> features/mongodb/ - redis_connection_form, redis_view -> features/redis/ --- lib/features/main_screen/main_screen.dart | 6 +++--- lib/features/main_screen/workspace_panel.dart | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index fc5bb422..1aeef583 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -4,9 +4,9 @@ import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/core/theme/app_theme.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; -import 'connections_panel.dart'; -import 'driver_manager_dialog.dart'; -import 'new_connection_dialog.dart'; +import 'package:querya_desktop/features/connections/connections_panel.dart'; +import 'package:querya_desktop/features/connections/driver_manager_dialog.dart'; +import 'package:querya_desktop/features/connections/new_connection_dialog.dart'; import 'workspace_panel.dart'; class MainScreen extends StatefulWidget { diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index 8875fe48..f2345118 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart' as material show Container, EdgeInsets, B import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; -import 'mongo_databases_view.dart'; +import 'package:querya_desktop/features/mongodb/mongo_databases_view.dart'; +import 'package:querya_desktop/features/redis/redis_view.dart'; import 'query_editor_tab.dart'; -import 'redis_view.dart'; import 'results_tab.dart'; /// Main workspace: top = Query Editor / Query History, bottom = Data Output / Messages (pgAdmin-style). Uses shadcn layout. From d9bdae928eaf20e2dbce061ba0f36d062d4d603b Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:31:23 +0300 Subject: [PATCH 05/17] fix: close stale Redis connections on page refresh and widget dispose --- lib/core/database/redis_connection.dart | 11 ++++++++--- lib/core/database/redis_service.dart | 15 +++++++++++++-- lib/features/redis/redis_view.dart | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/core/database/redis_connection.dart b/lib/core/database/redis_connection.dart index a79f3ea0..d894de38 100644 --- a/lib/core/database/redis_connection.dart +++ b/lib/core/database/redis_connection.dart @@ -46,10 +46,15 @@ class RedisConnection { } Future disconnect() async { - await _conn?.close(); - _conn = null; - _command = null; _isConnected = false; + _command = null; + final c = _conn; + _conn = null; + try { + await c?.close(); + } catch (_) { + // Connection may already be closed — ignore. + } } Future info() async { diff --git a/lib/core/database/redis_service.dart b/lib/core/database/redis_service.dart index 6d07113c..e1bd6905 100644 --- a/lib/core/database/redis_service.dart +++ b/lib/core/database/redis_service.dart @@ -8,19 +8,30 @@ class RedisService { final Map _connections = {}; + /// Creates (or replaces) a [RedisConnection] for the given [ConnectionRow]. + /// If a connection with the same ID already exists it is disconnected first. RedisConnection createConnection(ConnectionRow row) { if (row.type != 'redis') { throw ArgumentError('Connection type must be redis'); } + + final id = row.id ?? 0; + + // Disconnect previous connection for this ID, if any. + final existing = _connections[id]; + if (existing != null) { + existing.disconnect(); // fire-and-forget; disconnect is safe + } + final conn = RedisConnection( - id: row.id ?? 0, + id: id, name: row.name, host: row.host ?? 'localhost', port: row.port ?? 6379, username: row.username, password: row.password, ); - _connections[conn.id] = conn; + _connections[id] = conn; return conn; } diff --git a/lib/features/redis/redis_view.dart b/lib/features/redis/redis_view.dart index 42696693..c2e0b801 100644 --- a/lib/features/redis/redis_view.dart +++ b/lib/features/redis/redis_view.dart @@ -38,6 +38,7 @@ class _RedisViewState extends material.State { super.didUpdateWidget(oldWidget); if (oldWidget.connectionRow.id != widget.connectionRow.id) { _timer?.cancel(); + _disconnectCurrent(); _load(); } } @@ -45,11 +46,22 @@ class _RedisViewState extends material.State { @override void dispose() { _timer?.cancel(); + _disconnectCurrent(); super.dispose(); } + /// Safely disconnects and clears the current Redis connection. + void _disconnectCurrent() { + final conn = _connection; + _connection = null; + if (conn != null) { + conn.disconnect(); // fire-and-forget; disconnect handles errors + } + } + Future _load() async { _timer?.cancel(); + _disconnectCurrent(); if (!mounted) return; setState(() { _loading = true; @@ -59,7 +71,11 @@ class _RedisViewState extends material.State { try { final conn = RedisService.instance.createConnection(widget.connectionRow); await conn.connect(); - if (!mounted) return; + if (!mounted) { + // Widget was disposed while connecting — clean up immediately. + conn.disconnect(); + return; + } _connection = conn; await _fetch(); if (mounted) _startTimer(); From e467b635c5a269a9350735760dfbda2a0d38fa49 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:33:15 +0300 Subject: [PATCH 06/17] chore: update auto-generated Flutter plugin files --- .flutter-plugins-dependencies | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 132886d4..f1aa1ebf 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"/home/reei/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/reei/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"/home/reei/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"/home/reei/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"/home/reei/.pub-cache/hosted/pub.dev/bitsdojo_window_macos-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/reei/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/reei/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"/home/reei/.pub-cache/hosted/pub.dev/bitsdojo_window_linux-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/reei/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"/home/reei/.pub-cache/hosted/pub.dev/bitsdojo_window_windows-0.1.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/reei/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-03-05 04:09:30.456439","version":"3.38.9","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_android-2.4.2+2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_macos-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_linux-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_windows-0.1.6\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-03-05 22:16:33.139337","version":"3.38.5","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file From 31227564f7d556dd089b336c0f405496c9127c92 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:48:19 +0300 Subject: [PATCH 07/17] test: fix broken widget_test.dart placeholder --- test/widget_test.dart | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 1f55acac..f23ec7a3 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,12 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. +// Basic smoke test placeholder. +// The original counter test referenced a non-existent MyApp class. +// Replace with actual widget tests as the UI stabilises. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:querya_desktop/main.dart'; - void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + test('placeholder – project compiles', () { + // This test simply verifies the test harness runs. + expect(1 + 1, 2); }); } From a131cb866139b4c63d3dda07e39aa9fc4cd156d5 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:48:24 +0300 Subject: [PATCH 08/17] test: add Redis connection and service tests - redis_info_test.dart: tests for parseRedisInfo() and helper functions (sectionValue, sectionInt, sectionDouble) - redis_connection_test.dart: tests for RedisConnection initial state, disconnect safety, and error handling - redis_service_test.dart: tests for RedisService.createConnection(), getConnection(), disconnect(), and validation --- test/core/database/redis_connection_test.dart | 104 ++++++++++ test/core/database/redis_info_test.dart | 191 ++++++++++++++++++ test/core/database/redis_service_test.dart | 132 ++++++++++++ 3 files changed, 427 insertions(+) create mode 100644 test/core/database/redis_connection_test.dart create mode 100644 test/core/database/redis_info_test.dart create mode 100644 test/core/database/redis_service_test.dart diff --git a/test/core/database/redis_connection_test.dart b/test/core/database/redis_connection_test.dart new file mode 100644 index 00000000..3d7dee87 --- /dev/null +++ b/test/core/database/redis_connection_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/database/redis_connection.dart'; + +void main() { + group('RedisConnection initial state', () { + test('isConnected is false before connect()', () { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + expect(conn.isConnected, false); + }); + + test('default port is 6379', () { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + expect(conn.port, 6379); + }); + + test('custom port is stored', () { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + port: 6380, + ); + expect(conn.port, 6380); + }); + + test('stores all constructor parameters', () { + final conn = RedisConnection( + id: 42, + name: 'My Redis', + host: 'redis.local', + port: 6380, + username: 'admin', + password: 's3cret', + ); + expect(conn.id, 42); + expect(conn.name, 'My Redis'); + expect(conn.host, 'redis.local'); + expect(conn.port, 6380); + expect(conn.username, 'admin'); + expect(conn.password, 's3cret'); + }); + + test('username and password default to null', () { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + expect(conn.username, isNull); + expect(conn.password, isNull); + }); + }); + + group('RedisConnection.disconnect', () { + test('disconnect on a never-connected instance does not throw', () async { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + // Should complete without error + await conn.disconnect(); + expect(conn.isConnected, false); + }); + + test('double disconnect does not throw', () async { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + await conn.disconnect(); + await conn.disconnect(); + expect(conn.isConnected, false); + }); + }); + + group('RedisConnection.info', () { + test('throws StateError when not connected', () { + final conn = RedisConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + expect(() => conn.info(), throwsStateError); + }); + }); + + group('RedisConnectionException', () { + test('stores message and toString returns it', () { + final ex = RedisConnectionException('something went wrong'); + expect(ex.message, 'something went wrong'); + expect(ex.toString(), 'something went wrong'); + }); + }); +} diff --git a/test/core/database/redis_info_test.dart b/test/core/database/redis_info_test.dart new file mode 100644 index 00000000..d7457c08 --- /dev/null +++ b/test/core/database/redis_info_test.dart @@ -0,0 +1,191 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/database/redis_info.dart'; + +/// Sample Redis INFO output (trimmed to a few sections). +const _sampleInfo = ''' +# Server +redis_version:7.2.4 +redis_mode:standalone +os:Linux 6.5.0 +tcp_port:6379 +uptime_in_days:12 + +# Clients +connected_clients:5 +maxclients:10000 + +# Memory +used_memory:1048576 +used_memory_human:1.00M +used_memory_rss_human:2.50M +used_memory_peak_human:3.00M +mem_fragmentation_ratio:2.38 + +# Stats +instantaneous_ops_per_sec:42 +total_commands_processed:123456 +keyspace_hits:9000 +keyspace_misses:1000 + +# CPU +used_cpu_sys_main_thread:1.23 +used_cpu_user_main_thread:4.56 + +# Errorstats +errorstat_ERR:count=7 + +# Keyspace +db0:keys=100,expires=10,avg_ttl=5000 + +# Replication +role:master +'''; + +void main() { + group('parseRedisInfo', () { + late RedisInfoSections info; + + setUp(() { + info = parseRedisInfo(_sampleInfo); + }); + + test('parses all sections', () { + expect(info.keys, containsAll([ + 'Server', + 'Clients', + 'Memory', + 'Stats', + 'CPU', + 'Errorstats', + 'Keyspace', + 'Replication', + ])); + }); + + test('parses key:value pairs correctly', () { + expect(info['Server']?['redis_version'], '7.2.4'); + expect(info['Server']?['redis_mode'], 'standalone'); + expect(info['Server']?['tcp_port'], '6379'); + expect(info['Server']?['uptime_in_days'], '12'); + }); + + test('parses Clients section', () { + expect(info['Clients']?['connected_clients'], '5'); + expect(info['Clients']?['maxclients'], '10000'); + }); + + test('parses Memory section', () { + expect(info['Memory']?['used_memory'], '1048576'); + expect(info['Memory']?['used_memory_human'], '1.00M'); + expect(info['Memory']?['mem_fragmentation_ratio'], '2.38'); + }); + + test('parses Stats section', () { + expect(info['Stats']?['instantaneous_ops_per_sec'], '42'); + expect(info['Stats']?['total_commands_processed'], '123456'); + expect(info['Stats']?['keyspace_hits'], '9000'); + expect(info['Stats']?['keyspace_misses'], '1000'); + }); + + test('parses CPU section', () { + expect(info['CPU']?['used_cpu_sys_main_thread'], '1.23'); + expect(info['CPU']?['used_cpu_user_main_thread'], '4.56'); + }); + + test('handles Errorstats (colon-delimited, value contains =)', () { + // errorstat_ERR:count=7 — parser splits on first colon: + // key = 'errorstat_ERR', value = 'count=7' + expect(info['Errorstats']?['errorstat_ERR'], 'count=7'); + }); + + test('parses Keyspace db entries', () { + expect(info['Keyspace']?['db0'], 'keys=100,expires=10,avg_ttl=5000'); + }); + + test('parses Replication section', () { + expect(info['Replication']?['role'], 'master'); + }); + + test('returns empty map for empty input', () { + final empty = parseRedisInfo(''); + expect(empty, isEmpty); + }); + + test('handles input with only blank lines', () { + final blank = parseRedisInfo('\n\n\n'); + expect(blank, isEmpty); + }); + + test('handles section header without any keys', () { + final result = parseRedisInfo('# EmptySection\n\n# Another\nkey:val'); + expect(result.containsKey('EmptySection'), true); + expect(result['EmptySection'], isEmpty); + expect(result['Another']?['key'], 'val'); + }); + + test('ignores lines before any section header', () { + final result = parseRedisInfo('orphan_key:orphan_val\n# S\nk:v'); + expect(result['S']?['k'], 'v'); + // orphan_key should not appear anywhere since no section was active + expect(result.values.expand((m) => m.keys).contains('orphan_key'), false); + }); + }); + + group('sectionValue', () { + test('returns value when key exists', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionValue(info, 'Server', 'redis_version'), '7.2.4'); + }); + + test('returns null for missing key', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionValue(info, 'Server', 'nonexistent'), isNull); + }); + + test('returns null for missing section', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionValue(info, 'NoSuchSection', 'key'), isNull); + }); + }); + + group('sectionInt', () { + test('parses integer value', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionInt(info, 'Clients', 'connected_clients'), 5); + expect(sectionInt(info, 'Stats', 'keyspace_hits'), 9000); + }); + + test('returns null for non-integer value', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionInt(info, 'Memory', 'used_memory_human'), isNull); + }); + + test('returns null for missing key', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionInt(info, 'Server', 'missing'), isNull); + }); + }); + + group('sectionDouble', () { + test('parses double value', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionDouble(info, 'Memory', 'mem_fragmentation_ratio'), 2.38); + expect(sectionDouble(info, 'CPU', 'used_cpu_sys_main_thread'), 1.23); + }); + + test('parses integer as double', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionDouble(info, 'Stats', 'keyspace_hits'), 9000.0); + }); + + test('returns null for non-numeric value', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionDouble(info, 'Server', 'redis_mode'), isNull); + }); + + test('returns null for missing key', () { + final info = parseRedisInfo(_sampleInfo); + expect(sectionDouble(info, 'Server', 'nope'), isNull); + }); + }); +} diff --git a/test/core/database/redis_service_test.dart b/test/core/database/redis_service_test.dart new file mode 100644 index 00000000..e4cccd14 --- /dev/null +++ b/test/core/database/redis_service_test.dart @@ -0,0 +1,132 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/database/redis_service.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; + +void main() { + group('RedisService.createConnection', () { + test('creates RedisConnection from ConnectionRow', () { + final row = ConnectionRow( + id: 10, + type: 'redis', + name: 'My Redis', + host: 'redis.local', + port: 6380, + username: 'admin', + password: 's3cret', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = RedisService.instance.createConnection(row); + + expect(conn.id, 10); + expect(conn.name, 'My Redis'); + expect(conn.host, 'redis.local'); + expect(conn.port, 6380); + expect(conn.username, 'admin'); + expect(conn.password, 's3cret'); + expect(conn.isConnected, false); + }); + + test('uses default host and port when null', () { + final row = ConnectionRow( + id: 1, + type: 'redis', + name: 'Defaults', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = RedisService.instance.createConnection(row); + + expect(conn.host, 'localhost'); + expect(conn.port, 6379); + }); + + test('uses id=0 when ConnectionRow.id is null', () { + final row = ConnectionRow( + type: 'redis', + name: 'No ID', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = RedisService.instance.createConnection(row); + expect(conn.id, 0); + }); + + test('throws ArgumentError for non-redis type', () { + final row = ConnectionRow( + id: 1, + type: 'mongodb', + name: 'Mongo', + createdAt: '2026-01-01T00:00:00Z', + ); + + expect( + () => RedisService.instance.createConnection(row), + throwsArgumentError, + ); + }); + + test('replaces existing connection with same ID', () { + final row1 = ConnectionRow( + id: 99, + type: 'redis', + name: 'First', + host: 'host1', + createdAt: '2026-01-01T00:00:00Z', + ); + final row2 = ConnectionRow( + id: 99, + type: 'redis', + name: 'Second', + host: 'host2', + createdAt: '2026-01-01T00:00:00Z', + ); + + RedisService.instance.createConnection(row1); + final conn2 = RedisService.instance.createConnection(row2); + + // The service should now hold the second connection + final stored = RedisService.instance.getConnection(99); + expect(stored, same(conn2)); + expect(stored?.name, 'Second'); + expect(stored?.host, 'host2'); + }); + }); + + group('RedisService.getConnection', () { + test('returns connection by ID', () { + final row = ConnectionRow( + id: 50, + type: 'redis', + name: 'Findable', + createdAt: '2026-01-01T00:00:00Z', + ); + + final created = RedisService.instance.createConnection(row); + final found = RedisService.instance.getConnection(50); + + expect(found, same(created)); + }); + + test('returns null for unknown ID', () { + expect(RedisService.instance.getConnection(999999), isNull); + }); + }); + + group('RedisService.disconnect', () { + test('removes connection from service after disconnect', () async { + final row = ConnectionRow( + id: 77, + type: 'redis', + name: 'ToDisconnect', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = RedisService.instance.createConnection(row); + expect(RedisService.instance.getConnection(77), isNotNull); + + await RedisService.instance.disconnect(conn); + expect(RedisService.instance.getConnection(77), isNull); + }); + }); +} From d87b439775418f6f33c40ccceb9ee4220518cf5d Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:48:29 +0300 Subject: [PATCH 09/17] test: add MongoDB service and connection data tests - mongo_service_test.dart: tests for MongoService.createConnection(), getConnection(), validation, and connection replacement - mongo_connection_data_test.dart: tests for MongoConnectionData validation logic (isValid) and default values --- test/core/database/mongo_service_test.dart | 122 ++++++++++++++++++ .../mongodb/mongo_connection_data_test.dart | 119 +++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 test/core/database/mongo_service_test.dart create mode 100644 test/features/mongodb/mongo_connection_data_test.dart diff --git a/test/core/database/mongo_service_test.dart b/test/core/database/mongo_service_test.dart new file mode 100644 index 00000000..4dfa7f1f --- /dev/null +++ b/test/core/database/mongo_service_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/database/mongodb_service.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; + +void main() { + group('MongoService.createConnection', () { + test('creates MongoConnection from ConnectionRow', () { + final row = ConnectionRow( + id: 10, + type: 'mongodb', + name: 'My Mongo', + host: 'mongo.local', + port: 27018, + username: 'root', + password: 'secret', + databaseName: 'testdb', + authSource: 'admin', + useSSL: true, + connectionString: 'mongodb://custom', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = MongoService.instance.createConnection(row); + + expect(conn.id, 10); + expect(conn.name, 'My Mongo'); + expect(conn.host, 'mongo.local'); + expect(conn.port, 27018); + expect(conn.username, 'root'); + expect(conn.password, 'secret'); + expect(conn.database, 'testdb'); + expect(conn.authSource, 'admin'); + expect(conn.useSSL, true); + expect(conn.connectionString, 'mongodb://custom'); + expect(conn.isConnected, false); + }); + + test('uses default host and port when null', () { + final row = ConnectionRow( + id: 1, + type: 'mongodb', + name: 'Defaults', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = MongoService.instance.createConnection(row); + + expect(conn.host, 'localhost'); + expect(conn.port, 27017); + }); + + test('uses id=0 when ConnectionRow.id is null', () { + final row = ConnectionRow( + type: 'mongodb', + name: 'No ID', + createdAt: '2026-01-01T00:00:00Z', + ); + + final conn = MongoService.instance.createConnection(row); + expect(conn.id, 0); + }); + + test('throws ArgumentError for non-mongodb type', () { + final row = ConnectionRow( + id: 1, + type: 'redis', + name: 'Redis', + createdAt: '2026-01-01T00:00:00Z', + ); + + expect( + () => MongoService.instance.createConnection(row), + throwsArgumentError, + ); + }); + + test('replaces existing connection with same ID', () { + final row1 = ConnectionRow( + id: 88, + type: 'mongodb', + name: 'First', + host: 'host1', + createdAt: '2026-01-01T00:00:00Z', + ); + final row2 = ConnectionRow( + id: 88, + type: 'mongodb', + name: 'Second', + host: 'host2', + createdAt: '2026-01-01T00:00:00Z', + ); + + MongoService.instance.createConnection(row1); + final conn2 = MongoService.instance.createConnection(row2); + + final stored = MongoService.instance.getConnection(88); + expect(stored, same(conn2)); + expect(stored?.name, 'Second'); + expect(stored?.host, 'host2'); + }); + }); + + group('MongoService.getConnection', () { + test('returns connection by ID', () { + final row = ConnectionRow( + id: 55, + type: 'mongodb', + name: 'Findable', + createdAt: '2026-01-01T00:00:00Z', + ); + + final created = MongoService.instance.createConnection(row); + final found = MongoService.instance.getConnection(55); + + expect(found, same(created)); + }); + + test('returns null for unknown ID', () { + expect(MongoService.instance.getConnection(999998), isNull); + }); + }); +} diff --git a/test/features/mongodb/mongo_connection_data_test.dart b/test/features/mongodb/mongo_connection_data_test.dart new file mode 100644 index 00000000..68a2446a --- /dev/null +++ b/test/features/mongodb/mongo_connection_data_test.dart @@ -0,0 +1,119 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart'; + +void main() { + group('MongoConnectionData.isValid', () { + test('valid when name and host are provided', () { + final data = MongoConnectionData( + name: 'My Server', + host: 'localhost', + ); + expect(data.isValid, true); + }); + + test('invalid when name is empty', () { + final data = MongoConnectionData( + name: '', + host: 'localhost', + ); + expect(data.isValid, false); + }); + + test('invalid when host is empty', () { + final data = MongoConnectionData( + name: 'Server', + host: '', + ); + expect(data.isValid, false); + }); + + test('invalid when both name and host are empty', () { + final data = MongoConnectionData( + name: '', + host: '', + ); + expect(data.isValid, false); + }); + + test('invalid when name is only whitespace', () { + final data = MongoConnectionData( + name: ' ', + host: 'localhost', + ); + expect(data.isValid, false); + }); + + test('invalid when host is only whitespace', () { + final data = MongoConnectionData( + name: 'Server', + host: ' ', + ); + expect(data.isValid, false); + }); + + test('valid with connection string (overrides name+host)', () { + final data = MongoConnectionData( + name: '', + host: '', + connectionString: 'mongodb://localhost:27017/db', + ); + expect(data.isValid, true); + }); + + test('invalid with empty connection string and empty name', () { + final data = MongoConnectionData( + name: '', + host: 'localhost', + connectionString: '', + ); + expect(data.isValid, false); + }); + + test('valid with connection string ignoring name and host', () { + final data = MongoConnectionData( + name: '', + host: '', + connectionString: 'mongodb+srv://cluster.example.com', + ); + expect(data.isValid, true); + }); + }); + + group('MongoConnectionData defaults', () { + test('default values are correct', () { + final data = MongoConnectionData(); + expect(data.name, ''); + expect(data.host, 'localhost'); + expect(data.port, 27017); + expect(data.username, isNull); + expect(data.password, isNull); + expect(data.database, isNull); + expect(data.authSource, isNull); + expect(data.useSSL, false); + expect(data.connectionString, isNull); + }); + + test('custom values are stored', () { + final data = MongoConnectionData( + name: 'Prod', + host: 'db.prod.io', + port: 27018, + username: 'admin', + password: 'pass', + database: 'appdb', + authSource: 'admin', + useSSL: true, + connectionString: 'mongodb://custom', + ); + expect(data.name, 'Prod'); + expect(data.host, 'db.prod.io'); + expect(data.port, 27018); + expect(data.username, 'admin'); + expect(data.password, 'pass'); + expect(data.database, 'appdb'); + expect(data.authSource, 'admin'); + expect(data.useSSL, true); + expect(data.connectionString, 'mongodb://custom'); + }); + }); +} From 93ed11abb5af1cf2266d6a7dca0f9955954a7d77 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:48:50 +0300 Subject: [PATCH 10/17] test: add existing MongoDB connection and ConnectionRow tests - mongodb_connection_test.dart: comprehensive tests for MongoConnection.buildConnectionUri() and initial state - connection_row_test.dart: tests for ConnectionRow serialization (toMap/fromMap), round-trip, and edge cases --- .../database/mongodb_connection_test.dart | 184 ++++++++++++++++++ test/core/storage/connection_row_test.dart | 173 ++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 test/core/database/mongodb_connection_test.dart create mode 100644 test/core/storage/connection_row_test.dart diff --git a/test/core/database/mongodb_connection_test.dart b/test/core/database/mongodb_connection_test.dart new file mode 100644 index 00000000..32f72a65 --- /dev/null +++ b/test/core/database/mongodb_connection_test.dart @@ -0,0 +1,184 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/database/mongodb_connection.dart'; + +void main() { + group('MongoConnection.buildConnectionUri', () { + test('minimal: host only (default port)', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + ); + expect(conn.buildConnectionUri(), 'mongodb://localhost'); + }); + + test('custom port is included when not 27017', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'db.example.com', + port: 27018, + ); + expect(conn.buildConnectionUri(), 'mongodb://db.example.com:27018'); + }); + + test('default port 27017 is omitted', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'db.example.com', + port: 27017, + ); + final uri = conn.buildConnectionUri(); + expect(uri, 'mongodb://db.example.com'); + expect(uri.contains(':27017'), false); + }); + + test('username and password are included and encoded', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + username: 'admin', + password: 'p@ss:word', + ); + final uri = conn.buildConnectionUri(); + expect(uri, contains('admin:')); + expect(uri, contains('@localhost')); + // Special chars should be percent-encoded + expect(uri, contains(Uri.encodeComponent('p@ss:word'))); + }); + + test('username without password', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + username: 'admin', + ); + final uri = conn.buildConnectionUri(); + expect(uri, startsWith('mongodb://admin@localhost')); + }); + + test('database is appended as path', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + database: 'mydb', + ); + expect(conn.buildConnectionUri(), 'mongodb://localhost/mydb'); + }); + + test('authSource is added as query parameter', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + database: 'mydb', + authSource: 'admin', + ); + final uri = conn.buildConnectionUri(); + expect(uri, contains('?authSource=admin')); + }); + + test('replicaSet is added as query parameter', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + replicaSet: 'rs0', + ); + final uri = conn.buildConnectionUri(); + expect(uri, contains('replicaSet=rs0')); + }); + + test('SSL flag is added as query parameter', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + useSSL: true, + ); + final uri = conn.buildConnectionUri(); + expect(uri, contains('ssl=true')); + }); + + test('multiple query params are joined with &', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + database: 'db', + authSource: 'admin', + replicaSet: 'rs0', + useSSL: true, + ); + final uri = conn.buildConnectionUri(); + expect(uri, contains('authSource=admin')); + expect(uri, contains('replicaSet=rs0')); + expect(uri, contains('ssl=true')); + // All params separated by & + final queryPart = uri.split('?').last; + expect(queryPart.split('&').length, 3); + }); + + test('full URI with all fields', () { + final conn = MongoConnection( + id: 1, + name: 'prod', + host: 'mongo.prod.io', + port: 27018, + username: 'root', + password: 'secret', + database: 'app', + authSource: 'admin', + replicaSet: 'rs-main', + useSSL: true, + ); + final uri = conn.buildConnectionUri(); + expect(uri, startsWith('mongodb://root:secret@mongo.prod.io:27018/app?')); + expect(uri, contains('authSource=admin')); + expect(uri, contains('replicaSet=rs-main')); + expect(uri, contains('ssl=true')); + }); + + test('connectionString overrides everything', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + port: 27018, + username: 'admin', + password: 'pass', + connectionString: 'mongodb+srv://custom-cluster.example.com/mydb', + ); + expect( + conn.buildConnectionUri(), + 'mongodb+srv://custom-cluster.example.com/mydb', + ); + }); + + test('empty connectionString falls back to building URI', () { + final conn = MongoConnection( + id: 1, + name: 'test', + host: 'localhost', + connectionString: '', + ); + expect(conn.buildConnectionUri(), 'mongodb://localhost'); + }); + }); + + group('MongoConnection initial state', () { + test('isConnected is false before connect()', () { + final conn = MongoConnection(id: 1, name: 'test', host: 'localhost'); + expect(conn.isConnected, false); + }); + + test('db is null before connect()', () { + final conn = MongoConnection(id: 1, name: 'test', host: 'localhost'); + expect(conn.db, isNull); + }); + }); +} diff --git a/test/core/storage/connection_row_test.dart b/test/core/storage/connection_row_test.dart new file mode 100644 index 00000000..ca0d0815 --- /dev/null +++ b/test/core/storage/connection_row_test.dart @@ -0,0 +1,173 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; + +void main() { + group('ConnectionRow', () { + test('toMap produces correct keys and values', () { + final row = ConnectionRow( + type: 'mongodb', + name: 'Test Mongo', + host: 'mongo.example.com', + port: 27017, + username: 'admin', + password: 's3cret', + databaseName: 'mydb', + authSource: 'admin', + useSSL: true, + connectionString: 'mongodb://custom', + folderId: 5, + sortOrder: 2, + createdAt: '2026-01-01T00:00:00Z', + ); + + final map = row.toMap(); + + expect(map['type'], 'mongodb'); + expect(map['name'], 'Test Mongo'); + expect(map['host'], 'mongo.example.com'); + expect(map['port'], 27017); + expect(map['username'], 'admin'); + expect(map['password'], 's3cret'); + expect(map['database_name'], 'mydb'); + expect(map['auth_source'], 'admin'); + expect(map['use_ssl'], 1); // bool → int + expect(map['connection_string'], 'mongodb://custom'); + expect(map['folder_id'], 5); + expect(map['sort_order'], 2); + expect(map['created_at'], '2026-01-01T00:00:00Z'); + }); + + test('toMap encodes useSSL=false as 0', () { + final row = ConnectionRow( + type: 'redis', + name: 'Redis', + createdAt: '2026-01-01T00:00:00Z', + ); + expect(row.toMap()['use_ssl'], 0); + }); + + test('fromMap restores all fields correctly', () { + final map = { + 'id': 42, + 'type': 'redis', + 'name': 'My Redis', + 'host': 'redis.local', + 'port': 6379, + 'username': 'default', + 'password': 'pass', + 'database_name': null, + 'auth_source': null, + 'use_ssl': 0, + 'connection_string': null, + 'folder_id': 3, + 'sort_order': 1, + 'created_at': '2026-03-01T12:00:00Z', + }; + + final row = ConnectionRow.fromMap(map); + + expect(row.id, 42); + expect(row.type, 'redis'); + expect(row.name, 'My Redis'); + expect(row.host, 'redis.local'); + expect(row.port, 6379); + expect(row.username, 'default'); + expect(row.password, 'pass'); + expect(row.databaseName, isNull); + expect(row.authSource, isNull); + expect(row.useSSL, false); + expect(row.connectionString, isNull); + expect(row.folderId, 3); + expect(row.sortOrder, 1); + expect(row.createdAt, '2026-03-01T12:00:00Z'); + }); + + test('fromMap decodes use_ssl=1 as true', () { + final map = { + 'id': 1, + 'type': 'mongodb', + 'name': 'SSL Mongo', + 'host': 'h', + 'port': 27017, + 'use_ssl': 1, + 'sort_order': 0, + 'created_at': '2026-01-01T00:00:00Z', + }; + + final row = ConnectionRow.fromMap(map); + expect(row.useSSL, true); + }); + + test('round-trip: toMap -> fromMap preserves data', () { + final original = ConnectionRow( + type: 'mongodb', + name: 'Round Trip', + host: 'localhost', + port: 27017, + username: 'u', + password: 'p', + databaseName: 'testdb', + authSource: 'admin', + useSSL: true, + connectionString: 'mongodb://localhost/testdb', + folderId: 10, + sortOrder: 5, + createdAt: '2026-06-15T09:30:00Z', + ); + + // Simulate DB round-trip: toMap -> add id -> fromMap + final map = original.toMap(); + map['id'] = 99; + + final restored = ConnectionRow.fromMap(map); + + expect(restored.id, 99); + expect(restored.type, original.type); + expect(restored.name, original.name); + expect(restored.host, original.host); + expect(restored.port, original.port); + expect(restored.username, original.username); + expect(restored.password, original.password); + expect(restored.databaseName, original.databaseName); + expect(restored.authSource, original.authSource); + expect(restored.useSSL, original.useSSL); + expect(restored.connectionString, original.connectionString); + expect(restored.folderId, original.folderId); + expect(restored.sortOrder, original.sortOrder); + expect(restored.createdAt, original.createdAt); + }); + + test('fromMap handles missing optional fields gracefully', () { + final map = { + 'type': 'postgresql', + 'name': 'PG Minimal', + 'created_at': '2026-01-01T00:00:00Z', + }; + + final row = ConnectionRow.fromMap(map); + + expect(row.id, isNull); + expect(row.host, isNull); + expect(row.port, isNull); + expect(row.username, isNull); + expect(row.password, isNull); + expect(row.databaseName, isNull); + expect(row.authSource, isNull); + expect(row.useSSL, false); // null use_ssl → false + expect(row.connectionString, isNull); + expect(row.folderId, isNull); + expect(row.sortOrder, 0); // null sort_order → 0 + }); + + test('toMap does not include id field', () { + final row = ConnectionRow( + id: 123, + type: 'redis', + name: 'R', + createdAt: '2026-01-01T00:00:00Z', + ); + final map = row.toMap(); + expect(map.containsKey('id'), false); + }); + }); +} From 0ebbf6404a7a62c756cf52544f31ab254412d431 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 22:50:06 +0300 Subject: [PATCH 11/17] ci: add GitHub Actions workflow for tests and analysis - Run tests on push/PR to main, dev, and connector branches - Run flutter analyze for code quality checks - Uses Flutter 3.38.5 stable channel - Both jobs run in parallel on Ubuntu latest --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..05ef09f0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [ main, dev, '**-connector', '**-connection-pull' ] + pull_request: + branches: [ main, dev ] + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.38.5' + channel: 'stable' + cache: true + + - name: Get dependencies + run: flutter pub get + + - name: Run tests + run: flutter test + + analyze: + name: Code Analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.38.5' + channel: 'stable' + cache: true + + - name: Get dependencies + run: flutter pub get + + - name: Analyze code + run: flutter analyze From 886df8f2110da6e2f6de2cc398a0d155b21a389e Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 23:01:27 +0300 Subject: [PATCH 12/17] fix: resolve analyzer issues in core files - driver_urls.dart: add library directive to fix dangling doc comment - folders_storage.dart: add curly braces in for loop - app_theme.dart: use const with ThemeData.dark and Typography.geist() --- lib/core/drivers/driver_urls.dart | 1 + lib/core/storage/folders_storage.dart | 4 +++- lib/core/theme/app_theme.dart | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/core/drivers/driver_urls.dart b/lib/core/drivers/driver_urls.dart index fc38efdb..c8261088 100644 --- a/lib/core/drivers/driver_urls.dart +++ b/lib/core/drivers/driver_urls.dart @@ -1,6 +1,7 @@ /// Official download URLs and file names for database drivers. /// Used by [DriverStorage] for download/install. /// Redis and MongoDB use Dart packages (built-in); only JAR-based drivers are listed here. +library; /// PostgreSQL JDBC driver (official jdbc.postgresql.org). const postgresqlDriverUrl = 'https://jdbc.postgresql.org/download/postgresql-42.7.10.jar'; diff --git a/lib/core/storage/folders_storage.dart b/lib/core/storage/folders_storage.dart index 95b526e0..da9dc917 100644 --- a/lib/core/storage/folders_storage.dart +++ b/lib/core/storage/folders_storage.dart @@ -56,7 +56,9 @@ class FoldersStorage { Future save(List folders) async { _folders = List.from(folders); await LocalDb.instance.clearFolders(); - for (final name in _folders) await LocalDb.instance.addFolder(name); + for (final name in _folders) { + await LocalDb.instance.addFolder(name); + } } Future add(String name) async { diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index b77e01d0..e66c1385 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -2,10 +2,10 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; /// App theme: dark only. Used for both theme and darkTheme so the app is always dark. abstract class AppTheme { - static ThemeData get dark => ThemeData.dark( + static ThemeData get dark => const ThemeData.dark( colorScheme: ColorSchemes.darkSlate, radius: 0.5, scaling: 1, - typography: const Typography.geist(), + typography: Typography.geist(), ); } From a2327613fe6e0fe1250f1d7232576d8326bba89a Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 23:01:30 +0300 Subject: [PATCH 13/17] fix: add const constructors in connection dialogs - connections_panel.dart: const Text and Padding widgets - driver_manager_dialog.dart: const constructors, remove unused _refreshStatus - new_connection_dialog.dart: const Dialog and Text widgets - new_folder_dialog.dart: const Dialog and Text widgets --- .../connections/connections_panel.dart | 6 ++-- .../connections/driver_manager_dialog.dart | 29 ++++++++----------- .../connections/new_connection_dialog.dart | 18 ++++++------ .../connections/new_folder_dialog.dart | 18 ++++++------ 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index dbe9a573..46e504ea 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -142,7 +142,7 @@ class _ConnectionsPanelState extends State { padding: const material.EdgeInsets.fromLTRB(20, 24, 16, 16), child: material.DefaultTextStyle( style: material.TextStyle(color: theme.colorScheme.mutedForeground), - child: Text('Browser').semiBold().small(), + child: const Text('Browser').semiBold().small(), ), ), Divider(height: 1, color: theme.colorScheme.border.withValues(alpha: 0.3)), @@ -198,8 +198,8 @@ class _ConnectionsPanelState extends State { ), // Empty state if (_connections.isEmpty && _folders.isEmpty) - material.Padding( - padding: const material.EdgeInsets.only(top: 8), + const material.Padding( + padding: material.EdgeInsets.only(top: 8), child: _EmptyState(message: 'No connections yet'), ), ], diff --git a/lib/features/connections/driver_manager_dialog.dart b/lib/features/connections/driver_manager_dialog.dart index e3ed38f0..dd97f289 100644 --- a/lib/features/connections/driver_manager_dialog.dart +++ b/lib/features/connections/driver_manager_dialog.dart @@ -31,10 +31,10 @@ void showDriverManagerDialog(BuildContext context) { showDialog( context: context, barrierColor: material.Colors.black54, - builder: (context) => material.Dialog( + builder: (context) => const material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: const _DriverManagerDialogContent(), + insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + child: _DriverManagerDialogContent(), ), ); } @@ -64,11 +64,6 @@ class _DriverManagerDialogContentState extends material.State<_DriverManagerDial } } - Future _refreshStatus(DownloadableDriver driver) async { - final installed = await DriverStorage.instance.isInstalled(driver); - if (mounted) setState(() => _installed[driver] = installed); - } - Future _download(DownloadableDriver driver) async { setState(() { _errors.remove(driver); @@ -126,9 +121,9 @@ class _DriverManagerDialogContentState extends material.State<_DriverManagerDial child: material.Column( crossAxisAlignment: material.CrossAxisAlignment.start, children: [ - Text('Driver Manager').large().semiBold(), - material.SizedBox(height: 6), - Text( + const Text('Driver Manager').large().semiBold(), + const material.SizedBox(height: 6), + const Text( 'Built-in database drivers. Add new connection types via Connection → New Database Connection.', ).muted().small(), ], @@ -238,17 +233,17 @@ class _DriverRow extends material.StatelessWidget { ) : material.Icon(type.icon, size: 40, color: theme.primary), ), - material.SizedBox(width: 16), + const material.SizedBox(width: 16), material.Expanded( child: material.Column( crossAxisAlignment: material.CrossAxisAlignment.start, mainAxisSize: material.MainAxisSize.min, children: [ Text(type.label).semiBold().small(), - material.SizedBox(height: 2), + const material.SizedBox(height: 2), Text(description).muted().xSmall(), if (error != null) ...[ - material.SizedBox(height: 4), + const material.SizedBox(height: 4), Text( error!, style: material.TextStyle( @@ -261,7 +256,7 @@ class _DriverRow extends material.StatelessWidget { ), ), if (isBuiltIn) ...[ - material.SizedBox(width: 8), + const material.SizedBox(width: 8), material.Container( padding: const material.EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: material.BoxDecoration( @@ -281,7 +276,7 @@ class _DriverRow extends material.StatelessWidget { ), ), ] else if (showDownload || showUninstall) ...[ - material.SizedBox(width: 8), + const material.SizedBox(width: 8), if (showDownload) loading ? material.SizedBox( @@ -324,7 +319,7 @@ class _DriverRow extends material.StatelessWidget { ) : IconButton.destructive( onPressed: onUninstall, - icon: material.Icon( + icon: const material.Icon( material.Icons.delete_outline_rounded, size: 20, ), diff --git a/lib/features/connections/new_connection_dialog.dart b/lib/features/connections/new_connection_dialog.dart index fc627805..ec5776ef 100644 --- a/lib/features/connections/new_connection_dialog.dart +++ b/lib/features/connections/new_connection_dialog.dart @@ -44,10 +44,10 @@ Future showNewConnectionDialog(BuildContext context) { return showDialog( context: context, barrierColor: material.Colors.black54, - builder: (context) => material.Dialog( + builder: (context) => const material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: const _NewConnectionDialogContent(), + insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + child: _NewConnectionDialogContent(), ), ); } @@ -109,12 +109,12 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial child: Column( crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ - Text('Select your database').large().semiBold(), - material.SizedBox(height: 6), - Text( + const Text('Select your database').large().semiBold(), + const material.SizedBox(height: 6), + const Text( 'Create new database connection. Find your database driver in the list below.', ).muted().small(), - material.SizedBox(height: 12), + const material.SizedBox(height: 12), material.Container( decoration: material.BoxDecoration( color: theme.muted.withValues(alpha: 0.2), @@ -129,7 +129,7 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial size: 20, color: theme.mutedForeground, ), - material.SizedBox(width: 10), + const material.SizedBox(width: 10), material.Expanded( child: TextField( controller: _searchController, @@ -230,7 +230,7 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial onPressed: () => material.Navigator.of(context).pop(), child: const Text('Cancel'), ), - material.SizedBox(width: 12), + const material.SizedBox(width: 12), PrimaryButton( onPressed: _selectedType == null ? null diff --git a/lib/features/connections/new_folder_dialog.dart b/lib/features/connections/new_folder_dialog.dart index 200977c2..79c11e2d 100644 --- a/lib/features/connections/new_folder_dialog.dart +++ b/lib/features/connections/new_folder_dialog.dart @@ -7,10 +7,10 @@ Future showNewFolderDialog(BuildContext context) { return showDialog( context: context, barrierColor: material.Colors.black54, - builder: (context) => material.Dialog( + builder: (context) => const material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: const _NewFolderDialogContent(), + insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + child: _NewFolderDialogContent(), ), ); } @@ -54,12 +54,12 @@ class _NewFolderDialogContentState extends material.State<_NewFolderDialogConten child: material.Column( crossAxisAlignment: material.CrossAxisAlignment.start, children: [ - Text('New folder').large().semiBold(), - material.SizedBox(height: 6), - Text( + const Text('New folder').large().semiBold(), + const material.SizedBox(height: 6), + const Text( 'Enter a name for the new folder in the browser tree.', ).muted().small(), - material.SizedBox(height: 16), + const material.SizedBox(height: 16), material.Container( decoration: material.BoxDecoration( color: theme.muted.withValues(alpha: 0.2), @@ -74,7 +74,7 @@ class _NewFolderDialogContentState extends material.State<_NewFolderDialogConten size: 20, color: theme.mutedForeground, ), - material.SizedBox(width: 10), + const material.SizedBox(width: 10), material.Expanded( child: TextField( controller: _nameController, @@ -102,7 +102,7 @@ class _NewFolderDialogContentState extends material.State<_NewFolderDialogConten onPressed: () => material.Navigator.of(context).pop(), child: const Text('Cancel'), ), - material.SizedBox(width: 12), + const material.SizedBox(width: 12), PrimaryButton( onPressed: _name.isEmpty ? null From e6e2080a02b20ae83b5b4b6395c5ee55f4595311 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 23:01:33 +0300 Subject: [PATCH 14/17] fix: add const constructors and fix child property order in main screen - main_screen.dart: const SizedBox, Text, Icon widgets; sort child properties last - query_editor_tab.dart: const Padding and Card widgets - results_tab.dart: fix const Center with non-const child --- lib/features/main_screen/main_screen.dart | 42 +++++++++---------- .../main_screen/query_editor_tab.dart | 8 ++-- lib/features/main_screen/results_tab.dart | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index 1aeef583..24068bb0 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -137,29 +137,28 @@ class _CustomTitleBarState extends State<_CustomTitleBar> { child: MoveWindow( child: Row( children: [ - material.Container(width: 16), - Text('Querya').semiBold().small(), + const SizedBox(width: 16), + const Text('Querya').semiBold().small(), const Gap(24), Menubar( border: false, popoverOffset: const Offset(0, 8), children: [ MenuButton( - child: const Text('File'), subMenu: [ - MenuButton(child: const Text('New'), onPressed: (_) {}), - MenuButton(child: const Text('Open...'), onPressed: (_) {}), - MenuButton(child: const Text('Save'), onPressed: (_) {}), + MenuButton(onPressed: (_) {}, child: const Text('New')), + MenuButton(onPressed: (_) {}, child: const Text('Open...')), + MenuButton(onPressed: (_) {}, child: const Text('Save')), const MenuDivider(), - MenuButton(child: const Text('Exit'), onPressed: (_) {}), + MenuButton(onPressed: (_) {}, child: const Text('Exit')), ], + child: const Text('File'), ), MenuButton( - child: const Text('Connection'), subMenu: [ MenuButton( - leading: material.Icon(material.Icons.add_link_rounded, size: 18), - trailing: Text('Shift+Ctrl+N').xSmall().muted(), + leading: const material.Icon(material.Icons.add_link_rounded, size: 18), + trailing: const Text('Shift+Ctrl+N').xSmall().muted(), onPressed: (ctx) async { final type = await showNewConnectionDialog(ctx); if (type != null) { @@ -169,48 +168,49 @@ class _CustomTitleBarState extends State<_CustomTitleBar> { child: const Text('New Database Connection'), ), MenuButton( - leading: material.Icon(material.Icons.link_rounded, size: 18), + leading: const material.Icon(material.Icons.link_rounded, size: 18), onPressed: (_) {}, child: const Text('New Connection from URL'), ), MenuButton( - leading: material.Icon(material.Icons.settings_rounded, size: 18), + leading: const material.Icon(material.Icons.settings_rounded, size: 18), onPressed: (ctx) => showDriverManagerDialog(ctx), child: const Text('Driver Manager'), ), const MenuDivider(), MenuButton( enabled: false, - leading: material.Icon(material.Icons.power_rounded, size: 18), + leading: const material.Icon(material.Icons.power_rounded, size: 18), onPressed: (_) {}, child: const Text('Connect'), ), MenuButton( - leading: material.Icon(material.Icons.refresh_rounded, size: 18), + leading: const material.Icon(material.Icons.refresh_rounded, size: 18), onPressed: (_) {}, child: const Text('Invalidate/Reconnect'), ), MenuButton( - leading: material.Icon(material.Icons.power_off_rounded, size: 18), + leading: const material.Icon(material.Icons.power_off_rounded, size: 18), onPressed: (_) {}, child: const Text('Disconnect'), ), - MenuButton(child: const Text('Disconnect All'), onPressed: (_) {}), - MenuButton(child: const Text('Disconnect Others'), onPressed: (_) {}), + MenuButton(onPressed: (_) {}, child: const Text('Disconnect All')), + MenuButton(onPressed: (_) {}, child: const Text('Disconnect Others')), const MenuDivider(), MenuButton( - leading: material.Icon(material.Icons.lock_outline_rounded, size: 18), + leading: const material.Icon(material.Icons.lock_outline_rounded, size: 18), onPressed: (_) {}, child: const Text('Read-only'), ), ], + child: const Text('Connection'), ), MenuButton( - child: const Text('Help'), subMenu: [ - MenuButton(child: const Text('About'), onPressed: (_) {}), - MenuButton(child: const Text('Documentation'), onPressed: (_) {}), + MenuButton(onPressed: (_) {}, child: const Text('About')), + MenuButton(onPressed: (_) {}, child: const Text('Documentation')), ], + child: const Text('Help'), ), ], ), diff --git a/lib/features/main_screen/query_editor_tab.dart b/lib/features/main_screen/query_editor_tab.dart index e86826dd..1c092236 100644 --- a/lib/features/main_screen/query_editor_tab.dart +++ b/lib/features/main_screen/query_editor_tab.dart @@ -6,18 +6,18 @@ class QueryEditorTab extends StatelessWidget { @override Widget build(BuildContext context) { - return material.Padding( - padding: const material.EdgeInsets.all(12), + return const material.Padding( + padding: material.EdgeInsets.all(12), child: Card( padding: material.EdgeInsets.zero, child: TextField( maxLines: null, expands: true, - style: const material.TextStyle( + style: material.TextStyle( fontFamily: 'monospace', fontSize: 13, ), - placeholder: const Text('-- Enter SQL here…\nSELECT 1;'), + placeholder: Text('-- Enter SQL here…\nSELECT 1;'), ), ), ); diff --git a/lib/features/main_screen/results_tab.dart b/lib/features/main_screen/results_tab.dart index 8352af7c..0597cd30 100644 --- a/lib/features/main_screen/results_tab.dart +++ b/lib/features/main_screen/results_tab.dart @@ -7,7 +7,7 @@ class ResultsTab extends StatelessWidget { @override Widget build(BuildContext context) { return material.Center( - child: Text('Results').muted(), + child: const Text('Results').muted(), ); } } From 90e9a4e7877a59194414ced7b646e3cad657e134 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 23:01:36 +0300 Subject: [PATCH 15/17] fix: add const constructors and remove unused imports in MongoDB views - mongo_databases_view.dart: remove unused BorderSide, MainAxisAlignment, Divider imports; add const Text widgets - mongodb_connection_form.dart: add const Text, Divider, SizedBox, CircularProgressIndicator widgets --- .../mongodb/mongo_databases_view.dart | 19 ++++++------- .../mongodb/mongodb_connection_form.dart | 28 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/lib/features/mongodb/mongo_databases_view.dart b/lib/features/mongodb/mongo_databases_view.dart index a48b0834..74f101be 100644 --- a/lib/features/mongodb/mongo_databases_view.dart +++ b/lib/features/mongodb/mongo_databases_view.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart' as material Container, BoxDecoration, Border, - BorderSide, BorderRadius, Icon, IconData, @@ -13,7 +12,6 @@ import 'package:flutter/material.dart' as material Center, CrossAxisAlignment, MainAxisSize, - MainAxisAlignment, Column, SizedBox, CircularProgressIndicator, @@ -29,8 +27,7 @@ import 'package:flutter/material.dart' as material Curves, SelectableText, TextEditingController, - DefaultTextStyle, - Divider; + DefaultTextStyle; import 'package:querya_desktop/core/database/mongodb_connection.dart'; import 'package:querya_desktop/core/database/mongodb_service.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; @@ -222,7 +219,7 @@ class _MongoDatabasesViewState extends State { color: cs.destructive, ), const Gap(16), - Text('Connection Error').large().semiBold(), + const Text('Connection Error').large().semiBold(), const Gap(8), material.SelectableText( _error!, @@ -317,7 +314,7 @@ class _MongoDatabasesViewState extends State { ), child: Row( children: [ - Text('Databases').semiBold(), + const Text('Databases').semiBold(), const Spacer(), material.SizedBox( width: 200, @@ -355,7 +352,7 @@ class _MongoDatabasesViewState extends State { material.Padding( padding: const material.EdgeInsets.all(24), child: material.Center( - child: Text('No databases found').muted(), + child: const Text('No databases found').muted(), ), ), ], @@ -388,7 +385,7 @@ class _MongoDatabasesViewState extends State { topRight: Radius.circular(8), ), ), - child: Text('Server Status').semiBold(), + child: const Text('Server Status').semiBold(), ), // Status rows material.Padding( @@ -488,8 +485,8 @@ class _DatabaseRowState extends State<_DatabaseRow> { child: material.InkWell( onTap: widget.onView, child: material.DefaultTextStyle( - style: material.TextStyle( - color: const Color(0xFF42A5F5), + style: const material.TextStyle( + color: Color(0xFF42A5F5), fontSize: 15, fontWeight: material.FontWeight.w500, ), @@ -556,7 +553,7 @@ class _ActionButtonState extends State<_ActionButton> { material.Icon(widget.icon, size: 16, color: material.Colors.white), const Gap(6), material.DefaultTextStyle( - style: material.TextStyle( + style: const material.TextStyle( color: material.Colors.white, fontSize: 13, fontWeight: material.FontWeight.w500, diff --git a/lib/features/mongodb/mongodb_connection_form.dart b/lib/features/mongodb/mongodb_connection_form.dart index 01520895..2e35a13c 100644 --- a/lib/features/mongodb/mongodb_connection_form.dart +++ b/lib/features/mongodb/mongodb_connection_form.dart @@ -219,15 +219,15 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo color: theme.primary, ), const Gap(12), - Text('MongoDB Connection').large().semiBold(), + const Text('MongoDB Connection').large().semiBold(), ], ), const Gap(8), - Text('Configure your MongoDB connection settings').muted().small(), + const Text('Configure your MongoDB connection settings').muted().small(), ], ), ), - material.Divider(height: 1), + const material.Divider(height: 1), material.Expanded( child: material.SingleChildScrollView( padding: const material.EdgeInsets.all(24), @@ -242,12 +242,12 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo onChanged: (v) => setState(() => _useConnectionString = v ?? false), ), const Gap(8), - Text('Use connection string').small(), + const Text('Use connection string').small(), ], ), const Gap(16), if (_useConnectionString) ...[ - Text('Connection String').small().semiBold(), + const Text('Connection String').small().semiBold(), const Gap(8), TextField( controller: _connectionStringController, @@ -256,7 +256,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo ), ] else ...[ // Connection name - Text('Connection Name').small().semiBold(), + const Text('Connection Name').small().semiBold(), const Gap(8), TextField( controller: _nameController, @@ -272,7 +272,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo crossAxisAlignment: material.CrossAxisAlignment.stretch, mainAxisSize: material.MainAxisSize.min, children: [ - Text('Host').small().semiBold(), + const Text('Host').small().semiBold(), const Gap(8), TextField( controller: _hostController, @@ -288,7 +288,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo crossAxisAlignment: material.CrossAxisAlignment.stretch, mainAxisSize: material.MainAxisSize.min, children: [ - Text('Port').small().semiBold(), + const Text('Port').small().semiBold(), const Gap(8), TextField( controller: _portController, @@ -301,7 +301,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo ), const Gap(16), // Authentication - Text('Authentication (Optional)').small().semiBold(), + const Text('Authentication (Optional)').small().semiBold(), const Gap(8), TextField( controller: _usernameController, @@ -342,7 +342,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo crossAxisAlignment: material.CrossAxisAlignment.stretch, mainAxisSize: material.MainAxisSize.min, children: [ - Text('Default Database (Optional)').small().semiBold(), + const Text('Default Database (Optional)').small().semiBold(), const Gap(8), TextField( controller: _databaseController, @@ -357,7 +357,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo crossAxisAlignment: material.CrossAxisAlignment.stretch, mainAxisSize: material.MainAxisSize.min, children: [ - Text('Auth Source (Optional)').small().semiBold(), + const Text('Auth Source (Optional)').small().semiBold(), const Gap(8), TextField( controller: _authSourceController, @@ -377,7 +377,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo onChanged: (v) => setState(() => _useSSL = v ?? false), ), const Gap(8), - Text('Use SSL/TLS').small(), + const Text('Use SSL/TLS').small(), ], ), ], @@ -385,7 +385,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo ), ), ), - material.Divider(height: 1), + const material.Divider(height: 1), if (_testResult != null) material.Container( padding: const material.EdgeInsets.symmetric(horizontal: 24, vertical: 8), @@ -429,7 +429,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo OutlineButton( onPressed: _isTesting ? null : _testConnection, leading: _isTesting - ? material.SizedBox( + ? const material.SizedBox( width: 16, height: 16, child: material.CircularProgressIndicator(strokeWidth: 2), From 8822c06350daf3d1a8e6d1314d16872d7cd1af54 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 23:01:39 +0300 Subject: [PATCH 16/17] fix: add const constructors in Redis connection forms and views - redis_connection_form.dart: const Text, Divider widgets - redis_view.dart: const Text, SizedBox, CircularProgressIndicator widgets --- lib/features/redis/redis_connection_form.dart | 18 +++++++++--------- lib/features/redis/redis_view.dart | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/features/redis/redis_connection_form.dart b/lib/features/redis/redis_connection_form.dart index d811d6c8..8678cea4 100644 --- a/lib/features/redis/redis_connection_form.dart +++ b/lib/features/redis/redis_connection_form.dart @@ -157,22 +157,22 @@ class _RedisConnectionFormContentState extends material.State<_RedisConnectionFo children: [ material.Icon(material.Icons.memory_rounded, size: 24, color: theme.primary), const Gap(12), - Text('Redis Connection').large().semiBold(), + const Text('Redis Connection').large().semiBold(), ], ), const Gap(8), - Text('Configure Redis connection.').muted().small(), + const Text('Configure Redis connection.').muted().small(), ], ), ), - material.Divider(height: 1), + const material.Divider(height: 1), material.Expanded( child: material.SingleChildScrollView( padding: const material.EdgeInsets.all(24), child: material.Column( crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ - Text('Connection Name').small().semiBold(), + const Text('Connection Name').small().semiBold(), const Gap(8), TextField( controller: _nameController, @@ -187,7 +187,7 @@ class _RedisConnectionFormContentState extends material.State<_RedisConnectionFo crossAxisAlignment: material.CrossAxisAlignment.stretch, mainAxisSize: material.MainAxisSize.min, children: [ - Text('Host').small().semiBold(), + const Text('Host').small().semiBold(), const Gap(8), TextField( controller: _hostController, @@ -203,7 +203,7 @@ class _RedisConnectionFormContentState extends material.State<_RedisConnectionFo crossAxisAlignment: material.CrossAxisAlignment.stretch, mainAxisSize: material.MainAxisSize.min, children: [ - Text('Port').small().semiBold(), + const Text('Port').small().semiBold(), const Gap(8), TextField( controller: _portController, @@ -215,14 +215,14 @@ class _RedisConnectionFormContentState extends material.State<_RedisConnectionFo ], ), const Gap(16), - Text('Username (optional, Redis 6+ ACL)').small().semiBold(), + const Text('Username (optional, Redis 6+ ACL)').small().semiBold(), const Gap(8), TextField( controller: _usernameController, placeholder: const Text('default (leave empty for default user)'), ), const Gap(16), - Text('Password (optional)').small().semiBold(), + const Text('Password (optional)').small().semiBold(), const Gap(8), material.Stack( children: [ @@ -255,7 +255,7 @@ class _RedisConnectionFormContentState extends material.State<_RedisConnectionFo ), ), ), - material.Divider(height: 1), + const material.Divider(height: 1), if (_testResult != null) material.Padding( padding: const material.EdgeInsets.fromLTRB(24, 8, 16, 8), diff --git a/lib/features/redis/redis_view.dart b/lib/features/redis/redis_view.dart index c2e0b801..d219dbc7 100644 --- a/lib/features/redis/redis_view.dart +++ b/lib/features/redis/redis_view.dart @@ -120,13 +120,13 @@ class _RedisViewState extends material.State { child: material.Column( mainAxisSize: material.MainAxisSize.min, children: [ - material.SizedBox( + const material.SizedBox( width: 32, height: 32, - child: material.CircularProgressIndicator(strokeWidth: 2, color: cs.primary), + child: material.CircularProgressIndicator(strokeWidth: 2), ), const Gap(16), - Text('Connecting...').muted().small(), + const Text('Connecting...').muted().small(), ], ), ); @@ -142,7 +142,7 @@ class _RedisViewState extends material.State { children: [ material.Icon(material.Icons.error_outline_rounded, size: 48, color: cs.destructive), const Gap(16), - Text('Connection Error').large().semiBold(), + const Text('Connection Error').large().semiBold(), const Gap(8), material.SelectableText(err, style: material.TextStyle(color: cs.mutedForeground, fontSize: 13)), const Gap(24), From 06ada201e63b8ef48cb762871d88147382ddb7b5 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 5 Mar 2026 23:01:42 +0300 Subject: [PATCH 17/17] fix: add const to ConnectionRow constructors in tests - mongo_service_test.dart: const ConnectionRow instances - redis_service_test.dart: const ConnectionRow instances - connection_row_test.dart: const ConnectionRow instances --- test/core/database/mongo_service_test.dart | 14 +++++++------- test/core/database/redis_service_test.dart | 16 ++++++++-------- test/core/storage/connection_row_test.dart | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/test/core/database/mongo_service_test.dart b/test/core/database/mongo_service_test.dart index 4dfa7f1f..0c018cbd 100644 --- a/test/core/database/mongo_service_test.dart +++ b/test/core/database/mongo_service_test.dart @@ -5,7 +5,7 @@ import 'package:querya_desktop/core/storage/local_db.dart'; void main() { group('MongoService.createConnection', () { test('creates MongoConnection from ConnectionRow', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 10, type: 'mongodb', name: 'My Mongo', @@ -36,7 +36,7 @@ void main() { }); test('uses default host and port when null', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 1, type: 'mongodb', name: 'Defaults', @@ -50,7 +50,7 @@ void main() { }); test('uses id=0 when ConnectionRow.id is null', () { - final row = ConnectionRow( + const row = ConnectionRow( type: 'mongodb', name: 'No ID', createdAt: '2026-01-01T00:00:00Z', @@ -61,7 +61,7 @@ void main() { }); test('throws ArgumentError for non-mongodb type', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 1, type: 'redis', name: 'Redis', @@ -75,14 +75,14 @@ void main() { }); test('replaces existing connection with same ID', () { - final row1 = ConnectionRow( + const row1 = ConnectionRow( id: 88, type: 'mongodb', name: 'First', host: 'host1', createdAt: '2026-01-01T00:00:00Z', ); - final row2 = ConnectionRow( + const row2 = ConnectionRow( id: 88, type: 'mongodb', name: 'Second', @@ -102,7 +102,7 @@ void main() { group('MongoService.getConnection', () { test('returns connection by ID', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 55, type: 'mongodb', name: 'Findable', diff --git a/test/core/database/redis_service_test.dart b/test/core/database/redis_service_test.dart index e4cccd14..20ef6994 100644 --- a/test/core/database/redis_service_test.dart +++ b/test/core/database/redis_service_test.dart @@ -5,7 +5,7 @@ import 'package:querya_desktop/core/storage/local_db.dart'; void main() { group('RedisService.createConnection', () { test('creates RedisConnection from ConnectionRow', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 10, type: 'redis', name: 'My Redis', @@ -28,7 +28,7 @@ void main() { }); test('uses default host and port when null', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 1, type: 'redis', name: 'Defaults', @@ -42,7 +42,7 @@ void main() { }); test('uses id=0 when ConnectionRow.id is null', () { - final row = ConnectionRow( + const row = ConnectionRow( type: 'redis', name: 'No ID', createdAt: '2026-01-01T00:00:00Z', @@ -53,7 +53,7 @@ void main() { }); test('throws ArgumentError for non-redis type', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 1, type: 'mongodb', name: 'Mongo', @@ -67,14 +67,14 @@ void main() { }); test('replaces existing connection with same ID', () { - final row1 = ConnectionRow( + const row1 = ConnectionRow( id: 99, type: 'redis', name: 'First', host: 'host1', createdAt: '2026-01-01T00:00:00Z', ); - final row2 = ConnectionRow( + const row2 = ConnectionRow( id: 99, type: 'redis', name: 'Second', @@ -95,7 +95,7 @@ void main() { group('RedisService.getConnection', () { test('returns connection by ID', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 50, type: 'redis', name: 'Findable', @@ -115,7 +115,7 @@ void main() { group('RedisService.disconnect', () { test('removes connection from service after disconnect', () async { - final row = ConnectionRow( + const row = ConnectionRow( id: 77, type: 'redis', name: 'ToDisconnect', diff --git a/test/core/storage/connection_row_test.dart b/test/core/storage/connection_row_test.dart index ca0d0815..afe40230 100644 --- a/test/core/storage/connection_row_test.dart +++ b/test/core/storage/connection_row_test.dart @@ -4,7 +4,7 @@ import 'package:querya_desktop/core/storage/local_db.dart'; void main() { group('ConnectionRow', () { test('toMap produces correct keys and values', () { - final row = ConnectionRow( + const row = ConnectionRow( type: 'mongodb', name: 'Test Mongo', host: 'mongo.example.com', @@ -38,7 +38,7 @@ void main() { }); test('toMap encodes useSSL=false as 0', () { - final row = ConnectionRow( + const row = ConnectionRow( type: 'redis', name: 'Redis', createdAt: '2026-01-01T00:00:00Z', @@ -99,7 +99,7 @@ void main() { }); test('round-trip: toMap -> fromMap preserves data', () { - final original = ConnectionRow( + const original = ConnectionRow( type: 'mongodb', name: 'Round Trip', host: 'localhost', @@ -160,7 +160,7 @@ void main() { }); test('toMap does not include id field', () { - final row = ConnectionRow( + const row = ConnectionRow( id: 123, type: 'redis', name: 'R',