#![cfg(feature = "test_hooks")]
use cfait::client::core::RustyClient;
use cfait::journal::Action;
use cfait::journal::Journal;
use cfait::model::CalendarListEntry;
use cfait::model::Task;
use cfait::storage::{LocalCalendarRegistry, LocalStorage};
use serial_test::serial;
use std::collections::HashMap;
use std::sync::Mutex;
use cfait::client::core::{TEST_FETCH_REMOTE_HOOK, TEST_FORCE_SYNC_ERROR};
#[tokio::test]
#[serial]
async fn test_sync_journal_moves_failed_task_to_recovery() {
let ctx = std::sync::Arc::new(cfait::context::TestContext::new());
let mut regs = LocalCalendarRegistry::load(ctx.as_ref()).unwrap_or_default();
regs.retain(|c| c.href != "local://recovery");
LocalCalendarRegistry::save(ctx.as_ref(), ®s).unwrap();
let mut task = Task::new("WillFail", &HashMap::new(), None);
task.uid = "sync-err-1".to_string();
task.calendar_href = "https://example.com/cal/".to_string();
task.summary = "Should fail and be recovered".to_string();
Journal::push(ctx.as_ref(), Action::Create(task.clone())).unwrap();
let hook = TEST_FORCE_SYNC_ERROR.get_or_init(|| {
Mutex::new(None::<Box<dyn Fn(&Action) -> Option<anyhow::Error> + Send + Sync + 'static>>)
});
{
let mut g = hook.lock().unwrap();
*g = Some(Box::new(move |action: &Action| match action {
Action::Create(t) if t.uid == "sync-err-1" => Some(anyhow::anyhow!("403 Forbidden")),
Action::Update(t) if t.uid == "sync-err-1" => Some(anyhow::anyhow!("403 Forbidden")),
Action::Move(t, _) if t.uid == "sync-err-1" => Some(anyhow::anyhow!("403 Forbidden")),
_ => None,
}));
}
let client = RustyClient::new(
ctx.clone(),
"http://dummy.test",
"user",
"pass",
false,
None,
)
.unwrap();
let (warnings, _synced): (Vec<String>, Vec<cfait::model::Task>) =
client.sync_journal().await.unwrap();
assert!(
warnings
.iter()
.any(|w| w.contains("Recovery") || w.contains("Local (Recovery)")),
"Expected a warning about recovery"
);
let recovered = LocalStorage::load_for_href(ctx.as_ref(), "local://recovery").unwrap();
assert!(
recovered.iter().any(|t| t.uid == "sync-err-1"),
"Task should be moved to recovery"
);
if let Some(h) = TEST_FETCH_REMOTE_HOOK.get() {
*h.lock().unwrap() = None;
}
if let Some(h) = TEST_FORCE_SYNC_ERROR.get() {
*h.lock().unwrap() = None;
}
}
#[tokio::test]
#[serial]
async fn test_recovery_calendar_visibility() {
let ctx = std::sync::Arc::new(cfait::context::TestContext::new());
let mut regs = LocalCalendarRegistry::load(ctx.as_ref()).unwrap_or_default();
regs.retain(|c| c.href != "local://recovery");
LocalCalendarRegistry::save(ctx.as_ref(), ®s).unwrap();
let client = RustyClient {
client: None,
ctx: ctx.clone(),
};
let (cals, _) = client.get_calendars().await.unwrap();
assert!(
!cals.iter().any(|c| c.href == "local://recovery"),
"Recovery calendar should not be visible when absent and empty"
);
let mut regs = LocalCalendarRegistry::load(ctx.as_ref()).unwrap_or_default();
if !regs.iter().any(|c| c.href == "local://recovery") {
regs.push(CalendarListEntry {
name: "Local (Recovery)".to_string(),
href: "local://recovery".to_string(),
color: Some("#DB4437".to_string()),
});
LocalCalendarRegistry::save(ctx.as_ref(), ®s).unwrap();
}
let (cals2, _) = client.get_calendars().await.unwrap();
assert!(
!cals2.iter().any(|c| c.href == "local://recovery"),
"Recovery calendar should not be visible when present but empty"
);
let mut task = Task::new("Recovered", &HashMap::new(), None);
task.uid = "rec-1".to_string();
task.calendar_href = "local://recovery".to_string();
LocalStorage::save_for_href(
ctx.as_ref(),
"local://recovery",
std::slice::from_ref(&task),
)
.unwrap();
let (cals3, _) = client.get_calendars().await.unwrap();
assert!(
cals3.iter().any(|c| c.href == "local://recovery"),
"Recovery calendar should be visible when it has tasks"
);
}
#[tokio::test]
#[serial]
async fn test_controller_move_local_to_remote() {
let ctx = std::sync::Arc::new(cfait::context::TestContext::new());
let store = std::sync::Arc::new(tokio::sync::Mutex::new(cfait::store::TaskStore::new(
ctx.clone(),
)));
let mut task = Task::new("T1", &std::collections::HashMap::new(), None);
task.uid = "uid-123".to_string();
task.calendar_href = "local://src".to_string();
cfait::storage::LocalStorage::save_for_href(ctx.as_ref(), "local://src", &[task.clone()])
.unwrap();
store.lock().await.add_task(task.clone());
let client_arc = std::sync::Arc::new(tokio::sync::Mutex::new(None));
let controller = cfait::controller::TaskController::new(store.clone(), client_arc, ctx.clone());
let dest = "https://example.com/cal/dest/";
let (orig, mut updated) = store.lock().await.move_task(&task.uid, dest.to_string()).unwrap();
updated.href = String::new();
updated.etag = String::new();
let res = controller.persist_changes(vec![
cfait::journal::Action::Delete(orig),
cfait::journal::Action::Create(updated)
]).await;
assert!(
res.is_ok(),
"Expected controller persist_changes to succeed in queuing migration"
);
let src_tasks =
cfait::storage::LocalStorage::load_for_href(ctx.as_ref(), "local://src").unwrap();
assert!(
src_tasks.is_empty(),
"Source local storage should be deleted after move"
);
let j = cfait::journal::Journal::load(ctx.as_ref());
assert_eq!(j.queue.len(), 1, "Expected one action in journal (Create)");
match &j.queue[0] {
cfait::journal::Action::Create(t) => assert_eq!(t.calendar_href, dest),
other => panic!("Expected action to be Create(remote), got: {:?}", other),
}
}