aven-core 0.1.36

Core library for the Aven local-first task manager
Documentation
use super::test_support::{make_change_wire, test_workspace};
use super::*;
use crate::change_log::{ChangePayload, op_type};

#[test]
fn sync_timestamps_accept_fractional_utc_precision() {
    validate_timestamp_value("created_at", "2026-07-28T06:41:44Z").unwrap();
    validate_timestamp_value("created_at", "2026-07-28T06:41:44.000000Z").unwrap();
    validate_timestamp_value("created_at", "2026-07-28T06:41:44.123456789Z").unwrap();
}

#[test]
fn sync_timestamps_reject_offsets_and_invalid_calendar_values() {
    for value in ["2026-07-28T08:41:44+02:00", "2026-13-28T06:41:44Z", "today"] {
        assert!(validate_timestamp_value("created_at", value).is_err());
    }
}

fn attachment_add_payload() -> Value {
    ChangePayload::workspace(&test_workspace())
        .set("attachment_id", "7KQ9A1X4MV2P8D6R")
        .set(
            "sha256",
            "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
        )
        .set("byte_size", 12_i64)
        .set("media_type", "image/png")
        .set("filename", None::<&str>)
        .set("alt_text", None::<&str>)
        .set("width", 320_i64)
        .set("height", 240_i64)
        .set("created_at", "2026-06-01T00:00:00Z")
        .into_value()
}

fn attachment_change(op_type: &str, payload: Value) -> ChangeWire {
    let mut change = make_change_wire(op_type, "task", "BBBBBBBBBBBBBBBB", payload);
    change.field = Some("attachments".to_string());
    change
}

#[test]
fn constructed_attachment_payloads_parse_without_json_or_protocol_changes() {
    let add_value = attachment_add_payload();
    let add = AttachmentAddPayload::from_change(&attachment_change(
        op_type::ATTACHMENT_ADD,
        add_value.clone(),
    ))
    .unwrap();
    assert_eq!(serde_json::to_value(add).unwrap(), add_value);

    let delete_value = ChangePayload::workspace(&test_workspace())
        .set("attachment_id", "7KQ9A1X4MV2P8D6R")
        .set("deleted_at", "2026-06-01T00:00:00Z")
        .into_value();
    let delete = AttachmentDeletePayload::from_change(&attachment_change(
        op_type::ATTACHMENT_DELETE,
        delete_value.clone(),
    ))
    .unwrap();
    assert_eq!(serde_json::to_value(delete).unwrap(), delete_value);
}

#[test]
fn attachment_payloads_accept_extensions_and_missing_optional_text() {
    let mut payload = attachment_add_payload();
    let object = payload.as_object_mut().unwrap();
    object.remove("filename");
    object.remove("alt_text");
    object.insert("extension".to_string(), serde_json::json!({ "version": 1 }));

    AttachmentAddPayload::from_change(&attachment_change(op_type::ATTACHMENT_ADD, payload))
        .unwrap();
}

#[test]
fn attachment_payload_authority_rejects_malformed_fields() {
    let cases = [
        ("workspace_id", Value::Null, "workspace_id"),
        (
            "attachment_id",
            Value::String("short".to_string()),
            "attachment_id",
        ),
        (
            "sha256",
            Value::String("short".to_string()),
            "invalid-sha256",
        ),
        ("byte_size", Value::String("12".to_string()), "byte_size"),
        ("byte_size", Value::from(-1), "invalid-attachment-size"),
        (
            "media_type",
            Value::String("image/svg+xml".to_string()),
            "unsupported-attachment-media-type",
        ),
        (
            "filename",
            Value::String("bad/name.png".to_string()),
            "invalid-attachment-filename",
        ),
        (
            "alt_text",
            Value::String("bad\nalt".to_string()),
            "invalid-attachment-alt-text",
        ),
        ("height", Value::Null, "invalid-attachment-dimensions"),
        ("width", Value::from(0), "invalid-attachment-dimensions"),
        (
            "created_at",
            Value::String("today".to_string()),
            "invalid-timestamp",
        ),
    ];
    for (key, value, expected) in cases {
        let mut payload = attachment_add_payload();
        payload[key] = value;
        let error =
            AttachmentAddPayload::from_change(&attachment_change(op_type::ATTACHMENT_ADD, payload))
                .unwrap_err();
        assert!(
            error.to_string().contains(expected),
            "{key} error did not contain {expected}: {error}"
        );
    }

    let mut wrong_field = attachment_change(op_type::ATTACHMENT_ADD, attachment_add_payload());
    wrong_field.field = Some("description".to_string());
    assert!(
        AttachmentAddPayload::from_change(&wrong_field)
            .unwrap_err()
            .to_string()
            .contains("field=attachments")
    );

    let mut invalid_entity = attachment_change(op_type::ATTACHMENT_ADD, attachment_add_payload());
    invalid_entity.entity_id = "invalid".to_string();
    assert!(
        AttachmentAddPayload::from_change(&invalid_entity)
            .unwrap_err()
            .to_string()
            .contains("entity_id")
    );
}