use grounddb::Store;
use std::path::Path;
use tempfile::TempDir;
const SCHEMA: &str = r#"collections:
messages:
path: "{recipient}/{id}.md"
fields:
recipient: { type: string, required: true }
subject: { type: string, default: "" }
content: true
"#;
fn new_store() -> TempDir {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("schema.yaml"), SCHEMA).unwrap();
dir
}
fn open(dir: &Path) -> Store {
Store::open(&dir.to_string_lossy()).unwrap()
}
fn stored_hash(dir: &Path) -> String {
let db = rusqlite::Connection::open(dir.join("_system.db")).unwrap();
db.query_row(
"SELECT hash FROM directory_hashes WHERE collection = 'messages'",
[],
|r| r.get(0),
)
.unwrap()
}
fn insert(store: &Store, id: &str, recipient: &str, body: &str) {
let data = serde_json::json!({ "id": id, "recipient": recipient, "subject": "s" });
store.insert_dynamic("messages", data, Some(body)).unwrap();
}
fn assert_survives_reopen(dir: &Path, what: &str) {
let after_write = stored_hash(dir);
drop(open(dir));
let after_reopen = stored_hash(dir);
assert_eq!(
after_write, after_reopen,
"hash moved when reopening after {what}: the write derived a hash \
that disagrees with disk, so every boot will rescan"
);
}
#[test]
fn insert_leaves_a_hash_that_matches_disk() {
let dir = new_store();
{
let store = open(dir.path());
insert(&store, "m1", "justin", "hello");
}
assert_survives_reopen(dir.path(), "an insert");
}
#[test]
fn update_in_place_leaves_a_hash_that_matches_disk() {
let dir = new_store();
{
let store = open(dir.path());
insert(&store, "m1", "justin", "hello");
store
.update_dynamic_with_content(
"messages",
"m1",
serde_json::json!({ "id": "m1", "recipient": "justin", "subject": "edited" }),
Some("edited body"),
)
.unwrap();
}
assert_survives_reopen(dir.path(), "an in-place update");
}
#[test]
fn update_that_moves_the_file_forgets_the_old_path() {
let dir = new_store();
{
let store = open(dir.path());
insert(&store, "m1", "justin", "hello");
store
.update_dynamic_with_content(
"messages",
"m1",
serde_json::json!({ "id": "m1", "recipient": "tom", "subject": "moved" }),
Some("hello"),
)
.unwrap();
}
assert!(
dir.path().join("tom").join("m1.md").exists(),
"precondition: the file really moved"
);
assert!(
!dir.path().join("justin").join("m1.md").exists(),
"precondition: the old file is gone"
);
assert_survives_reopen(dir.path(), "an update that moved the file");
}
#[test]
fn delete_leaves_a_hash_that_matches_disk() {
let dir = new_store();
{
let store = open(dir.path());
insert(&store, "m1", "justin", "hello");
insert(&store, "m2", "justin", "second");
store.delete_dynamic("messages", "m1").unwrap();
}
assert_survives_reopen(dir.path(), "a delete");
}
#[test]
fn two_edits_inside_the_same_second_are_both_reflected() {
let dir = new_store();
{
let store = open(dir.path());
insert(&store, "m1", "justin", "first");
store
.update_dynamic_with_content(
"messages",
"m1",
serde_json::json!({ "id": "m1", "recipient": "justin", "subject": "a" }),
Some("second"),
)
.unwrap();
store
.update_dynamic_with_content(
"messages",
"m1",
serde_json::json!({ "id": "m1", "recipient": "justin", "subject": "b" }),
Some("third"),
)
.unwrap();
}
assert_survives_reopen(dir.path(), "two edits inside one second");
}
#[test]
fn a_long_mixed_sequence_still_agrees_with_disk() {
let dir = new_store();
{
let store = open(dir.path());
for i in 0..25 {
insert(&store, &format!("m{i}"), "justin", &format!("body {i}"));
}
for i in 0..5 {
store.delete_dynamic("messages", &format!("m{i}")).unwrap();
}
for i in 5..10 {
store
.update_dynamic_with_content(
"messages",
&format!("m{i}"),
serde_json::json!({ "id": format!("m{i}"), "recipient": "tom", "subject": "x" }),
Some("moved"),
)
.unwrap();
}
}
assert_survives_reopen(dir.path(), "a mixed insert/delete/move sequence");
}
#[test]
fn a_store_with_no_mirrored_entries_reseeds_on_boot() {
let dir = new_store();
{
let store = open(dir.path());
insert(&store, "m1", "justin", "hello");
insert(&store, "m2", "tom", "hi");
}
{
let db = rusqlite::Connection::open(dir.path().join("_system.db")).unwrap();
db.execute("DELETE FROM collection_entries", []).unwrap();
}
drop(open(dir.path()));
let db = rusqlite::Connection::open(dir.path().join("_system.db")).unwrap();
let mirrored: i64 = db
.query_row("SELECT COUNT(*) FROM collection_entries", [], |r| r.get(0))
.unwrap();
assert_eq!(mirrored, 2, "boot must reseed the entry mirror it found empty");
{
let store = open(dir.path());
insert(&store, "m3", "justin", "third");
}
assert_survives_reopen(dir.path(), "a write after the mirror was reseeded");
}