diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/sync.rs | 1 | ||||
| -rw-r--r-- | tests/sync/maintainer_reprocessing.rs | 453 | ||||
| -rw-r--r-- | tests/sync/mod.rs | 1 |
3 files changed, 455 insertions, 0 deletions
diff --git a/tests/sync.rs b/tests/sync.rs index ad5ca96..104e815 100644 --- a/tests/sync.rs +++ b/tests/sync.rs | |||
| @@ -35,6 +35,7 @@ mod sync { | |||
| 35 | pub mod discovery; | 35 | pub mod discovery; |
| 36 | pub mod historic_sync; | 36 | pub mod historic_sync; |
| 37 | pub mod live_sync; | 37 | pub mod live_sync; |
| 38 | pub mod maintainer_reprocessing; | ||
| 38 | pub mod metrics; | 39 | pub mod metrics; |
| 39 | pub mod tag_variations; | 40 | pub mod tag_variations; |
| 40 | } | 41 | } |
diff --git a/tests/sync/maintainer_reprocessing.rs b/tests/sync/maintainer_reprocessing.rs new file mode 100644 index 0000000..2b7fb0f --- /dev/null +++ b/tests/sync/maintainer_reprocessing.rs | |||
| @@ -0,0 +1,453 @@ | |||
| 1 | //! Integration tests for GRASP-02 PR3: Maintainer Announcement Re-Processing | ||
| 2 | //! | ||
| 3 | //! Tests the two-tier rejected events index and immediate re-processing of | ||
| 4 | //! maintainer announcements when owner announcements are accepted. | ||
| 5 | |||
| 6 | use std::time::Duration; | ||
| 7 | |||
| 8 | use nostr_sdk::prelude::*; | ||
| 9 | |||
| 10 | use crate::common::{sync_helpers::*, TestRelay}; | ||
| 11 | |||
| 12 | /// Test that maintainer announcements are re-processed immediately when owner announcement accepted | ||
| 13 | /// | ||
| 14 | /// Flow: | ||
| 15 | /// 1. relay_a: Maintainer sends announcement (gets rejected - doesn't list relay_b) | ||
| 16 | /// 2. relay_b: Owner sends announcement (lists relay_a + maintainer) | ||
| 17 | /// 3. relay_b syncs from relay_a, maintainer announcement enters rejected index | ||
| 18 | /// 4. relay_b processes owner announcement, invalidates and re-processes maintainer announcement | ||
| 19 | /// 5. Both announcements should be in relay_b's database | ||
| 20 | /// | ||
| 21 | /// Expected time: <5 seconds (vs 24 hours without hot cache) | ||
| 22 | #[tokio::test] | ||
| 23 | async fn test_maintainer_announcement_reprocessed_immediately() { | ||
| 24 | // Start relay_a (where maintainer announcement will be sent) | ||
| 25 | let relay_a = TestRelay::start().await; | ||
| 26 | println!("relay_a started at {}", relay_a.url()); | ||
| 27 | |||
| 28 | // Start relay_b with sync enabled (will sync from relay_a) | ||
| 29 | let relay_b = TestRelay::start_with_sync(None).await; | ||
| 30 | println!("relay_b started at {}", relay_b.url()); | ||
| 31 | |||
| 32 | // Create keys | ||
| 33 | let owner_keys = Keys::generate(); | ||
| 34 | let maintainer_keys = Keys::generate(); | ||
| 35 | |||
| 36 | let identifier = "test-repo"; | ||
| 37 | |||
| 38 | let start = std::time::Instant::now(); | ||
| 39 | |||
| 40 | // Step 1: Send maintainer announcement to relay_a (will be rejected - doesn't list relay_b) | ||
| 41 | let client_a = TestClient::new(relay_a.url(), maintainer_keys.clone()) | ||
| 42 | .await | ||
| 43 | .expect("Failed to connect to relay_a"); | ||
| 44 | |||
| 45 | let maintainer_announcement = EventBuilder::new( | ||
| 46 | Kind::GitRepoAnnouncement, | ||
| 47 | "Maintainer's repository", | ||
| 48 | ) | ||
| 49 | .tags(vec![ | ||
| 50 | Tag::identifier(identifier), | ||
| 51 | Tag::custom( | ||
| 52 | TagKind::custom("clone"), | ||
| 53 | vec![format!("https://{}/{}.git", relay_a.domain(), identifier)], | ||
| 54 | ), | ||
| 55 | Tag::custom( | ||
| 56 | TagKind::custom("relays"), | ||
| 57 | vec![relay_a.url().to_string()], | ||
| 58 | ), | ||
| 59 | ]) | ||
| 60 | .sign_with_keys(&maintainer_keys) | ||
| 61 | .unwrap(); | ||
| 62 | |||
| 63 | client_a.send_event(&maintainer_announcement).await.unwrap(); | ||
| 64 | println!("✓ Maintainer announcement sent to relay_a"); | ||
| 65 | |||
| 66 | // Step 2: Send owner announcement to relay_b (lists relay_a + maintainer) | ||
| 67 | let client_b = TestClient::new(relay_b.url(), owner_keys.clone()) | ||
| 68 | .await | ||
| 69 | .expect("Failed to connect to relay_b"); | ||
| 70 | |||
| 71 | let owner_announcement = EventBuilder::new( | ||
| 72 | Kind::GitRepoAnnouncement, | ||
| 73 | "Owner's repository", | ||
| 74 | ) | ||
| 75 | .tags(vec![ | ||
| 76 | Tag::identifier(identifier), | ||
| 77 | Tag::custom( | ||
| 78 | TagKind::custom("clone"), | ||
| 79 | vec![format!("https://{}/{}.git", relay_b.domain(), identifier)], | ||
| 80 | ), | ||
| 81 | Tag::custom( | ||
| 82 | TagKind::custom("relays"), | ||
| 83 | vec![relay_a.url().to_string(), relay_b.url().to_string()], | ||
| 84 | ), | ||
| 85 | Tag::custom( | ||
| 86 | TagKind::custom("maintainers"), | ||
| 87 | vec![maintainer_keys.public_key().to_hex()], | ||
| 88 | ), | ||
| 89 | ]) | ||
| 90 | .sign_with_keys(&owner_keys) | ||
| 91 | .unwrap(); | ||
| 92 | |||
| 93 | client_b.send_event(&owner_announcement).await.unwrap(); | ||
| 94 | println!("✓ Owner announcement sent to relay_b"); | ||
| 95 | |||
| 96 | // Step 3: Wait for sync and re-processing (relay_b discovers relay_a, syncs, re-processes) | ||
| 97 | tokio::time::sleep(Duration::from_secs(3)).await; | ||
| 98 | |||
| 99 | let elapsed = start.elapsed(); | ||
| 100 | |||
| 101 | // Step 4: Verify both announcements are in relay_b's database | ||
| 102 | let owner_filter = Filter::new() | ||
| 103 | .kind(Kind::GitRepoAnnouncement) | ||
| 104 | .author(owner_keys.public_key()) | ||
| 105 | .identifier(identifier); | ||
| 106 | |||
| 107 | let owner_found = wait_for_event_on_relay(relay_b.url(), owner_filter, Duration::from_secs(2)).await; | ||
| 108 | assert!(owner_found, "Owner announcement should be in relay_b"); | ||
| 109 | |||
| 110 | let maintainer_filter = Filter::new() | ||
| 111 | .kind(Kind::GitRepoAnnouncement) | ||
| 112 | .author(maintainer_keys.public_key()) | ||
| 113 | .identifier(identifier); | ||
| 114 | |||
| 115 | let maintainer_found = wait_for_event_on_relay(relay_b.url(), maintainer_filter, Duration::from_secs(2)).await; | ||
| 116 | assert!(maintainer_found, "Maintainer announcement should be re-processed and accepted in relay_b"); | ||
| 117 | |||
| 118 | // Step 5: Verify it happened quickly (not 24 hours!) | ||
| 119 | assert!( | ||
| 120 | elapsed.as_secs() < 10, | ||
| 121 | "Re-processing should happen in <10 seconds, took {:?}", | ||
| 122 | elapsed | ||
| 123 | ); | ||
| 124 | |||
| 125 | println!("✅ Maintainer announcement re-processed in {:?}", elapsed); | ||
| 126 | |||
| 127 | client_a.disconnect().await; | ||
| 128 | client_b.disconnect().await; | ||
| 129 | relay_a.stop().await; | ||
| 130 | relay_b.stop().await; | ||
| 131 | } | ||
| 132 | |||
| 133 | /// Test that maintainer announcements NOT in hot cache are still prevented from re-fetching | ||
| 134 | /// | ||
| 135 | /// Flow: | ||
| 136 | /// 1. Maintainer announcement arrives → Rejected (added to hot cache + cold index) | ||
| 137 | /// 2. Wait for hot cache to expire (2+ minutes) | ||
| 138 | /// 3. Owner announcement arrives → Invalidates cold index | ||
| 139 | /// 4. Maintainer announcement should NOT be re-fetched (cold index prevents) | ||
| 140 | /// 5. Only owner announcement should be in database | ||
| 141 | /// | ||
| 142 | /// This test verifies the cold index prevents repeated downloads after hot cache expiry. | ||
| 143 | /// Note: This test is slow (2+ minutes) so we'll skip it in normal test runs. | ||
| 144 | #[tokio::test] | ||
| 145 | #[ignore] // Skip by default due to 2+ minute duration | ||
| 146 | async fn test_maintainer_announcement_cold_index_prevents_refetch() { | ||
| 147 | let relay = TestRelay::start().await; | ||
| 148 | |||
| 149 | // Create keys | ||
| 150 | let owner_keys = Keys::generate(); | ||
| 151 | let maintainer_keys = Keys::generate(); | ||
| 152 | |||
| 153 | let identifier = "test-repo-cold"; | ||
| 154 | |||
| 155 | // Create client using TestClient helper | ||
| 156 | let client = TestClient::new(relay.url(), maintainer_keys.clone()) | ||
| 157 | .await | ||
| 158 | .expect("Failed to connect to relay"); | ||
| 159 | |||
| 160 | // Step 1: Send maintainer announcement (will be rejected - doesn't list our relay) | ||
| 161 | let maintainer_announcement = EventBuilder::new( | ||
| 162 | Kind::GitRepoAnnouncement, | ||
| 163 | "Maintainer's repository", | ||
| 164 | ) | ||
| 165 | .tags(vec![ | ||
| 166 | Tag::identifier(identifier), | ||
| 167 | Tag::custom( | ||
| 168 | TagKind::custom("clone"), | ||
| 169 | vec![format!("https://example.com/{}.git", identifier)], | ||
| 170 | ), | ||
| 171 | Tag::custom( | ||
| 172 | TagKind::custom("relays"), | ||
| 173 | vec!["wss://example.com".to_string()], | ||
| 174 | ), | ||
| 175 | ]) | ||
| 176 | .sign_with_keys(&maintainer_keys) | ||
| 177 | .unwrap(); | ||
| 178 | |||
| 179 | // Send maintainer announcement - expect it to be rejected | ||
| 180 | let _ = client.send_event(&maintainer_announcement).await; | ||
| 181 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 182 | |||
| 183 | // Step 2: Wait for hot cache to expire (default: 120 seconds) | ||
| 184 | println!("⏳ Waiting for hot cache to expire (120 seconds)..."); | ||
| 185 | tokio::time::sleep(Duration::from_secs(125)).await; | ||
| 186 | |||
| 187 | // Step 3: Send owner announcement (lists maintainer) | ||
| 188 | let owner_announcement = EventBuilder::new( | ||
| 189 | Kind::GitRepoAnnouncement, | ||
| 190 | "Owner's repository", | ||
| 191 | ) | ||
| 192 | .tags(vec![ | ||
| 193 | Tag::identifier(identifier), | ||
| 194 | Tag::custom( | ||
| 195 | TagKind::custom("clone"), | ||
| 196 | vec![format!("https://{}/{}.git", relay.domain(), identifier)], | ||
| 197 | ), | ||
| 198 | Tag::custom( | ||
| 199 | TagKind::custom("relays"), | ||
| 200 | vec![relay.url().to_string()], | ||
| 201 | ), | ||
| 202 | Tag::custom( | ||
| 203 | TagKind::custom("maintainers"), | ||
| 204 | vec![maintainer_keys.public_key().to_hex()], | ||
| 205 | ), | ||
| 206 | ]) | ||
| 207 | .sign_with_keys(&owner_keys) | ||
| 208 | .unwrap(); | ||
| 209 | |||
| 210 | client.send_event(&owner_announcement).await.unwrap(); | ||
| 211 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 212 | |||
| 213 | // Step 4: Verify only owner announcement is in database | ||
| 214 | let owner_filter = Filter::new() | ||
| 215 | .kind(Kind::GitRepoAnnouncement) | ||
| 216 | .author(owner_keys.public_key()) | ||
| 217 | .identifier(identifier); | ||
| 218 | |||
| 219 | let owner_found = wait_for_event_on_relay(relay.url(), owner_filter, Duration::from_secs(2)).await; | ||
| 220 | assert!(owner_found, "Owner announcement should be accepted"); | ||
| 221 | |||
| 222 | let maintainer_filter = Filter::new() | ||
| 223 | .kind(Kind::GitRepoAnnouncement) | ||
| 224 | .author(maintainer_keys.public_key()) | ||
| 225 | .identifier(identifier); | ||
| 226 | |||
| 227 | let maintainer_found = wait_for_event_on_relay(relay.url(), maintainer_filter, Duration::from_millis(500)).await; | ||
| 228 | assert!( | ||
| 229 | !maintainer_found, | ||
| 230 | "Maintainer announcement should NOT be re-processed (hot cache expired)" | ||
| 231 | ); | ||
| 232 | |||
| 233 | println!("✅ Cold index prevented re-fetch after hot cache expiry"); | ||
| 234 | |||
| 235 | client.disconnect().await; | ||
| 236 | relay.stop().await; | ||
| 237 | } | ||
| 238 | |||
| 239 | /// Test multiple maintainers are all re-processed when owner announcement accepted | ||
| 240 | /// | ||
| 241 | /// Flow: | ||
| 242 | /// 1. relay_a: Three maintainers send announcements (get rejected - don't list relay_b) | ||
| 243 | /// 2. relay_b: Owner sends announcement (lists relay_a + all three maintainers) | ||
| 244 | /// 3. relay_b syncs from relay_a, all maintainer announcements enter rejected index | ||
| 245 | /// 4. relay_b processes owner announcement, invalidates and re-processes all maintainer announcements | ||
| 246 | /// 5. All four announcements should be in relay_b's database | ||
| 247 | #[tokio::test] | ||
| 248 | async fn test_multiple_maintainers_all_reprocessed() { | ||
| 249 | // Start relay_a (where maintainer announcements will be sent) | ||
| 250 | let relay_a = TestRelay::start().await; | ||
| 251 | println!("relay_a started at {}", relay_a.url()); | ||
| 252 | |||
| 253 | // Start relay_b with sync enabled (will sync from relay_a) | ||
| 254 | let relay_b = TestRelay::start_with_sync(None).await; | ||
| 255 | println!("relay_b started at {}", relay_b.url()); | ||
| 256 | |||
| 257 | // Create keys | ||
| 258 | let owner_keys = Keys::generate(); | ||
| 259 | let maintainer1_keys = Keys::generate(); | ||
| 260 | let maintainer2_keys = Keys::generate(); | ||
| 261 | let maintainer3_keys = Keys::generate(); | ||
| 262 | |||
| 263 | let identifier = "multi-maintainer-repo"; | ||
| 264 | |||
| 265 | // Step 1: Send three maintainer announcements to relay_a | ||
| 266 | let client_a = TestClient::new(relay_a.url(), maintainer1_keys.clone()) | ||
| 267 | .await | ||
| 268 | .expect("Failed to connect to relay_a"); | ||
| 269 | |||
| 270 | for (idx, maintainer_keys) in [&maintainer1_keys, &maintainer2_keys, &maintainer3_keys].iter().enumerate() { | ||
| 271 | let announcement = EventBuilder::new( | ||
| 272 | Kind::GitRepoAnnouncement, | ||
| 273 | format!("Maintainer {} repository", idx + 1), | ||
| 274 | ) | ||
| 275 | .tags(vec![ | ||
| 276 | Tag::identifier(identifier), | ||
| 277 | Tag::custom( | ||
| 278 | TagKind::custom("clone"), | ||
| 279 | vec![format!("https://{}/{}.git", relay_a.domain(), identifier)], | ||
| 280 | ), | ||
| 281 | Tag::custom( | ||
| 282 | TagKind::custom("relays"), | ||
| 283 | vec![relay_a.url().to_string()], | ||
| 284 | ), | ||
| 285 | ]) | ||
| 286 | .sign_with_keys(maintainer_keys) | ||
| 287 | .unwrap(); | ||
| 288 | |||
| 289 | client_a.send_event(&announcement).await.unwrap(); | ||
| 290 | } | ||
| 291 | println!("✓ Three maintainer announcements sent to relay_a"); | ||
| 292 | |||
| 293 | // Step 2: Send owner announcement to relay_b (lists relay_a + all three maintainers) | ||
| 294 | let client_b = TestClient::new(relay_b.url(), owner_keys.clone()) | ||
| 295 | .await | ||
| 296 | .expect("Failed to connect to relay_b"); | ||
| 297 | |||
| 298 | let owner_announcement = EventBuilder::new( | ||
| 299 | Kind::GitRepoAnnouncement, | ||
| 300 | "Owner's repository", | ||
| 301 | ) | ||
| 302 | .tags(vec![ | ||
| 303 | Tag::identifier(identifier), | ||
| 304 | Tag::custom( | ||
| 305 | TagKind::custom("clone"), | ||
| 306 | vec![format!("https://{}/{}.git", relay_b.domain(), identifier)], | ||
| 307 | ), | ||
| 308 | Tag::custom( | ||
| 309 | TagKind::custom("relays"), | ||
| 310 | vec![relay_a.url().to_string(), relay_b.url().to_string()], | ||
| 311 | ), | ||
| 312 | Tag::custom( | ||
| 313 | TagKind::custom("maintainers"), | ||
| 314 | vec![ | ||
| 315 | maintainer1_keys.public_key().to_hex(), | ||
| 316 | maintainer2_keys.public_key().to_hex(), | ||
| 317 | maintainer3_keys.public_key().to_hex(), | ||
| 318 | ], | ||
| 319 | ), | ||
| 320 | ]) | ||
| 321 | .sign_with_keys(&owner_keys) | ||
| 322 | .unwrap(); | ||
| 323 | |||
| 324 | client_b.send_event(&owner_announcement).await.unwrap(); | ||
| 325 | println!("✓ Owner announcement sent to relay_b"); | ||
| 326 | |||
| 327 | // Step 3: Wait for sync and re-processing | ||
| 328 | tokio::time::sleep(Duration::from_secs(3)).await; | ||
| 329 | |||
| 330 | // Step 4: Verify all four announcements are in relay_b's database | ||
| 331 | for (name, keys) in [ | ||
| 332 | ("owner", &owner_keys), | ||
| 333 | ("maintainer1", &maintainer1_keys), | ||
| 334 | ("maintainer2", &maintainer2_keys), | ||
| 335 | ("maintainer3", &maintainer3_keys), | ||
| 336 | ] { | ||
| 337 | let filter = Filter::new() | ||
| 338 | .kind(Kind::GitRepoAnnouncement) | ||
| 339 | .author(keys.public_key()) | ||
| 340 | .identifier(identifier); | ||
| 341 | |||
| 342 | let found = wait_for_event_on_relay(relay_b.url(), filter, Duration::from_secs(2)).await; | ||
| 343 | assert!( | ||
| 344 | found, | ||
| 345 | "{} announcement should be in relay_b", | ||
| 346 | name | ||
| 347 | ); | ||
| 348 | } | ||
| 349 | |||
| 350 | println!("✅ All three maintainer announcements re-processed successfully"); | ||
| 351 | |||
| 352 | client_a.disconnect().await; | ||
| 353 | client_b.disconnect().await; | ||
| 354 | relay_a.stop().await; | ||
| 355 | relay_b.stop().await; | ||
| 356 | } | ||
| 357 | |||
| 358 | /// Test that invalid maintainer public keys don't cause panics | ||
| 359 | /// | ||
| 360 | /// Flow: | ||
| 361 | /// 1. Maintainer announcement arrives → Rejected | ||
| 362 | /// 2. Owner announcement arrives with INVALID maintainer hex → Should handle gracefully | ||
| 363 | /// 3. Owner announcement should still be accepted | ||
| 364 | /// 4. Maintainer announcement should NOT be re-processed (invalid pubkey) | ||
| 365 | #[tokio::test] | ||
| 366 | async fn test_invalid_maintainer_pubkey_handled_gracefully() { | ||
| 367 | let relay = TestRelay::start().await; | ||
| 368 | |||
| 369 | // Create keys | ||
| 370 | let owner_keys = Keys::generate(); | ||
| 371 | let maintainer_keys = Keys::generate(); | ||
| 372 | |||
| 373 | let identifier = "invalid-maintainer-repo"; | ||
| 374 | |||
| 375 | // Create client using TestClient helper | ||
| 376 | let client = TestClient::new(relay.url(), owner_keys.clone()) | ||
| 377 | .await | ||
| 378 | .expect("Failed to connect to relay"); | ||
| 379 | |||
| 380 | // Step 1: Send maintainer announcement (will be rejected - doesn't list our relay) | ||
| 381 | let maintainer_announcement = EventBuilder::new( | ||
| 382 | Kind::GitRepoAnnouncement, | ||
| 383 | "Maintainer's repository", | ||
| 384 | ) | ||
| 385 | .tags(vec![ | ||
| 386 | Tag::identifier(identifier), | ||
| 387 | Tag::custom( | ||
| 388 | TagKind::custom("clone"), | ||
| 389 | vec![format!("https://example.com/{}.git", identifier)], | ||
| 390 | ), | ||
| 391 | Tag::custom( | ||
| 392 | TagKind::custom("relays"), | ||
| 393 | vec!["wss://example.com".to_string()], | ||
| 394 | ), | ||
| 395 | ]) | ||
| 396 | .sign_with_keys(&maintainer_keys) | ||
| 397 | .unwrap(); | ||
| 398 | |||
| 399 | // Send maintainer announcement - expect it to be rejected | ||
| 400 | let _ = client.send_event(&maintainer_announcement).await; | ||
| 401 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 402 | |||
| 403 | // Step 2: Send owner announcement with INVALID maintainer hex | ||
| 404 | let owner_announcement = EventBuilder::new( | ||
| 405 | Kind::GitRepoAnnouncement, | ||
| 406 | "Owner's repository", | ||
| 407 | ) | ||
| 408 | .tags(vec![ | ||
| 409 | Tag::identifier(identifier), | ||
| 410 | Tag::custom( | ||
| 411 | TagKind::custom("clone"), | ||
| 412 | vec![format!("https://{}/{}.git", relay.domain(), identifier)], | ||
| 413 | ), | ||
| 414 | Tag::custom( | ||
| 415 | TagKind::custom("relays"), | ||
| 416 | vec![relay.url().to_string()], | ||
| 417 | ), | ||
| 418 | Tag::custom( | ||
| 419 | TagKind::custom("maintainers"), | ||
| 420 | vec!["invalid-hex-not-a-pubkey".to_string()], | ||
| 421 | ), | ||
| 422 | ]) | ||
| 423 | .sign_with_keys(&owner_keys) | ||
| 424 | .unwrap(); | ||
| 425 | |||
| 426 | client.send_event(&owner_announcement).await.unwrap(); | ||
| 427 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 428 | |||
| 429 | // Step 3: Verify owner announcement accepted, maintainer not re-processed | ||
| 430 | let owner_filter = Filter::new() | ||
| 431 | .kind(Kind::GitRepoAnnouncement) | ||
| 432 | .author(owner_keys.public_key()) | ||
| 433 | .identifier(identifier); | ||
| 434 | |||
| 435 | let owner_found = wait_for_event_on_relay(relay.url(), owner_filter, Duration::from_secs(2)).await; | ||
| 436 | assert!(owner_found, "Owner announcement should be accepted despite invalid maintainer"); | ||
| 437 | |||
| 438 | let maintainer_filter = Filter::new() | ||
| 439 | .kind(Kind::GitRepoAnnouncement) | ||
| 440 | .author(maintainer_keys.public_key()) | ||
| 441 | .identifier(identifier); | ||
| 442 | |||
| 443 | let maintainer_found = wait_for_event_on_relay(relay.url(), maintainer_filter, Duration::from_millis(500)).await; | ||
| 444 | assert!( | ||
| 445 | !maintainer_found, | ||
| 446 | "Maintainer announcement should NOT be re-processed (invalid pubkey)" | ||
| 447 | ); | ||
| 448 | |||
| 449 | println!("✅ Invalid maintainer pubkey handled gracefully without panic"); | ||
| 450 | |||
| 451 | client.disconnect().await; | ||
| 452 | relay.stop().await; | ||
| 453 | } | ||
diff --git a/tests/sync/mod.rs b/tests/sync/mod.rs index 58b7354..400341f 100644 --- a/tests/sync/mod.rs +++ b/tests/sync/mod.rs | |||
| @@ -134,5 +134,6 @@ pub mod historic_sync; | |||
| 134 | pub mod catchup; | 134 | pub mod catchup; |
| 135 | pub mod discovery; | 135 | pub mod discovery; |
| 136 | pub mod live_sync; | 136 | pub mod live_sync; |
| 137 | pub mod maintainer_reprocessing; | ||
| 137 | pub mod metrics; | 138 | pub mod metrics; |
| 138 | pub mod tag_variations; \ No newline at end of file | 139 | pub mod tag_variations; \ No newline at end of file |