chore: extract tests from catalog.rs into their own module implementation

This commit is contained in:
2025-10-22 06:56:15 +02:00
parent dedfe7a5d2
commit f5f0f189fa
15 changed files with 2726 additions and 2678 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn delete_should_mark_entity_as_to_delete() {
// Should mark an existing entity's state as 'ToDelete'.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-123".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item_id = item.id.clone();
let _ = catalog.upsert(item);
catalog.delete(&item_id);
let entity = catalog.items.get(&item_id).unwrap();
assert_eq!(entity.state, EntityState::ToDelete);
}
#[test]
fn delete_should_have_no_effect_for_nonexistent_id() {
// Should have no effect if the entity ID does not exist.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-123".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item);
let original_items = catalog.items.clone();
// Attempt to delete a non-existent entity, which should not panic or change anything.
catalog.delete("nonexistent-id");
assert_eq!(catalog.items, original_items);
}
}

View File

@@ -0,0 +1,37 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn get_should_retrieve_and_convert_entity_by_id() {
// Should retrieve an entity by its ID and correctly convert it to the target type 'T'.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-123".to_string(),
subclass: Some("subitem".to_string()),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item.clone());
let retrieved_item: Option<Item> = catalog.get::<Item>("item-123").unwrap();
assert_eq!(retrieved_item, Some(item));
}
#[test]
fn get_should_return_none_for_nonexistent_id() {
// Should return 'None' if the ID does not exist.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-123".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item.clone());
let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap();
assert!(retrieved_item.is_none());
}
}

View File

@@ -0,0 +1,52 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn init_should_create_db_file_if_not_exists() {
// Should create the SQLite database file if it doesn't exist.
let db_path = "target/test_dbs/init_should_create_db_file_if_not_exists.db";
let path = std::path::Path::new(db_path);
// Ensure the directory exists
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
// Ensure the file does not exist before the test
if path.exists() {
std::fs::remove_file(path).unwrap();
}
let catalog = Catalog::new(db_path);
let result = catalog.init();
assert!(result.is_ok());
assert!(path.exists());
// Clean up the created file
std::fs::remove_file(path).unwrap();
}
#[test]
fn init_should_not_fail_if_db_file_exists() {
// Should not fail if the database file already exists.
let db_path = "target/test_dbs/init_should_not_fail_if_db_file_exists.db";
let path = std::path::Path::new(db_path);
// Ensure the directory exists
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
// Create the DB file first
let catalog = Catalog::new(db_path);
catalog.init().unwrap();
// Calling init() again should not fail
let result = catalog.init();
assert!(result.is_ok());
assert!(path.exists());
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn init_should_return_error_for_invalid_path() {
// Should return an error for an invalid path or permissions issue.
// Using a directory as a path should fail.
let invalid_path = "target/test_dbs/an_invalid_path_dir";
std::fs::create_dir_all(invalid_path).unwrap();
let catalog = Catalog::new(invalid_path);
let result = catalog.init();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), FailedTo::InitDatabase);
// Clean up
std::fs::remove_dir_all(invalid_path).unwrap();
}
}

View File

@@ -0,0 +1,39 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn insert_many_should_add_all_entities() {
// 'insert_many()': Should add all provided entities to the 'items' map.
let mut catalog = Catalog::new("dummy.db");
let items = vec![
Item {
id: "item-1".to_string(),
name: "Item 1".to_string(),
price: 10,
sell_trend: 0,
in_stock: true,
..Item::default()
},
Item {
id: "item-2".to_string(),
name: "Item 2".to_string(),
price: 20,
sell_trend: 0,
in_stock: false,
..Item::default()
},
];
let _ = catalog.insert_many(items);
assert_eq!(catalog.items.len(), 2);
let entity1 = catalog.items.get("item-1").unwrap();
assert_eq!(entity1.state, EntityState::New);
assert_eq!(entity1.value_of("name"), Some(&Value::from("Item 1")));
assert_eq!(entity1.value_of("price"), Some(&Value::from(10u64)));
assert_eq!(entity1.value_of("sell_trend"), Some(&Value::from(0i64)));
let entity2 = catalog.items.get("item-2").unwrap();
assert_eq!(entity2.state, EntityState::New);
assert_eq!(entity2.value_of("name"), Some(&Value::from("Item 2")));
assert_eq!(entity2.value_of("price"), Some(&Value::from(20u64)));
assert_eq!(entity2.value_of("sell_trend"), Some(&Value::from(0i64)));
}
}

View File

@@ -0,0 +1,189 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn integration_test_init_insert_persist_load_get() {
// Scenario: 'init' -> 'insert' -> 'persist' -> create a new catalog instance -> 'load_by_id' -> 'get' -> verify data integrity.
let db_path = "target/test_dbs/integration_test_init_insert_persist_load_get.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. 'init' -> 'insert' -> 'persist'
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_to_insert = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Integration Test Item".to_string(),
price: 999,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item_to_insert.clone());
catalog1.persist().unwrap();
// 2. create a new catalog instance -> 'load_by_id' -> 'get'
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-1").unwrap();
let loaded_item: Option<Item> = catalog2.get("item-1").unwrap();
// 3. verify data integrity
assert_eq!(loaded_item, Some(item_to_insert));
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn integration_test_init_insert_many_persist_load_list() {
// Scenario: 'init' -> 'insert_many' -> 'persist' -> new catalog -> 'load_by_class' -> 'list_by_class' -> verify all items are loaded.
let db_path = "target/test_dbs/integration_test_init_insert_many_persist_load_list.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. 'init' -> 'insert_many' -> 'persist'
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let items_to_insert = vec![
Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
},
Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
},
];
let _ = catalog1.insert_many(items_to_insert.clone());
catalog1.persist().unwrap();
// 2. new catalog -> 'load_by_class' -> 'list_by_class'
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_class::<Item>().unwrap();
let mut loaded_items: Vec<Item> = catalog2
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
// Sort by ID to ensure consistent order for comparison
let mut expected_items = items_to_insert;
loaded_items.sort_by(|a, b| a.id.cmp(&b.id));
expected_items.sort_by(|a, b| a.id.cmp(&b.id));
// 3. verify all items are loaded
assert_eq!(loaded_items.len(), 2);
assert_eq!(loaded_items, expected_items);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn integration_test_insert_persist_load_delete_persist_load() {
// Scenario: 'insert' -> 'persist' -> 'load_by_id' -> 'delete' -> 'persist' -> 'load_by_id' should now return nothing for the deleted ID.
let db_path = "target/test_dbs/integration_test_insert_persist_load_delete_persist_load.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. 'insert' -> 'persist'
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_to_delete = Item {
id: "item-to-delete".to_string(),
name: "Test Item".to_string(),
price: 123,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item_to_delete.clone());
catalog1.persist().unwrap();
// 2. 'load_by_id' to confirm it's there
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-to-delete").unwrap();
assert!(catalog2.get::<Item>("item-to-delete").unwrap().is_some());
// 3. 'delete' -> 'persist'
catalog2.delete("item-to-delete");
catalog2.persist().unwrap();
// 4. 'load_by_id' should now return nothing
let mut catalog3 = Catalog::new(db_path);
catalog3.load_by_id("item-to-delete").unwrap();
let loaded_item: Option<Item> = catalog3.get("item-to-delete").unwrap();
assert!(loaded_item.is_none());
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
#[ignore] // This test can be flaky as it depends on thread scheduling.
fn integration_test_concurrency() {
// Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file?
// This test demonstrates that without application-level locking, a "last-write-wins"
// race condition can occur, leading to lost updates.
let db_path = "target/test_dbs/integration_test_concurrency.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Initial setup
let mut catalog_setup = Catalog::new(db_path);
catalog_setup.init().unwrap();
let initial_item = Item {
id: "item-1".to_string(),
name: "Original".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog_setup.upsert(initial_item);
catalog_setup.persist().unwrap();
let db_path_arc = std::sync::Arc::new(String::from(db_path));
// 2. Thread 1: Loads, updates name, and persists.
let db_path_arc1 = std::sync::Arc::clone(&db_path_arc);
let handle1 = std::thread::spawn(move || {
let mut catalog1 = Catalog::new(&db_path_arc1);
catalog1.load_by_id("item-1").unwrap();
let mut item = catalog1.get::<Item>("item-1").unwrap().unwrap();
item.name = "Updated by Thread 1".to_string();
let _ = catalog1.upsert(item);
catalog1.persist().unwrap();
});
// 3. Thread 2: Loads, updates price, and persists.
let db_path_arc2 = std::sync::Arc::clone(&db_path_arc);
let handle2 = std::thread::spawn(move || {
let mut catalog2 = Catalog::new(&db_path_arc2);
catalog2.load_by_id("item-1").unwrap();
let mut item = catalog2.get::<Item>("item-1").unwrap().unwrap();
item.price = 200;
let _ = catalog2.upsert(item);
catalog2.persist().unwrap();
});
handle1.join().unwrap();
handle2.join().unwrap();
// 4. Verification: Load the data and check the final state.
let mut catalog_verify = Catalog::new(db_path);
catalog_verify.load_by_id("item-1").unwrap();
let final_item: Item = catalog_verify.get("item-1").unwrap().unwrap();
// The final state depends on which thread persisted last. One update will have been lost.
let thread1_won = final_item.name == "Updated by Thread 1"
&& final_item.price == 100
&& final_item.sell_trend == 0;
let thread2_won =
final_item.name == "Original" && final_item.price == 200 && final_item.sell_trend == 0;
assert!(
thread1_won || thread2_won,
"Final state must be the result of one of the threads winning the race."
);
// Clean up
std::fs::remove_file(path).unwrap();
}
}

View File

@@ -0,0 +1,46 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn list_by_class_should_return_all_entities_of_class() {
// Should return an iterator with all entities of a specific class.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone());
let results: Vec<Item> = catalog
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
assert_eq!(results.len(), 2);
assert!(results.contains(&item1));
assert!(results.contains(&item2));
}
#[test]
fn list_by_class_should_return_empty_iterator_if_no_match() {
// Should return an empty iterator if no entities of that class exist.
let catalog = Catalog::new("dummy.db");
let results: Vec<Item> = catalog
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
assert!(results.is_empty());
}
}

View File

@@ -0,0 +1,49 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn list_by_class_and_subclass_should_return_matching_entities() {
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("electronics".to_string()),
..Default::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("books".to_string()),
..Default::default()
};
let item3 = Item {
id: "item-3".to_string(),
subclass: Some("electronics".to_string()),
..Default::default()
};
let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone());
let _ = catalog.upsert(item3.clone());
let results: Vec<Item> = catalog
.list_by_class_and_subclass("electronics")
.map(|item| item.unwrap())
.collect();
assert_eq!(results.len(), 2);
assert!(results.contains(&item1));
assert!(results.contains(&item3));
assert!(!results.contains(&item2));
}
#[test]
fn list_by_class_and_subclass_should_return_empty_if_no_match() {
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("electronics".to_string()),
..Default::default()
};
let _ = catalog.upsert(item1.clone());
let results: Vec<Item> = catalog
.list_by_class_and_subclass("books")
.map(|item| item.unwrap())
.collect();
assert!(results.is_empty());
}
}

View File

@@ -0,0 +1,165 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn load_by_class_should_load_all_entities_of_class() {
// Should load all entities of a given class from the database into the 'items' map.
let db_path = "target/test_dbs/load_by_class_should_load_all_entities_of_class.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Setup DB with a few items of the same class
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog1.upsert(item1.clone());
let _ = catalog1.upsert(item2.clone());
catalog1.persist().unwrap();
// 2. Create a new catalog and load the items by class
let mut catalog2 = Catalog::new(db_path);
let result = catalog2.load_by_class::<Item>();
assert!(result.is_ok());
// 3. Verify that all items of that class were loaded
assert_eq!(catalog2.items.len(), 2);
let loaded_item1: Item = catalog2.get("item-1").unwrap().unwrap();
let loaded_item2: Item = catalog2.get("item-2").unwrap().unwrap();
assert_eq!(loaded_item1, item1);
assert_eq!(loaded_item2, item2);
assert_eq!(
catalog2.items.get("item-1").unwrap().state,
EntityState::Loaded
);
assert_eq!(
catalog2.items.get("item-2").unwrap().state,
EntityState::Loaded
);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn load_by_class_should_overwrite_in_memory_entities() {
// Should overwrite any existing in-memory entities with the same IDs.
let db_path = "target/test_dbs/load_by_class_should_overwrite_in_memory_entities.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Persist an item to the database.
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_in_db = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "DB Version".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item_in_db.clone());
catalog1.persist().unwrap();
// 2. Create a new catalog with a different in-memory version of the same item.
let mut catalog2 = Catalog::new(db_path);
let item_in_memory = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Memory Version".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog2.upsert(item_in_memory);
assert_eq!(catalog2.items.len(), 1);
assert_eq!(
catalog2.get::<Item>("item-1").unwrap().unwrap().name,
"Memory Version"
);
// 3. Load from the database, which should overwrite the in-memory version.
let result = catalog2.load_by_class::<Item>();
assert!(result.is_ok());
// 4. Verify that the in-memory entity has been replaced with the one from the DB.
assert_eq!(catalog2.items.len(), 1);
let loaded_item: Item = catalog2.get("item-1").unwrap().unwrap();
assert_eq!(loaded_item, item_in_db);
assert_eq!(
catalog2.items.get("item-1").unwrap().state,
EntityState::Loaded
);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn load_by_class_should_do_nothing_if_none_found() {
// Should do nothing if no entities of that class are found in the database.
let db_path = "target/test_dbs/load_by_class_should_do_nothing_if_none_found.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Create an empty, initialized database.
let mut catalog = Catalog::new(db_path);
catalog.init().unwrap();
// 2. Attempt to load from the empty DB.
let result = catalog.load_by_class::<Item>();
assert!(result.is_ok());
assert!(catalog.items.is_empty());
// 3. Add an item to memory and try loading again from the empty DB.
let item_in_memory = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "In-memory only".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item_in_memory.clone());
assert_eq!(catalog.items.len(), 1);
let result2 = catalog.load_by_class::<Item>();
assert!(result2.is_ok());
// 4. Verify the in-memory item is untouched because nothing was loaded from DB.
assert_eq!(catalog.items.len(), 1);
let retrieved_item: Item = catalog.get("item-1").unwrap().unwrap();
assert_eq!(retrieved_item, item_in_memory);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn load_by_class_should_return_error_on_db_failure() {
// Should return an error if the database operation fails.
// Using a directory as a path should cause a failure.
let invalid_path = "target/test_dbs/a_directory_for_load_class_fail";
std::fs::create_dir_all(invalid_path).unwrap();
let mut catalog = Catalog::new(invalid_path);
// Attempt to load from the invalid path.
let result = catalog.load_by_class::<Item>();
assert!(result.is_err());
// Based on `load_by_id`, the error should be `LoadFromDB`.
// This assumes `From<SqliteFailedTo>` is implemented to produce `FailedTo::LoadFromDB`.
assert_eq!(result.unwrap_err(), FailedTo::LoadFromDB);
// Clean up
std::fs::remove_dir_all(invalid_path).unwrap();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn load_by_id_should_load_entity_from_db() {
// Should load a single entity from the database into the 'items' map.
let db_path = "target/test_dbs/load_by_id_should_load_entity_from_db.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Create a catalog, insert an item, and persist it to the DB.
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_to_persist = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Test Item".to_string(),
price: 123,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item_to_persist.clone());
catalog1.persist().unwrap();
// 2. Create a new, empty catalog instance for the same DB.
let mut catalog2 = Catalog::new(db_path);
assert!(catalog2.items.is_empty());
// 3. Load the item by its ID.
let result = catalog2.load_by_id("item-1");
assert!(result.is_ok());
// 4. Verify the item is now in the in-memory 'items' map.
assert_eq!(catalog2.items.len(), 1);
let loaded_entity = catalog2.items.get("item-1").unwrap();
// 5. Verify the loaded entity's data and state.
assert_eq!(loaded_entity.id, "item-1");
assert_eq!(loaded_entity.class, "item");
assert_eq!(
loaded_entity.value_of("name"),
Some(&Value::from("Test Item"))
);
assert_eq!(loaded_entity.value_of("price"), Some(&Value::from(123u64)));
assert_eq!(
loaded_entity.value_of("sell_trend"),
Some(&Value::from(0i64))
);
assert_eq!(loaded_entity.value_of("in_stock"), Some(&Value::from(true)));
assert_eq!(loaded_entity.state, EntityState::Loaded); // Should be Synced after loading.
// 6. Also verify by using the public 'get' method.
let retrieved_item: Option<Item> = catalog2.get("item-1").unwrap();
assert_eq!(retrieved_item, Some(item_to_persist));
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn load_by_id_should_overwrite_in_memory_entity() {
// Should overwrite an existing in-memory entity with the same ID.
let db_path = "target/test_dbs/load_by_id_should_overwrite_in_memory_entity.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Persist an item to the database.
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_in_db = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item from DB".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item_in_db.clone());
catalog1.persist().unwrap();
// 2. Create a new catalog and add a *different* in-memory version of the same item.
let mut catalog2 = Catalog::new(db_path);
let item_in_memory = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "In-memory version".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog2.upsert(item_in_memory);
let entity_before_load = catalog2.items.get("item-1").unwrap();
assert_eq!(entity_before_load.state, EntityState::New);
assert_eq!(
entity_before_load.value_of("name"),
Some(&Value::from("In-memory version"))
);
// 3. Load the item from the database, which should overwrite the in-memory version.
let result = catalog2.load_by_id("item-1");
assert!(result.is_ok());
// 4. Verify that the in-memory entity has been replaced with the one from the DB.
let entity_after_load = catalog2.items.get("item-1").unwrap();
assert_eq!(entity_after_load.state, EntityState::Loaded);
assert_eq!(
entity_after_load.value_of("name"),
Some(&Value::from("Item from DB"))
);
assert_eq!(
entity_after_load.value_of("price"),
Some(&Value::from(100u64))
);
assert_eq!(
entity_after_load.value_of("sell_trend"),
Some(&Value::from(0i64))
);
// 5. Verify using the public 'get' method.
let retrieved_item: Item = catalog2.get("item-1").unwrap().unwrap();
assert_eq!(retrieved_item, item_in_db);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn load_by_id_should_do_nothing_if_not_found() {
// Should do nothing if the entity is not found in the database.
let db_path = "target/test_dbs/load_by_id_should_do_nothing_if_not_found.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Create an empty, initialized database.
let mut catalog = Catalog::new(db_path);
catalog.init().unwrap();
// 2. Attempt to load an ID that does not exist.
let result = catalog.load_by_id("nonexistent-id");
// 3. Verify that the operation succeeded and the catalog remains empty.
assert!(result.is_ok());
assert!(catalog.items.is_empty());
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn load_by_id_should_return_error_on_db_failure() {
// Should return an error if the database operation fails.
// Using a directory as a path should cause a failure.
let invalid_path = "target/test_dbs/a_directory_for_load_fail";
std::fs::create_dir_all(invalid_path).unwrap();
let mut catalog = Catalog::new(invalid_path);
// Attempt to load from the invalid path.
let result = catalog.load_by_id("any-id");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), FailedTo::LoadFromDB);
// Clean up
std::fs::remove_dir_all(invalid_path).unwrap();
}
}

View File

@@ -0,0 +1,12 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn new_should_create_catalog_with_path_and_empty_items() {
// Should create a new Catalog with the given path and an empty 'items' map.
let path = "test.db";
let catalog = Catalog::new(path);
assert_eq!(catalog.path, path);
assert!(catalog.items.is_empty());
}
}

View File

@@ -0,0 +1,364 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn persist_should_insert_new_entities() {
// Should insert entities with 'EntityState::New' into the database.
let db_path = "target/test_dbs/persist_should_insert_new_entities.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Create catalog, insert an item, and persist
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Test Item".to_string(),
price: 123,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item1.clone());
assert!(catalog1.persist().is_ok());
// 2. Create a new catalog and load the item to verify it was persisted
let mut catalog2 = Catalog::new(db_path);
assert!(catalog2.load_by_id("item-1").is_ok());
// 3. Get the item and assert it's the same as the one we inserted
let loaded_item: Option<Item> = catalog2.get("item-1").unwrap();
assert_eq!(loaded_item, Some(item1));
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn persist_should_delete_to_delete_entities() {
// Should delete entities with 'EntityState::ToDelete' from the database.
let db_path = "target/test_dbs/persist_should_delete_to_delete_entities.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Create catalog, insert an item, and persist it.
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item1 = Item {
id: "item-to-delete".to_string(),
name: "Test Item".to_string(),
price: 123,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(item1.clone());
assert!(catalog1.persist().is_ok());
// 2. Mark the item for deletion and persist again.
catalog1.delete(&item1.id);
assert!(catalog1.persist().is_ok());
// 3. Create a new catalog and try to load the deleted item.
let mut catalog2 = Catalog::new(db_path);
assert!(catalog2.load_by_id(&item1.id).is_ok());
// 4. Assert that the item was not found.
let loaded_item: Option<Item> = catalog2.get(&item1.id).unwrap();
assert!(loaded_item.is_none());
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn persist_should_update_updated_entities() {
// Should update entities with 'EntityState::Updated' in the database.
let db_path = "target/test_dbs/persist_should_update_updated_entities.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Insert an entity and persist it.
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let original_item = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Original Name".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog1.upsert(original_item.clone());
catalog1.persist().unwrap();
// 2. Load it into a new catalog to simulate a separate session.
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-1").unwrap();
// 3. Upsert updated data for the same item. This should mark it as 'Updated'.
let updated_item = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Updated Name".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog2.upsert(updated_item.clone());
assert_eq!(
catalog2.items.get("item-1").unwrap().state,
EntityState::Updated
);
// 4. Persist the changes.
catalog2.persist().unwrap();
// 5. Load the data into a third catalog to verify the update was written to the DB.
let mut catalog3 = Catalog::new(db_path);
catalog3.load_by_id("item-1").unwrap();
let loaded_item: Item = catalog3.get("item-1").unwrap().unwrap();
// 6. Assert that the loaded item has the updated values.
assert_eq!(loaded_item, updated_item);
assert_ne!(loaded_item, original_item);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn persist_should_handle_mixed_entity_states() {
// Should handle a mix of new, updated, and deleted entities in one operation.
let db_path = "target/test_dbs/persist_should_handle_mixed_entity_states.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Setup: Pre-populate the database with some items.
let mut catalog_setup = Catalog::new(db_path);
catalog_setup.init().unwrap();
let item_to_update_original = Item {
id: "update-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Original".to_string(),
price: 10,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item_to_delete = Item {
id: "delete-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Delete Me".to_string(),
price: 20,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item_to_keep = Item {
id: "keep-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Keep Me".to_string(),
price: 30,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog_setup.upsert(item_to_update_original.clone());
let _ = catalog_setup.upsert(item_to_delete.clone());
let _ = catalog_setup.upsert(item_to_keep.clone());
catalog_setup.persist().unwrap();
// 2. Manipulation: Load the data and perform mixed operations.
let mut catalog_ops = Catalog::new(db_path);
catalog_ops.load_by_class::<Item>().unwrap(); // Load all items
// A new item to be inserted.
let item_to_add = Item {
id: "add-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Add Me".to_string(),
price: 40,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog_ops.upsert(item_to_add.clone()); // State: New
// An updated version of an existing item.
let item_to_update_new = Item {
id: "update-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Updated".to_string(),
price: 11,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog_ops.upsert(item_to_update_new.clone()); // State: Updated
// An item to be deleted.
catalog_ops.delete("delete-me"); // State: ToDelete
// item_to_keep is left untouched (State: Synced after load)
// 3. Execution: Persist all the changes in one go.
catalog_ops.persist().unwrap();
// 4. Verification: Load into a new catalog and check the final state of the DB.
let mut catalog_verify = Catalog::new(db_path);
catalog_verify.load_by_class::<Item>().unwrap();
// Check total count
assert_eq!(catalog_verify.items.len(), 3);
// Verify added item
let added_item: Item = catalog_verify.get("add-me").unwrap().unwrap();
assert_eq!(added_item, item_to_add);
// Verify updated item
let updated_item: Item = catalog_verify.get("update-me").unwrap().unwrap();
assert_eq!(updated_item, item_to_update_new);
// Verify deleted item
let deleted_item: Option<Item> = catalog_verify.get("delete-me").unwrap();
assert!(deleted_item.is_none());
// Verify untouched item
let kept_item: Item = catalog_verify.get("keep-me").unwrap().unwrap();
assert_eq!(kept_item, item_to_keep);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn persist_should_return_error_on_db_failure() {
// Should return an error if the database connection fails or a query fails.
// Using a directory as a path should cause a failure.
let invalid_path = "target/test_dbs/a_directory_for_persist_fail";
std::fs::create_dir_all(invalid_path).unwrap();
let mut catalog = Catalog::new(invalid_path);
let item = Item {
id: "item-1".to_string(),
name: "Test".to_string(),
price: 10,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item);
let result = catalog.persist();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), FailedTo::PersistCatalog);
// Clean up
std::fs::remove_dir_all(invalid_path).unwrap();
}
#[test]
fn persist_should_update_in_memory_state() {
// After persisting, the in-memory state of entities should be considered.
// (e.g., should deleted items be removed from the 'items' map, all other items should be marked as Loaded).
let db_path = "target/test_dbs/persist_should_update_in_memory_state.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Setup: Create a catalog and pre-populate it with some data.
let mut catalog = Catalog::new(db_path);
catalog.init().unwrap();
let item_to_update = Item {
id: "update-me".to_string(),
name: "Original".to_string(),
price: 0,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let item_to_delete = Item {
id: "delete-me".to_string(),
name: "Delete Me".to_string(),
price: 0,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let item_untouched = Item {
id: "keep-me".to_string(),
name: "Keep Me".to_string(),
price: 0,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item_to_update.clone());
let _ = catalog.upsert(item_to_delete.clone());
let _ = catalog.upsert(item_untouched.clone());
catalog.persist().unwrap();
// At this point, all items are in the DB and in-memory state is `Loaded`.
assert_eq!(catalog.items.len(), 3);
assert_eq!(
catalog.items.get("update-me").unwrap().state,
EntityState::Loaded
);
assert_eq!(
catalog.items.get("delete-me").unwrap().state,
EntityState::Loaded
);
assert_eq!(
catalog.items.get("keep-me").unwrap().state,
EntityState::Loaded
);
// 2. Manipulate the catalog to have entities in various states.
// A new item to be inserted.
let item_new = Item {
id: "add-me".to_string(),
name: "Add Me".to_string(),
price: 0,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item_new.clone()); // State: New
// An updated version of an existing item.
let item_updated = Item {
id: "update-me".to_string(),
name: "Updated".to_string(),
price: 0,
sell_trend: 10,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item_updated.clone()); // State: Updated
// An item to be deleted.
catalog.delete("delete-me"); // State: ToDelete
// 'item_untouched' remains with state `Loaded`.
// Check states before final persist
assert_eq!(catalog.items.get("add-me").unwrap().state, EntityState::New);
assert_eq!(
catalog.items.get("update-me").unwrap().state,
EntityState::Updated
);
assert_eq!(
catalog.items.get("delete-me").unwrap().state,
EntityState::ToDelete
);
assert_eq!(
catalog.items.get("keep-me").unwrap().state,
EntityState::Loaded
);
assert_eq!(catalog.items.len(), 4);
// 3. Persist all changes.
catalog.persist().unwrap();
// 4. Verify the in-memory state after persisting.
// The item marked for deletion should be gone.
assert!(!catalog.items.contains_key("delete-me"));
assert_eq!(catalog.items.len(), 3);
// All remaining items should have their state as `Loaded`.
let new_item_entity = catalog.items.get("add-me").unwrap();
assert_eq!(new_item_entity.state, EntityState::Loaded);
assert_eq!(
new_item_entity.value_of("name"),
Some(&Value::from("Add Me"))
);
let updated_item_entity = catalog.items.get("update-me").unwrap();
assert_eq!(updated_item_entity.state, EntityState::Loaded);
assert_eq!(
updated_item_entity.value_of("name"),
Some(&Value::from("Updated"))
);
assert_eq!(
updated_item_entity.value_of("sell_trend"),
Some(&Value::from(10i64))
);
let untouched_item_entity = catalog.items.get("keep-me").unwrap();
assert_eq!(untouched_item_entity.state, EntityState::Loaded);
assert_eq!(
untouched_item_entity.value_of("name"),
Some(&Value::from("Keep Me"))
);
// Clean up
std::fs::remove_file(path).unwrap();
}
}

View File

@@ -0,0 +1,58 @@
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn upsert_should_add_single_entity_as_new() {
// 'upsert()': Should add a single entity to the 'items' map with 'EntityState::New'.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-123".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item_id = item.id.clone();
let _ = catalog.upsert(item);
let entity = catalog.items.get(&item_id).unwrap();
assert_eq!(entity.id, item_id);
assert_eq!(entity.state, EntityState::New);
assert_eq!(entity.class, "item");
assert_eq!(entity.value_of("name"), Some(&Value::from("Test Item")));
assert_eq!(entity.value_of("price"), Some(&Value::from(100u64)));
assert_eq!(entity.value_of("sell_trend"), Some(&Value::from(0i64)));
assert_eq!(entity.value_of("in_stock"), Some(&Value::from(true)));
}
#[test]
fn upsert_should_overwrite_existing_entity() {
// 'upsert()': Should overwrite an existing entity with the same ID.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-123".to_string(),
name: "First Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item_id = item1.id.clone();
let _ = catalog.upsert(item1);
let item2 = Item {
id: "item-123".to_string(),
name: "Second Item".to_string(),
price: 200,
sell_trend: 10,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item2);
assert_eq!(catalog.items.len(), 1);
let entity = catalog.items.get(&item_id).unwrap();
assert_eq!(entity.value_of("name"), Some(&Value::from("Second Item")));
assert_eq!(entity.value_of("price"), Some(&Value::from(200u64)));
assert_eq!(entity.value_of("sell_trend"), Some(&Value::from(10i64)));
assert_eq!(entity.value_of("in_stock"), Some(&Value::from(false)));
assert_eq!(entity.state, EntityState::Updated);
}
}

View File

@@ -1 +1,13 @@
pub mod catalog_delete;
pub mod catalog_get;
pub mod catalog_init;
pub mod catalog_insert_many;
pub mod catalog_integration;
pub mod catalog_list_by_class;
pub mod catalog_list_by_class_and_subclass;
pub mod catalog_load_by_class;
pub mod catalog_load_by_filter;
pub mod catalog_load_by_id;
pub mod catalog_new;
pub mod catalog_persist;
pub mod catalog_upsert;