use cfait::cache::Cache;
use cfait::client::RustyClient;
use cfait::context::TestContext;
use cfait::journal::{Action, Journal};
use cfait::model::Task;
use mockito::Server;
use std::collections::HashMap;
use std::sync::Arc;
#[tokio::test]
async fn test_412_resolves_via_three_way_merge_no_copy() {
let ctx = Arc::new(TestContext::new());
let mut server = Server::new_async().await;
let url = server.url();
let task_uid = "merge-uid";
let task_path = format!("/cal/{}.ics", task_uid);
let mock_412 = server
.mock("PUT", task_path.as_str())
.match_header("If-Match", "old-etag")
.with_status(412)
.create_async()
.await;
let server_ics = "BEGIN:VCALENDAR\nBEGIN:VTODO\nUID:merge-uid\nSUMMARY:Base Title\nDESCRIPTION:Server Description\nEND:VTODO\nEND:VCALENDAR"
.to_string();
let mock_fetch = server
.mock("REPORT", "/cal/")
.with_status(207)
.with_body(format!(
r#"
<d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>{}</d:href>
<d:propstat>
<d:prop>
<cal:calendar-data>{}</cal:calendar-data>
<d:getetag>"server-etag"</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
"#,
task_path, server_ics
))
.create_async()
.await;
let mock_retry_ok = server
.mock("PUT", task_path.as_str())
.match_header("If-Match", "\"server-etag\"")
.with_status(201)
.with_header("ETag", "\"new-etag\"")
.create_async()
.await;
let mock_conflict_copy = server
.mock(
"PUT",
mockito::Matcher::Regex(r"^/cal/.*\.ics$".to_string()),
)
.match_body(mockito::Matcher::Regex(r"Conflict Copy".to_string()))
.with_status(201)
.expect(0)
.create_async()
.await;
let client = RustyClient::new(ctx.clone(), &url, "u", "p", true, None).unwrap();
let mut base_task = Task::new("Base Title", &HashMap::new(), None);
base_task.uid = task_uid.to_string();
base_task.href = format!("{}{}", url, task_path);
base_task.calendar_href = format!("{}/cal/", url);
base_task.etag = "\"old-etag\"".to_string();
base_task.description = "Base Description".to_string();
Cache::save(
ctx.as_ref(),
&base_task.calendar_href,
&[base_task.clone()],
Some("token".to_string()),
)
.unwrap();
let mut local_task = base_task.clone();
local_task.summary = "Local Title".to_string();
local_task.etag = "old-etag".to_string();
Journal::push(ctx.as_ref(), Action::Update(local_task)).unwrap();
let res = tokio::time::timeout(std::time::Duration::from_secs(10), client.sync_journal()).await;
assert!(res.is_ok(), "Test timed out!");
let sync_res = res.unwrap();
assert!(sync_res.is_ok(), "Sync failed: {:?}", sync_res.err());
mock_412.assert();
mock_fetch.assert();
mock_retry_ok.assert();
mock_conflict_copy.assert();
let journal = Journal::load(ctx.as_ref());
assert!(
journal.is_empty(),
"Journal should be empty after a successful merged retry"
);
}
#[tokio::test]
async fn test_412_fetch_failure_leaves_queued_no_copy() {
let ctx = Arc::new(TestContext::new());
let mut server = Server::new_async().await;
let url = server.url();
let task_uid = "fetch-fail-uid";
let task_path = format!("/cal/{}.ics", task_uid);
let mock_412 = server
.mock("PUT", task_path.as_str())
.match_header("If-Match", "old-etag")
.with_status(412)
.create_async()
.await;
let mock_fetch_fail = server
.mock("REPORT", "/cal/")
.with_status(500)
.create_async()
.await;
let mock_conflict_copy = server
.mock(
"PUT",
mockito::Matcher::Regex(r"^/cal/.*\.ics$".to_string()),
)
.match_body(mockito::Matcher::Regex(r"Conflict Copy".to_string()))
.with_status(201)
.expect(0)
.create_async()
.await;
let client = RustyClient::new(ctx.clone(), &url, "u", "p", true, None).unwrap();
let mut base_task = Task::new("Fetch Fail Task", &HashMap::new(), None);
base_task.uid = task_uid.to_string();
base_task.href = format!("{}{}", url, task_path);
base_task.calendar_href = format!("{}/cal/", url);
base_task.etag = "\"old-etag\"".to_string();
base_task.description = "Base Description".to_string();
Cache::save(
ctx.as_ref(),
&base_task.calendar_href,
&[base_task.clone()],
Some("token".to_string()),
)
.unwrap();
let mut local_task = base_task.clone();
local_task.summary = "Local Title".to_string();
local_task.etag = "old-etag".to_string();
Journal::push(ctx.as_ref(), Action::Update(local_task)).unwrap();
let res = tokio::time::timeout(std::time::Duration::from_secs(10), client.sync_journal()).await;
assert!(res.is_ok(), "Test timed out!");
let sync_res = res.unwrap();
assert!(
sync_res.is_err(),
"Sync should surface a transient error, not silently duplicate the task"
);
mock_412.assert();
mock_fetch_fail.assert();
mock_conflict_copy.assert();
let journal = Journal::load(ctx.as_ref());
assert_eq!(
journal.queue.len(),
1,
"The update must remain queued, not be dropped or duplicated"
);
}