"""Tests for the db_query tool.""" from __future__ import annotations import sqlite3 from openjarvis.tools.db_query import DatabaseQueryTool class TestDatabaseQueryTool: """Tests for DatabaseQueryTool.""" def test_spec(self): tool = DatabaseQueryTool() assert tool.spec.name == "db_query" assert tool.spec.category == "database" assert tool.spec.timeout_seconds == 30.0 assert "code:execute" in tool.spec.required_capabilities assert "query" in tool.spec.parameters["required"] def test_no_query(self): tool = DatabaseQueryTool() result = tool.execute(query="") assert result.success is False assert "No query" in result.content def test_simple_select_in_memory(self): """SELECT on in-memory database using a literal values query.""" tool = DatabaseQueryTool() result = tool.execute(query="SELECT 1 AS value, 'hello' AS greeting") assert result.success is True assert "value" in result.content assert "greeting" in result.content assert "1" in result.content assert "hello" in result.content assert result.metadata["db_type"] == "sqlite" assert result.metadata["row_count"] == 1 assert result.metadata["column_names"] == ["value", "greeting"] def test_read_only_blocks_insert(self): tool = DatabaseQueryTool() result = tool.execute( query="INSERT INTO users VALUES (1, 'Alice')", ) assert result.success is False lower = result.content.lower() assert "blocked" in lower or "read-only" in lower def test_read_only_blocks_delete(self): tool = DatabaseQueryTool() result = tool.execute(query="DELETE FROM users WHERE id=1") assert result.success is False lower = result.content.lower() assert "blocked" in lower or "read-only" in lower def test_read_only_blocks_drop(self): tool = DatabaseQueryTool() result = tool.execute(query="DROP TABLE users") assert result.success is False lower = result.content.lower() assert "blocked" in lower or "read-only" in lower def test_read_only_blocks_update(self): tool = DatabaseQueryTool() result = tool.execute(query="UPDATE users SET name='Bob' WHERE id=1") assert result.success is False def test_read_only_blocks_alter(self): tool = DatabaseQueryTool() result = tool.execute(query="ALTER TABLE users ADD COLUMN age INTEGER") assert result.success is False def test_read_only_blocks_create(self): tool = DatabaseQueryTool() result = tool.execute(query="CREATE TABLE evil (id INTEGER)") assert result.success is False def test_read_only_blocks_truncate(self): tool = DatabaseQueryTool() result = tool.execute(query="TRUNCATE TABLE users") assert result.success is False def test_pragma_allowed(self): tool = DatabaseQueryTool() result = tool.execute(query="PRAGMA table_info('sqlite_master')") assert result.success is True def test_explain_allowed(self): tool = DatabaseQueryTool() result = tool.execute(query="EXPLAIN SELECT 1") assert result.success is True def test_max_rows_limit(self, tmp_path): """Create a table with many rows and verify max_rows is honored.""" db_file = tmp_path / "test.db" conn = sqlite3.connect(str(db_file)) conn.execute("CREATE TABLE numbers (n INTEGER)") for i in range(50): conn.execute("INSERT INTO numbers VALUES (?)", (i,)) conn.commit() conn.close() tool = DatabaseQueryTool() result = tool.execute( query="SELECT n FROM numbers", db_path=str(db_file), max_rows=10, read_only=True, ) assert result.success is True assert result.metadata["row_count"] == 10 def test_db_path_to_file(self, tmp_path): """Test querying a real SQLite file.""" db_file = tmp_path / "mydata.db" conn = sqlite3.connect(str(db_file)) conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)") conn.execute("INSERT INTO items VALUES (1, 'apple')") conn.execute("INSERT INTO items VALUES (2, 'banana')") conn.commit() conn.close() tool = DatabaseQueryTool() result = tool.execute( query="SELECT * FROM items ORDER BY id", db_path=str(db_file), ) assert result.success is True assert "apple" in result.content assert "banana" in result.content assert result.metadata["row_count"] == 2 assert result.metadata["column_names"] == ["id", "name"] assert result.metadata["db_type"] == "sqlite" def test_blocks_sensitive_db_paths(self, tmp_path): """Sensitive file patterns (e.g. .env) should be blocked.""" f = tmp_path / ".env" f.write_text("SECRET=foo", encoding="utf-8") tool = DatabaseQueryTool() result = tool.execute( query="SELECT 1", db_path=str(f), ) assert result.success is False assert "sensitive" in result.content.lower() def test_blocks_pem_db_path(self, tmp_path): """Sensitive file patterns (.pem) should be blocked.""" f = tmp_path / "server.pem" f.write_text("data", encoding="utf-8") tool = DatabaseQueryTool() result = tool.execute( query="SELECT 1", db_path=str(f), ) assert result.success is False assert "sensitive" in result.content.lower() def test_with_select_allowed(self): """WITH ... SELECT (CTE) should be allowed in read-only mode.""" tool = DatabaseQueryTool() result = tool.execute( query="WITH cte AS (SELECT 1 AS x) SELECT x FROM cte", ) assert result.success is True assert "x" in result.content assert "1" in result.content def test_with_insert_blocked(self): """WITH ... INSERT should be blocked in read-only mode.""" tool = DatabaseQueryTool() result = tool.execute( query="WITH cte AS (SELECT 1) INSERT INTO t SELECT * FROM cte", ) assert result.success is False def test_format_output_has_column_headers(self): """Verify pipe-delimited table format with column headers.""" tool = DatabaseQueryTool() result = tool.execute( query="SELECT 42 AS answer, 'test' AS label", ) assert result.success is True lines = result.content.strip().split("\n") # First line: column headers assert "answer" in lines[0] assert "label" in lines[0] # Second line: separator assert "-" in lines[1] # Third line: data row assert "42" in lines[2] assert "test" in lines[2] def test_postgresql_url_without_psycopg2_gives_helpful_error(self): """When db_url is provided but psycopg2 is not installed, the tool should return a helpful error message.""" tool = DatabaseQueryTool() result = tool.execute( query="SELECT 1", db_url="postgresql://user:pass@localhost/testdb", ) # psycopg2 is not installed in the test environment # so we expect a helpful error assert result.success is False assert "psycopg2" in result.content assert "pip install" in result.content def test_read_only_false_allows_write(self, tmp_path): """When read_only=False, write queries should be allowed.""" db_file = tmp_path / "writable.db" conn = sqlite3.connect(str(db_file)) conn.execute("CREATE TABLE data (id INTEGER)") conn.commit() conn.close() tool = DatabaseQueryTool() result = tool.execute( query="INSERT INTO data VALUES (42)", db_path=str(db_file), read_only=False, ) assert result.success is True # Verify the insert actually worked result2 = tool.execute( query="SELECT id FROM data", db_path=str(db_file), ) assert result2.success is True assert "42" in result2.content def test_sql_error_returns_failure(self): """Invalid SQL should return a failure result, not raise.""" tool = DatabaseQueryTool() result = tool.execute(query="SELECT * FROM nonexistent_table_xyz") assert result.success is False assert "error" in result.content.lower() def test_nonexistent_db_file(self): """Opening a non-existent file in read-only mode should fail.""" tool = DatabaseQueryTool() result = tool.execute( query="SELECT 1", db_path="/tmp/this_does_not_exist_12345.db", read_only=True, ) assert result.success is False def test_tool_id(self): tool = DatabaseQueryTool() assert tool.tool_id == "db_query" def test_openai_function_format(self): tool = DatabaseQueryTool() fn = tool.to_openai_function() assert fn["function"]["name"] == "db_query" assert "query" in fn["function"]["parameters"]["properties"] assert "db_path" in fn["function"]["parameters"]["properties"] assert "db_url" in fn["function"]["parameters"]["properties"] assert "read_only" in fn["function"]["parameters"]["properties"] assert "max_rows" in fn["function"]["parameters"]["properties"] def test_multiple_columns_alignment(self, tmp_path): """Verify the pipe-delimited format aligns columns properly.""" db_file = tmp_path / "align.db" conn = sqlite3.connect(str(db_file)) conn.execute("CREATE TABLE t (short TEXT, longer_column TEXT)") conn.execute("INSERT INTO t VALUES ('a', 'xyz')") conn.commit() conn.close() tool = DatabaseQueryTool() result = tool.execute( query="SELECT * FROM t", db_path=str(db_file), ) assert result.success is True # Check pipe delimiters are present assert "|" in result.content assert "short" in result.content assert "longer_column" in result.content