upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 17:03:40 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 17:03:40 +0000
commitb167f1b2ae7edbcab95554b5203d22d9e372c8b5 (patch)
tree39b3bb879302cb6a4eaabded4a5d20f7d0d68ffc /tests
parentfdbc8895e1e9e712882bd854908295a95e7afcb9 (diff)
feat(sync): Phase 1 MVP - single relay proactive sync
- Add src/sync/ module with SyncManager - Add NGIT_SYNC_RELAY_URL config option - Subscribe to kind 30617 on configured relay - Validate synced events through Nip34WritePolicy - Integration test with two TestRelay instances
Diffstat (limited to 'tests')
-rw-r--r--tests/common/relay.rs42
-rw-r--r--tests/proactive_sync_basic.rs262
2 files changed, 298 insertions, 6 deletions
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
index 449b4cb..9fb7b1d 100644
--- a/tests/common/relay.rs
+++ b/tests/common/relay.rs
@@ -33,11 +33,36 @@ impl TestRelay {
33 /// } 33 /// }
34 /// ``` 34 /// ```
35 pub async fn start() -> Self { 35 pub async fn start() -> Self {
36 Self::start_with_port(Self::find_free_port()).await 36 Self::start_with_options(Self::find_free_port(), None).await
37 } 37 }
38 38
39 /// Start relay on a specific port 39 /// Start relay on a specific port
40 pub async fn start_with_port(port: u16) -> Self { 40 pub async fn start_with_port(port: u16) -> Self {
41 Self::start_with_options(port, None).await
42 }
43
44 /// Start relay with sync from another relay
45 ///
46 /// # Example
47 ///
48 /// ```no_run
49 /// use common::TestRelay;
50 ///
51 /// #[tokio::test]
52 /// async fn test_sync() {
53 /// let source = TestRelay::start().await;
54 /// let syncing = TestRelay::start_with_sync(source.url()).await;
55 /// // ... test sync behavior ...
56 /// syncing.stop().await;
57 /// source.stop().await;
58 /// }
59 /// ```
60 pub async fn start_with_sync(sync_relay_url: &str) -> Self {
61 Self::start_with_options(Self::find_free_port(), Some(sync_relay_url.to_string())).await
62 }
63
64 /// Start relay with options
65 async fn start_with_options(port: u16, sync_relay_url: Option<String>) -> Self {
41 let bind_address = format!("127.0.0.1:{}", port); 66 let bind_address = format!("127.0.0.1:{}", port);
42 let url = format!("ws://127.0.0.1:{}", port); 67 let url = format!("ws://127.0.0.1:{}", port);
43 68
@@ -62,16 +87,21 @@ impl TestRelay {
62 .expect("Failed to generate test npub"); 87 .expect("Failed to generate test npub");
63 88
64 // Start the relay process 89 // Start the relay process
65 let process = Command::new(&binary_path) 90 let mut cmd = Command::new(&binary_path);
66 .env("NGIT_BIND_ADDRESS", &bind_address) 91 cmd.env("NGIT_BIND_ADDRESS", &bind_address)
67 .env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address 92 .env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
68 .env("NGIT_GIT_DATA_PATH", git_data_dir.path()) 93 .env("NGIT_GIT_DATA_PATH", git_data_dir.path())
69 .env("NGIT_OWNER_NPUB", &test_npub) 94 .env("NGIT_OWNER_NPUB", &test_npub)
70 .env("RUST_LOG", "warn") // Less logging during tests 95 .env("RUST_LOG", "warn") // Less logging during tests
71 .stdout(Stdio::null()) 96 .stdout(Stdio::null())
72 .stderr(Stdio::null()) 97 .stderr(Stdio::null());
73 .spawn() 98
74 .expect("Failed to start relay process"); 99 // Add sync relay URL if provided
100 if let Some(ref sync_url) = sync_relay_url {
101 cmd.env("NGIT_SYNC_RELAY_URL", sync_url);
102 }
103
104 let process = cmd.spawn().expect("Failed to start relay process");
75 105
76 let relay = Self { process, url, port }; 106 let relay = Self { process, url, port };
77 107
diff --git a/tests/proactive_sync_basic.rs b/tests/proactive_sync_basic.rs
new file mode 100644
index 0000000..b0b2cbf
--- /dev/null
+++ b/tests/proactive_sync_basic.rs
@@ -0,0 +1,262 @@
1//! GRASP-02 Phase 1: Proactive Sync Basic Integration Tests
2//!
3//! Tests the basic proactive sync functionality using two TestRelay instances:
4//! - relay_a: Source relay with events
5//! - relay_b: Sync relay configured to sync from relay_a
6//!
7//! # Running Tests
8//!
9//! ```bash
10//! cargo test --test proactive_sync_basic
11//! cargo test --test proactive_sync_basic -- --nocapture
12//! ```
13
14mod common;
15
16use std::time::Duration;
17
18use common::TestRelay;
19use nostr_sdk::prelude::*;
20
21/// Kind 30617 - Repository State (NIP-34)
22const KIND_REPOSITORY_STATE: u16 = 30617;
23
24/// Create a valid repository announcement event for testing
25///
26/// This creates a kind 30617 event with required clone and relays tags
27fn create_valid_repo_announcement(
28 keys: &Keys,
29 domain: &str,
30 identifier: &str,
31) -> Event {
32 // Build tags for repository announcement
33 let tags = vec![
34 Tag::identifier(identifier),
35 Tag::custom(
36 TagKind::custom("clone"),
37 vec![format!("http://{}/{}", domain, identifier)],
38 ),
39 Tag::custom(
40 TagKind::custom("relays"),
41 vec![format!("ws://{}", domain)],
42 ),
43 ];
44
45 EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Repository state")
46 .tags(tags)
47 .sign_with_keys(keys)
48 .expect("Failed to sign event")
49}
50
51/// Test that syncing relay connects to source relay
52#[tokio::test]
53async fn test_sync_relay_connects_to_source() {
54 // Start source relay (relay_a)
55 let relay_a = TestRelay::start().await;
56
57 // Start syncing relay (relay_b) configured to sync from relay_a
58 let relay_b = TestRelay::start_with_sync(relay_a.url()).await;
59
60 // Give some time for connection to establish
61 tokio::time::sleep(Duration::from_millis(500)).await;
62
63 // If we got here without panicking, the relays started successfully
64 // The sync connection happens in the background
65
66 relay_b.stop().await;
67 relay_a.stop().await;
68}
69
70/// Test that valid events sync from source to syncing relay
71#[tokio::test]
72async fn test_valid_event_syncs_to_relay() {
73 // Start source relay (relay_a)
74 let relay_a = TestRelay::start().await;
75
76 // Give relay_a time to start
77 tokio::time::sleep(Duration::from_millis(200)).await;
78
79 // Start syncing relay (relay_b) configured to sync from relay_a
80 let relay_b = TestRelay::start_with_sync(relay_a.url()).await;
81
82 // Create test keys
83 let keys = Keys::generate();
84
85 // Create and submit a valid repository announcement to relay_a
86 let event = create_valid_repo_announcement(&keys, &relay_a.domain(), "test-repo");
87 let event_id = event.id;
88
89 // Submit event to relay_a
90 let client_a = Client::default();
91 client_a.add_relay(relay_a.url()).await.expect("Failed to add relay_a");
92 client_a.connect().await;
93
94 let send_result = client_a.send_event(&event).await;
95 assert!(send_result.is_ok(), "Failed to send event to relay_a: {:?}", send_result.err());
96
97 // Wait for sync to occur
98 tokio::time::sleep(Duration::from_secs(2)).await;
99
100 // Query relay_b to verify the event was synced
101 let client_b = Client::default();
102 client_b.add_relay(relay_b.url()).await.expect("Failed to add relay_b");
103 client_b.connect().await;
104
105 // Create filter to find our event
106 let filter = Filter::new()
107 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
108 .author(keys.public_key());
109
110 let events = client_b
111 .fetch_events(filter, Duration::from_secs(5))
112 .await
113 .expect("Failed to fetch events from relay_b");
114
115 // Check if our event was synced
116 let found = events.iter().any(|e| e.id == event_id);
117
118 // Clean up
119 client_a.disconnect().await;
120 client_b.disconnect().await;
121 relay_b.stop().await;
122 relay_a.stop().await;
123
124 assert!(
125 found,
126 "Event {} was not synced to relay_b. Found {} events",
127 event_id,
128 events.len()
129 );
130}
131
132/// Test that invalid events are rejected by syncing relay validation
133#[tokio::test]
134async fn test_invalid_event_rejected_by_sync_validation() {
135 // Start source relay (relay_a) - this is a simple relay without GRASP validation
136 // For this test, we'll use a second ngit-grasp relay, but the key insight is that
137 // the syncing relay should reject events that don't pass its own validation
138
139 let relay_a = TestRelay::start().await;
140 let relay_b = TestRelay::start_with_sync(relay_a.url()).await;
141
142 // Give time for connection
143 tokio::time::sleep(Duration::from_millis(500)).await;
144
145 // Create test keys
146 let keys = Keys::generate();
147
148 // Create an INVALID repository announcement (missing clone tag)
149 let tags = vec![
150 Tag::identifier("test-invalid-repo"),
151 // Missing required "clone" tag!
152 Tag::custom(
153 TagKind::custom("relays"),
154 vec![format!("ws://{}", relay_a.domain())],
155 ),
156 ];
157
158 let invalid_event = EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Invalid repo")
159 .tags(tags)
160 .sign_with_keys(&keys)
161 .expect("Failed to sign event");
162
163 let invalid_event_id = invalid_event.id;
164
165 // Submit invalid event to relay_a
166 // Note: relay_a will also reject it due to GRASP validation
167 let client_a = Client::default();
168 client_a.add_relay(relay_a.url()).await.expect("Failed to add relay_a");
169 client_a.connect().await;
170
171 // This will likely fail since relay_a also validates, but let's try
172 let _ = client_a.send_event(&invalid_event).await;
173
174 // Wait for potential sync
175 tokio::time::sleep(Duration::from_secs(1)).await;
176
177 // Query relay_b - the event should NOT be present
178 let client_b = Client::default();
179 client_b.add_relay(relay_b.url()).await.expect("Failed to add relay_b");
180 client_b.connect().await;
181
182 let filter = Filter::new()
183 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
184 .author(keys.public_key());
185
186 let events = client_b
187 .fetch_events(filter, Duration::from_secs(3))
188 .await
189 .expect("Failed to fetch events from relay_b");
190
191 let found = events.iter().any(|e| e.id == invalid_event_id);
192
193 // Clean up
194 client_a.disconnect().await;
195 client_b.disconnect().await;
196 relay_b.stop().await;
197 relay_a.stop().await;
198
199 assert!(
200 !found,
201 "Invalid event {} should NOT have been synced to relay_b",
202 invalid_event_id
203 );
204}
205
206/// Test that syncing relay maintains its own validation policy
207#[tokio::test]
208async fn test_sync_respects_local_validation() {
209 // This test verifies that synced events go through the local Nip34WritePolicy
210 // by testing that orphan events (events referencing non-existent repos) are rejected
211
212 let relay_a = TestRelay::start().await;
213 let relay_b = TestRelay::start_with_sync(relay_a.url()).await;
214
215 tokio::time::sleep(Duration::from_millis(500)).await;
216
217 let keys = Keys::generate();
218
219 // First, create a VALID repository announcement and submit it
220 let valid_event = create_valid_repo_announcement(&keys, &relay_a.domain(), "valid-repo");
221 let valid_event_id = valid_event.id;
222
223 let client_a = Client::default();
224 client_a.add_relay(relay_a.url()).await.expect("Failed to add relay_a");
225 client_a.connect().await;
226
227 client_a
228 .send_event(&valid_event)
229 .await
230 .expect("Failed to send valid event");
231
232 // Wait for sync
233 tokio::time::sleep(Duration::from_secs(2)).await;
234
235 // Query relay_b to verify the valid event was synced
236 let client_b = Client::default();
237 client_b.add_relay(relay_b.url()).await.expect("Failed to add relay_b");
238 client_b.connect().await;
239
240 let filter = Filter::new()
241 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
242 .author(keys.public_key());
243
244 let events = client_b
245 .fetch_events(filter, Duration::from_secs(5))
246 .await
247 .expect("Failed to fetch events from relay_b");
248
249 let found = events.iter().any(|e| e.id == valid_event_id);
250
251 // Clean up
252 client_a.disconnect().await;
253 client_b.disconnect().await;
254 relay_b.stop().await;
255 relay_a.stop().await;
256
257 assert!(
258 found,
259 "Valid event {} should have been synced to relay_b",
260 valid_event_id
261 );
262} \ No newline at end of file