@@ -51,6 +51,183 @@ def test_list_connections_shows_db(self, request, cli_runner):
5151 assert connection in result .stdout
5252 assert self .config .display_name in result .stdout
5353
54+ def test_docker_container_detection (self , request ):
55+ """Test that docker discovery detects the database container.
56+
57+ This ensures that the docker auto-discovery feature can find
58+ containers for this database type in the connection picker.
59+ """
60+ # Skip for file-based databases (they don't use Docker containers)
61+ from sqlit .db .providers import is_file_based
62+
63+ if is_file_based (self .config .db_type ):
64+ pytest .skip (f"{ self .config .display_name } is file-based, no Docker container" )
65+
66+ from sqlit .services .docker_detector import (
67+ IMAGE_PATTERNS ,
68+ DockerStatus ,
69+ detect_database_containers ,
70+ )
71+
72+ # Skip if this database type has no Docker image patterns defined
73+ has_pattern = any (
74+ db_type == self .config .db_type
75+ for db_type in IMAGE_PATTERNS .values ()
76+ )
77+ if not has_pattern :
78+ pytest .skip (f"{ self .config .display_name } has no Docker image patterns" )
79+
80+ status , containers = detect_database_containers ()
81+
82+ if status != DockerStatus .AVAILABLE :
83+ pytest .skip ("Docker is not available" )
84+
85+ # Find a container matching this database type
86+ matching_containers = [
87+ c for c in containers if c .db_type == self .config .db_type
88+ ]
89+
90+ assert len (matching_containers ) > 0 , (
91+ f"No Docker container detected for { self .config .display_name } . "
92+ f"Found containers: { [(c .container_name , c .db_type ) for c in containers ]} "
93+ )
94+
95+ # Verify the container has a port detected
96+ container = matching_containers [0 ]
97+ assert container .port is not None , (
98+ f"Container { container .container_name } has no port detected"
99+ )
100+
101+ def test_docker_container_no_password_prompt_when_not_needed (self , request ):
102+ """Test that docker discovery doesn't trigger password prompts for no-auth databases.
103+
104+ Some databases (CockroachDB, Turso) can run without authentication in
105+ local/insecure mode. When docker discovery detects these containers,
106+ it should return password="" (empty string) rather than password=None.
107+
108+ - password=None means "not set" -> UI will prompt for password
109+ - password="" means "explicitly empty" -> UI will NOT prompt
110+
111+ This test ensures users aren't asked for passwords for databases
112+ that don't need them.
113+ """
114+ # Skip for file-based databases (they don't use Docker containers)
115+ from sqlit .db .providers import is_file_based
116+
117+ if is_file_based (self .config .db_type ):
118+ pytest .skip (f"{ self .config .display_name } is file-based, no Docker container" )
119+
120+ from sqlit .services .docker_detector import (
121+ DockerStatus ,
122+ container_to_connection_config ,
123+ detect_database_containers ,
124+ )
125+
126+ status , containers = detect_database_containers ()
127+
128+ if status != DockerStatus .AVAILABLE :
129+ pytest .skip ("Docker is not available" )
130+
131+ # Find a container matching this database type
132+ matching_containers = [
133+ c for c in containers if c .db_type == self .config .db_type
134+ ]
135+
136+ if not matching_containers :
137+ pytest .skip (f"No Docker container found for { self .config .display_name } " )
138+
139+ container = matching_containers [0 ]
140+ config = container_to_connection_config (container )
141+
142+ # Databases that don't require auth should have password="" not None
143+ # This prevents the UI from showing "Password Required" dialog
144+ from sqlit .db .providers import requires_auth
145+
146+ if not requires_auth (self .config .db_type ):
147+ assert config .password is not None , (
148+ f"{ self .config .display_name } doesn't require authentication, but "
149+ f"password is None. This will cause the UI to prompt for a password. "
150+ f"Set password='' (empty string) in docker_detector.py for databases "
151+ f"that don't need auth."
152+ )
153+
154+ def test_docker_container_connection (self , request ):
155+ """Test that docker-discovered credentials actually work.
156+
157+ This tests the full docker discovery flow:
158+ 1. Detect the container
159+ 2. Convert to ConnectionConfig
160+ 3. Connect using discovered credentials
161+ 4. Run a simple query
162+
163+ This catches issues like:
164+ - Wrong host (localhost vs 127.0.0.1 for MySQL/MariaDB)
165+ - Missing or incorrect credentials
166+ - Wrong port mappings
167+ """
168+ # Skip for file-based databases (they don't use Docker containers)
169+ from sqlit .db .providers import is_file_based
170+
171+ if is_file_based (self .config .db_type ):
172+ pytest .skip (f"{ self .config .display_name } is file-based, no Docker container" )
173+
174+ from sqlit .db .adapters import get_adapter
175+ from sqlit .services .docker_detector import (
176+ DockerStatus ,
177+ container_to_connection_config ,
178+ detect_database_containers ,
179+ )
180+
181+ status , containers = detect_database_containers ()
182+
183+ if status != DockerStatus .AVAILABLE :
184+ pytest .skip ("Docker is not available" )
185+
186+ # Find a container matching this database type
187+ matching_containers = [
188+ c for c in containers if c .db_type == self .config .db_type
189+ ]
190+
191+ if not matching_containers :
192+ pytest .skip (f"No Docker container found for { self .config .display_name } " )
193+
194+ container = matching_containers [0 ]
195+ if not container .connectable :
196+ pytest .skip (f"Container { container .container_name } is not connectable" )
197+
198+ # Convert to ConnectionConfig (this is what the UI does)
199+ config = container_to_connection_config (container )
200+
201+ # Get the adapter and try to connect
202+ adapter = get_adapter (config .db_type )
203+
204+ try :
205+ conn = adapter .connect (config )
206+ except Exception as e :
207+ pytest .fail (
208+ f"Failed to connect using docker-discovered credentials:\n "
209+ f" Container: { container .container_name } \n "
210+ f" Host: { config .server } \n "
211+ f" Port: { config .port } \n "
212+ f" Username: { config .username } \n "
213+ f" Password: { '***' if config .password else 'None' } \n "
214+ f" Database: { config .database } \n "
215+ f" Error: { e } "
216+ )
217+
218+ # Run a simple query to verify connection works
219+ try :
220+ adapter .execute_test_query (conn )
221+ except Exception as e :
222+ pytest .fail (
223+ f"Connected but failed to execute query: { e } "
224+ )
225+ finally :
226+ try :
227+ conn .close ()
228+ except Exception :
229+ pass
230+
54231 def test_query_select (self , request , cli_runner ):
55232 """Test executing SELECT query."""
56233 connection = request .getfixturevalue (self .config .connection_fixture )
@@ -520,6 +697,133 @@ def test_get_sequences(self, request):
520697 f"Found sequences: { [seq .name for seq in sequences ]} "
521698 )
522699
700+ def test_get_trigger_definition (self , request ):
701+ """Test that adapter correctly retrieves trigger definitions.
702+
703+ This tests the code path used when a user clicks on a trigger
704+ in the TUI tree view to see its details.
705+ """
706+ from sqlit .config import load_connections
707+ from sqlit .db .adapters import get_adapter
708+ from sqlit .services .session import ConnectionSession
709+
710+ connection_name = request .getfixturevalue (self .config .connection_fixture )
711+ connections = load_connections ()
712+ config = next ((c for c in connections if c .name == connection_name ), None )
713+ assert config is not None , f"Connection { connection_name } not found"
714+
715+ with ConnectionSession .create (config , get_adapter ) as session :
716+ if not session .adapter .supports_triggers :
717+ pytest .skip (f"{ self .config .display_name } does not support triggers" )
718+
719+ # First get triggers to find one to look up
720+ triggers = session .adapter .get_triggers (
721+ session .connection ,
722+ database = config .database if session .adapter .supports_multiple_databases else None ,
723+ )
724+
725+ test_trigger = next (
726+ (trg for trg in triggers if "test_users_audit" in trg .name .lower ()),
727+ None ,
728+ )
729+ if test_trigger is None :
730+ pytest .skip ("Test trigger not found" )
731+
732+ # Now get the definition (simulates user clicking on trigger in TUI)
733+ info = session .adapter .get_trigger_definition (
734+ session .connection ,
735+ test_trigger .name ,
736+ test_trigger .table_name ,
737+ database = config .database if session .adapter .supports_multiple_databases else None ,
738+ )
739+
740+ assert isinstance (info , dict ), "get_trigger_definition should return a dict"
741+ assert "name" in info , "Trigger info should contain 'name'"
742+
743+ def test_get_sequence_definition (self , request ):
744+ """Test that adapter correctly retrieves sequence definitions.
745+
746+ This tests the code path used when a user clicks on a sequence
747+ in the TUI tree view to see its details.
748+ """
749+ from sqlit .config import load_connections
750+ from sqlit .db .adapters import get_adapter
751+ from sqlit .services .session import ConnectionSession
752+
753+ connection_name = request .getfixturevalue (self .config .connection_fixture )
754+ connections = load_connections ()
755+ config = next ((c for c in connections if c .name == connection_name ), None )
756+ assert config is not None , f"Connection { connection_name } not found"
757+
758+ with ConnectionSession .create (config , get_adapter ) as session :
759+ if not session .adapter .supports_sequences :
760+ pytest .skip (f"{ self .config .display_name } does not support sequences" )
761+
762+ # First get sequences to find one to look up
763+ sequences = session .adapter .get_sequences (
764+ session .connection ,
765+ database = config .database if session .adapter .supports_multiple_databases else None ,
766+ )
767+
768+ test_sequence = next (
769+ (seq for seq in sequences if "test_sequence" in seq .name .lower ()),
770+ None ,
771+ )
772+ if test_sequence is None :
773+ pytest .skip ("Test sequence not found" )
774+
775+ # Now get the definition (simulates user clicking on sequence in TUI)
776+ info = session .adapter .get_sequence_definition (
777+ session .connection ,
778+ test_sequence .name ,
779+ database = config .database if session .adapter .supports_multiple_databases else None ,
780+ )
781+
782+ assert isinstance (info , dict ), "get_sequence_definition should return a dict"
783+ assert "name" in info , "Sequence info should contain 'name'"
784+
785+ def test_get_index_definition (self , request ):
786+ """Test that adapter correctly retrieves index definitions.
787+
788+ This tests the code path used when a user clicks on an index
789+ in the TUI tree view to see its details.
790+ """
791+ from sqlit .config import load_connections
792+ from sqlit .db .adapters import get_adapter
793+ from sqlit .services .session import ConnectionSession
794+
795+ connection_name = request .getfixturevalue (self .config .connection_fixture )
796+ connections = load_connections ()
797+ config = next ((c for c in connections if c .name == connection_name ), None )
798+ assert config is not None , f"Connection { connection_name } not found"
799+
800+ with ConnectionSession .create (config , get_adapter ) as session :
801+ if not session .adapter .supports_indexes :
802+ pytest .skip (f"{ self .config .display_name } does not support indexes" )
803+
804+ # First get indexes to find one to look up
805+ indexes = session .adapter .get_indexes (
806+ session .connection ,
807+ database = config .database if session .adapter .supports_multiple_databases else None ,
808+ )
809+
810+ test_index = next (
811+ (idx for idx in indexes if "test_users_email" in idx .name .lower ()),
812+ None ,
813+ )
814+ if test_index is None :
815+ pytest .skip ("Test index not found" )
816+
817+ # Now get the definition (simulates user clicking on index in TUI)
818+ info = session .adapter .get_index_definition (
819+ session .connection ,
820+ test_index .name ,
821+ test_index .table_name ,
822+ database = config .database if session .adapter .supports_multiple_databases else None ,
823+ )
824+
825+ assert isinstance (info , dict ), "get_index_definition should return a dict"
826+ assert "name" in info , "Index info should contain 'name'"
523827
524828class BaseDatabaseTestsWithLimit (BaseDatabaseTests ):
525829 """Base tests for databases that support LIMIT syntax."""
0 commit comments