cfait 1.0.4

Powerful, fast and elegant task / TODO manager. (GUI & TUI, CalDAV & local)
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later
//! Tests for synchronization safety and 404 recovery.
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_safety_resurrection_on_404() {
    let ctx = Arc::new(TestContext::new());

    let mut server = Server::new_async().await;
    let url = server.url();
    let task_path = "/cal/important_task.ics";

    let mock_update_404 = server
        .mock("PUT", task_path)
        .with_status(404)
        .create_async()
        .await;

    let mock_resurrect = server
        .mock("PUT", task_path)
        .match_header("If-Match", mockito::Matcher::Missing)
        .with_status(201)
        .with_header("ETag", "\"new-etag\"")
        .create_async()
        .await;

    let client = RustyClient::new(ctx.clone(), &url, "u", "p", true, None).unwrap();

    let mut task = Task::new("Important Work", &HashMap::new(), None);
    task.uid = "important_task".to_string();
    task.href = format!("{}{}", url, task_path);
    task.calendar_href = format!("{}/cal/", url);
    task.etag = "\"old-etag\"".to_string();

    Journal::push(ctx.as_ref(), Action::Update(task)).unwrap();

    let res = client.sync_journal().await;

    assert!(
        res.is_ok(),
        "Sync should succeed by converting Update to Create"
    );
    mock_update_404.assert();
    mock_resurrect.assert();
}

#[tokio::test]
async fn test_safety_conflict_copy_on_hard_412() {
    let ctx = Arc::new(TestContext::new());

    let mut server = Server::new_async().await;
    let url = server.url();
    let task_path = "/cal/conflict.ics";

    let mock_412 = server
        .mock("PUT", task_path)
        .with_status(412)
        .create_async()
        .await;

    let _mock_get = server
        .mock("PROPFIND", "/cal/")
        .match_header("depth", "1")
        .with_status(207)
        .with_body(format!(
            r#"
            <d:multistatus xmlns:d="DAV:">
                <d:response>
                    <d:href>{}</d:href>
                    <d:propstat>
                        <d:prop><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
        ))
        .create_async()
        .await;

    let server_ics = "BEGIN:VCALENDAR\nBEGIN:VTODO\nUID:conflict\nSUMMARY:Local Version\nDESCRIPTION:Server Change\nEND:VTODO\nEND:VCALENDAR".to_string();

    let _mock_fetch_item = 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_create_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)
        .create_async()
        .await;

    let client = RustyClient::new(ctx.clone(), &url, "u", "p", true, None).unwrap();

    let mut base_task = Task::new("Local Version", &HashMap::new(), None);
    base_task.uid = "conflict".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();

    cfait::cache::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.description = "Local Change".to_string();

    Journal::push(ctx.as_ref(), Action::Update(local_task)).unwrap();

    let res = tokio::time::timeout(std::time::Duration::from_secs(5), 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_create_copy.assert();
}