notedthat-write 0.7.0

Shared write path (commit, patch, replace) for NotedThat HTTP API and WebDAV surfaces
Documentation
use bytes::Bytes;

use super::support::{
    ReplaceArgs, TestStorage, conditionals, expect_replace_err, run_replace, run_replace_with,
};

#[tokio::test]
async fn single_match_happy_replaces_once() {
    let storage = TestStorage::with_body(b"hello world");
    let before = storage.read().await;
    let outcome = run_replace(&storage, ReplaceArgs::one("world", "planet"))
        .await
        .expect("single match replace succeeds");

    assert_eq!(outcome.match_count, 1);
    let after = storage.read().await;
    assert_eq!(after.bytes, Bytes::from_static(b"hello planet"));
    assert_ne!(after.meta.etag, before.meta.etag);
}

#[tokio::test]
async fn replace_all_true_across_three_non_overlapping_matches() {
    let storage = TestStorage::with_body(b"a b a b a");
    let outcome = run_replace(&storage, ReplaceArgs::all("a", "Z"))
        .await
        .expect("replace_all succeeds");

    assert_eq!(outcome.match_count, 3);
    assert_eq!(storage.read().await.bytes, Bytes::from_static(b"Z b Z b Z"));
}

#[tokio::test]
async fn zero_matches_returns_replace_no_match_and_leaves_storage_untouched() {
    let storage = TestStorage::with_body(b"foo");
    let before = storage.read().await;
    let err = expect_replace_err(run_replace(&storage, ReplaceArgs::one("bar", "baz")).await);

    assert!(matches!(err, crate::WriteError::ReplaceNoMatch));
    let after = storage.read().await;
    assert_eq!(after.bytes, before.bytes);
    assert_eq!(after.meta.etag, before.meta.etag);
}

#[tokio::test]
async fn multiple_matches_with_replace_all_false_returns_ambiguous_with_count() {
    let storage = TestStorage::with_body(b"a b a");
    let before = storage.read().await;
    let err = expect_replace_err(run_replace(&storage, ReplaceArgs::one("a", "Z")).await);

    assert!(matches!(
        err,
        crate::WriteError::ReplaceAmbiguous { count: 2 }
    ));
    let after = storage.read().await;
    assert_eq!(after.bytes, before.bytes);
    assert_eq!(after.meta.etag, before.meta.etag);
}

#[tokio::test]
async fn empty_new_string_deletes_in_place() {
    let storage = TestStorage::with_body(b"prefix_MID_suffix");
    let outcome = run_replace(&storage, ReplaceArgs::one("MID", ""))
        .await
        .expect("empty replacement succeeds");

    assert_eq!(outcome.match_count, 1);
    assert_eq!(
        storage.read().await.bytes,
        Bytes::from_static(b"prefix__suffix")
    );
}

#[tokio::test]
async fn crlf_line_ending_matches_exact_bytes_not_bare_newline() {
    let storage = TestStorage::with_body(b"a\r\nb");
    let crlf_outcome = run_replace(&storage, ReplaceArgs::one("\r\n", "|"))
        .await
        .expect("CRLF byte substring matches once");
    assert_eq!(crlf_outcome.match_count, 1);
    assert_eq!(storage.read().await.bytes, Bytes::from_static(b"a|b"));

    let storage = TestStorage::with_body(b"a\r\nb");
    let lf_outcome = run_replace(&storage, ReplaceArgs::one("\n", "|"))
        .await
        .expect("bare LF byte inside CRLF matches once");
    assert_eq!(lf_outcome.match_count, 1);
    assert_eq!(storage.read().await.bytes, Bytes::from_static(b"a\r|b"));
}

#[tokio::test]
async fn multi_byte_utf8_old_string_matches_without_splitting_codepoints() {
    let storage = TestStorage::with_body("café".as_bytes());
    let outcome = run_replace(&storage, ReplaceArgs::one("café", "cafe"))
        .await
        .expect("multi-byte UTF-8 old string matches");

    assert_eq!(outcome.match_count, 1);
    assert_eq!(storage.read().await.bytes, Bytes::from_static(b"cafe"));
}

#[tokio::test]
async fn post_splice_size_over_max_patchable_returns_patch_too_large() {
    let storage = TestStorage::with_body(b"hello world xes");
    let before = storage.read().await;
    let err = expect_replace_err(
        run_replace_with(
            &storage,
            ReplaceArgs::one("x", "ABCDEFGHIJKL"),
            conditionals(Some("etag1")),
            20,
            None,
        )
        .await,
    );

    assert!(matches!(
        err,
        crate::WriteError::PatchTooLarge {
            size: 26,
            limit: 20
        }
    ));
    let after = storage.read().await;
    assert_eq!(after.bytes, before.bytes);
    assert_eq!(after.meta.etag, before.meta.etag);
}