use super::*;
fn local_roster_artifact(core: &AppCore) -> Event {
core.build_local_identity_artifacts()
.1
.into_iter()
.map(|(_, event)| event)
.find(is_app_keys_event)
.expect("published roster")
}
#[test]
fn unchanged_local_roster_republication_keeps_one_durable_signed_head() {
let owner = Keys::generate();
let device = Keys::generate();
let directory = tempfile::TempDir::new().unwrap();
let data_dir = directory.path().to_string_lossy().to_string();
let mut core = logged_in_test_core_at_data_dir(&owner, &device, data_dir.clone());
let known = known_app_keys_from_ndr(
owner.public_key(),
&AppKeys::new(vec![DeviceEntry::new(
device.public_key(),
unix_now().get(),
)]),
unix_now().get(),
);
core.app_keys
.insert(owner.public_key().to_hex(), known.clone());
let first = local_roster_artifact(&core);
assert_eq!(
local_roster_artifact(&core),
first,
"unchanged reads reuse the signed roster"
);
core.handle_action(AppAction::CreateChat {
peer_input: Keys::generate().public_key().to_hex(),
});
core.handle_action(AppAction::CreateChat {
peer_input: owner.public_key().to_hex(),
});
core.send_message(&owner.public_key().to_hex(), "an old note", None);
core.publish_local_app_keys();
core.publish_local_app_keys_snapshot_only("test_roster_republish");
assert_eq!(
local_roster_artifact(&core),
first,
"chat activity does not create a conflicting head"
);
for pending in core
.pending_relay_publishes
.values()
.filter(|pending| pending.label == "app-keys")
{
let event: Event = serde_json::from_str(&pending.event_json).unwrap();
assert_eq!(event.id, first.id);
}
core.persist_best_effort_inner();
drop(core);
let mut restarted = logged_in_test_core_at_data_dir(&owner, &device, data_dir);
restarted
.app_keys
.insert(owner.public_key().to_hex(), known);
assert_eq!(
local_roster_artifact(&restarted),
first,
"restarting retains the exact signed head"
);
}
#[test]
fn changed_local_roster_requires_new_revision_and_reuses_imported_signed_head() {
let owner = Keys::generate();
let device = Keys::generate();
let sibling = Keys::generate();
let core = logged_in_test_core("signed-roster-revision", &owner, &device);
let now = unix_now().get();
let mut roster = AppKeys::new(vec![DeviceEntry::new(device.public_key(), now)]);
let first = core
.signed_local_app_keys_snapshot(&roster, now, &owner)
.unwrap();
roster.add_device(DeviceEntry::new(sibling.public_key(), now));
assert!(core
.signed_local_app_keys_snapshot(&roster, now, &owner)
.is_err());
let next = core
.signed_local_app_keys_snapshot(&roster, now + 1, &owner)
.unwrap();
assert_ne!(next.id, first.id);
assert_eq!(
core.signed_local_app_keys_snapshot(&roster, now + 1, &owner)
.unwrap(),
next
);
assert!(core
.signed_local_app_keys_snapshot(&roster, now, &owner)
.is_err());
let mut core = core;
let imported =
crate::core::account_signer::canonical_signer_roster(&roster, owner.public_key(), now + 2)
.sign_with_keys(&owner)
.unwrap();
core.cache_local_fips_identity(&imported);
assert_eq!(
core.signed_local_app_keys_snapshot(&roster, now + 2, &owner)
.unwrap(),
imported
);
}
#[test]
fn nostrconnect_roster_repair_refuses_real_conflicts_and_replays_one_exact_head() {
let owner = Keys::generate();
let device = Keys::generate();
let client = Keys::generate();
let now = unix_now().get();
let roster = AppKeys::new(vec![DeviceEntry::new(device.public_key(), now - 20)]);
let first = roster
.get_event_at(owner.public_key(), now - 1)
.sign_with_keys(&owner)
.unwrap();
let duplicate = roster
.get_event_at(owner.public_key(), now - 1)
.sign_with_keys(&owner)
.unwrap();
let mut core = logged_in_test_core("roster-repair", &owner, &device);
core.app_keys.insert(
owner.public_key().to_hex(),
known_app_keys_from_ndr(owner.public_key(), &roster, now - 1),
);
let relay = crate::local_relay::TestRelay::start();
let uri = crate::core::remote_signer_uri::client_connection_uri(
&client,
&[RelayUrl::parse(relay.url()).unwrap()],
"repair-challenge",
);
core.start_device_link_signer(&uri, false);
let token = core
.pending_device_link_signer
.as_ref()
.unwrap()
.token
.clone();
let heads = vec![first.clone(), duplicate.clone()];
let repaired = core
.prepare_device_link_roster_repair(&token, &heads)
.unwrap();
assert!(repaired.created_at > first.created_at);
let template = heads.iter().min_by_key(|event| event.id).unwrap();
assert_eq!(repaired.tags, template.tags);
assert_eq!(repaired.content, template.content);
assert_eq!(
core.prepare_device_link_roster_repair(&token, &heads)
.unwrap(),
repaired
);
for change in [
"membership",
"join",
"extra",
"index",
"subject",
"duplicate_subject",
"content",
] {
let mut tags = duplicate.tags.clone().to_vec();
let mut content = String::new();
match change {
"membership" => tags.push(
nostr::Tag::parse([
"device".into(),
Keys::generate().public_key().to_hex(),
now.to_string(),
])
.unwrap(),
),
"join" => {
tags.retain(|tag| tag.as_slice().first().is_none_or(|key| key != "device"));
tags.push(
nostr::Tag::parse([
"device".into(),
device.public_key().to_hex(),
now.to_string(),
])
.unwrap(),
);
}
"extra" => tags.push(nostr::Tag::parse(["extra", "value"]).unwrap()),
"index" => tags.push(nostr::Tag::public_key(Keys::generate().public_key())),
"subject" => {
tags.retain(|tag| tag.as_slice().first().is_none_or(|key| key != "i"));
tags.push(nostr::Tag::parse(["i", "not-the-profile", "subject"]).unwrap());
}
"duplicate_subject" => {
tags.push(nostr::Tag::parse(["d", &uuid::Uuid::new_v4().to_string()]).unwrap())
}
"content" => content = "unexpected".into(),
_ => unreachable!(),
}
let changed = UnsignedEvent::new(
owner.public_key(),
duplicate.created_at,
duplicate.kind,
tags,
content,
)
.sign_with_keys(&owner)
.unwrap();
assert!(
core.prepare_device_link_roster_repair(&token, &[first.clone(), changed])
.is_err(),
"accepted {change}"
);
}
core.app_keys
.get_mut(&owner.public_key().to_hex())
.unwrap()
.created_at_secs = now + 1;
assert!(
core.prepare_device_link_roster_repair(&token, &heads)
.is_err(),
"local edits made during network preparation invalidate cached repair"
);
core.stop_device_link_signer();
assert!(core
.prepare_device_link_roster_repair(&token, &heads)
.is_err());
}
#[test]
fn nostrconnect_roster_repair_never_republishes_obsolete_private_labels() {
for (wrapped, legacy_count) in [(false, 40), (true, 40), (false, 75), (true, 75)] {
let owner = Keys::generate();
let device = Keys::generate();
let client = Keys::generate();
let now = unix_now().get();
let mut devices = vec![DeviceEntry::new(device.public_key(), now - 20)];
devices.extend((0..3).map(|_| DeviceEntry::new(Keys::generate().public_key(), now - 20)));
let roster = AppKeys::new(devices);
let mut private_tag = vec![
nostr_double_ratchet::APP_KEYS_ENCRYPTED_DEVICE_LABELS_FACT,
"old-private-ciphertext",
];
if wrapped {
private_tag.insert(0, "f");
}
let heads = (0..75)
.map(|index| {
let mut unsigned = roster.get_event_at(owner.public_key(), now - 1);
if index < legacy_count {
let mut tag = private_tag
.iter()
.map(|value| value.to_string())
.collect::<Vec<_>>();
*tag.last_mut().unwrap() = format!("old-private-ciphertext-{index}");
unsigned.tags.push(nostr::Tag::parse(tag).unwrap());
}
UnsignedEvent::new(
owner.public_key(),
unsigned.created_at,
unsigned.kind,
unsigned.tags.to_vec(),
unsigned.content,
)
.sign_with_keys(&owner)
.unwrap()
})
.collect::<Vec<_>>();
let mut core = logged_in_test_core("roster-repair-private-labels", &owner, &device);
core.app_keys.insert(
owner.public_key().to_hex(),
known_app_keys_from_ndr(owner.public_key(), &roster, now - 1),
);
let relay = crate::local_relay::TestRelay::start();
let uri = crate::core::remote_signer_uri::client_connection_uri(
&client,
&[RelayUrl::parse(relay.url()).unwrap()],
"repair-challenge",
);
core.start_device_link_signer(&uri, false);
let token = core
.pending_device_link_signer
.as_ref()
.unwrap()
.token
.clone();
let repaired = core
.prepare_device_link_roster_repair(&token, &heads)
.expect("equivalent device lists with retired labels must remain linkable");
assert!(repaired.verify().is_ok());
assert!(repaired.created_at > heads[0].created_at);
let members = |keys: &AppKeys| {
keys.get_all_devices()
.into_iter()
.map(|entry| (entry.identity_pubkey, entry.created_at))
.collect::<BTreeMap<_, _>>()
};
assert_eq!(
members(&AppKeys::from_event(&repaired).unwrap()),
members(&roster)
);
assert!(!serde_json::to_string(&repaired)
.unwrap()
.contains("encrypted_device_labels"));
assert!(!serde_json::to_string(&repaired)
.unwrap()
.contains("old-private-ciphertext"));
assert_eq!(
core.prepare_device_link_roster_repair(&token, &heads)
.unwrap(),
repaired
);
}
}
#[test]
fn device_link_diagnostics_survive_background_receipt_traffic() {
let owner = Keys::generate();
let device = Keys::generate();
let mut core = logged_in_test_core("link-diagnostics", &owner, &device);
core.debug_log.clear();
core.push_debug_log("device_link.approval", "checking_device_list");
core.push_debug_log("device_link.approval", "device_list_conflict");
for index in 0..512 {
core.push_debug_log("fips_nearby.receipt", format!("background receipt {index}"));
}
let snapshot = core.build_runtime_debug_snapshot();
assert!(snapshot
.recent_log
.iter()
.any(|entry| entry.detail == "device_list_conflict"));
assert!(snapshot.recent_log.len() <= MAX_DEBUG_LOG_ENTRIES);
for _ in 0..512 {
core.push_debug_log("device_link.approval", "connecting");
}
assert!(core.debug_log.len() <= MAX_DEBUG_LOG_ENTRIES);
}