use super::*; use crate::message::Message; use crate::{EventType, test_utils::TestContext}; #[test] fn test_maybe_add_file() { let mut files = Default::default(); maybe_add_file(&mut files, "$BLOBDIR/hello"); maybe_add_file(&mut files, "$BLOBDIR/world.txt"); maybe_add_file(&mut files, "world2.txt"); maybe_add_file(&mut files, "$BLOBDIR"); assert!(files.contains("hello")); assert!(files.contains("world.txt")); assert!(!files.contains("world2.txt")); assert!(!files.contains("$BLOBDIR")); } #[test] fn test_is_file_in_use() { let mut files = Default::default(); maybe_add_file(&mut files, "$BLOBDIR/hello"); maybe_add_file(&mut files, "$BLOBDIR/world.txt"); maybe_add_file(&mut files, "world2.txt"); assert!(is_file_in_use(&files, None, "hello")); assert!(!is_file_in_use(&files, Some(".txt"), "hello")); assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix")); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_table_exists() { let t = TestContext::new().await; assert!(t.ctx.sql.table_exists("msgs").await.unwrap()); assert!(!t.ctx.sql.table_exists("foobar").await.unwrap()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_col_exists() { let t = TestContext::new().await; assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap()); assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap()); assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap()); } /// Tests that auto_vacuum is enabled for new databases. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_auto_vacuum() -> Result<()> { let t = TestContext::new().await; let query_only = true; let auto_vacuum = t .sql .call(query_only, |conn| { let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| { let auto_vacuum: i32 = row.get(0)?; Ok(auto_vacuum) })?; Ok(auto_vacuum) }) .await?; // auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL assert_eq!(auto_vacuum, 2); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_housekeeping_db_closed() { let t = TestContext::new().await; let avatar_src = t.dir.path().join("avatar.png"); let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap(); t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) .await .unwrap(); let event_source = t.get_event_emitter(); let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]); t.sql.close().await; housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail t.sql.open(&t, "".to_string()).await.unwrap(); let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]); while let Ok(event) = event_source.try_recv() { match event.typ { EventType::Info(s) => assert!( !s.contains("Keeping new unreferenced file"), "File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)" ), EventType::Error(s) => panic!("{}", s), _ => {} } } } /// Regression test for a bug where housekeeping deleted drafts since their /// `hidden` flag is set. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_housekeeping_dont_delete_drafts() { let t = TestContext::new_alice().await; let chat = t.create_chat_with_contact("bob", "bob@example.com").await; let mut new_draft = Message::new_text("This is my draft".to_string()); chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap(); housekeeping(&t).await.unwrap(); let loaded_draft = chat.id.get_draft(&t).await.unwrap(); assert_eq!(loaded_draft.unwrap().text, "This is my draft"); } /// Tests that `housekeeping` deletes the blobs backup dir which is created normally by /// `imex::import_backup`. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_housekeeping_delete_blobs_backup_dir() { let t = TestContext::new_alice().await; let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME); tokio::fs::create_dir(&dir).await.unwrap(); tokio::fs::write(dir.join("f"), "").await.unwrap(); housekeeping(&t).await.unwrap(); tokio::fs::create_dir(&dir).await.unwrap(); } /// Regression test. /// /// Previously the code checking for existence of `config` table /// checked it with `PRAGMA table_info("config")` but did not /// drain `SqlitePool.fetch` result, only using the first row /// returned. As a result, prepared statement for `PRAGMA` was not /// finalized early enough, leaving reader connection in a broken /// state after reopening the database, when `config` table /// existed and `PRAGMA` returned non-empty result. /// /// Statements were not finalized due to a bug in sqlx: /// #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_db_reopen() -> Result<()> { use tempfile::tempdir; // The context is used only for logging. let t = TestContext::new().await; // Create a separate empty database for testing. let dir = tempdir()?; let dbfile = dir.path().join("testdb.sqlite"); let sql = Sql::new(dbfile); // Create database with all the tables. sql.open(&t, "".to_string()).await.unwrap(); sql.close().await; // Reopen the database sql.open(&t, "".to_string()).await?; sql.execute( "INSERT INTO config (keyname, value) VALUES (?, ?);", ("foo", "bar"), ) .await?; let value: Option = sql .query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",)) .await?; assert_eq!(value.unwrap(), "bar"); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_migration_flags() -> Result<()> { let t = TestContext::new().await; t.evtracker.get_info_contains("Opened database").await; // as migrations::run() was already executed on context creation, // another call should not result in any action needed. // this test catches some bugs where dbversion was forgotten to be persisted. let recode_avatar = migrations::run(&t, &t.sql).await?; assert!(!recode_avatar); info!(&t, "test_migration_flags: XXX END MARKER"); loop { let evt = t .evtracker .get_matching(|evt| matches!(evt, EventType::Info(_))) .await; match evt { EventType::Info(msg) => { assert!( !msg.contains("[migration]"), "Migrations were run twice, you probably forgot to update the db version" ); if msg.contains("test_migration_flags: XXX END MARKER") { break; } } _ => unreachable!(), } } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_check_passphrase() -> Result<()> { use tempfile::tempdir; // The context is used only for logging. let t = TestContext::new().await; // Create a separate empty database for testing. let dir = tempdir()?; let dbfile = dir.path().join("testdb.sqlite"); let sql = Sql::new(dbfile.clone()); sql.check_passphrase("foo".to_string()).await?; sql.open(&t, "foo".to_string()) .await .context("failed to open the database first time")?; sql.close().await; // Reopen the database let sql = Sql::new(dbfile); // Test that we can't open encrypted database without a passphrase. assert!(sql.open(&t, "".to_string()).await.is_err()); // Now open the database with passpharse, it should succeed. sql.check_passphrase("foo".to_string()).await?; sql.open(&t, "foo".to_string()) .await .context("failed to open the database second time")?; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sql_change_passphrase() -> Result<()> { use tempfile::tempdir; // The context is used only for logging. let t = TestContext::new().await; // Create a separate empty database for testing. let dir = tempdir()?; let dbfile = dir.path().join("testdb.sqlite"); let sql = Sql::new(dbfile.clone()); sql.open(&t, "foo".to_string()) .await .context("failed to open the database first time")?; sql.close().await; // Change the passphrase from "foo" to "bar". let sql = Sql::new(dbfile.clone()); sql.open(&t, "foo".to_string()) .await .context("failed to open the database second time")?; sql.change_passphrase("bar".to_string()) .await .context("failed to change passphrase")?; // Test that at least two connections are still working. // This ensures that not only the connection which changed the password is working, // but other connections as well. { let lock = sql.pool.read().await; let pool = lock.as_ref().unwrap(); let query_only = true; let conn1 = pool.get(query_only).await?; let conn2 = pool.get(query_only).await?; conn1 .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) .unwrap(); conn2 .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) .unwrap(); } sql.close().await; let sql = Sql::new(dbfile); // Test that old passphrase is not working. assert!(sql.open(&t, "foo".to_string()).await.is_err()); // Open the database with the new passphrase. sql.check_passphrase("bar".to_string()).await?; sql.open(&t, "bar".to_string()) .await .context("failed to open the database third time")?; sql.close().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_query_only() -> Result<()> { let t = TestContext::new().await; // `query_row` does not acquire write lock // and operates on read-only connection. // Using it to `INSERT` should fail. let res = t .sql .query_row( "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1", ("xyz", "ijk"), |row| { let res: u32 = row.get(0)?; Ok(res) }, ) .await; assert!(res.is_err()); // If you want to `INSERT` and get value via `RETURNING`, // use `call_write` or `transaction`. let res: Result = t .sql .call_write(|conn| { let val = conn.query_row( "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2", ("foo", "bar"), |row| { let res: u32 = row.get(0)?; Ok(res) }, )?; Ok(val) }) .await; assert_eq!(res.unwrap(), 2); let res = t .sql .transaction(|t| { let val = t.query_row( "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3", ("abc", "def"), |row| { let res: u32 = row.get(0)?; Ok(res) }, )?; Ok(val) }) .await; assert_eq!(res.unwrap(), 3); Ok(()) } /// Tests that incremental_vacuum does not fail. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_incremental_vacuum() -> Result<()> { let t = TestContext::new().await; incremental_vacuum(&t).await?; Ok(()) }