mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
🤖 I’m Larry, updating this description on Logan’s behalf. ## Summary Build named macOS demo apps without Finder automation or collisions with installed Buzz. `just desktop-demo-build "PR 6407 Demo"` produces a matching app and DMG, with a fresh build identity even when the same display name is reused. - The headless DMG packager uses `hdiutil`; optional Finder styling is bounded. The existing production release recipe is unchanged. - Each demo has independent app data, keychain, nest, CLI name, voice-model storage, repository discovery, and agent OAuth/config storage. Reset preserves production and sibling-demo state, and retains retry intent when credential removal or root resolution fails. - Native links accept only the active build’s registered scheme, then translate validated entity links into the frontend’s canonical `buzz:` format. - The recipe builds all six executable sidecars. Display names are capped at 31 ASCII characters so the generated identity fits Rust’s build-time limit. **Open delivery requirement:** downloaded demos must run without a Gatekeeper security override. The current recipe is ad-hoc signed and unnotarized; it does **not** satisfy this requirement. Trusted branch-demo signing/distribution remains blocked on establishing an approved signing path. This PR is not being presented as complete download-and-run delivery. ### Related issue N/A — reported in the Buzz DMG-packaging workstream. ### Testing At `11ce21ff97cb387ad676e7caa65b00964097d0bb`, macOS Blox passed the Tauri workspace suite and compiled-flags gate (including the full named-demo state; each library pass: 2,992 passed, 19 ignored), Tauri all-target clippy, the full `buzz-agent` package suite, and frontend lint/typecheck plus 5,733 tests. Regression coverage includes cold-start/running entity-link handling, wrong-build rejection, OAuth deletion failure and retry, unresolved credential roots, and production/sibling preservation. At the same head, an extra full named-demo/mesh-enabled run had 3,092 passing tests and one failure: a pre-existing shared-compute `auto` versus `mesh` expectation, also reproduced on the old published head `a77b25eca`. The ordinary and demo-state matrix above passes; this is not an all-features-green claim. Live macOS Launch Services delivery remains unverified. GitHub CI completed with 30 successful checks and 9 skipped. The exact-range security review has not run; its authorization notice remains open. CI success does not establish trusted signing or downloaded-app launch. Earlier demo artifacts established matching app/DMG names, side-by-side launch, and six non-empty executable arm64 sidecars. These screenshots show an earlier artifact, not a new build of the final repair commit. Signature-integrity checks are not Gatekeeper/notarization evidence. <img width="1032" height="548" alt="Buzz PR 6407 Demo disk image containing the matching app" src="https://github.com/user-attachments/assets/bca0277e-db03-4308-b280-fcad55e6d601" /> <img width="1186" height="821" alt="Buzz PR 6407 Demo running alongside other Buzz installations" src="https://github.com/user-attachments/assets/b4bf4ae5-c341-4e15-8090-9d2ea7c623b6" /> --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz> Co-authored-by: Larry <loganj+sandbox-larry@squareup.com> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
607 lines
27 KiB
Rust
607 lines
27 KiB
Rust
use url::Url;
|
|
|
|
use super::{
|
|
canonical_entity_deep_link, parse_add_community_deep_link, parse_channel_deep_link,
|
|
parse_entity_deep_link, parse_join_deep_link, parse_message_deep_link,
|
|
parse_nostr_bind_deep_link, PendingCommunityDeepLink, PendingCommunityDeepLinks,
|
|
PendingEntityDeepLinks, PendingNavigationDeepLink, PendingNavigationDeepLinks,
|
|
ENTITY_LINK_TABS,
|
|
};
|
|
|
|
fn entity_link_golden() -> serde_json::Value {
|
|
serde_json::from_str(include_str!("../../../test-fixtures/entity-links.json"))
|
|
.expect("valid entity-links golden fixture")
|
|
}
|
|
|
|
#[test]
|
|
fn demo_entity_transport_produces_the_frontend_golden_contract() {
|
|
let golden = entity_link_golden();
|
|
let scheme = "buzz-demo-board-1234567812345678";
|
|
for canonical in golden["links"].as_object().unwrap().values() {
|
|
let canonical = canonical.as_str().unwrap();
|
|
let transport = Url::parse(&canonical.replacen("buzz:", &format!("{scheme}:"), 1)).unwrap();
|
|
let href = canonical_entity_deep_link(&transport, scheme).unwrap();
|
|
// This same fixture is parsed and routed by the frontend entity tests.
|
|
assert_eq!(href, canonical);
|
|
let queue = PendingEntityDeepLinks::default();
|
|
let pending = queue.enqueue(href);
|
|
assert_eq!(queue.first().unwrap().href, canonical);
|
|
assert!(queue.acknowledge(&pending.id));
|
|
assert!(queue.first().is_none());
|
|
assert!(canonical_entity_deep_link(&transport, "buzz").is_none());
|
|
assert!(
|
|
canonical_entity_deep_link(&transport, "buzz-demo-other-8765432187654321").is_none()
|
|
);
|
|
assert!(canonical_entity_deep_link(&Url::parse(canonical).unwrap(), scheme).is_none());
|
|
assert_eq!(
|
|
canonical_entity_deep_link(&Url::parse(canonical).unwrap(), "buzz").as_deref(),
|
|
Some(canonical)
|
|
);
|
|
}
|
|
let invalid = Url::parse(&format!("{scheme}://repo?owner=bad&d=repo")).unwrap();
|
|
assert!(canonical_entity_deep_link(&invalid, scheme).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_entity_deep_link_accepts_every_share_link_shape() {
|
|
let golden = entity_link_golden();
|
|
let owner = golden["owner"].as_str().unwrap();
|
|
let dtag = golden["dtag"].as_str().unwrap();
|
|
for raw in golden["links"]
|
|
.as_object()
|
|
.unwrap()
|
|
.values()
|
|
.map(|value| value.as_str().unwrap().to_owned())
|
|
.chain(golden["tabs"].as_array().unwrap().iter().map(|tab| {
|
|
format!(
|
|
"buzz://repo?owner={owner}&d={dtag}&tab={}",
|
|
tab.as_str().unwrap()
|
|
)
|
|
}))
|
|
{
|
|
assert!(
|
|
parse_entity_deep_link(&Url::parse(&raw).unwrap()).is_some(),
|
|
"{raw}"
|
|
);
|
|
}
|
|
let commit_link = format!(
|
|
"buzz://repo?owner={owner}&d={dtag}&tab=commits&commit={}",
|
|
golden["eventId"].as_str().unwrap()
|
|
);
|
|
assert!(parse_entity_deep_link(&Url::parse(&commit_link).unwrap()).is_some());
|
|
let expected_tabs = golden["tabs"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|tab| tab.as_str().unwrap())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(ENTITY_LINK_TABS.as_slice(), expected_tabs);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_entity_deep_link_rejects_malformed_and_non_canonical_links() {
|
|
let golden = entity_link_golden();
|
|
let owner = golden["owner"].as_str().unwrap();
|
|
let event_id = golden["eventId"].as_str().unwrap();
|
|
for raw in [
|
|
// Missing or malformed identifiers.
|
|
format!("buzz://repo?owner={owner}"),
|
|
"buzz://repo?owner=nope&d=buzz-world".to_owned(),
|
|
format!("buzz://repo?owner={owner}&d=.hidden"),
|
|
format!("buzz://repo?owner={owner}&d=has%20space"),
|
|
format!("buzz://pr?owner={owner}&d=buzz-world"),
|
|
format!("buzz://pr?id=short&owner={owner}&d=buzz-world"),
|
|
// Coordinate links take no event id.
|
|
format!("buzz://repo?id={event_id}&owner={owner}&d=buzz-world"),
|
|
// Non-canonical: unknown param, duplicate param, path, fragment.
|
|
format!("buzz://repo?owner={owner}&d=buzz-world&relay=wss%3A%2F%2Fx.example"),
|
|
format!("buzz://repo?owner={owner}&owner={owner}&d=buzz-world"),
|
|
// Unknown tab value, duplicate tab, and tab on an event link.
|
|
format!("buzz://repo?owner={owner}&d=buzz-world&tab=overview"),
|
|
format!("buzz://repo?owner={owner}&d=buzz-world&tab=prs&tab=prs"),
|
|
format!("buzz://repo?owner={owner}&d=buzz-world&tab=files&commit={event_id}"),
|
|
format!("buzz://repo?owner={owner}&d=buzz-world&tab=commits&commit=short"),
|
|
format!("buzz://pr?id={event_id}&owner={owner}&d=buzz-world&tab=prs"),
|
|
format!("buzz://repo/extra?owner={owner}&d=buzz-world"),
|
|
format!("buzz://repo?owner={owner}&d=buzz-world#top"),
|
|
// Not an entity host.
|
|
format!("buzz://message?owner={owner}&d=buzz-world"),
|
|
] {
|
|
assert!(
|
|
parse_entity_deep_link(&Url::parse(&raw).unwrap()).is_none(),
|
|
"{raw}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn pending(id: &str, relay_url: &str, code: Option<&str>) -> PendingCommunityDeepLink {
|
|
PendingCommunityDeepLink {
|
|
id: id.to_owned(),
|
|
kind: if code.is_some() { "join" } else { "connect" }.to_owned(),
|
|
relay_url: relay_url.to_owned(),
|
|
code: code.map(str::to_owned),
|
|
policy_receipt: None,
|
|
name: None,
|
|
}
|
|
}
|
|
|
|
fn pending_navigation(
|
|
id: &str,
|
|
kind: &str,
|
|
channel_id: &str,
|
|
message_id: Option<&str>,
|
|
thread_root_id: Option<&str>,
|
|
) -> PendingNavigationDeepLink {
|
|
PendingNavigationDeepLink {
|
|
id: id.to_owned(),
|
|
kind: kind.to_owned(),
|
|
channel_id: channel_id.to_owned(),
|
|
message_id: message_id.map(str::to_owned),
|
|
thread_root_id: thread_root_id.map(str::to_owned),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pending_navigation_links_are_fifo_acknowledged_and_deduplicated() {
|
|
let queue = PendingNavigationDeepLinks::default();
|
|
queue.enqueue(pending_navigation(
|
|
"first",
|
|
"channel",
|
|
"channel-1",
|
|
None,
|
|
None,
|
|
));
|
|
queue.enqueue(pending_navigation(
|
|
"duplicate",
|
|
"channel",
|
|
"channel-1",
|
|
None,
|
|
None,
|
|
));
|
|
queue.enqueue(pending_navigation(
|
|
"second",
|
|
"message",
|
|
"channel-1",
|
|
Some("message-1"),
|
|
Some("root-1"),
|
|
));
|
|
|
|
assert_eq!(queue.first().unwrap().id, "first");
|
|
assert!(!queue.acknowledge("second"));
|
|
assert!(queue.acknowledge("first"));
|
|
assert_eq!(queue.first().unwrap().id, "second");
|
|
assert!(queue.acknowledge("second"));
|
|
assert!(queue.first().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pending_navigation_links_can_be_cleared() {
|
|
let queue = PendingNavigationDeepLinks::default();
|
|
queue.enqueue(pending_navigation(
|
|
"first",
|
|
"channel",
|
|
"channel-1",
|
|
None,
|
|
None,
|
|
));
|
|
queue.enqueue(pending_navigation(
|
|
"second",
|
|
"message",
|
|
"channel-1",
|
|
Some("message-1"),
|
|
None,
|
|
));
|
|
|
|
queue.clear();
|
|
assert!(queue.first().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pending_navigation_queue_recovers_after_mutex_poisoning() {
|
|
let queue = std::sync::Arc::new(PendingNavigationDeepLinks::default());
|
|
let poisoner = std::sync::Arc::clone(&queue);
|
|
assert!(std::thread::spawn(move || {
|
|
let _guard = poisoner.0.lock().unwrap();
|
|
panic!("poison queue for recovery regression");
|
|
})
|
|
.join()
|
|
.is_err());
|
|
|
|
queue.enqueue(pending_navigation(
|
|
"after-poison",
|
|
"channel",
|
|
"channel-1",
|
|
None,
|
|
None,
|
|
));
|
|
assert_eq!(queue.first().unwrap().id, "after-poison");
|
|
assert!(queue.acknowledge("after-poison"));
|
|
assert!(queue.first().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pending_join_serializes_policy_receipt_for_cold_launch_recovery() {
|
|
let mut link = pending("join", "wss://relay.example", Some("invite"));
|
|
link.policy_receipt = Some("relay-signed-receipt".to_owned());
|
|
|
|
let payload = serde_json::to_value(link).unwrap();
|
|
assert_eq!(payload["policyReceipt"], "relay-signed-receipt");
|
|
}
|
|
|
|
#[test]
|
|
fn pending_community_links_are_fifo_and_acknowledged_in_order() {
|
|
let queue = PendingCommunityDeepLinks::default();
|
|
queue.enqueue(pending("first", "wss://one.example", Some("one")));
|
|
queue.enqueue(pending("second", "wss://two.example", Some("two")));
|
|
assert_eq!(queue.first().unwrap().id, "first");
|
|
assert!(!queue.acknowledge("second"));
|
|
assert!(queue.acknowledge("first"));
|
|
assert_eq!(queue.first().unwrap().id, "second");
|
|
}
|
|
|
|
#[test]
|
|
fn pending_community_links_dedupe_exact_intents() {
|
|
let queue = PendingCommunityDeepLinks::default();
|
|
queue.enqueue(pending("first", "wss://one.example", Some("one")));
|
|
queue.enqueue(pending("duplicate", "wss://one.example", Some("one")));
|
|
assert!(queue.acknowledge("first"));
|
|
assert!(queue.first().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pending_entity_links_survive_until_acknowledged_in_order() {
|
|
let queue = PendingEntityDeepLinks::default();
|
|
let first = queue.enqueue("buzz://project?owner=aa&d=first".to_owned());
|
|
let second = queue.enqueue("buzz://project?owner=aa&d=second".to_owned());
|
|
|
|
assert_eq!(queue.first(), Some(first.clone()));
|
|
assert!(!queue.acknowledge(&second.id));
|
|
assert!(queue.acknowledge(&first.id));
|
|
assert_eq!(queue.first(), Some(second));
|
|
}
|
|
|
|
#[test]
|
|
fn pending_entity_links_dedupe_launch_and_open_callbacks() {
|
|
let queue = PendingEntityDeepLinks::default();
|
|
let href = "buzz://project?owner=aa&d=buzz".to_owned();
|
|
let first = queue.enqueue(href.clone());
|
|
let duplicate = queue.enqueue(href);
|
|
|
|
assert_eq!(duplicate.id, first.id);
|
|
assert!(queue.acknowledge(&first.id));
|
|
assert!(queue.first().is_none());
|
|
}
|
|
|
|
fn valid_nostr_bind_url() -> Url {
|
|
Url::parse(
|
|
"buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard",
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn parse_add_community_deep_link_extracts_relay_and_name() {
|
|
let url = Url::parse(
|
|
"buzz://add-community?relay=wss%3A%2F%2Facme.communities.buzz.xyz&name=Acme%20Team&ignored=value",
|
|
)
|
|
.unwrap();
|
|
let payload = parse_add_community_deep_link(&url).unwrap();
|
|
assert_eq!(payload.relay_url, "wss://acme.communities.buzz.xyz");
|
|
assert_eq!(payload.name.as_deref(), Some("Acme Team"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_add_community_deep_link_accepts_an_omitted_or_empty_name() {
|
|
for raw in [
|
|
"buzz://add-community?relay=wss%3A%2F%2Facme.example",
|
|
"buzz://add-community?relay=wss%3A%2F%2Facme.example&name=",
|
|
] {
|
|
assert!(parse_add_community_deep_link(&Url::parse(raw).unwrap())
|
|
.unwrap()
|
|
.name
|
|
.is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_add_community_deep_link_rejects_invalid_relays() {
|
|
for raw in [
|
|
"buzz://add-community",
|
|
"buzz://add-community?relay=",
|
|
"buzz://add-community?relay=not-a-url",
|
|
"buzz://add-community?relay=https%3A%2F%2Facme.example",
|
|
"buzz://add-community?relay=wss%3A%2F%2F",
|
|
] {
|
|
assert!(parse_add_community_deep_link(&Url::parse(raw).unwrap()).is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_channel_deep_link_accepts_one_path_segment() {
|
|
let url = Url::parse("buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32").unwrap();
|
|
let payload = parse_channel_deep_link(&url).unwrap();
|
|
assert_eq!(payload["channelId"], "580ca78b-9dae-46f3-8854-bd671853ba32");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_channel_deep_link_accepts_message_path() {
|
|
let message_id = "8455293f0123456789abcdef0123456789abcdef0123456789abcdef01234567";
|
|
let url = Url::parse(&format!(
|
|
"buzz://channel/a372f080-5961-4535-b1a3-edffface377d/{message_id}"
|
|
))
|
|
.unwrap();
|
|
let payload = parse_channel_deep_link(&url).unwrap();
|
|
assert_eq!(payload["channelId"], "a372f080-5961-4535-b1a3-edffface377d");
|
|
assert_eq!(payload["messageId"], message_id);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_channel_deep_link_accepts_v7_and_normalizes_uppercase() {
|
|
for (raw, expected) in [
|
|
(
|
|
"buzz://channel/018fdb5d-3a64-7c35-b5f9-4a23e1f9d2d9",
|
|
"018fdb5d-3a64-7c35-b5f9-4a23e1f9d2d9",
|
|
),
|
|
(
|
|
"buzz://channel/580CA78B-9DAE-46F3-8854-BD671853BA32",
|
|
"580ca78b-9dae-46f3-8854-bd671853ba32",
|
|
),
|
|
] {
|
|
let payload = parse_channel_deep_link(&Url::parse(raw).unwrap()).unwrap();
|
|
assert_eq!(payload["channelId"], expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_channel_deep_link_rejects_malformed_forms() {
|
|
for raw in [
|
|
"buzz://channel",
|
|
"buzz://channel/",
|
|
"buzz://channel/one/two",
|
|
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/not-hex",
|
|
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/extra",
|
|
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/",
|
|
"buzz://channel/one?extra=true",
|
|
"buzz://channel/one#fragment",
|
|
"buzz://:pass@channel/580ca78b-9dae-46f3-8854-bd671853ba32/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"buzz://channel/not-a-uuid",
|
|
"buzz://channel/%2F",
|
|
"buzz://channel/%00",
|
|
] {
|
|
assert!(parse_channel_deep_link(&Url::parse(raw).unwrap()).is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_extracts_required_params() {
|
|
let url = Url::parse("buzz://message?channel=abc&id=xyz").unwrap();
|
|
let payload = parse_message_deep_link(&url).expect("required params present");
|
|
assert_eq!(payload["channelId"], "abc");
|
|
assert_eq!(payload["messageId"], "xyz");
|
|
assert!(payload["threadRootId"].is_null());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_accepts_buzz_scheme() {
|
|
let url = Url::parse("buzz://message?channel=abc&id=xyz").unwrap();
|
|
let payload = parse_message_deep_link(&url).expect("required params present");
|
|
assert_eq!(payload["channelId"], "abc");
|
|
assert_eq!(payload["messageId"], "xyz");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_includes_thread_root() {
|
|
let url = Url::parse("buzz://message?channel=abc&id=xyz&thread=root1").unwrap();
|
|
let payload = parse_message_deep_link(&url).expect("required params present");
|
|
assert_eq!(payload["threadRootId"], "root1");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_rejects_missing_id() {
|
|
let url = Url::parse("buzz://message?channel=abc").unwrap();
|
|
assert!(parse_message_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_rejects_empty_channel() {
|
|
// Regression: `channel=&id=foo` previously produced channelId: "".
|
|
let url = Url::parse("buzz://message?channel=&id=foo").unwrap();
|
|
assert!(parse_message_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_rejects_empty_id() {
|
|
let url = Url::parse("buzz://message?channel=abc&id=").unwrap();
|
|
assert!(parse_message_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_message_deep_link_treats_empty_thread_as_absent() {
|
|
let url = Url::parse("buzz://message?channel=abc&id=xyz&thread=").unwrap();
|
|
let payload = parse_message_deep_link(&url).expect("required params present");
|
|
assert!(payload["threadRootId"].is_null());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_join_deep_link_extracts_relay_and_code() {
|
|
let url = Url::parse("buzz://join?relay=wss%3A%2F%2Frelay.example&code=abc.def").unwrap();
|
|
let payload = parse_join_deep_link(&url).expect("required params present");
|
|
assert_eq!(payload["relayUrl"], "wss://relay.example");
|
|
assert_eq!(payload["code"], "abc.def");
|
|
assert!(payload["policyReceipt"].is_null());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_join_deep_link_extracts_policy_receipt() {
|
|
let url = Url::parse(
|
|
"buzz://join?relay=wss%3A%2F%2Frelay.example&code=abc.def&policy_receipt=receipt.value",
|
|
)
|
|
.unwrap();
|
|
let payload = parse_join_deep_link(&url).expect("required params present");
|
|
assert_eq!(payload["policyReceipt"], "receipt.value");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_join_deep_link_rejects_missing_code() {
|
|
let url = Url::parse("buzz://join?relay=wss%3A%2F%2Frelay.example").unwrap();
|
|
assert!(parse_join_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_join_deep_link_rejects_empty_code() {
|
|
let url = Url::parse("buzz://join?relay=wss%3A%2F%2Frelay.example&code=").unwrap();
|
|
assert!(parse_join_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_join_deep_link_rejects_missing_relay() {
|
|
let url = Url::parse("buzz://join?code=abc.def").unwrap();
|
|
assert!(parse_join_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_join_deep_link_rejects_non_websocket_relay() {
|
|
let url = Url::parse("buzz://join?relay=https%3A%2F%2Frelay.example&code=abc.def").unwrap();
|
|
assert!(parse_join_deep_link(&url).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_accepts_valid_url() {
|
|
let payload = parse_nostr_bind_deep_link(&valid_nostr_bind_url()).unwrap();
|
|
assert_eq!(payload.challenge_id, "550e8400-e29b-41d4-a716-446655440000");
|
|
assert_eq!(payload.nonce, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567");
|
|
assert_eq!(payload.verification_code, "123456");
|
|
assert_eq!(payload.audience, "buzz:nostr-identity");
|
|
assert_eq!(payload.action, "bind_nostr_identity");
|
|
assert_eq!(payload.protocol, "buzz-nostr-identity");
|
|
assert_eq!(payload.version, "1");
|
|
assert_eq!(payload.origin, "https://example.com");
|
|
assert_eq!(payload.expires_at, "2999-01-01T00:00:00Z");
|
|
assert_eq!(payload.return_mode, "clipboard");
|
|
assert_eq!(payload.callback_url, None);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_accepts_same_origin_callback_url() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard&callback_url=https%3A%2F%2Fexample.com%2Fbuzz%3FmockSession%3D1").unwrap();
|
|
let payload = parse_nostr_bind_deep_link(&url).unwrap();
|
|
assert_eq!(
|
|
payload.callback_url.as_deref(),
|
|
Some("https://example.com/buzz?mockSession=1")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_accepts_browser_fragment_return() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=browser_fragment_v1&callback_url=https%3A%2F%2Fexample.com%2Fbuzz").unwrap();
|
|
let payload = parse_nostr_bind_deep_link(&url).unwrap();
|
|
|
|
assert_eq!(payload.return_mode, "browser_fragment_v1");
|
|
assert_eq!(
|
|
payload.callback_url.as_deref(),
|
|
Some("https://example.com/buzz")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_requires_callback_for_browser_fragment_return() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=browser_fragment_v1").unwrap();
|
|
|
|
assert_eq!(
|
|
parse_nostr_bind_deep_link(&url).unwrap_err(),
|
|
"browser_fragment_v1 requires callback_url"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_cross_origin_callback_url() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard&callback_url=https%3A%2F%2Fevil.example%2Fbuzz").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_http_callback_url() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard&callback_url=http%3A%2F%2Fexample.com%2Fbuzz").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_missing_challenge_id() {
|
|
let url = Url::parse("buzz://nostr-bind?nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_empty_nonce() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_missing_verification_code() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_short_verification_code() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=12345&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_long_verification_code() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=1234567&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_non_digit_verification_code() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=12345a&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_wrong_action() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=wrong&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_wrong_audience() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=other&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_non_https_origin() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=http%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_origin_with_path() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com%2Fbind&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_origin_with_credentials() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fuser%40example.com&expires_at=2999-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_rejects_unsupported_return_mode() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2999-01-01T00%3A00%3A00Z&return=callback").unwrap();
|
|
assert!(parse_nostr_bind_deep_link(&url).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_nostr_bind_deep_link_accepts_expired_link_for_user_facing_error() {
|
|
let url = Url::parse("buzz://nostr-bind?challenge_id=550e8400-e29b-41d4-a716-446655440000&nonce=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi01234567&verification_code=123456&audience=buzz%3Anostr-identity&action=bind_nostr_identity&protocol=buzz-nostr-identity&version=1&origin=https%3A%2F%2Fexample.com&expires_at=2000-01-01T00%3A00%3A00Z&return=clipboard").unwrap();
|
|
let payload = parse_nostr_bind_deep_link(&url).unwrap();
|
|
assert_eq!(payload.expires_at, "2000-01-01T00:00:00Z");
|
|
}
|