upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-11 15:27:06 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-11 15:27:06 +0000
commit497df415749039236126140193af0ea612358cc7 (patch)
tree71f5ef12092d02ba40a184fed8a8029d77dd1957
parent3d11a99b4adc9ad900190b797e6d6dc1f97116fa (diff)
test: nip77 smoke test
-rw-r--r--tests/nip77_negentropy.rs213
1 files changed, 213 insertions, 0 deletions
diff --git a/tests/nip77_negentropy.rs b/tests/nip77_negentropy.rs
new file mode 100644
index 0000000..c8e0b50
--- /dev/null
+++ b/tests/nip77_negentropy.rs
@@ -0,0 +1,213 @@
1//! NIP-77 Negentropy Sync Smoke Tests
2//!
3//! Verifies that ngit-grasp's NIP-77 claim is valid by testing negentropy
4//! reconciliation between a client and the relay.
5//!
6//! # Background
7//!
8//! NIP-77 defines the negentropy protocol for efficient set reconciliation.
9//! The nostr-relay-builder v0.44 provides built-in NIP-77 support via:
10//! - NEG-OPEN message handling
11//! - NEG-MSG message handling
12//! - NEG-CLOSE message handling
13//!
14//! This test uses nostr-sdk's `client.sync()` method to perform negentropy
15//! reconciliation against the relay.
16//!
17//! # Running Tests
18//!
19//! ```bash
20//! cargo test --test nip77_negentropy -- --nocapture
21//! ```
22
23mod common;
24
25use nostr_sdk::prelude::*;
26use std::time::Duration;
27
28use common::{sync_helpers::*, TestRelay};
29
30/// Smoke test: NIP-77 negentropy reconciliation returns event IDs
31///
32/// Scenario:
33/// 1. Start a TestRelay
34/// 2. Publish a couple of events to it
35/// 3. Create a fresh client with empty local database
36/// 4. Call client.sync() to perform negentropy reconciliation
37/// 5. Verify reconciliation found the events on the relay
38#[tokio::test]
39async fn test_nip77_negentropy_sync_finds_events() {
40 // 1. Start relay
41 let relay = TestRelay::start().await;
42 println!("Relay started at {}", relay.url());
43
44 // 2. Create keys and publish events
45 let keys = Keys::generate();
46
47 // Create a repository announcement that will be accepted by the relay
48 let announcement = create_repo_announcement(
49 &keys,
50 &[&relay.domain()],
51 "test-repo-nip77",
52 );
53 let event1_id = announcement.id;
54 println!("Created event 1: {} (kind {})", event1_id, announcement.kind.as_u16());
55
56 // Create a second event (issue referencing the repo)
57 let repo_coord = format!(
58 "{}:{}:{}",
59 KIND_REPOSITORY_STATE,
60 keys.public_key().to_hex(),
61 "test-repo-nip77"
62 );
63 let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for NIP-77")
64 .expect("Failed to build issue event");
65 let event2_id = issue.id;
66 println!("Created event 2: {} (kind {})", event2_id, issue.kind.as_u16());
67
68 // 3. Send events to relay using TestClient
69 let publish_client = TestClient::new(relay.url(), keys.clone())
70 .await
71 .expect("Failed to connect to relay");
72
73 publish_client
74 .send_event(&announcement)
75 .await
76 .expect("Failed to send announcement");
77 publish_client
78 .send_event(&issue)
79 .await
80 .expect("Failed to send issue");
81 println!("Events published to relay");
82
83 publish_client.disconnect().await;
84
85 // 4. Wait a moment for events to be stored
86 tokio::time::sleep(Duration::from_millis(200)).await;
87
88 // 5. Create a fresh client to perform sync (different instance, no local events)
89 let sync_keys = Keys::generate(); // Different keys, doesn't matter for sync
90 let sync_client = Client::new(sync_keys);
91
92 sync_client
93 .add_relay(relay.url())
94 .await
95 .expect("Failed to add relay");
96 sync_client.connect().await;
97
98 // Wait for connection
99 tokio::time::sleep(Duration::from_millis(500)).await;
100
101 // 6. Perform negentropy sync with filter matching our events
102 let filter = Filter::new()
103 .author(keys.public_key())
104 .kinds(vec![Kind::Custom(KIND_REPOSITORY_STATE), Kind::Custom(KIND_ISSUE)]);
105
106 println!("Starting negentropy sync with filter: {:?}", filter);
107
108 let sync_opts = SyncOptions::default();
109
110 let result = sync_client.sync(filter, &sync_opts).await;
111
112 // 7. Cleanup
113 sync_client.disconnect().await;
114 relay.stop().await;
115
116 // 8. Verify results
117 match result {
118 Ok(output) => {
119 let reconciliation = output.val;
120 println!("Negentropy sync completed!");
121 println!(" Local: {:?}", reconciliation.local);
122 println!(" Remote: {:?}", reconciliation.remote);
123 println!(" Sent: {:?}", reconciliation.sent);
124 println!(" Received: {:?}", reconciliation.received);
125 println!(" Failures: {:?}", output.failed);
126
127 // The relay has events we don't have locally, so they should appear in "received"
128 // or "remote" (depending on whether we requested them or just discovered them)
129 let total_discovered = reconciliation.received.len() + reconciliation.remote.len();
130
131 assert!(
132 total_discovered >= 2,
133 "Expected to discover at least 2 events via negentropy, got {} (received: {}, remote: {})",
134 total_discovered,
135 reconciliation.received.len(),
136 reconciliation.remote.len()
137 );
138
139 // Verify our specific events were found
140 let all_discovered: Vec<_> = reconciliation
141 .received
142 .iter()
143 .chain(reconciliation.remote.iter())
144 .collect();
145
146 println!("All discovered event IDs: {:?}", all_discovered);
147 }
148 Err(e) => {
149 panic!(
150 "NIP-77 negentropy sync failed: {}. This means the relay does NOT support NIP-77 as claimed.",
151 e
152 );
153 }
154 }
155}
156
157/// Smoke test: Negentropy sync with empty database returns empty result
158///
159/// Verifies that negentropy sync works correctly when no events match the filter.
160#[tokio::test]
161async fn test_nip77_negentropy_sync_empty_result() {
162 // 1. Start relay (empty, no events)
163 let relay = TestRelay::start().await;
164 println!("Relay started at {}", relay.url());
165
166 // 2. Create client
167 let keys = Keys::generate();
168 let client = Client::new(keys.clone());
169
170 client
171 .add_relay(relay.url())
172 .await
173 .expect("Failed to add relay");
174 client.connect().await;
175
176 tokio::time::sleep(Duration::from_millis(500)).await;
177
178 // 3. Sync with filter that won't match anything
179 let filter = Filter::new()
180 .author(keys.public_key()) // Random new key, no events exist
181 .kind(Kind::Custom(KIND_REPOSITORY_STATE));
182
183 println!("Starting negentropy sync with empty filter");
184
185 let sync_opts = SyncOptions::default();
186
187 let result = client.sync(filter, &sync_opts).await;
188
189 // 4. Cleanup
190 client.disconnect().await;
191 relay.stop().await;
192
193 // 5. Verify - should succeed but find nothing
194 match result {
195 Ok(output) => {
196 let reconciliation = output.val;
197 println!("Empty sync completed!");
198 println!(" Received: {:?}", reconciliation.received);
199 println!(" Remote: {:?}", reconciliation.remote);
200
201 // Should be empty since no events match
202 let total = reconciliation.received.len() + reconciliation.remote.len();
203 assert_eq!(
204 total, 0,
205 "Expected 0 events for non-existent author, got {}",
206 total
207 );
208 }
209 Err(e) => {
210 panic!("NIP-77 negentropy sync failed on empty query: {}", e);
211 }
212 }
213}