diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 20:41:01 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 20:41:01 +0000 |
| commit | 7467aa9ace94b4e132eedd03c9daddb2d59813c4 (patch) | |
| tree | 32c4571a7376138eb429017a48dbde7ad8b15be6 | |
| parent | e7c18cf2a96b1f45e5f21a83ee1fe2e18a6dc7e2 (diff) | |
test: added purgatory git data sync intergration tests agregating from mulitple git servers
| -rw-r--r-- | tests/common/mock_relay.rs | 336 | ||||
| -rw-r--r-- | tests/common/mod.rs | 2 | ||||
| -rw-r--r-- | tests/common/purgatory_helpers.rs | 115 | ||||
| -rw-r--r-- | tests/purgatory_sync.rs | 496 |
4 files changed, 930 insertions, 19 deletions
diff --git a/tests/common/mock_relay.rs b/tests/common/mock_relay.rs new file mode 100644 index 0000000..123c29e --- /dev/null +++ b/tests/common/mock_relay.rs | |||
| @@ -0,0 +1,336 @@ | |||
| 1 | //! Mock Nostr Relay for Testing | ||
| 2 | //! | ||
| 3 | //! Provides a simple Nostr relay that accepts all events without validation. | ||
| 4 | //! Uses rust-nostr's `LocalRelayBuilder` to create an in-memory relay. | ||
| 5 | //! | ||
| 6 | //! # Usage | ||
| 7 | //! | ||
| 8 | //! ```ignore | ||
| 9 | //! use common::MockRelay; | ||
| 10 | //! | ||
| 11 | //! #[tokio::test] | ||
| 12 | //! async fn test_mock_relay() { | ||
| 13 | //! // Start the mock relay | ||
| 14 | //! let mock = MockRelay::start().await; | ||
| 15 | //! | ||
| 16 | //! // Use mock.url() for WebSocket connections | ||
| 17 | //! let client = Client::new(keys); | ||
| 18 | //! client.add_relay(mock.url()).await.unwrap(); | ||
| 19 | //! | ||
| 20 | //! // All events are accepted without validation | ||
| 21 | //! client.send_event(&event).await.unwrap(); | ||
| 22 | //! | ||
| 23 | //! // Cleanup | ||
| 24 | //! mock.stop().await; | ||
| 25 | //! } | ||
| 26 | //! ``` | ||
| 27 | //! | ||
| 28 | //! # How It Works | ||
| 29 | //! | ||
| 30 | //! The mock relay: | ||
| 31 | //! - Uses `LocalRelayBuilder::default().build()` which accepts all events | ||
| 32 | //! - Runs an HTTP server with WebSocket upgrade support | ||
| 33 | //! - Stores events in an in-memory database | ||
| 34 | //! - Does NOT perform any GRASP validation (no purgatory, no git data checks) | ||
| 35 | |||
| 36 | use std::net::SocketAddr; | ||
| 37 | use std::sync::Arc; | ||
| 38 | |||
| 39 | use http_body_util::Full; | ||
| 40 | use hyper::body::Bytes; | ||
| 41 | use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, UPGRADE}; | ||
| 42 | use hyper::server::conn::http1; | ||
| 43 | use hyper::service::service_fn; | ||
| 44 | use hyper::{Request, Response, StatusCode}; | ||
| 45 | use hyper_util::rt::TokioIo; | ||
| 46 | use nostr_relay_builder::prelude::*; | ||
| 47 | use tokio::net::TcpListener; | ||
| 48 | use tokio::sync::oneshot; | ||
| 49 | |||
| 50 | /// Mock Nostr relay that accepts all events without validation. | ||
| 51 | /// | ||
| 52 | /// This relay is useful for testing scenarios where you need a relay | ||
| 53 | /// that serves events without GRASP validation (no purgatory, no git checks). | ||
| 54 | pub struct MockRelay { | ||
| 55 | /// Shutdown signal sender | ||
| 56 | shutdown_tx: Option<oneshot::Sender<()>>, | ||
| 57 | /// Server task handle | ||
| 58 | handle: Option<tokio::task::JoinHandle<()>>, | ||
| 59 | /// Server URL (ws://127.0.0.1:<port>) | ||
| 60 | url: String, | ||
| 61 | /// Server port | ||
| 62 | #[allow(dead_code)] | ||
| 63 | port: u16, | ||
| 64 | /// The underlying LocalRelay (kept alive for the server lifetime) | ||
| 65 | #[allow(dead_code)] | ||
| 66 | relay: LocalRelay, | ||
| 67 | } | ||
| 68 | |||
| 69 | impl MockRelay { | ||
| 70 | /// Start a mock relay on a random free port. | ||
| 71 | /// | ||
| 72 | /// The relay accepts all events without validation and stores them | ||
| 73 | /// in an in-memory database. | ||
| 74 | pub async fn start() -> Self { | ||
| 75 | let port = find_free_port(); | ||
| 76 | Self::start_on_port(port).await | ||
| 77 | } | ||
| 78 | |||
| 79 | /// Start a mock relay on a specific port. | ||
| 80 | pub async fn start_on_port(port: u16) -> Self { | ||
| 81 | let addr: SocketAddr = ([127, 0, 0, 1], port).into(); | ||
| 82 | |||
| 83 | // Create a simple relay with no write policy (accepts all events) | ||
| 84 | let relay = LocalRelayBuilder::default().build(); | ||
| 85 | |||
| 86 | // Create shutdown channel | ||
| 87 | let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); | ||
| 88 | |||
| 89 | // Clone relay for the server task | ||
| 90 | let server_relay = relay.clone(); | ||
| 91 | |||
| 92 | // Start the HTTP/WebSocket server | ||
| 93 | let listener = TcpListener::bind(addr) | ||
| 94 | .await | ||
| 95 | .expect("Failed to bind to address"); | ||
| 96 | |||
| 97 | let handle = tokio::spawn(async move { | ||
| 98 | loop { | ||
| 99 | tokio::select! { | ||
| 100 | accept_result = listener.accept() => { | ||
| 101 | match accept_result { | ||
| 102 | Ok((stream, remote_addr)) => { | ||
| 103 | let relay = server_relay.clone(); | ||
| 104 | let io = TokioIo::new(stream); | ||
| 105 | |||
| 106 | tokio::spawn(async move { | ||
| 107 | let service = service_fn(move |req| { | ||
| 108 | let relay = relay.clone(); | ||
| 109 | async move { handle_request(req, relay, remote_addr).await } | ||
| 110 | }); | ||
| 111 | |||
| 112 | if let Err(e) = http1::Builder::new() | ||
| 113 | .serve_connection(io, service) | ||
| 114 | .with_upgrades() | ||
| 115 | .await | ||
| 116 | { | ||
| 117 | // Connection errors are expected when client disconnects | ||
| 118 | if !e.to_string().contains("connection") { | ||
| 119 | eprintln!("MockRelay connection error: {}", e); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | }); | ||
| 123 | } | ||
| 124 | Err(e) => { | ||
| 125 | eprintln!("MockRelay accept error: {}", e); | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } | ||
| 129 | _ = &mut shutdown_rx => { | ||
| 130 | // Shutdown signal received | ||
| 131 | break; | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | }); | ||
| 136 | |||
| 137 | let url = format!("ws://127.0.0.1:{}", port); | ||
| 138 | |||
| 139 | // Wait for server to be ready | ||
| 140 | wait_for_server_ready(port).await; | ||
| 141 | |||
| 142 | Self { | ||
| 143 | shutdown_tx: Some(shutdown_tx), | ||
| 144 | handle: Some(handle), | ||
| 145 | url, | ||
| 146 | port, | ||
| 147 | relay, | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | /// Get the relay WebSocket URL. | ||
| 152 | pub fn url(&self) -> &str { | ||
| 153 | &self.url | ||
| 154 | } | ||
| 155 | |||
| 156 | /// Stop the mock relay. | ||
| 157 | pub async fn stop(mut self) { | ||
| 158 | // Send shutdown signal | ||
| 159 | if let Some(tx) = self.shutdown_tx.take() { | ||
| 160 | let _ = tx.send(()); | ||
| 161 | } | ||
| 162 | |||
| 163 | // Wait for server task to complete | ||
| 164 | if let Some(handle) = self.handle.take() { | ||
| 165 | let _ = handle.await; | ||
| 166 | } | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | impl Drop for MockRelay { | ||
| 171 | fn drop(&mut self) { | ||
| 172 | // Send shutdown signal if not already sent | ||
| 173 | if let Some(tx) = self.shutdown_tx.take() { | ||
| 174 | let _ = tx.send(()); | ||
| 175 | } | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | /// Handle an HTTP request, upgrading to WebSocket if requested. | ||
| 180 | async fn handle_request( | ||
| 181 | req: Request<hyper::body::Incoming>, | ||
| 182 | relay: LocalRelay, | ||
| 183 | addr: SocketAddr, | ||
| 184 | ) -> Result<Response<Full<Bytes>>, hyper::Error> { | ||
| 185 | // Check for WebSocket upgrade request | ||
| 186 | let is_websocket = req | ||
| 187 | .headers() | ||
| 188 | .get(UPGRADE) | ||
| 189 | .map(|v| v.to_str().unwrap_or("").to_lowercase() == "websocket") | ||
| 190 | .unwrap_or(false); | ||
| 191 | |||
| 192 | if is_websocket { | ||
| 193 | // Get the Sec-WebSocket-Key header | ||
| 194 | let key = req | ||
| 195 | .headers() | ||
| 196 | .get(SEC_WEBSOCKET_KEY) | ||
| 197 | .and_then(|k| k.to_str().ok()) | ||
| 198 | .map(|k| k.to_string()); | ||
| 199 | |||
| 200 | if let Some(key) = key { | ||
| 201 | let accept_key = derive_accept_key(key.as_bytes()); | ||
| 202 | |||
| 203 | // Spawn task to handle the upgraded connection | ||
| 204 | tokio::spawn(async move { | ||
| 205 | match hyper::upgrade::on(req).await { | ||
| 206 | Ok(upgraded) => { | ||
| 207 | if let Err(e) = relay.take_connection(TokioIo::new(upgraded), addr).await { | ||
| 208 | eprintln!("MockRelay WebSocket error: {}", e); | ||
| 209 | } | ||
| 210 | } | ||
| 211 | Err(e) => eprintln!("MockRelay upgrade error: {}", e), | ||
| 212 | } | ||
| 213 | }); | ||
| 214 | |||
| 215 | // Return 101 Switching Protocols | ||
| 216 | return Ok(Response::builder() | ||
| 217 | .status(StatusCode::SWITCHING_PROTOCOLS) | ||
| 218 | .header(CONNECTION, "upgrade") | ||
| 219 | .header(UPGRADE, "websocket") | ||
| 220 | .header(SEC_WEBSOCKET_ACCEPT, accept_key) | ||
| 221 | .body(Full::new(Bytes::new())) | ||
| 222 | .unwrap()); | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | // Non-WebSocket request - return simple response | ||
| 227 | Ok(Response::builder() | ||
| 228 | .status(StatusCode::OK) | ||
| 229 | .header("Content-Type", "text/plain") | ||
| 230 | .body(Full::new(Bytes::from("MockRelay - Nostr test relay"))) | ||
| 231 | .unwrap()) | ||
| 232 | } | ||
| 233 | |||
| 234 | /// Derive the Sec-WebSocket-Accept key from the request key. | ||
| 235 | fn derive_accept_key(request_key: &[u8]) -> String { | ||
| 236 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; | ||
| 237 | use nostr_sdk::hashes::{Hash, HashEngine}; | ||
| 238 | |||
| 239 | const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; | ||
| 240 | |||
| 241 | let mut engine = Sha1Hash::engine(); | ||
| 242 | engine.input(request_key); | ||
| 243 | engine.input(WS_GUID); | ||
| 244 | let hash = Sha1Hash::from_engine(engine); | ||
| 245 | base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash.as_byte_array()) | ||
| 246 | } | ||
| 247 | |||
| 248 | /// Find a free port to use for the server. | ||
| 249 | fn find_free_port() -> u16 { | ||
| 250 | use std::net::TcpListener; | ||
| 251 | |||
| 252 | let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port"); | ||
| 253 | let port = listener | ||
| 254 | .local_addr() | ||
| 255 | .expect("Failed to get local addr") | ||
| 256 | .port(); | ||
| 257 | drop(listener); | ||
| 258 | port | ||
| 259 | } | ||
| 260 | |||
| 261 | /// Wait for the server to be ready to accept connections. | ||
| 262 | async fn wait_for_server_ready(port: u16) { | ||
| 263 | let max_attempts = 50; // 5 seconds total | ||
| 264 | let delay = std::time::Duration::from_millis(100); | ||
| 265 | |||
| 266 | for attempt in 0..max_attempts { | ||
| 267 | match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await { | ||
| 268 | Ok(_) => { | ||
| 269 | // Connection successful, server is ready | ||
| 270 | tokio::time::sleep(std::time::Duration::from_millis(50)).await; | ||
| 271 | return; | ||
| 272 | } | ||
| 273 | Err(_) => { | ||
| 274 | if attempt == max_attempts - 1 { | ||
| 275 | panic!( | ||
| 276 | "MockRelay failed to start after {} attempts", | ||
| 277 | max_attempts | ||
| 278 | ); | ||
| 279 | } | ||
| 280 | tokio::time::sleep(delay).await; | ||
| 281 | } | ||
| 282 | } | ||
| 283 | } | ||
| 284 | } | ||
| 285 | |||
| 286 | #[cfg(test)] | ||
| 287 | mod tests { | ||
| 288 | use super::*; | ||
| 289 | use nostr_sdk::prelude::*; | ||
| 290 | use std::time::Duration; | ||
| 291 | |||
| 292 | #[tokio::test] | ||
| 293 | async fn test_mock_relay_starts_and_stops() { | ||
| 294 | let mock = MockRelay::start().await; | ||
| 295 | |||
| 296 | // Verify URL is set | ||
| 297 | assert!(mock.url().starts_with("ws://127.0.0.1:")); | ||
| 298 | |||
| 299 | mock.stop().await; | ||
| 300 | } | ||
| 301 | |||
| 302 | #[tokio::test] | ||
| 303 | async fn test_mock_relay_accepts_events() { | ||
| 304 | let mock = MockRelay::start().await; | ||
| 305 | |||
| 306 | // Create a client and connect | ||
| 307 | let keys = Keys::generate(); | ||
| 308 | let client = Client::new(keys.clone()); | ||
| 309 | client.add_relay(mock.url()).await.expect("Failed to add relay"); | ||
| 310 | client.connect().await; | ||
| 311 | |||
| 312 | // Wait for connection | ||
| 313 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 314 | |||
| 315 | // Create and send a simple event | ||
| 316 | let event = EventBuilder::text_note("Test note from MockRelay test") | ||
| 317 | .sign_with_keys(&keys) | ||
| 318 | .expect("Failed to sign event"); | ||
| 319 | |||
| 320 | let result = client.send_event(&event).await; | ||
| 321 | assert!(result.is_ok(), "MockRelay should accept events"); | ||
| 322 | |||
| 323 | // Verify event was stored by fetching it back | ||
| 324 | let filter = Filter::new().id(event.id); | ||
| 325 | let events = client | ||
| 326 | .fetch_events(filter, Duration::from_secs(2)) | ||
| 327 | .await | ||
| 328 | .expect("Failed to fetch events"); | ||
| 329 | |||
| 330 | assert!(!events.is_empty(), "Event should be stored and retrievable"); | ||
| 331 | assert_eq!(events.first().unwrap().id, event.id); | ||
| 332 | |||
| 333 | client.disconnect().await; | ||
| 334 | mock.stop().await; | ||
| 335 | } | ||
| 336 | } | ||
diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e70bd71..32ce1b7 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs | |||
| @@ -3,11 +3,13 @@ | |||
| 3 | #![allow(unused_imports)] // Re-exports may not be used in all test configurations | 3 | #![allow(unused_imports)] // Re-exports may not be used in all test configurations |
| 4 | 4 | ||
| 5 | pub mod git_server; | 5 | pub mod git_server; |
| 6 | pub mod mock_relay; | ||
| 6 | pub mod purgatory_helpers; | 7 | pub mod purgatory_helpers; |
| 7 | pub mod relay; | 8 | pub mod relay; |
| 8 | pub mod sync_helpers; | 9 | pub mod sync_helpers; |
| 9 | 10 | ||
| 10 | pub use git_server::SimpleGitServer; | 11 | pub use git_server::SimpleGitServer; |
| 12 | pub use mock_relay::MockRelay; | ||
| 11 | pub use purgatory_helpers::*; | 13 | pub use purgatory_helpers::*; |
| 12 | pub use relay::TestRelay; | 14 | pub use relay::TestRelay; |
| 13 | pub use sync_helpers::*; | 15 | pub use sync_helpers::*; |
diff --git a/tests/common/purgatory_helpers.rs b/tests/common/purgatory_helpers.rs index 7d8e908..fa1be73 100644 --- a/tests/common/purgatory_helpers.rs +++ b/tests/common/purgatory_helpers.rs | |||
| @@ -271,6 +271,60 @@ pub fn create_pr_event( | |||
| 271 | .map_err(|e| format!("Failed to sign PR event: {}", e)) | 271 | .map_err(|e| format!("Failed to sign PR event: {}", e)) |
| 272 | } | 272 | } |
| 273 | 273 | ||
| 274 | /// Create a PR event (kind 1618) with clone URLs. | ||
| 275 | /// | ||
| 276 | /// Creates a properly formatted NIP-34 PR event that references a repository | ||
| 277 | /// via an `a` tag, includes the commit hash via a `c` tag, and specifies | ||
| 278 | /// clone URLs where the PR commit can be fetched from. | ||
| 279 | /// | ||
| 280 | /// Per NIP-34, PR events can include a `clone` tag: | ||
| 281 | /// ```jsonc | ||
| 282 | /// { | ||
| 283 | /// "kind": 1618, | ||
| 284 | /// "tags": [ | ||
| 285 | /// ["c", "<current-commit-id>"], | ||
| 286 | /// ["clone", "<clone-url>", ...], // at least one git clone url where commit can be downloaded | ||
| 287 | /// // ... | ||
| 288 | /// ] | ||
| 289 | /// } | ||
| 290 | /// ``` | ||
| 291 | /// | ||
| 292 | /// # Arguments | ||
| 293 | /// * `keys` - Keys for signing | ||
| 294 | /// * `repo_coord` - Repository coordinate (format: "30617:pubkey_hex:identifier") | ||
| 295 | /// * `commit_hash` - The commit hash (c-tag) | ||
| 296 | /// * `title` - PR title (used as content) | ||
| 297 | /// * `clone_urls` - Clone URLs where the PR commit can be fetched | ||
| 298 | /// | ||
| 299 | /// # Returns | ||
| 300 | /// * `Ok(Event)` - Signed PR event ready to send | ||
| 301 | /// * `Err(String)` - If signing fails | ||
| 302 | pub fn create_pr_event_with_clone( | ||
| 303 | keys: &Keys, | ||
| 304 | repo_coord: &str, | ||
| 305 | commit_hash: &str, | ||
| 306 | title: &str, | ||
| 307 | clone_urls: &[&str], | ||
| 308 | ) -> Result<Event, String> { | ||
| 309 | let mut tags = vec![ | ||
| 310 | // a-tag referencing the repository | ||
| 311 | Tag::custom(TagKind::custom("a"), vec![repo_coord.to_string()]), | ||
| 312 | // c-tag with the commit hash | ||
| 313 | Tag::custom(TagKind::custom("c"), vec![commit_hash.to_string()]), | ||
| 314 | ]; | ||
| 315 | |||
| 316 | // Add clone URLs if provided | ||
| 317 | if !clone_urls.is_empty() { | ||
| 318 | let urls: Vec<String> = clone_urls.iter().map(|s| s.to_string()).collect(); | ||
| 319 | tags.push(Tag::custom(TagKind::Clone, urls)); | ||
| 320 | } | ||
| 321 | |||
| 322 | EventBuilder::new(Kind::Custom(KIND_PR), title) | ||
| 323 | .tags(tags) | ||
| 324 | .sign_with_keys(keys) | ||
| 325 | .map_err(|e| format!("Failed to sign PR event: {}", e)) | ||
| 326 | } | ||
| 327 | |||
| 274 | /// Build a repository coordinate string for use in 'a' tags. | 328 | /// Build a repository coordinate string for use in 'a' tags. |
| 275 | /// | 329 | /// |
| 276 | /// Format: `30617:pubkey_hex:identifier` | 330 | /// Format: `30617:pubkey_hex:identifier` |
| @@ -738,4 +792,65 @@ mod tests { | |||
| 738 | let branch_commit = String::from_utf8_lossy(&output.stdout).trim().to_string(); | 792 | let branch_commit = String::from_utf8_lossy(&output.stdout).trim().to_string(); |
| 739 | assert_eq!(branch_commit, commit_hash); | 793 | assert_eq!(branch_commit, commit_hash); |
| 740 | } | 794 | } |
| 795 | |||
| 796 | #[test] | ||
| 797 | fn test_create_pr_event_with_clone_has_correct_tags() { | ||
| 798 | let keys = Keys::generate(); | ||
| 799 | let repo_coord = build_repo_coord(&keys, "test-repo"); | ||
| 800 | let event = create_pr_event_with_clone( | ||
| 801 | &keys, | ||
| 802 | &repo_coord, | ||
| 803 | "abc123def456", | ||
| 804 | "Test PR with clone", | ||
| 805 | &["http://fork-server.com/repo.git", "http://another-server.com/repo.git"], | ||
| 806 | ) | ||
| 807 | .expect("Failed to create PR event with clone"); | ||
| 808 | |||
| 809 | assert_eq!(event.kind.as_u16(), KIND_PR); | ||
| 810 | |||
| 811 | // Check a-tag | ||
| 812 | let has_a_tag = event.tags.iter().any(|tag| { | ||
| 813 | let slice = tag.as_slice(); | ||
| 814 | slice.first().is_some_and(|t| t == "a") && slice.get(1).is_some_and(|v| v == &repo_coord) | ||
| 815 | }); | ||
| 816 | assert!(has_a_tag, "Event should have 'a' tag"); | ||
| 817 | |||
| 818 | // Check c-tag | ||
| 819 | let has_c_tag = event.tags.iter().any(|tag| { | ||
| 820 | let slice = tag.as_slice(); | ||
| 821 | slice.first().is_some_and(|t| t == "c") | ||
| 822 | && slice.get(1).is_some_and(|v| v == "abc123def456") | ||
| 823 | }); | ||
| 824 | assert!(has_c_tag, "Event should have 'c' tag with commit"); | ||
| 825 | |||
| 826 | // Check clone tag with both URLs | ||
| 827 | let has_clone_tag = event.tags.iter().any(|tag| { | ||
| 828 | let slice = tag.as_slice(); | ||
| 829 | slice.first().is_some_and(|t| t == "clone") | ||
| 830 | && slice.get(1).is_some_and(|v| v == "http://fork-server.com/repo.git") | ||
| 831 | && slice.get(2).is_some_and(|v| v == "http://another-server.com/repo.git") | ||
| 832 | }); | ||
| 833 | assert!(has_clone_tag, "Event should have 'clone' tag with URLs"); | ||
| 834 | } | ||
| 835 | |||
| 836 | #[test] | ||
| 837 | fn test_create_pr_event_with_clone_empty_urls() { | ||
| 838 | let keys = Keys::generate(); | ||
| 839 | let repo_coord = build_repo_coord(&keys, "test-repo"); | ||
| 840 | let event = create_pr_event_with_clone( | ||
| 841 | &keys, | ||
| 842 | &repo_coord, | ||
| 843 | "abc123def456", | ||
| 844 | "Test PR without clone URLs", | ||
| 845 | &[], // Empty clone URLs | ||
| 846 | ) | ||
| 847 | .expect("Failed to create PR event"); | ||
| 848 | |||
| 849 | // Should not have clone tag when no URLs provided | ||
| 850 | let has_clone_tag = event.tags.iter().any(|tag| { | ||
| 851 | let slice = tag.as_slice(); | ||
| 852 | slice.first().is_some_and(|t| t == "clone") | ||
| 853 | }); | ||
| 854 | assert!(!has_clone_tag, "Event should not have 'clone' tag when no URLs provided"); | ||
| 855 | } | ||
| 741 | } | 856 | } |
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs index 0b4d864..fe03455 100644 --- a/tests/purgatory_sync.rs +++ b/tests/purgatory_sync.rs | |||
| @@ -29,9 +29,10 @@ mod common; | |||
| 29 | 29 | ||
| 30 | use common::{ | 30 | use common::{ |
| 31 | add_commit_to_repo, build_repo_coord, check_ref_at_commit, create_pr_event, | 31 | add_commit_to_repo, build_repo_coord, check_ref_at_commit, create_pr_event, |
| 32 | create_repo_announcement, create_state_event, create_test_repo_with_commit, push_ref_to_relay, | 32 | create_pr_event_with_clone, create_repo_announcement, create_state_event, |
| 33 | push_to_relay, verify_event_not_served, wait_for_event_served, wait_for_sync_connection, | 33 | create_test_repo_with_commit, push_ref_to_relay, push_to_relay, verify_event_not_served, |
| 34 | CommitVariant, TestRelay, | 34 | wait_for_event_served, wait_for_sync_connection, CommitVariant, MockRelay, SimpleGitServer, |
| 35 | TestRelay, | ||
| 35 | }; | 36 | }; |
| 36 | use nostr_sdk::prelude::*; | 37 | use nostr_sdk::prelude::*; |
| 37 | use std::time::Duration; | 38 | use std::time::Duration; |
| @@ -55,9 +56,8 @@ async fn test_push_triggers_unified_processing() { | |||
| 55 | 56 | ||
| 56 | // 2. Create test repository locally with deterministic commit | 57 | // 2. Create test repository locally with deterministic commit |
| 57 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | 58 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); |
| 58 | let commit_hash = | 59 | let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) |
| 59 | create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | 60 | .expect("Failed to create test repo"); |
| 60 | .expect("Failed to create test repo"); | ||
| 61 | 61 | ||
| 62 | // 3. Create and send announcement | 62 | // 3. Create and send announcement |
| 63 | let announcement = create_repo_announcement(&keys, &[&relay.domain()], identifier); | 63 | let announcement = create_repo_announcement(&keys, &[&relay.domain()], identifier); |
| @@ -343,8 +343,13 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 343 | // The PR event goes to purgatory on source relay, which authorizes the push | 343 | // The PR event goes to purgatory on source relay, which authorizes the push |
| 344 | let repo_coord = build_repo_coord(&owner_keys, identifier); | 344 | let repo_coord = build_repo_coord(&owner_keys, identifier); |
| 345 | 345 | ||
| 346 | let pr_event = create_pr_event(&pr_author_keys, &repo_coord, &commit_hash, "Test PR for sync") | 346 | let pr_event = create_pr_event( |
| 347 | .expect("Failed to create PR event"); | 347 | &pr_author_keys, |
| 348 | &repo_coord, | ||
| 349 | &commit_hash, | ||
| 350 | "Test PR for sync", | ||
| 351 | ) | ||
| 352 | .expect("Failed to create PR event"); | ||
| 348 | 353 | ||
| 349 | let pr_event_id = pr_event.id; | 354 | let pr_event_id = pr_event.id; |
| 350 | 355 | ||
| @@ -418,15 +423,10 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 418 | ); | 423 | ); |
| 419 | 424 | ||
| 420 | // 8. Verify refs/nostr/<event-id> was created on syncing relay | 425 | // 8. Verify refs/nostr/<event-id> was created on syncing relay |
| 421 | let ref_correct = check_ref_at_commit( | 426 | let ref_correct = |
| 422 | &syncing_domain, | 427 | check_ref_at_commit(&syncing_domain, &npub, identifier, &ref_name, &commit_hash) |
| 423 | &npub, | 428 | .await |
| 424 | identifier, | 429 | .expect("Failed to check PR ref"); |
| 425 | &ref_name, | ||
| 426 | &commit_hash, | ||
| 427 | ) | ||
| 428 | .await | ||
| 429 | .expect("Failed to check PR ref"); | ||
| 430 | 430 | ||
| 431 | assert!( | 431 | assert!( |
| 432 | ref_correct, | 432 | ref_correct, |
| @@ -624,8 +624,8 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 624 | state_found.err() | 624 | state_found.err() |
| 625 | ); | 625 | ); |
| 626 | 626 | ||
| 627 | let pr_found = wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)) | 627 | let pr_found = |
| 628 | .await; | 628 | wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await; |
| 629 | 629 | ||
| 630 | assert!( | 630 | assert!( |
| 631 | pr_found.is_ok(), | 631 | pr_found.is_ok(), |
| @@ -674,3 +674,461 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 674 | syncing_relay.stop().await; | 674 | syncing_relay.stop().await; |
| 675 | source_relay.stop().await; | 675 | source_relay.stop().await; |
| 676 | } | 676 | } |
| 677 | |||
| 678 | /// Test PR event clone tag sync with relay discovery from announcement tags and partial git data sync | ||
| 679 | /// from multiple servers (state and pr git data from different places) | ||
| 680 | /// | ||
| 681 | /// This comprehensive test verifies: | ||
| 682 | /// 1. Relay discovery: syncing_relay discovers other relays from announcement's `relays` tag | ||
| 683 | /// 2. PR clone tag sync: PR events with `clone` tags have their URLs used during purgatory sync | ||
| 684 | /// 3. OID aggregation: OIDs can be aggregated from multiple sources when no single server has all data | ||
| 685 | /// | ||
| 686 | /// ## Key Difference from Bootstrap-Based Sync | ||
| 687 | /// | ||
| 688 | /// Unlike tests that use bootstrap relay configuration, this test: | ||
| 689 | /// - Starts syncing_relay with NO bootstrap relay | ||
| 690 | /// - Publishes announcement DIRECTLY to syncing_relay | ||
| 691 | /// - syncing_relay discovers source_grasp and mock_relay from announcement's `relays` tag | ||
| 692 | /// | ||
| 693 | /// This validates the relay discovery mechanism that allows GRASP relays to find | ||
| 694 | /// and sync from other relays listed in repository announcements. | ||
| 695 | /// | ||
| 696 | /// ## Architecture | ||
| 697 | /// | ||
| 698 | /// ```text | ||
| 699 | /// ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐ | ||
| 700 | /// │ source_grasp │ │ mock_relay │ │ git_server │ | ||
| 701 | /// │ (GRASP relay) │ │ (rust-nostr relay) │ │ (SimpleGitServer) │ | ||
| 702 | /// │ │ │ │ │ │ | ||
| 703 | /// │ Has: │ │ Has: │ │ Has: │ | ||
| 704 | /// │ - Announcement │ │ - PR event │ │ - PR commit (commit_b) │ | ||
| 705 | /// │ - State event (served) │ │ (served immediately, │ │ at refs/heads/main │ | ||
| 706 | /// │ - refs/heads/main │ │ no purgatory) │ │ │ | ||
| 707 | /// │ → commit_a │ │ │ │ Does NOT have: │ | ||
| 708 | /// │ │ │ PR event has clone tag │ │ - commit_a │ | ||
| 709 | /// │ Does NOT have: │ │ pointing to git_server │ │ │ | ||
| 710 | /// │ - PR commit (commit_b) │ │ │ │ │ | ||
| 711 | /// └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ | ||
| 712 | /// │ │ │ | ||
| 713 | /// └───────────────────────────────┼───────────────────────────────┘ | ||
| 714 | /// ▼ | ||
| 715 | /// ┌─────────────────────────────────────────────────────────────────────────────────────────┐ | ||
| 716 | /// │ syncing_relay (GRASP relay under test) │ | ||
| 717 | /// │ │ | ||
| 718 | /// │ Flow: │ | ||
| 719 | /// │ 1. Started with NO bootstrap relay (sync enabled but no initial connections) │ | ||
| 720 | /// │ 2. Announcement published DIRECTLY to syncing_relay │ | ||
| 721 | /// │ 3. Relay discovers source_grasp and mock_relay from announcement's `relays` tag │ | ||
| 722 | /// │ 4. Syncs state event from source_grasp → purgatory (no commit_a locally) │ | ||
| 723 | /// │ 5. Syncs PR event from mock_relay → purgatory (no commit_b locally) │ | ||
| 724 | /// │ 6. Purgatory sync triggers │ | ||
| 725 | /// │ 7. Fetches commit_a from source_grasp clone URL (from announcement clone tag) │ | ||
| 726 | /// │ 8. Fetches commit_b from git_server (from PR event's clone tag) │ | ||
| 727 | /// │ 9. Both events released when all OIDs available │ | ||
| 728 | /// │ │ | ||
| 729 | /// │ Result: │ | ||
| 730 | /// │ - State event served │ | ||
| 731 | /// │ - PR event served │ | ||
| 732 | /// │ - refs/heads/main → commit_a (from source_grasp) │ | ||
| 733 | /// │ - refs/nostr/<event-id> → commit_b (from git_server via PR clone tag) │ | ||
| 734 | /// └─────────────────────────────────────────────────────────────────────────────────────────┘ | ||
| 735 | /// ``` | ||
| 736 | #[tokio::test] | ||
| 737 | async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple_server() { | ||
| 738 | // ======================================================================== | ||
| 739 | // Step 1: Setup Repositories | ||
| 740 | // ======================================================================== | ||
| 741 | |||
| 742 | // Repo A: main branch with commit_a (for state event) | ||
| 743 | let repo_a = tempfile::tempdir().expect("Failed to create temp dir for repo_a"); | ||
| 744 | let commit_a = create_test_repo_with_commit(repo_a.path(), CommitVariant::StateTest) | ||
| 745 | .expect("Failed to create commit_a"); | ||
| 746 | |||
| 747 | // Repo B: PR commit (commit_b) - different content | ||
| 748 | let repo_b = tempfile::tempdir().expect("Failed to create temp dir for repo_b"); | ||
| 749 | let commit_b = create_test_repo_with_commit(repo_b.path(), CommitVariant::PrTest) | ||
| 750 | .expect("Failed to create commit_b"); | ||
| 751 | |||
| 752 | // ======================================================================== | ||
| 753 | // Step 2: Start Servers | ||
| 754 | // ======================================================================== | ||
| 755 | |||
| 756 | // 1. source_grasp - GRASP relay with main branch data | ||
| 757 | let source_grasp = TestRelay::start().await; | ||
| 758 | |||
| 759 | // 2. mock_relay - rust-nostr relay for PR event (no validation, no purgatory) | ||
| 760 | let mock_relay = MockRelay::start().await; | ||
| 761 | |||
| 762 | // 3. git_server - SimpleGitServer with PR commit only | ||
| 763 | let git_server = SimpleGitServer::start(repo_b.path()).await; | ||
| 764 | |||
| 765 | // 4. Pre-allocate syncing_relay port for announcement tags | ||
| 766 | let syncing_port = TestRelay::find_free_port(); | ||
| 767 | let syncing_domain = format!("127.0.0.1:{}", syncing_port); | ||
| 768 | |||
| 769 | // ======================================================================== | ||
| 770 | // Step 3: Setup source_grasp with announcement and state event | ||
| 771 | // ======================================================================== | ||
| 772 | |||
| 773 | let owner_keys = Keys::generate(); | ||
| 774 | let pr_author_keys = Keys::generate(); | ||
| 775 | let identifier = "pr-clone-partial-oid-test"; | ||
| 776 | let npub = owner_keys | ||
| 777 | .public_key() | ||
| 778 | .to_bech32() | ||
| 779 | .expect("Failed to get npub"); | ||
| 780 | |||
| 781 | // Build URLs for announcement | ||
| 782 | // - clone tag: ONLY source_grasp (has main branch data) | ||
| 783 | // - relays tag: source_grasp + mock_relay (mock_relay will serve PR event) | ||
| 784 | let clone_url_source = format!( | ||
| 785 | "http://{}/{}/{}.git", | ||
| 786 | source_grasp.domain(), | ||
| 787 | npub, | ||
| 788 | identifier | ||
| 789 | ); | ||
| 790 | let clone_url_syncing = format!("http://{}/{}/{}.git", syncing_domain, npub, identifier); | ||
| 791 | |||
| 792 | // Create announcement with custom clone/relay URLs | ||
| 793 | // Clone URLs: source_grasp + syncing (NOT git_server - PR commit only via PR's clone tag) | ||
| 794 | // Relay URLs: source_grasp + mock_relay + syncing | ||
| 795 | let announcement = nostr_sdk::EventBuilder::new( | ||
| 796 | nostr_sdk::Kind::Custom(30617), | ||
| 797 | "Repository for PR clone tag + partial OID test", | ||
| 798 | ) | ||
| 799 | .tags(vec![ | ||
| 800 | nostr_sdk::Tag::identifier(identifier), | ||
| 801 | nostr_sdk::Tag::custom( | ||
| 802 | nostr_sdk::TagKind::custom("clone"), | ||
| 803 | vec![clone_url_source.clone(), clone_url_syncing.clone()], | ||
| 804 | ), | ||
| 805 | nostr_sdk::Tag::custom( | ||
| 806 | nostr_sdk::TagKind::custom("relays"), | ||
| 807 | vec![ | ||
| 808 | source_grasp.url().to_string(), | ||
| 809 | mock_relay.url().to_string(), | ||
| 810 | format!("ws://{}", syncing_domain), | ||
| 811 | ], | ||
| 812 | ), | ||
| 813 | ]) | ||
| 814 | .sign_with_keys(&owner_keys) | ||
| 815 | .expect("Failed to sign announcement"); | ||
| 816 | |||
| 817 | // Connect to source_grasp and send announcement | ||
| 818 | let source_client = Client::new(owner_keys.clone()); | ||
| 819 | source_client | ||
| 820 | .add_relay(source_grasp.url()) | ||
| 821 | .await | ||
| 822 | .expect("Failed to add source_grasp relay"); | ||
| 823 | source_client.connect().await; | ||
| 824 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 825 | |||
| 826 | source_client | ||
| 827 | .send_event(&announcement) | ||
| 828 | .await | ||
| 829 | .expect("Failed to send announcement to source_grasp"); | ||
| 830 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 831 | |||
| 832 | // Create state event referencing commit_a | ||
| 833 | let state_event = create_state_event( | ||
| 834 | &owner_keys, | ||
| 835 | identifier, | ||
| 836 | &[("main", &commit_a)], | ||
| 837 | &[], | ||
| 838 | &[&clone_url_source, &clone_url_syncing], | ||
| 839 | &[ | ||
| 840 | source_grasp.url(), | ||
| 841 | mock_relay.url(), | ||
| 842 | &format!("ws://{}", syncing_domain), | ||
| 843 | ], | ||
| 844 | ) | ||
| 845 | .expect("Failed to create state event"); | ||
| 846 | |||
| 847 | let state_event_id = state_event.id; | ||
| 848 | |||
| 849 | // Send state event to source_grasp (goes to purgatory - no git data yet) | ||
| 850 | source_client | ||
| 851 | .send_event(&state_event) | ||
| 852 | .await | ||
| 853 | .expect("Failed to send state event to source_grasp"); | ||
| 854 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 855 | |||
| 856 | // Push main branch (commit_a) to source_grasp - releases state event | ||
| 857 | push_to_relay(repo_a.path(), &source_grasp.domain(), &npub, identifier) | ||
| 858 | .expect("Push to source_grasp should succeed"); | ||
| 859 | |||
| 860 | // Verify state event is served on source_grasp | ||
| 861 | wait_for_event_served(source_grasp.url(), &state_event_id, Duration::from_secs(5)) | ||
| 862 | .await | ||
| 863 | .expect("State event should be served on source_grasp after push"); | ||
| 864 | |||
| 865 | // ======================================================================== | ||
| 866 | // Step 4: Setup mock_relay with PR event | ||
| 867 | // ======================================================================== | ||
| 868 | |||
| 869 | // First, send announcement to mock_relay so it has the repo context | ||
| 870 | // This is needed because the sync system filters events based on whether | ||
| 871 | // they reference repos that list our relay | ||
| 872 | let mock_client = Client::new(owner_keys.clone()); | ||
| 873 | mock_client | ||
| 874 | .add_relay(mock_relay.url()) | ||
| 875 | .await | ||
| 876 | .expect("Failed to add mock_relay for announcement"); | ||
| 877 | mock_client.connect().await; | ||
| 878 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 879 | |||
| 880 | mock_client | ||
| 881 | .send_event(&announcement) | ||
| 882 | .await | ||
| 883 | .expect("Failed to send announcement to mock_relay"); | ||
| 884 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 885 | |||
| 886 | let repo_coord = build_repo_coord(&owner_keys, identifier); | ||
| 887 | |||
| 888 | // Create PR event with clone tag pointing to git_server | ||
| 889 | // This is the KEY part - the PR's clone tag provides the URL for commit_b | ||
| 890 | let pr_event = create_pr_event_with_clone( | ||
| 891 | &pr_author_keys, | ||
| 892 | &repo_coord, | ||
| 893 | &commit_b, | ||
| 894 | "Test PR for partial OID aggregation", | ||
| 895 | &[git_server.url()], // Clone URL points to SimpleGitServer | ||
| 896 | ) | ||
| 897 | .expect("Failed to create PR event"); | ||
| 898 | |||
| 899 | let pr_event_id = pr_event.id; | ||
| 900 | |||
| 901 | // Send PR event to mock_relay | ||
| 902 | // MockRelay accepts all events without validation (no purgatory) | ||
| 903 | let pr_client = Client::new(pr_author_keys.clone()); | ||
| 904 | pr_client | ||
| 905 | .add_relay(mock_relay.url()) | ||
| 906 | .await | ||
| 907 | .expect("Failed to add mock_relay"); | ||
| 908 | pr_client.connect().await; | ||
| 909 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 910 | |||
| 911 | pr_client | ||
| 912 | .send_event(&pr_event) | ||
| 913 | .await | ||
| 914 | .expect("Failed to send PR event to mock_relay"); | ||
| 915 | |||
| 916 | // Verify PR event is served on mock_relay (immediate, no purgatory) | ||
| 917 | wait_for_event_served(mock_relay.url(), &pr_event_id, Duration::from_secs(5)) | ||
| 918 | .await | ||
| 919 | .expect("PR event should be served on mock_relay immediately"); | ||
| 920 | |||
| 921 | // ======================================================================== | ||
| 922 | // Step 5: Start syncing_relay WITHOUT bootstrap and publish announcement directly | ||
| 923 | // ======================================================================== | ||
| 924 | |||
| 925 | // Start syncing_relay with sync enabled but NO bootstrap relay | ||
| 926 | // This tests relay discovery from announcement's `relays` tag | ||
| 927 | // Note: We disable negentropy because MockRelay doesn't support NIP-77, | ||
| 928 | // and the sync system doesn't properly fall back to REQ+EOSE when negentropy fails. | ||
| 929 | let syncing_relay = TestRelay::start_on_port_with_options( | ||
| 930 | syncing_port, | ||
| 931 | None, // NO bootstrap - relay discovery via announcement tags | ||
| 932 | true, // Disable negentropy - MockRelay doesn't support NIP-77 | ||
| 933 | ) | ||
| 934 | .await; | ||
| 935 | |||
| 936 | // Publish announcement DIRECTLY to syncing_relay | ||
| 937 | // This triggers relay discovery from the announcement's `relays` tag | ||
| 938 | let syncing_client = Client::new(owner_keys.clone()); | ||
| 939 | syncing_client | ||
| 940 | .add_relay(syncing_relay.url()) | ||
| 941 | .await | ||
| 942 | .expect("Failed to add syncing_relay"); | ||
| 943 | syncing_client.connect().await; | ||
| 944 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 945 | |||
| 946 | syncing_client | ||
| 947 | .send_event(&announcement) | ||
| 948 | .await | ||
| 949 | .expect("Failed to send announcement to syncing_relay"); | ||
| 950 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 951 | |||
| 952 | // Wait for relay discovery and sync connections to establish | ||
| 953 | // syncing_relay should discover source_grasp and mock_relay from announcement's relays tag | ||
| 954 | println!("=== Waiting for sync connections ==="); | ||
| 955 | println!("syncing_relay URL: {}", syncing_relay.url()); | ||
| 956 | println!("source_grasp URL: {}", source_grasp.url()); | ||
| 957 | println!("mock_relay URL: {}", mock_relay.url()); | ||
| 958 | println!("git_server URL: {}", git_server.url()); | ||
| 959 | |||
| 960 | wait_for_sync_connection(syncing_relay.url(), 2, Duration::from_secs(10)) | ||
| 961 | .await | ||
| 962 | .expect( | ||
| 963 | "Sync connections should establish to discovered relays (source_grasp + mock_relay)", | ||
| 964 | ); | ||
| 965 | println!("Sync connections established!"); | ||
| 966 | |||
| 967 | // Debug: Check metrics to see what relays are connected | ||
| 968 | let metrics_url = syncing_relay | ||
| 969 | .url() | ||
| 970 | .replace("ws://", "http://") | ||
| 971 | .replace("/", "") | ||
| 972 | + "/metrics"; | ||
| 973 | println!("Checking metrics at: {}", metrics_url); | ||
| 974 | if let Ok(response) = reqwest::get(&metrics_url).await { | ||
| 975 | if let Ok(metrics) = response.text().await { | ||
| 976 | // Print sync-related metrics | ||
| 977 | for line in metrics.lines() { | ||
| 978 | if line.contains("sync") && !line.starts_with('#') { | ||
| 979 | println!(" {}", line); | ||
| 980 | } | ||
| 981 | } | ||
| 982 | } | ||
| 983 | } | ||
| 984 | |||
| 985 | // Give some time for sync to happen | ||
| 986 | println!("Waiting 10s for events to sync..."); | ||
| 987 | tokio::time::sleep(Duration::from_secs(10)).await; | ||
| 988 | |||
| 989 | // Check metrics again after waiting | ||
| 990 | println!("=== Checking metrics after sync wait ==="); | ||
| 991 | if let Ok(response) = reqwest::get(&metrics_url).await { | ||
| 992 | if let Ok(metrics) = response.text().await { | ||
| 993 | for line in metrics.lines() { | ||
| 994 | if line.contains("sync") && !line.starts_with('#') { | ||
| 995 | println!(" {}", line); | ||
| 996 | } | ||
| 997 | } | ||
| 998 | } | ||
| 999 | } | ||
| 1000 | |||
| 1001 | // Debug: Check if PR event is still on mock_relay | ||
| 1002 | println!("=== Debug: Checking PR event on mock_relay ==="); | ||
| 1003 | let pr_on_mock = | ||
| 1004 | wait_for_event_served(mock_relay.url(), &pr_event_id, Duration::from_secs(2)).await; | ||
| 1005 | println!("PR event on mock_relay: {:?}", pr_on_mock.is_ok()); | ||
| 1006 | if let Ok(ref pr) = pr_on_mock { | ||
| 1007 | println!("PR event tags:"); | ||
| 1008 | for tag in pr.tags.iter() { | ||
| 1009 | println!(" {:?}", tag.as_slice()); | ||
| 1010 | } | ||
| 1011 | } | ||
| 1012 | |||
| 1013 | // Debug: Check repo coordinate | ||
| 1014 | let repo_coord = build_repo_coord(&owner_keys, identifier); | ||
| 1015 | println!("Expected repo coordinate: {}", repo_coord); | ||
| 1016 | |||
| 1017 | // Debug: Test if mock_relay responds to tag-based filter (Layer 2 style) | ||
| 1018 | println!("=== Debug: Testing mock_relay tag filter response ==="); | ||
| 1019 | let test_client = Client::new(Keys::generate()); | ||
| 1020 | test_client | ||
| 1021 | .add_relay(mock_relay.url()) | ||
| 1022 | .await | ||
| 1023 | .expect("Failed to add mock_relay"); | ||
| 1024 | test_client.connect().await; | ||
| 1025 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 1026 | |||
| 1027 | // Build a Layer 2 style filter (by 'a' tag) | ||
| 1028 | let tag_filter = | ||
| 1029 | Filter::new().custom_tag(SingleLetterTag::lowercase(Alphabet::A), repo_coord.as_str()); | ||
| 1030 | println!("Tag filter: {:?}", tag_filter); | ||
| 1031 | |||
| 1032 | let tag_results = test_client | ||
| 1033 | .fetch_events(tag_filter, Duration::from_secs(5)) | ||
| 1034 | .await; | ||
| 1035 | match tag_results { | ||
| 1036 | Ok(events) => { | ||
| 1037 | println!("Tag filter returned {} events", events.len()); | ||
| 1038 | for event in events.iter() { | ||
| 1039 | println!(" Event ID: {}, Kind: {}", event.id, event.kind.as_u16()); | ||
| 1040 | } | ||
| 1041 | } | ||
| 1042 | Err(e) => { | ||
| 1043 | println!("Tag filter query failed: {:?}", e); | ||
| 1044 | } | ||
| 1045 | } | ||
| 1046 | test_client.disconnect().await; | ||
| 1047 | |||
| 1048 | // The syncing relay will: | ||
| 1049 | // 1. Receive announcement directly (creates bare repo) | ||
| 1050 | // 2. Discover source_grasp and mock_relay from announcement's `relays` tag | ||
| 1051 | // 3. Connect to discovered relays | ||
| 1052 | // 4. Sync state event from source_grasp → purgatory (no commit_a locally) | ||
| 1053 | // 5. Sync PR event from mock_relay → purgatory (no commit_b locally) | ||
| 1054 | // 6. Purgatory sync triggers | ||
| 1055 | // 7. Fetches commit_a from source_grasp clone URL (from announcement clone tag) | ||
| 1056 | // 8. Fetches commit_b from git_server (from PR event's clone tag) | ||
| 1057 | // 9. Both events released when all OIDs available | ||
| 1058 | |||
| 1059 | // ======================================================================== | ||
| 1060 | // Step 6: Verify Results | ||
| 1061 | // ======================================================================== | ||
| 1062 | |||
| 1063 | println!("=== Step 6: Verify Results ==="); | ||
| 1064 | println!("State event ID: {}", state_event_id); | ||
| 1065 | println!("PR event ID: {}", pr_event_id); | ||
| 1066 | println!("commit_a: {}", commit_a); | ||
| 1067 | println!("commit_b: {}", commit_b); | ||
| 1068 | |||
| 1069 | // Wait for state event to be served on syncing_relay | ||
| 1070 | println!("Waiting for state event on syncing_relay..."); | ||
| 1071 | let state_found = wait_for_event_served( | ||
| 1072 | syncing_relay.url(), | ||
| 1073 | &state_event_id, | ||
| 1074 | Duration::from_secs(30), | ||
| 1075 | ) | ||
| 1076 | .await; | ||
| 1077 | println!("State event result: {:?}", state_found); | ||
| 1078 | assert!( | ||
| 1079 | state_found.is_ok(), | ||
| 1080 | "State event should be served on syncing_relay: {:?}", | ||
| 1081 | state_found.err() | ||
| 1082 | ); | ||
| 1083 | |||
| 1084 | // Wait for PR event to be served on syncing_relay | ||
| 1085 | println!("Waiting for PR event on syncing_relay..."); | ||
| 1086 | let pr_found = | ||
| 1087 | wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await; | ||
| 1088 | println!("PR event result: {:?}", pr_found); | ||
| 1089 | assert!( | ||
| 1090 | pr_found.is_ok(), | ||
| 1091 | "PR event should be served on syncing_relay (fetched commit_b from git_server via PR clone tag): {:?}", | ||
| 1092 | pr_found.err() | ||
| 1093 | ); | ||
| 1094 | |||
| 1095 | // Verify refs/heads/main → commit_a (from source_grasp) | ||
| 1096 | let main_correct = check_ref_at_commit( | ||
| 1097 | &syncing_domain, | ||
| 1098 | &npub, | ||
| 1099 | identifier, | ||
| 1100 | "refs/heads/main", | ||
| 1101 | &commit_a, | ||
| 1102 | ) | ||
| 1103 | .await | ||
| 1104 | .expect("Failed to check main ref"); | ||
| 1105 | assert!( | ||
| 1106 | main_correct, | ||
| 1107 | "main should point to commit_a ({}) from source_grasp", | ||
| 1108 | commit_a | ||
| 1109 | ); | ||
| 1110 | |||
| 1111 | // Verify refs/nostr/<event-id> → commit_b (from git_server via PR clone tag) | ||
| 1112 | let pr_ref = format!("refs/nostr/{}", pr_event_id.to_hex()); | ||
| 1113 | let pr_correct = check_ref_at_commit(&syncing_domain, &npub, identifier, &pr_ref, &commit_b) | ||
| 1114 | .await | ||
| 1115 | .expect("Failed to check PR ref"); | ||
| 1116 | assert!( | ||
| 1117 | pr_correct, | ||
| 1118 | "PR ref should point to commit_b ({}) fetched from git_server via PR clone tag", | ||
| 1119 | commit_b | ||
| 1120 | ); | ||
| 1121 | |||
| 1122 | // ======================================================================== | ||
| 1123 | // Step 7: Cleanup | ||
| 1124 | // ======================================================================== | ||
| 1125 | |||
| 1126 | source_client.disconnect().await; | ||
| 1127 | mock_client.disconnect().await; | ||
| 1128 | pr_client.disconnect().await; | ||
| 1129 | syncing_client.disconnect().await; | ||
| 1130 | git_server.stop().await; | ||
| 1131 | mock_relay.stop().await; | ||
| 1132 | syncing_relay.stop().await; | ||
| 1133 | source_grasp.stop().await; | ||
| 1134 | } | ||