rc-core 0.1.30

Core library for rustfs-cli S3 CLI client
Documentation
use jiff::Timestamp;
use rc_core::{CopyObjectOptions, ObjectVersion, UndoAction, plan_object_undo};

fn version(
    key: &str,
    version_id: &str,
    modified: &str,
    is_latest: bool,
    is_delete_marker: bool,
) -> ObjectVersion {
    ObjectVersion {
        key: key.to_string(),
        version_id: version_id.to_string(),
        is_latest,
        is_delete_marker,
        last_modified: Some(modified.parse::<Timestamp>().expect("valid timestamp")),
        size_bytes: (!is_delete_marker).then_some(12),
        etag: (!is_delete_marker).then(|| format!("etag-{version_id}")),
    }
}

#[test]
fn latest_delete_marker_plans_exact_marker_removal() {
    let history = vec![
        version(
            "report.txt",
            "marker-v3",
            "2026-07-23T03:00:00Z",
            true,
            true,
        ),
        version(
            "report.txt",
            "data-v2",
            "2026-07-23T02:00:00Z",
            false,
            false,
        ),
    ];

    let plan = plan_object_undo("report.txt", &history, None).expect("reversible delete");

    assert_eq!(plan.expected_latest_version_id, "marker-v3");
    assert_eq!(
        plan.action,
        UndoAction::RemoveDeleteMarker {
            marker_version_id: "marker-v3".to_string(),
            revealed_version_id: "data-v2".to_string(),
        }
    );
}

#[test]
fn latest_overwrite_plans_previous_data_version_restore() {
    let history = vec![
        version("report.txt", "data-v3", "2026-07-23T03:00:00Z", true, false),
        version(
            "report.txt",
            "data-v2",
            "2026-07-23T02:00:00Z",
            false,
            false,
        ),
        version(
            "report.txt",
            "data-v1",
            "2026-07-23T01:00:00Z",
            false,
            false,
        ),
    ];

    let plan = plan_object_undo("report.txt", &history, None).expect("reversible overwrite");

    assert_eq!(plan.expected_latest_version_id, "data-v3");
    assert_eq!(
        plan.action,
        UndoAction::RestoreVersion {
            source_version_id: "data-v2".to_string(),
        }
    );
}

#[test]
fn explicit_historical_data_version_is_restored() {
    let history = vec![
        version("report.txt", "data-v3", "2026-07-23T03:00:00Z", true, false),
        version(
            "report.txt",
            "data-v2",
            "2026-07-23T02:00:00Z",
            false,
            false,
        ),
        version(
            "report.txt",
            "data-v1",
            "2026-07-23T01:00:00Z",
            false,
            false,
        ),
    ];

    let plan = plan_object_undo("report.txt", &history, Some("data-v1")).expect("explicit restore");

    assert_eq!(
        plan.action,
        UndoAction::RestoreVersion {
            source_version_id: "data-v1".to_string(),
        }
    );
}

#[test]
fn missing_or_ambiguous_history_is_rejected() {
    let first_put = vec![version(
        "report.txt",
        "data-v1",
        "2026-07-23T01:00:00Z",
        true,
        false,
    )];
    assert!(plan_object_undo("report.txt", &first_put, None).is_err());

    let two_latest = vec![
        version("report.txt", "data-v2", "2026-07-23T02:00:00Z", true, false),
        version("report.txt", "data-v1", "2026-07-23T01:00:00Z", true, false),
    ];
    assert!(plan_object_undo("report.txt", &two_latest, None).is_err());

    let tied_predecessors = vec![
        version("report.txt", "data-v3", "2026-07-23T03:00:00Z", true, false),
        version(
            "report.txt",
            "data-v2a",
            "2026-07-23T02:00:00Z",
            false,
            false,
        ),
        version(
            "report.txt",
            "data-v2b",
            "2026-07-23T02:00:00Z",
            false,
            false,
        ),
    ];
    assert!(plan_object_undo("report.txt", &tied_predecessors, None).is_err());
}

#[test]
fn overwrite_after_delete_marker_is_refused_instead_of_guessing() {
    let history = vec![
        version("report.txt", "data-v3", "2026-07-23T03:00:00Z", true, false),
        version(
            "report.txt",
            "marker-v2",
            "2026-07-23T02:00:00Z",
            false,
            true,
        ),
        version(
            "report.txt",
            "data-v1",
            "2026-07-23T01:00:00Z",
            false,
            false,
        ),
    ];

    let error = plan_object_undo("report.txt", &history, None).expect_err("ambiguous undo");
    assert!(error.to_string().contains("delete marker"));
}

#[test]
fn copy_options_reject_empty_source_version_ids() {
    assert!(CopyObjectOptions::for_source_version(Some(String::new())).is_err());
    assert_eq!(
        CopyObjectOptions::for_source_version(Some("data-v1".to_string()))
            .expect("valid source version")
            .source_version_id
            .as_deref(),
        Some("data-v1")
    );
}