v1.0.8 (fixes #29 fixes #30 fixes #31)

This commit is contained in:
Maurus Decimus
2026-08-16 12:29:05 +02:00
parent c4e06bd87d
commit 1cc633f39c
43 changed files with 4082 additions and 686 deletions
+161
View File
@@ -689,6 +689,167 @@ fn prune_non_tree_type_contact_card() {
seeder::teardown(base_url()).expect("teardown");
}
const ISSUE30_CARD_UID: &str = "vandelay-issue30-card";
const ISSUE30_EVENT_UID: &str = "vandelay-issue30-event";
fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> Vec<u8> {
use base64::Engine;
let uri = resource
.get(uri_key)
.and_then(Value::as_str)
.unwrap_or_else(|| panic!("no {uri_key} on {resource}"));
assert!(
resource.get("blobId").is_none(),
"target must not hold a blobId for {uri_key}: {resource}"
);
let prefix = format!("data:{expect_media_type};base64,");
let payload = uri
.strip_prefix(&prefix)
.unwrap_or_else(|| panic!("{uri_key} is not a {prefix}... data URI: {resource}"));
base64::engine::general_purpose::STANDARD
.decode(payload)
.expect("base64 payload")
}
#[test]
#[ignore = "requires Docker"]
fn export_inlines_contact_and_event_blobs_instead_of_blob_ids() {
let fx = seeder::provision(base_url()).expect("provision");
let tgt = fx.account("test6").expect("test6");
let archive = tmp_archive("issue30");
let photo: &[u8] = b"\x89PNG\r\n\x1a\nvandelay issue 30 contact photo bytes";
let agenda: &[u8] = b"vandelay issue 30 calendar enclosure bytes";
{
let conn = vandelay::db::init::open(&archive).expect("init archive");
conn.execute(
"INSERT INTO address_books (id,name,is_default) VALUES (1,'Issue30 Book',1)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO calendars (id,name,is_default) VALUES (1,'Issue30 Calendar',1)",
[],
)
.unwrap();
let photo_blob = vandelay::db::blobs::intern_blob(&conn, photo).unwrap();
let agenda_blob = vandelay::db::blobs::intern_blob(&conn, agenda).unwrap();
let card = json!({
"@type": "Card",
"version": "1.0",
"name": { "full": "Issue Thirty" },
"media": { "photo": {
"@type": "Media", "kind": "photo",
"@blob": photo_blob, "mediaType": "image/png"
} }
})
.to_string();
conn.execute(
"INSERT INTO contact_cards (id,uid,address_book_ids,data) VALUES (1,?1,'[1]',?2)",
[ISSUE30_CARD_UID, card.as_str()],
)
.unwrap();
let event = json!({
"@type": "Event",
"uid": ISSUE30_EVENT_UID,
"title": "Issue 30 review",
"start": "2026-03-01T09:00:00",
"duration": "PT1H",
"timeZone": "Etc/UTC",
"links": { "1": {
"@type": "Link", "rel": "enclosure",
"@blob": agenda_blob, "contentType": "text/plain", "title": "agenda.txt"
} }
})
.to_string();
conn.execute(
"INSERT INTO calendar_events (id,calendar_ids,is_draft,use_default_alerts,data)
VALUES (1,'[1]',0,0,?1)",
[event.as_str()],
)
.unwrap();
}
let summary = sync::export::run(
common(&archive, false),
export_cfg("test6", &tgt.account_id, false),
)
.expect("export");
for name in ["ContactCard", "CalendarEvent"] {
let counts = summary
.per_type
.iter()
.find(|(t, _)| *t == name)
.map(|(_, c)| c.clone())
.unwrap_or_else(|| panic!("{name} counts missing: {summary:?}"));
assert_eq!(counts.created, 1, "{name} created exactly one: {counts:?}");
assert_eq!(counts.failed, 0, "{name} had no failures: {counts:?}");
}
let client = HttpClient::new(basic("test6"), RetryPolicy::new(5), true);
let session = Session::discover(&client, base_url()).expect("discover target session");
let api = session.api_url.clone();
let cards = client
.post_json(
&api,
&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:contacts"],
"methodCalls": [["ContactCard/get",
{ "accountId": tgt.account_id, "ids": Value::Null }, "g"]]
}),
)
.expect("ContactCard/get");
let card = cards
.pointer("/methodResponses/0/1/list")
.and_then(Value::as_array)
.expect("ContactCard list")
.iter()
.find(|c| c.get("uid").and_then(Value::as_str) == Some(ISSUE30_CARD_UID))
.unwrap_or_else(|| panic!("exported card not found on the target: {cards}"))
.clone();
let photo_resource = card
.pointer("/media/photo")
.unwrap_or_else(|| panic!("card has no media.photo: {card}"));
assert_eq!(
data_uri_bytes(photo_resource, "uri", "image/png"),
photo,
"contact photo bytes survived the export: {card}"
);
let events = client
.post_json(
&api,
&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:calendars"],
"methodCalls": [["CalendarEvent/get",
{ "accountId": tgt.account_id, "ids": Value::Null }, "g"]]
}),
)
.expect("CalendarEvent/get");
let event = events
.pointer("/methodResponses/0/1/list")
.and_then(Value::as_array)
.expect("CalendarEvent list")
.iter()
.find(|e| e.get("uid").and_then(Value::as_str) == Some(ISSUE30_EVENT_UID))
.unwrap_or_else(|| panic!("exported event not found on the target: {events}"))
.clone();
let link = event
.get("links")
.and_then(Value::as_object)
.and_then(|m| m.values().next())
.unwrap_or_else(|| panic!("event has no links: {event}"));
assert_eq!(
data_uri_bytes(link, "href", "text/plain"),
agenda,
"calendar enclosure bytes survived the export: {event}"
);
let _ = std::fs::remove_file(&archive);
seeder::teardown(base_url()).expect("teardown");
}
#[test]
#[ignore = "requires Docker"]
fn export_round_trip_and_convergence() {