use super::*; use crate::db::pool::{ConnectionPoolManager, DbHandle}; /// Connect directly via tokio-postgres (no tunnel) so tests are Tauri-free. async fn pool() -> (tokio::sync::Mutex, String) { let h = std::env::var("GRIDLINE_TEST_PG_HOST").expect("set GRIDLINE_TEST_PG_HOST"); let p: u16 = std::env::var("GRIDLINE_TEST_PG_PORT").unwrap_or_else(|_| "5432".into()).parse().unwrap(); let u = std::env::var("GRIDLINE_TEST_PG_USER").expect("set GRIDLINE_TEST_PG_USER"); let d = std::env::var("GRIDLINE_TEST_PG_DB").expect("set GRIDLINE_TEST_PG_DB"); let pw = std::env::var("GRIDLINE_TEST_PG_PASSWORD").unwrap_or_default(); let (client, conn) = tokio_postgres::connect( &format!("host={h} port={p} user={u} dbname={d} password={pw}"), tokio_postgres::NoTls, ).await.expect("connect to test PG"); let handle = tokio::spawn(async move { let _ = conn.await; }); let mut pm = ConnectionPoolManager::new(); let id = "test-conn".to_string(); pm.register(&id, DbHandle::Postgresql(client, handle)); (tokio::sync::Mutex::new(pm), id) } #[tokio::test] #[ignore] async fn schema_crud_create_rename_drop() { let (pm, id) = pool().await; let name = "gridline_test_schema"; create_schema_inner(&pm, &id, name).await.unwrap(); assert!(create_schema_inner(&pm, &id, name).await.is_err(), "duplicate should error"); rename_schema_inner(&pm, &id, name, "gridline_test_schema2").await.unwrap(); drop_schema_inner(&pm, &id, "gridline_test_schema2", false).await.unwrap(); } #[tokio::test] #[ignore] async fn search_objects_finds_table_and_function() { let (pm, id) = pool().await; let hits = search_objects_inner(&pm, &id, "public", "users").await.unwrap(); assert!(hits.iter().any(|h| h.name == "users" && h.object_type == "TABLE"), "demo has a users table"); let fns = search_objects_inner(&pm, &id, "public", "get").await.unwrap(); // substring match across types; assert it returns a Vec assert!(fns.iter().all(|h| h.object_type != "")); // empty needle returns nothing matched by position('' in name) > 0 is always true — so empty returns all (capped at 100) let all = search_objects_inner(&pm, &id, "public", "").await.unwrap(); assert!(all.len() <= 100); } #[tokio::test] #[ignore] async fn object_ddl_for_sequence_enum_function() { let (pm, id) = pool().await; // demo has users_id_seq, an enum, and a function let seq = get_object_ddl_inner(&pm, &id, "public", "sequence", "users_id_seq").await.unwrap(); assert!(seq.starts_with("CREATE SEQUENCE"), "{seq}"); // function: pg_get_functiondef passthrough let f = get_object_ddl_inner(&pm, &id, "public", "function", "audit_log").await; // name per demo assert!(f.is_ok()); assert!(f.clone().unwrap().contains("CREATE FUNCTION") || f.unwrap().contains("CREATE OR REPLACE FUNCTION")); } #[tokio::test] async fn build_object_ddl_inner_guards_missing_connection_and_rejects_unknown_op() { let pm = tokio::sync::Mutex::new(ConnectionPoolManager::new()); // Missing connection -> Connection not found let err = build_object_ddl_inner(&pm, "missing", "sequence", serde_json::json!({ "schema": "public", "name": "s", "action": { "op": "drop" } })).await.unwrap_err(); assert!(err.contains("Connection not found"), "{err}"); // SQLite now dispatches to the SQLite table builders; non-table ops are rejected. pm.lock().await.register("sqlite", DbHandle::Sqlite(rusqlite::Connection::open_in_memory().unwrap())); let err = build_object_ddl_inner(&pm, "sqlite", "sequence", serde_json::json!({ "schema": "public", "name": "s", "action": { "op": "drop" } })).await.unwrap_err(); assert!(err.contains("unknown op"), "{err}"); } /// In-memory SQLite pool registered under id "c" (no Tauri, no live PG). fn fresh_pool_with_sqlite() -> ConnectionPoolManager { let mut pm = ConnectionPoolManager::new(); pm.register("c", DbHandle::Sqlite(rusqlite::Connection::open_in_memory().unwrap())); pm } #[tokio::test] async fn build_object_ddl_sqlite_create_yields_sqlite_sql() { let pm = fresh_pool_with_sqlite(); let params = serde_json::json!({ "schema": "main", "name": "users", "action": { "op": "create", "columns": [ { "name": "id", "type": "integer", "nullable": false, "default": null, "is_pk": true, "auto_increment": true, "unique": false }, { "name": "name", "type": "text", "nullable": true, "default": null, "is_pk": false, "auto_increment": false, "unique": false } ] } }); let sqls = build_object_ddl_inner(&tokio::sync::Mutex::new(pm), "c", "table", params).await.unwrap(); assert!(sqls.iter().any(|s| s.contains("INTEGER PRIMARY KEY AUTOINCREMENT")), "{sqls:?}"); assert!(sqls.iter().all(|s| !s.contains("serial")), "{sqls:?}"); } #[tokio::test] async fn execute_change_sqlite_ddl_runs_create() { let pm = tokio::sync::Mutex::new(fresh_pool_with_sqlite()); let change = crate::models::db_viewer::Change::Ddl { id: "x".into(), sql: "CREATE TABLE u(id INTEGER)".into() }; let r = crate::commands::db_viewer::execute_change_inner(&pm, "c", change).await; assert!(r.is_ok(), "{r:?}"); // the table really landed on the live connection { let mut g = pm.lock().await; match g.get("c").unwrap() { crate::db::pool::DbHandle::Sqlite(conn) => { let n: i64 = conn .query_row("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='u'", [], |row| row.get(0)) .unwrap(); assert_eq!(n, 1); } _ => panic!("expected a sqlite handle"), } } } #[tokio::test] #[ignore] async fn build_object_ddl_inner_on_postgresql_pool() { let (pm, id) = pool().await; let sql = build_object_ddl_inner(&pm, &id, "sequence", serde_json::json!({ "schema": "public", "name": "s", "action": { "op": "drop" } })).await.unwrap(); assert_eq!(sql, vec!["DROP SEQUENCE \"public\".\"s\""]); } #[tokio::test] async fn get_available_extensions_inner_guards_postgresql_only() { let pm = tokio::sync::Mutex::new(ConnectionPoolManager::new()); // Missing connection -> Connection not found let err = get_available_extensions_inner(&pm, "missing").await.unwrap_err(); assert!(err.contains("Connection not found"), "{err}"); // Non-PostgreSQL handle -> PostgreSQL-only error pm.lock().await.register("sqlite", DbHandle::Sqlite(rusqlite::Connection::open_in_memory().unwrap())); let err = get_available_extensions_inner(&pm, "sqlite").await.unwrap_err(); assert!(err.contains("PostgreSQL-only"), "{err}"); } #[tokio::test] #[ignore] async fn get_available_extensions_on_postgresql_pool() { let (pm, id) = pool().await; let exts = get_available_extensions_inner(&pm, &id).await.unwrap(); assert!(!exts.is_empty(), "pg_available_extensions should list built-ins"); assert!(exts.iter().all(|e| !e.name.is_empty() && !e.version.is_empty()), "every extension needs name + default version: {exts:?}"); } #[tokio::test] #[ignore] async fn object_dependencies_for_table_includes_view() { let (pm, id) = pool().await; // demo has order_summary VIEW depending on orders — drop would break it let deps = get_object_dependencies_inner(&pm, &id, "public", "table", "orders").await.unwrap(); assert!(deps.iter().any(|d| d.class.contains("pg_class") && d.name.contains("order_summary")), "view depending on orders should surface: {deps:?}"); // schema contents path let contents = get_object_dependencies_inner(&pm, &id, "public", "schema", "public").await.unwrap(); assert!(!contents.is_empty(), "public schema should list contents"); } #[tokio::test] #[ignore] async fn get_roles_returns_current_role_and_memberships() { let (pm, id) = pool().await; let roles = crate::commands::objects::get_roles_inner(&pm, &id).await.unwrap(); assert!(roles.iter().any(|r| r.can_login), "at least one login role (the test user)"); } #[tokio::test] #[ignore] async fn rebuild_table_rolls_back_on_failure() { let (pm, id) = pool().await; // setup: a table with a PK + one row { let mut g = pm.lock().await; if let crate::db::pool::DbHandle::Postgresql(c, _) = g.get(&id).unwrap() { c.batch_execute("DROP TABLE IF EXISTS rebuild_t; CREATE TABLE rebuild_t (id int PRIMARY KEY, v text); INSERT INTO rebuild_t VALUES (1,'a');").await.unwrap(); } } // build a rebuild script whose final statement intentionally fails (syntax error) // so the whole transaction rolls back and rebuild_t keeps its row. let bad_script = "CREATE TABLE _gridline_rb_rebuild_t (id int PRIMARY KEY, v text); \ INSERT INTO _gridline_rb_rebuild_t (id, v) SELECT id, v FROM rebuild_t; \ DROP TABLE rebuild_t; \ ALTER TABLE _gridline_rb_rebuild_t RENAME TO rebuild_t; \ THIS IS NOT SQL;"; let change = crate::models::db_viewer::Change::RebuildTable { id: "rb".into(), sql: bad_script.to_string() }; let res = crate::commands::db_viewer::execute_change_inner(&pm, &id, change).await; assert!(res.is_err(), "expected rollback (transaction should fail on bad SQL)"); // table still intact let mut g = pm.lock().await; if let crate::db::pool::DbHandle::Postgresql(c, _) = g.get(&id).unwrap() { let row = c.query_one("SELECT count(*) FROM rebuild_t", &[]).await.unwrap(); assert_eq!(row.get::<_, i64>(0), 1, "rollback must preserve the original table"); c.batch_execute("DROP TABLE rebuild_t").await.unwrap(); } } #[tokio::test] #[ignore = "requires live PG"] async fn get_role_privileges_returns_grants_across_object_classes() { let (pm, id) = pool().await; let out = crate::commands::objects::get_role_privileges_inner(&pm, &id, "read_only").await .unwrap_or_else(|e| panic!("privileges command failed: {e}")); let classes: std::collections::HashSet = out.iter().map(|p| p.object_class.clone()).collect(); assert!(classes.contains("table"), "expected a table grant: {out:?}"); assert!(classes.contains("routine"), "expected a routine grant: {out:?}"); assert!(classes.contains("schema"), "expected a schema grant: {out:?}"); assert!(classes.contains("database"), "expected a database grant: {out:?}"); }