use std::fs;
use std::path::Path;
use hayson_engine::database::{
backup_database, restore_database, BackupOptions, Database, RestoreOptions, BACKUP_MANIFEST,
};
use serde_json::json;
use tempfile::TempDir;
fn seeded(root: &Path) {
let mut database = Database::open(root).unwrap();
database
.create_table(
"docs",
json!({
"schemaVersion": 1,
"table": "docs",
"primaryKey": "id",
"fields": { "id": { "type": "string" }, "cover": { "type": "image" } },
}),
)
.unwrap();
let docs = database.table_mut("docs").unwrap();
docs.put(
&json!({ "id": "d1", "cover": { "content": "Ynl0ZXM=", "virtualPath": "/a.png" } }),
false,
)
.unwrap();
database.close().unwrap();
}
fn count(root: &Path) -> usize {
let database = Database::open(root).unwrap();
let keys = database.table("docs").unwrap().keys().count();
database.close().unwrap();
keys
}
fn add(root: &Path, id: &str, content: &str, path: &str) {
let mut database = Database::open(root).unwrap();
database
.table_mut("docs")
.unwrap()
.put(
&json!({ "id": id, "cover": { "content": content, "virtualPath": path } }),
false,
)
.unwrap();
database.close().unwrap();
}
#[test]
fn a_backup_restores_into_an_empty_directory_with_data_and_images() {
let source = TempDir::new().unwrap();
seeded(source.path());
let holder = TempDir::new().unwrap();
let backup = holder.path().join("b1");
let made = backup_database(source.path(), &backup, &BackupOptions::default()).unwrap();
assert_eq!(made.files, 3); assert!(backup.join(BACKUP_MANIFEST).is_file());
let target = TempDir::new().unwrap();
let restored = restore_database(&backup, target.path(), &RestoreOptions::default()).unwrap();
assert_eq!(restored.files, 3);
assert_eq!(count(target.path()), 1);
let database = Database::open(target.path()).unwrap();
let docs = database.table("docs").unwrap();
let image = &docs.list_images().unwrap()[0];
assert_eq!(docs.read_image(&image.physical_path).unwrap(), Some(b"bytes".to_vec()));
database.close().unwrap();
}
#[test]
fn restoring_over_a_database_brings_back_its_earlier_state_and_leaves_subfolders_alone() {
let root = TempDir::new().unwrap();
seeded(root.path());
let holder = TempDir::new().unwrap();
let backup = holder.path().join("b1");
backup_database(root.path(), &backup, &BackupOptions::default()).unwrap();
add(root.path(), "d2", "b3RoZXI=", "/b.png");
assert_eq!(count(root.path()), 2);
let child = root.path().join("project");
fs::create_dir_all(&child).unwrap();
seeded(&child);
let restored = restore_database(&backup, root.path(), &RestoreOptions::default()).unwrap();
assert!(restored.replaced >= 3);
assert_eq!(count(root.path()), 1);
assert_eq!(count(&child), 1, "the database in the subfolder is not touched");
assert!(!root.path().join(".restore-old").exists());
assert!(!root.path().join(".restore-new").exists());
}
#[test]
fn a_backup_does_not_carry_other_databases_or_runtime_files() {
let root = TempDir::new().unwrap();
seeded(root.path());
let child = root.path().join("project");
fs::create_dir_all(&child).unwrap();
seeded(&child);
let holder = TempDir::new().unwrap();
let backup = holder.path().join("b1");
backup_database(root.path(), &backup, &BackupOptions::default()).unwrap();
assert!(!backup.join("project").exists());
assert!(!backup.join(".hayson.lock").exists());
}
#[test]
fn a_backup_with_a_tampered_file_is_refused_and_changes_nothing() {
let root = TempDir::new().unwrap();
seeded(root.path());
let holder = TempDir::new().unwrap();
let backup = holder.path().join("b1");
backup_database(root.path(), &backup, &BackupOptions::default()).unwrap();
add(root.path(), "d2", "b3RoZXI=", "/b.png");
fs::write(backup.join("docs.jsonl"), b"{broken\n").unwrap();
let refused = restore_database(&backup, root.path(), &RestoreOptions::default());
assert!(refused.is_err());
assert_eq!(count(root.path()), 2);
}
#[test]
fn a_failed_install_puts_the_previous_files_back() {
let root = TempDir::new().unwrap();
seeded(root.path());
let holder = TempDir::new().unwrap();
let backup = holder.path().join("b1");
backup_database(root.path(), &backup, &BackupOptions::default()).unwrap();
add(root.path(), "d2", "b3RoZXI=", "/b.png");
fs::write(root.path().join("extra"), b"x").unwrap();
fs::write(backup.join("extra.txt"), b"y").unwrap();
let manifest_path = backup.join(BACKUP_MANIFEST);
let mut manifest: serde_json::Value = serde_json::from_str(&fs::read_to_string(&manifest_path).unwrap()).unwrap();
fs::create_dir_all(backup.join("nested")).unwrap();
fs::write(backup.join("nested/f.txt"), b"z").unwrap();
manifest["files"]
.as_array_mut()
.unwrap()
.push(json!({ "path": "extra/f.txt", "bytes": 1 }));
fs::create_dir_all(backup.join("extra")).unwrap();
fs::write(backup.join("extra/f.txt"), b"z").unwrap();
fs::write(&manifest_path, manifest.to_string()).unwrap();
let refused = restore_database(&backup, root.path(), &RestoreOptions::default());
assert!(refused.is_err());
assert_eq!(count(root.path()), 2, "the database that was there is still there");
assert!(!root.path().join(".restore-old").exists());
}
#[test]
fn an_interrupted_restore_is_never_overwritten() {
let root = TempDir::new().unwrap();
seeded(root.path());
let holder = TempDir::new().unwrap();
let backup = holder.path().join("b1");
backup_database(root.path(), &backup, &BackupOptions::default()).unwrap();
fs::create_dir_all(root.path().join(".restore-old")).unwrap();
let refused = restore_database(&backup, root.path(), &RestoreOptions::default()).unwrap_err();
assert!(refused.to_string().contains(".restore-old"));
}
#[test]
fn a_backup_refuses_a_busy_destination_and_an_unfinished_unit_of_work() {
let root = TempDir::new().unwrap();
seeded(root.path());
let holder = TempDir::new().unwrap();
let busy = holder.path().join("busy");
fs::create_dir_all(&busy).unwrap();
fs::write(busy.join("x"), b"x").unwrap();
assert!(backup_database(root.path(), &busy, &BackupOptions::default()).is_err());
fs::write(root.path().join(".hayson.txn"), b"{}").unwrap();
let dest = holder.path().join("b2");
let refused = backup_database(root.path(), &dest, &BackupOptions::default()).unwrap_err();
assert!(refused.to_string().contains("unfinished unit of work"));
assert!(!dest.exists());
}