test(backend): add unit tests for caching, usage tracking, and snippet management

This commit is contained in:
ByteAtATime 2025-07-02 15:25:02 -07:00
parent e2f40553ac
commit 75182dd4aa
No known key found for this signature in database
3 changed files with 360 additions and 8 deletions

View file

@ -86,3 +86,97 @@ impl AppCache {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::thread;
use std::time::Duration;
fn setup_temp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"raycast_test_cache_{}_{}",
name,
rand::random::<u32>()
));
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn test_cache_file_roundtrip() {
let temp_dir = setup_temp_dir("roundtrip");
let cache_path = temp_dir.join("test_cache.bincode");
let mut dir_mod_times = HashMap::new();
dir_mod_times.insert(PathBuf::from("/test/path"), SystemTime::now());
let original_cache = AppCache {
apps: vec![App::new("TestApp".to_string())],
dir_mod_times,
};
original_cache.write_to_file(&cache_path).unwrap();
let read_cache = AppCache::read_from_file(&cache_path).unwrap();
assert_eq!(original_cache.apps.len(), read_cache.apps.len());
assert_eq!(original_cache.apps[0].name, read_cache.apps[0].name);
assert_eq!(original_cache.dir_mod_times, read_cache.dir_mod_times);
fs::remove_dir_all(temp_dir).unwrap();
}
fn get_mock_app_directories(mock_dir: PathBuf) -> Vec<PathBuf> {
vec![mock_dir]
}
fn is_stale_mock(cache: &AppCache, mock_dir: PathBuf) -> bool {
get_mock_app_directories(mock_dir).into_iter().any(|dir| {
let current_mod_time = fs::metadata(&dir).ok().and_then(|m| m.modified().ok());
let cached_mod_time = cache.dir_mod_times.get(&dir);
match (current_mod_time, cached_mod_time) {
(Some(current), Some(cached)) => current > *cached,
_ => true,
}
})
}
#[test]
fn test_is_stale_logic() {
let temp_dir = setup_temp_dir("is_stale");
let mod_time_before = fs::metadata(&temp_dir).unwrap().modified().unwrap();
let mut dir_mod_times = HashMap::new();
dir_mod_times.insert(temp_dir.clone(), mod_time_before);
let cache = AppCache {
apps: vec![],
dir_mod_times,
};
assert!(!is_stale_mock(&cache, temp_dir.clone()));
thread::sleep(Duration::from_millis(10));
let mut file = fs::File::create(temp_dir.join("test.txt")).unwrap();
file.write_all(b"Hello, world!").unwrap();
drop(file);
assert!(is_stale_mock(&cache, temp_dir.clone()));
let mod_time_after = fs::metadata(&temp_dir).unwrap().modified().unwrap();
let mut new_dir_mod_times = HashMap::new();
new_dir_mod_times.insert(temp_dir.clone(), mod_time_after);
let cache_updated = AppCache {
apps: vec![],
dir_mod_times: new_dir_mod_times,
};
assert!(!is_stale_mock(&cache_updated, temp_dir.clone()));
let cache_missing_entry = AppCache {
apps: vec![],
dir_mod_times: HashMap::new(),
};
assert!(is_stale_mock(&cache_missing_entry, temp_dir.clone()));
fs::remove_dir_all(temp_dir).unwrap();
}
}

View file

@ -43,8 +43,16 @@ impl FrecencyManager {
Ok(Self { store })
}
#[cfg(test)]
pub fn new_for_test() -> Result<Self, AppError> {
let store = Store::new_in_memory()?;
store.init_table(FRECENCY_SCHEMA)?;
store.init_table(HIDDEN_ITEMS_SCHEMA)?;
Ok(Self { store })
}
pub fn record_usage(&self, item_id: String) -> Result<(), AppError> {
let now = Utc::now().timestamp();
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
self.store.execute(
"INSERT INTO frecency (item_id, use_count, last_used_at) VALUES (?, 1, ?)
ON CONFLICT(item_id) DO UPDATE SET
@ -84,3 +92,102 @@ impl FrecencyManager {
.map_err(|e| e.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_record_usage_new_item() {
let manager = FrecencyManager::new_for_test().unwrap();
let item_id = "new_item".to_string();
manager.record_usage(item_id.clone()).unwrap();
let data = manager.get_frecency_data().unwrap();
assert_eq!(data.len(), 1);
assert_eq!(data[0].item_id, item_id);
assert_eq!(data[0].use_count, 1);
assert!(data[0].last_used_at > 0);
}
#[test]
fn test_record_usage_existing_item() {
let manager = FrecencyManager::new_for_test().unwrap();
let item_id = "existing_item".to_string();
manager.record_usage(item_id.clone()).unwrap();
let data1 = manager.get_frecency_data().unwrap();
let time1 = data1[0].last_used_at;
thread::sleep(Duration::from_millis(10));
manager.record_usage(item_id.clone()).unwrap();
let data2 = manager.get_frecency_data().unwrap();
let time2 = data2[0].last_used_at;
assert_eq!(data2.len(), 1);
assert_eq!(data2[0].use_count, 2);
assert!(time2 > time1, "last_used_at should be updated");
}
#[test]
fn test_get_frecency_data_empty() {
let manager = FrecencyManager::new_for_test().unwrap();
let data = manager.get_frecency_data().unwrap();
assert!(data.is_empty());
}
#[test]
fn test_delete_frecency_entry() {
let manager = FrecencyManager::new_for_test().unwrap();
let item_id = "to_delete".to_string();
manager.record_usage(item_id.clone()).unwrap();
assert_eq!(manager.get_frecency_data().unwrap().len(), 1);
manager.delete_frecency_entry(item_id).unwrap();
assert!(manager.get_frecency_data().unwrap().is_empty());
}
#[test]
fn test_delete_non_existent_entry() {
let manager = FrecencyManager::new_for_test().unwrap();
let result = manager.delete_frecency_entry("non_existent".to_string());
assert!(result.is_ok());
assert!(manager.get_frecency_data().unwrap().is_empty());
}
#[test]
fn test_hide_item_and_get_ids() {
let manager = FrecencyManager::new_for_test().unwrap();
let item1 = "hidden1".to_string();
let item2 = "hidden2".to_string();
assert!(manager.get_hidden_item_ids().unwrap().is_empty());
manager.hide_item(item1.clone()).unwrap();
let hidden = manager.get_hidden_item_ids().unwrap();
assert_eq!(hidden.len(), 1);
assert_eq!(hidden[0], item1);
manager.hide_item(item2.clone()).unwrap();
let hidden = manager.get_hidden_item_ids().unwrap();
assert_eq!(hidden.len(), 2);
assert!(hidden.contains(&item1));
assert!(hidden.contains(&item2));
}
#[test]
fn test_hide_item_is_idempotent() {
let manager = FrecencyManager::new_for_test().unwrap();
let item1 = "hidden1".to_string();
manager.hide_item(item1.clone()).unwrap();
assert_eq!(manager.get_hidden_item_ids().unwrap().len(), 1);
manager.hide_item(item1.clone()).unwrap();
assert_eq!(manager.get_hidden_item_ids().unwrap().len(), 1);
}
}

View file

@ -30,10 +30,10 @@ impl Storable for Snippet {
name: row.get(1)?,
keyword: row.get(2)?,
content: row.get(3)?,
created_at: DateTime::from_timestamp(created_at_ts, 0).unwrap_or_default(),
updated_at: DateTime::from_timestamp(updated_at_ts, 0).unwrap_or_default(),
created_at: DateTime::from_timestamp_nanos(created_at_ts),
updated_at: DateTime::from_timestamp_nanos(updated_at_ts),
times_used: row.get(6)?,
last_used_at: DateTime::from_timestamp(last_used_at_ts, 0).unwrap_or_default(),
last_used_at: DateTime::from_timestamp_nanos(last_used_at_ts),
})
}
}
@ -97,7 +97,7 @@ impl SnippetManager {
keyword: String,
content: String,
) -> Result<i64, AppError> {
let now = Utc::now().timestamp();
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
self.store.execute(
"INSERT INTO snippets (name, keyword, content, created_at, updated_at, times_used, last_used_at)
VALUES (?1, ?2, ?3, ?4, ?4, 0, 0)",
@ -129,7 +129,7 @@ impl SnippetManager {
keyword: String,
content: String,
) -> Result<(), AppError> {
let now = Utc::now().timestamp();
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
self.store.execute(
"UPDATE snippets SET name = ?1, keyword = ?2, content = ?3, updated_at = ?4 WHERE id = ?5",
params![name, keyword, content, now, id],
@ -144,7 +144,7 @@ impl SnippetManager {
}
pub fn snippet_was_used(&self, id: i64) -> Result<(), AppError> {
let now = Utc::now().timestamp();
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
self.store.execute(
"UPDATE snippets SET times_used = times_used + 1, last_used_at = ?1 WHERE id = ?2",
params![now, id],
@ -152,7 +152,7 @@ impl SnippetManager {
Ok(())
}
fn find_snippet_by_keyword(&self, keyword: &str) -> Result<Option<Snippet>, AppError> {
pub fn find_snippet_by_keyword(&self, keyword: &str) -> Result<Option<Snippet>, AppError> {
self.store.query_row(
"SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets WHERE keyword = ?1",
params![keyword],
@ -166,3 +166,154 @@ impl SnippetManager {
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{thread, time::Duration};
#[test]
fn test_create_and_list_snippets() {
let manager = SnippetManager::new_for_test().unwrap();
manager
.create_snippet(
"Test Snippet".into(),
"testkey".into(),
"This is a test.".into(),
)
.unwrap();
let snippets = manager.list_snippets(None).unwrap();
assert_eq!(snippets.len(), 1);
assert_eq!(snippets[0].name, "Test Snippet");
assert_eq!(snippets[0].keyword, "testkey");
assert_eq!(snippets[0].content, "This is a test.");
}
#[test]
fn test_create_snippet_with_duplicate_keyword_fails() {
let manager = SnippetManager::new_for_test().unwrap();
manager
.create_snippet("First".into(), "dupkey".into(), "content1".into())
.unwrap();
let result = manager.create_snippet("Second".into(), "dupkey".into(), "content2".into());
assert!(result.is_err());
match result.unwrap_err() {
AppError::Rusqlite(rusqlite::Error::SqliteFailure(e, _)) => {
assert_eq!(e.code, rusqlite::ErrorCode::ConstraintViolation);
}
_ => panic!("Expected a database constraint violation"),
}
}
#[test]
fn test_list_snippets_with_search() {
let manager = SnippetManager::new_for_test().unwrap();
manager
.create_snippet(
"Email Signature".into(),
"sig".into(),
"Best regards".into(),
)
.unwrap();
manager
.create_snippet(
"Boilerplate".into(),
"bp".into(),
"Some email content".into(),
)
.unwrap();
assert_eq!(
manager.list_snippets(Some("email".into())).unwrap().len(),
2
);
assert_eq!(manager.list_snippets(Some("sig".into())).unwrap().len(), 1);
assert_eq!(
manager.list_snippets(Some("regards".into())).unwrap().len(),
1
);
assert_eq!(
manager.list_snippets(Some("nothing".into())).unwrap().len(),
0
);
}
#[test]
fn test_update_snippet() {
let manager = SnippetManager::new_for_test().unwrap();
let id = manager
.create_snippet("Original".into(), "orig".into(), "original content".into())
.unwrap();
manager
.update_snippet(
id,
"Updated".into(),
"updated".into(),
"updated content".into(),
)
.unwrap();
let snippet = manager.find_snippet_by_keyword("updated").unwrap().unwrap();
assert_eq!(snippet.id, id);
assert_eq!(snippet.name, "Updated");
assert_eq!(snippet.content, "updated content");
}
#[test]
fn test_delete_snippet() {
let manager = SnippetManager::new_for_test().unwrap();
let id = manager
.create_snippet("To Delete".into(), "del".into(), "delete me".into())
.unwrap();
assert_eq!(manager.list_snippets(None).unwrap().len(), 1);
manager.delete_snippet(id).unwrap();
assert!(manager.list_snippets(None).unwrap().is_empty());
}
#[test]
fn test_snippet_was_used() {
let manager = SnippetManager::new_for_test().unwrap();
let id = manager
.create_snippet("Test".into(), "test".into(), "test content".into())
.unwrap();
let snippet1 = manager.find_snippet_by_keyword("test").unwrap().unwrap();
assert_eq!(snippet1.times_used, 0);
manager.snippet_was_used(id).unwrap();
let snippet2 = manager.find_snippet_by_keyword("test").unwrap().unwrap();
assert_eq!(snippet2.times_used, 1);
assert!(snippet2.last_used_at.timestamp() > 0);
manager.snippet_was_used(id).unwrap();
let snippet3 = manager.find_snippet_by_keyword("test").unwrap().unwrap();
assert_eq!(snippet3.times_used, 2);
}
#[test]
fn test_find_snippet_by_name() {
let manager = SnippetManager::new_for_test().unwrap();
manager
.create_snippet("Unique Name".into(), "key1".into(), "content1".into())
.unwrap();
thread::sleep(Duration::from_millis(10));
manager
.create_snippet("Shared Name".into(), "key2".into(), "content2".into())
.unwrap();
thread::sleep(Duration::from_millis(10));
let newest_id = manager
.create_snippet("Shared Name".into(), "key3".into(), "newest".into())
.unwrap();
let found = manager.find_snippet_by_name("Shared Name").unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().id, newest_id);
let not_found = manager.find_snippet_by_name("Non Existent").unwrap();
assert!(not_found.is_none());
}
}