use liteboxfs::{
Connection, CreateOptions, FileKind, JournalMode, OpenOptions, Owner, TransactionBehavior,
};
use std::{fs, io::Write};
use xpct::{be_err, be_ok, equal, expect, match_pattern, pattern};
#[test]
fn set_journal_mode_round_trip() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let mut conn =
Connection::create_new(temp_dir.path().join("test.litebox"), &CreateOptions::new())?;
expect!(conn.journal_mode())
.to(be_ok())
.to(equal(JournalMode::Delete));
expect!(conn.set_journal_mode(JournalMode::Wal)).to(be_ok());
expect!(conn.journal_mode())
.to(be_ok())
.to(equal(JournalMode::Wal));
Ok(())
}
#[test]
fn exec_with() -> liteboxfs::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
expect!(conn.exec_with(
TransactionBehavior::Exclusive,
|_| liteboxfs::Result::Ok(())
))
.to(be_ok());
Ok(())
}
#[test]
fn tx_with() -> liteboxfs::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
expect!(conn.tx_with(TransactionBehavior::Exclusive)).to(be_ok());
Ok(())
}
#[test]
fn set_block_size_out_of_bounds_does_not_error() {
let opts = CreateOptions::new().block_size(1024);
expect!(Connection::open_in_memory(&opts)).to(be_ok());
let opts = CreateOptions::new().block_size(1024usize.pow(2));
expect!(Connection::open_in_memory(&opts)).to(be_ok());
}
#[test]
fn invalid_format_version() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
Connection::create_new(&path, &CreateOptions::new()).unwrap();
{
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute(
"UPDATE liteboxfs_settings SET value = '9999' WHERE key = 'version'",
[],
)
.unwrap();
}
expect!(Connection::open(&path, &OpenOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(
liteboxfs::Error::UnsupportedFormatVersion
)));
}
#[test]
fn cannot_open_if_file_does_not_exist() {
expect!(Connection::open("nonexistent.litebox", &OpenOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::CannotOpen)));
}
#[test]
fn cannot_open_if_file_is_not_a_sqlite_db() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.txt");
let mut file = fs::File::create_new(&path).unwrap();
file.write_all(b"not a database").unwrap();
file.flush().unwrap();
expect!(Connection::open(&path, &OpenOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
}
#[test]
fn cannot_open_if_sqlite_db_is_not_a_litebox() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.sqlite");
rusqlite::Connection::open(temp_dir.path().join(&path)).unwrap();
expect!(Connection::open(
temp_dir.path().join(&path),
&OpenOptions::new()
))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotALitebox)));
}
#[test]
fn multiple_connections_to_same_litebox_can_coexist() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
Connection::create_new(&path, &CreateOptions::new()).unwrap();
let first = expect!(Connection::open(&path, &OpenOptions::new()))
.to(be_ok())
.into_inner();
let second = expect!(Connection::open(&path, &OpenOptions::new())).to(be_ok());
drop(first);
drop(second);
expect!(Connection::open(&path, &OpenOptions::new())).to(be_ok());
}
#[test]
fn can_create_or_open_if_file_does_not_exist() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
expect!(Connection::create(&path, &CreateOptions::new())).to(be_ok());
}
#[test]
fn can_create_or_open_if_file_exists_and_is_a_sqlite_db() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.sqlite");
rusqlite::Connection::open(temp_dir.path().join(&path)).unwrap();
expect!(Connection::create(&path, &CreateOptions::new())).to(be_ok());
}
#[test]
fn can_create_or_open_if_file_is_a_litebox() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
expect!(Connection::create_new(&path, &CreateOptions::new())).to(be_ok());
expect!(Connection::create(&path, &CreateOptions::new())).to(be_ok());
}
#[test]
fn cannot_create_or_open_if_file_is_not_a_sqlite_db() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.txt");
let mut file = fs::File::create_new(&path).unwrap();
file.write_all(b"not a database").unwrap();
file.flush().unwrap();
expect!(Connection::create(&path, &CreateOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
}
#[test]
fn cannot_create_new_if_file_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.txt");
let mut file = fs::File::create_new(&path).unwrap();
file.write_all(b"not a database").unwrap();
file.flush().unwrap();
expect!(Connection::create_new(&path, &CreateOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
}
#[test]
fn cannot_create_new_if_litebox_already_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
expect!(Connection::create_new(&path, &CreateOptions::new())).to(be_ok());
expect!(Connection::create_new(&path, &CreateOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(
liteboxfs::Error::LiteboxAlreadyExists
)));
}
#[test]
fn can_open_readonly() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let opts = OpenOptions::new().readonly(true);
expect!(Connection::create_new(&path, &CreateOptions::new())).to(be_ok());
expect!(Connection::open(&path, &opts)).to(be_ok());
}
#[test]
fn cannot_open_readonly_if_file_does_not_exist() {
let opts = OpenOptions::new().readonly(true);
expect!(Connection::open("nonexistent.litebox", &opts))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::CannotOpen)));
}
#[test]
fn cannot_open_readonly_if_file_is_not_a_sqlite_db() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.txt");
let mut file = fs::File::create_new(&path).unwrap();
file.write_all(b"not a database").unwrap();
file.flush().unwrap();
let opts = OpenOptions::new().readonly(true);
expect!(Connection::open(&path, &opts))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
}
#[test]
fn cannot_open_readonly_if_sqlite_db_is_not_a_litebox() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.sqlite");
rusqlite::Connection::open(temp_dir.path().join(&path)).unwrap();
let opts = OpenOptions::new().readonly(true);
expect!(Connection::open(temp_dir.path().join(&path), &opts))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotALitebox)));
}
#[test]
fn create_new_sets_application_id_by_default() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
expect!(Connection::create_new(&path, &CreateOptions::new())).to(be_ok());
let raw = rusqlite::Connection::open(&path).unwrap();
let application_id: i32 = raw
.pragma_query_value(None, "application_id", |row| row.get(0))
.unwrap();
expect!(application_id).to(equal(0x6c626f78i32));
}
#[test]
fn skip_application_id_does_not_set_application_id() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let opts = CreateOptions::new().skip_application_id(true);
expect!(Connection::create_new(&path, &opts)).to(be_ok());
let raw = rusqlite::Connection::open(&path).unwrap();
let application_id: i32 = raw
.pragma_query_value(None, "application_id", |row| row.get(0))
.unwrap();
expect!(application_id).to(equal(0i32));
}
#[test]
fn ignore_application_id_allows_opening_with_custom_application_id() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let create_opts = CreateOptions::new().skip_application_id(true);
expect!(Connection::create_new(&path, &create_opts)).to(be_ok());
{
let raw = rusqlite::Connection::open(&path).unwrap();
raw.pragma_update(None, "application_id", 0x12345678i32)
.unwrap();
}
expect!(Connection::open(&path, &OpenOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotALitebox)));
expect!(Connection::open(
&path,
&OpenOptions::new().ignore_application_id(true)
))
.to(be_ok());
}
#[test]
fn orphaned_unlinked_files_are_garbage_collected_on_open() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
{
let mut conn = Connection::create_new(&path, &CreateOptions::new())?;
conn.exec(|fs| {
let file = fs.create("orphaned.txt", FileKind::Regular, Owner::current())?;
drop(file.leak_fd()?);
fs.unlink("orphaned.txt")?;
liteboxfs::Result::Ok(())
})?;
}
expect!(Connection::open(&path, &OpenOptions::new())).to(be_ok());
Ok(())
}
#[cfg(feature = "_encryption")]
mod encryption {
use liteboxfs::{Connection, CreateOptions, EncryptionSecret, OpenOptions};
use xpct::{be_err, be_ok, expect, match_pattern, pattern};
#[test]
fn can_create_new_and_open_with_password() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let create_opts =
CreateOptions::new().encryption_secret(EncryptionSecret::password("hunter2"));
expect!(Connection::create_new(&path, &create_opts)).to(be_ok());
let open_opts = OpenOptions::new().encryption_secret(EncryptionSecret::password("hunter2"));
expect!(Connection::open(&path, &open_opts)).to(be_ok());
Ok(())
}
#[test]
fn can_create_new_and_open_with_key() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let key = [42u8; EncryptionSecret::KEY_LEN];
let create_opts = CreateOptions::new().encryption_secret(EncryptionSecret::key(&key));
expect!(Connection::create_new(&path, &create_opts)).to(be_ok());
let open_opts = OpenOptions::new().encryption_secret(EncryptionSecret::key(&key));
expect!(Connection::open(&path, &open_opts)).to(be_ok());
Ok(())
}
#[test]
fn cannot_open_encrypted_with_wrong_password() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let create_opts =
CreateOptions::new().encryption_secret(EncryptionSecret::password("correct"));
Connection::create_new(&path, &create_opts).unwrap();
let open_opts = OpenOptions::new().encryption_secret(EncryptionSecret::password("wrong"));
expect!(Connection::open(&path, &open_opts))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
}
#[test]
fn cannot_open_encrypted_without_secret() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let create_opts =
CreateOptions::new().encryption_secret(EncryptionSecret::password("secret"));
Connection::create_new(&path, &create_opts).unwrap();
expect!(Connection::open(&path, &OpenOptions::new()))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
}
#[test]
fn can_change_secret() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let create_opts = CreateOptions::new().encryption_secret(EncryptionSecret::password("old"));
let mut conn = Connection::create_new(&path, &create_opts)?;
expect!(conn.change_secret(&EncryptionSecret::password("new"))).to(be_ok());
drop(conn);
let open_opts = OpenOptions::new().encryption_secret(EncryptionSecret::password("new"));
expect!(Connection::open(&path, &open_opts)).to(be_ok());
Ok(())
}
#[test]
fn cannot_open_after_secret_change_with_old_secret() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let create_opts = CreateOptions::new().encryption_secret(EncryptionSecret::password("old"));
let mut conn = Connection::create_new(&path, &create_opts)?;
conn.change_secret(&EncryptionSecret::password("new"))?;
drop(conn);
let open_opts = OpenOptions::new().encryption_secret(EncryptionSecret::password("old"));
expect!(Connection::open(&path, &open_opts))
.to(be_err())
.to(match_pattern(pattern!(liteboxfs::Error::NotADatabase)));
Ok(())
}
#[test]
fn can_change_from_password_to_key() -> liteboxfs::Result<()> {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test.litebox");
let key = [42u8; EncryptionSecret::KEY_LEN];
let create_opts =
CreateOptions::new().encryption_secret(EncryptionSecret::password("password"));
let mut conn = Connection::create_new(&path, &create_opts)?;
expect!(conn.change_secret(&EncryptionSecret::key(&key))).to(be_ok());
drop(conn);
let open_opts = OpenOptions::new().encryption_secret(EncryptionSecret::key(&key));
expect!(Connection::open(&path, &open_opts)).to(be_ok());
Ok(())
}
}