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 16:53:03 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-11 16:53:03 +0000
commit2a9160836bb87fdea3ae891563b0169c68d1c2ab (patch)
tree583c890687beaf7f380fc0be131bdf17485f06fa
parent52489d3b1a7d79e164b4cc901b53fd06c05ce1b1 (diff)
fix: resolve all fmt and clippy warnings
Main lib (src/): - Add #[allow(dead_code)] for build_info field (stored to prevent Prometheus unregistration) - Add #[allow(dead_code)] for first_seen field (reserved for future rate limiting) - Replace .or_insert_with(RelaySyncNeeds::default) with .or_default() - Replace manual div_ceil implementations with .div_ceil(100) Test code (tests/): - Replace .expect(&format!(...)) with .unwrap_or_else(|_| panic!(...)) - Remove needless borrows in fetch_metrics() calls - Add #[allow(dead_code)] and #[allow(unused_imports)] to test helpers module grasp-audit: - Apply cargo fmt to fix formatting
-rw-r--r--build.rs2
-rw-r--r--grasp-audit/src/client.rs4
-rw-r--r--grasp-audit/src/fixtures.rs6
-rw-r--r--grasp-audit/src/result.rs41
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs4
-rw-r--r--grasp-audit/src/specs/grasp01/nip01_smoke.rs32
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs97
-rw-r--r--src/config.rs18
-rw-r--r--src/http/landing.rs5
-rw-r--r--src/http/mod.rs8
-rw-r--r--src/main.rs9
-rw-r--r--src/metrics/bandwidth.rs13
-rw-r--r--src/metrics/connection.rs58
-rw-r--r--src/metrics/mod.rs127
-rw-r--r--src/nostr/builder.rs22
-rw-r--r--src/nostr/policy/announcement.rs7
-rw-r--r--src/nostr/policy/mod.rs3
-rw-r--r--src/nostr/policy/pr_event.rs2
-rw-r--r--src/nostr/policy/related.rs7
-rw-r--r--src/nostr/policy/state.rs7
-rw-r--r--src/sync/algorithms.rs6
-rw-r--r--src/sync/filters.rs2
-rw-r--r--src/sync/health.rs19
-rw-r--r--src/sync/metrics.rs10
-rw-r--r--src/sync/mod.rs8
-rw-r--r--src/sync/relay_connection.rs24
-rw-r--r--src/sync/self_subscriber.rs42
-rw-r--r--tests/common/mod.rs2
-rw-r--r--tests/common/relay.rs6
-rw-r--r--tests/common/sync_helpers.rs54
-rw-r--r--tests/nip77_negentropy.rs27
-rw-r--r--tests/sync.rs2
-rw-r--r--tests/sync/bootstrap.rs9
-rw-r--r--tests/sync/discovery.rs15
-rw-r--r--tests/sync/live_sync.rs7
-rw-r--r--tests/sync/metrics.rs6
-rw-r--r--tests/sync/tag_variations.rs139
37 files changed, 516 insertions, 334 deletions
diff --git a/build.rs b/build.rs
index e7d9cba..d93d74d 100644
--- a/build.rs
+++ b/build.rs
@@ -17,4 +17,4 @@ fn main() {
17 // Re-run if HEAD changes (new commits) 17 // Re-run if HEAD changes (new commits)
18 println!("cargo:rerun-if-changed=.git/HEAD"); 18 println!("cargo:rerun-if-changed=.git/HEAD");
19 println!("cargo:rerun-if-changed=.git/refs/heads/"); 19 println!("cargo:rerun-if-changed=.git/refs/heads/");
20} \ No newline at end of file 20}
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs
index 21c70be..259a317 100644
--- a/grasp-audit/src/client.rs
+++ b/grasp-audit/src/client.rs
@@ -585,7 +585,9 @@ mod tests {
585 "Missing 'grasp-audit-test-event' tag" 585 "Missing 'grasp-audit-test-event' tag"
586 ); 586 );
587 assert!( 587 assert!(
588 tag_contents.iter().any(|t| t.starts_with("audit-isolated-")), 588 tag_contents
589 .iter()
590 .any(|t| t.starts_with("audit-isolated-")),
589 "Missing 'audit-isolated-*' tag" 591 "Missing 'audit-isolated-*' tag"
590 ); 592 );
591 assert!( 593 assert!(
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 174f83d..a15bd79 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -1973,11 +1973,11 @@ mod tests {
1973 1973
1974 #[test] 1974 #[test]
1975 fn test_context_mode_from_audit_mode() { 1975 fn test_context_mode_from_audit_mode() {
1976 assert_eq!(ContextMode::from(AuditMode::Isolated), ContextMode::Isolated);
1977 assert_eq!( 1976 assert_eq!(
1978 ContextMode::from(AuditMode::Shared), 1977 ContextMode::from(AuditMode::Isolated),
1979 ContextMode::Shared 1978 ContextMode::Isolated
1980 ); 1979 );
1980 assert_eq!(ContextMode::from(AuditMode::Shared), ContextMode::Shared);
1981 } 1981 }
1982 1982
1983 #[test] 1983 #[test]
diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs
index 0de16ae..f296633 100644
--- a/grasp-audit/src/result.rs
+++ b/grasp-audit/src/result.rs
@@ -38,7 +38,9 @@ fn parse_spec_lines(spec_ref: &str) -> Vec<u32> {
38 if line_part.contains('-') { 38 if line_part.contains('-') {
39 let range_parts: Vec<&str> = line_part.split('-').collect(); 39 let range_parts: Vec<&str> = line_part.split('-').collect();
40 if range_parts.len() == 2 { 40 if range_parts.len() == 2 {
41 if let (Ok(start), Ok(end)) = (range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>()) { 41 if let (Ok(start), Ok(end)) =
42 (range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>())
43 {
42 return (start..=end).collect(); 44 return (start..=end).collect();
43 } 45 }
44 } 46 }
@@ -162,10 +164,19 @@ impl AuditResult {
162 /// Print a detailed report aligned to GRASP-01 specification 164 /// Print a detailed report aligned to GRASP-01 specification
163 pub fn print_report(&self) { 165 pub fn print_report(&self) {
164 println!(); 166 println!();
165 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); 167 println!(
168 "{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
169 BOLD, RESET
170 );
166 println!("{}GRASP-01 Compliance Report{}", BOLD, RESET); 171 println!("{}GRASP-01 Compliance Report{}", BOLD, RESET);
167 println!("Source: github.com/nostr-protocol/grasp (commit: {})", GRASP_COMMIT_ID); 172 println!(
168 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); 173 "Source: github.com/nostr-protocol/grasp (commit: {})",
174 GRASP_COMMIT_ID
175 );
176 println!(
177 "{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
178 BOLD, RESET
179 );
169 180
170 // Build a map of spec line -> tests that cover it 181 // Build a map of spec line -> tests that cover it
171 let mut tests_by_line: BTreeMap<u32, Vec<&TestResult>> = BTreeMap::new(); 182 let mut tests_by_line: BTreeMap<u32, Vec<&TestResult>> = BTreeMap::new();
@@ -185,7 +196,10 @@ impl AuditResult {
185 println!(); 196 println!();
186 println!("{}{}## {}{}", CYAN, BOLD, section, RESET); 197 println!("{}{}## {}{}", CYAN, BOLD, section, RESET);
187 198
188 for req in GRASP_01_REQUIREMENTS.iter().filter(|r| r.section == section) { 199 for req in GRASP_01_REQUIREMENTS
200 .iter()
201 .filter(|r| r.section == section)
202 {
189 println!(); 203 println!();
190 // Print spec requirement in blue 204 // Print spec requirement in blue
191 println!("{}📘 Line {}: {}{}", BLUE, req.line, req.text, RESET); 205 println!("{}📘 Line {}: {}{}", BLUE, req.line, req.text, RESET);
@@ -218,7 +232,10 @@ impl AuditResult {
218 } 232 }
219 233
220 println!(); 234 println!();
221 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); 235 println!(
236 "{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
237 BOLD, RESET
238 );
222 239
223 // Summary statistics 240 // Summary statistics
224 let passed = self.passed_count(); 241 let passed = self.passed_count();
@@ -252,7 +269,10 @@ impl AuditResult {
252 "{}Test results: {}/{} tests passed ({:.1}%){}", 269 "{}Test results: {}/{} tests passed ({:.1}%){}",
253 summary_color, passed, total_tests, pass_rate, RESET 270 summary_color, passed, total_tests, pass_rate, RESET
254 ); 271 );
255 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); 272 println!(
273 "{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
274 BOLD, RESET
275 );
256 println!(); 276 println!();
257 } 277 }
258 278
@@ -313,7 +333,10 @@ mod tests {
313 #[test] 333 #[test]
314 fn test_parse_spec_lines_range() { 334 fn test_parse_spec_lines_range() {
315 assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7-9"), vec![7, 8, 9]); 335 assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7-9"), vec![7, 8, 9]);
316 assert_eq!(parse_spec_lines("GRASP-01:cors:44-47"), vec![44, 45, 46, 47]); 336 assert_eq!(
337 parse_spec_lines("GRASP-01:cors:44-47"),
338 vec![44, 45, 46, 47]
339 );
317 } 340 }
318 341
319 #[test] 342 #[test]
@@ -327,4 +350,4 @@ mod tests {
327 assert_eq!(parse_spec_lines("GRASP-01:invalid"), Vec::<u32>::new()); 350 assert_eq!(parse_spec_lines("GRASP-01:invalid"), Vec::<u32>::new());
328 assert_eq!(parse_spec_lines("GRASP-01:test:abc"), Vec::<u32>::new()); 351 assert_eq!(parse_spec_lines("GRASP-01:test:abc"), Vec::<u32>::new());
329 } 352 }
330} \ No newline at end of file 353}
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index ba27fef..fa05f35 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -29,6 +29,6 @@ pub use nip11_document::Nip11DocumentTests;
29pub use push_authorization::PushAuthorizationTests; 29pub use push_authorization::PushAuthorizationTests;
30pub use repository_creation::RepositoryCreationTests; 30pub use repository_creation::RepositoryCreationTests;
31pub use spec_requirements::{ 31pub use spec_requirements::{
32 get_requirement, get_requirements_for_section, get_sections, RequirementLevel, 32 get_requirement, get_requirements_for_section, get_sections, RequirementLevel, SpecRequirement,
33 SpecRequirement, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID, 33 GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID,
34}; 34};
diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
index 8a0a4d1..4dbcd3d 100644
--- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs
+++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
@@ -163,23 +163,27 @@ impl Nip01SmokeTests {
163 /// Spec: NIP-01 CLOSE message 163 /// Spec: NIP-01 CLOSE message
164 /// Requirement: Relay MUST support CLOSE to end subscriptions 164 /// Requirement: Relay MUST support CLOSE to end subscriptions
165 pub async fn test_close_subscription(client: &AuditClient) -> TestResult { 165 pub async fn test_close_subscription(client: &AuditClient) -> TestResult {
166 TestResult::new("close_subscription", "GRASP-01:nostr-relay:7", "Can close subscriptions") 166 TestResult::new(
167 .run(|| async { 167 "close_subscription",
168 // For now, we just verify we can query events 168 "GRASP-01:nostr-relay:7",
169 // Full subscription management with CLOSE would require 169 "Can close subscriptions",
170 // lower-level WebSocket access 170 )
171 .run(|| async {
172 // For now, we just verify we can query events
173 // Full subscription management with CLOSE would require
174 // lower-level WebSocket access
171 175
172 let filter = Filter::new().kind(Kind::TextNote).limit(1); 176 let filter = Filter::new().kind(Kind::TextNote).limit(1);
173 177
174 let _events = client 178 let _events = client
175 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2))) 179 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
176 .await 180 .await
177 .map_err(|e| format!("Failed to subscribe: {}", e))?; 181 .map_err(|e| format!("Failed to subscribe: {}", e))?;
178 182
179 // If we got here, subscription worked 183 // If we got here, subscription worked
180 Ok(()) 184 Ok(())
181 }) 185 })
182 .await 186 .await
183 } 187 }
184 188
185 /// Test 5: Rejects events with invalid signatures 189 /// Test 5: Rejects events with invalid signatures
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index c06da0d..ec08032 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -407,8 +407,12 @@ impl PushAuthorizationTests {
407 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 407 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
408 Ok(r) => r, 408 Ok(r) => r,
409 Err(e) => { 409 Err(e) => {
410 return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event") 410 return TestResult::new(
411 .fail(format!("Failed to create repo: {}", e)) 411 test_name,
412 "GRASP-01:git-http:30",
413 "Push rejected without state event",
414 )
415 .fail(format!("Failed to create repo: {}", e))
412 } 416 }
413 }; 417 };
414 418
@@ -427,8 +431,12 @@ impl PushAuthorizationTests {
427 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 431 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
428 Ok(p) => p, 432 Ok(p) => p,
429 Err(e) => { 433 Err(e) => {
430 return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event") 434 return TestResult::new(
431 .fail(&e) 435 test_name,
436 "GRASP-01:git-http:30",
437 "Push rejected without state event",
438 )
439 .fail(&e)
432 } 440 }
433 }; 441 };
434 let cleanup = || { 442 let cleanup = || {
@@ -437,8 +445,12 @@ impl PushAuthorizationTests {
437 445
438 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") { 446 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") {
439 cleanup(); 447 cleanup();
440 return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event") 448 return TestResult::new(
441 .fail(&e); 449 test_name,
450 "GRASP-01:git-http:30",
451 "Push rejected without state event",
452 )
453 .fail(&e);
442 } 454 }
443 455
444 // Do NOT publish state event - push should be rejected 456 // Do NOT publish state event - push should be rejected
@@ -446,14 +458,24 @@ impl PushAuthorizationTests {
446 cleanup(); 458 cleanup();
447 459
448 match push_result { 460 match push_result {
449 Ok(false) => { 461 Ok(false) => TestResult::new(
450 TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event").pass() 462 test_name,
451 } 463 "GRASP-01:git-http:30",
452 Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event") 464 "Push rejected without state event",
453 .fail("Push accepted but should be rejected"), 465 )
454 Err(e) => { 466 .pass(),
455 TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event").fail(&e) 467 Ok(true) => TestResult::new(
456 } 468 test_name,
469 "GRASP-01:git-http:30",
470 "Push rejected without state event",
471 )
472 .fail("Push accepted but should be rejected"),
473 Err(e) => TestResult::new(
474 test_name,
475 "GRASP-01:git-http:30",
476 "Push rejected without state event",
477 )
478 .fail(&e),
457 } 479 }
458 } 480 }
459 481
@@ -480,11 +502,18 @@ impl PushAuthorizationTests {
480 // The OwnerStateDataPushed fixture handles all stages: 502 // The OwnerStateDataPushed fixture handles all stages:
481 // Generate → Send → Verify → DataPush 503 // Generate → Send → Verify → DataPush
482 match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { 504 match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await {
483 Ok(_state_event) => { 505 Ok(_state_event) => TestResult::new(
484 TestResult::new(test_name, "GRASP-01:git-http:30", "Push authorized with matching state").pass() 506 test_name,
485 } 507 "GRASP-01:git-http:30",
486 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push authorized with matching state") 508 "Push authorized with matching state",
487 .fail(format!("{}", e)), 509 )
510 .pass(),
511 Err(e) => TestResult::new(
512 test_name,
513 "GRASP-01:git-http:30",
514 "Push authorized with matching state",
515 )
516 .fail(format!("{}", e)),
488 } 517 }
489 } 518 }
490 519
@@ -868,8 +897,12 @@ impl PushAuthorizationTests {
868 // Send the rogue state event using the raw client to bypass AuditClient's key check 897 // Send the rogue state event using the raw client to bypass AuditClient's key check
869 if let Err(e) = client.client().send_event(&rogue_state).await { 898 if let Err(e) = client.client().send_event(&rogue_state).await {
870 cleanup(); 899 cleanup();
871 return TestResult::new(test_name, "GRASP-01:git-http:30", "Non-maintainer state events ignored") 900 return TestResult::new(
872 .fail(format!("Failed to send rogue state event: {}", e)); 901 test_name,
902 "GRASP-01:git-http:30",
903 "Non-maintainer state events ignored",
904 )
905 .fail(format!("Failed to send rogue state event: {}", e));
873 } 906 }
874 907
875 // Wait for event to propagate 908 // Wait for event to propagate
@@ -1036,7 +1069,9 @@ impl PushAuthorizationTests {
1036 .await 1069 .await
1037 { 1070 {
1038 Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass(), 1071 Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass(),
1039 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)), 1072 Err(e) => {
1073 TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e))
1074 }
1040 } 1075 }
1041 } 1076 }
1042 1077
@@ -1062,7 +1097,8 @@ impl PushAuthorizationTests {
1062 { 1097 {
1063 Ok(e) => e, 1098 Ok(e) => e,
1064 Err(e) => { 1099 Err(e) => {
1065 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)); 1100 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1101 .fail(format!("{}", e));
1066 } 1102 }
1067 }; 1103 };
1068 1104
@@ -1072,7 +1108,8 @@ impl PushAuthorizationTests {
1072 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1108 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1073 Ok(r) => r, 1109 Ok(r) => r,
1074 Err(e) => { 1110 Err(e) => {
1075 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)); 1111 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1112 .fail(format!("{}", e));
1076 } 1113 }
1077 }; 1114 };
1078 1115
@@ -1146,7 +1183,8 @@ impl PushAuthorizationTests {
1146 { 1183 {
1147 Ok(e) => e, 1184 Ok(e) => e,
1148 Err(e) => { 1185 Err(e) => {
1149 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)); 1186 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1187 .fail(format!("{}", e));
1150 } 1188 }
1151 }; 1189 };
1152 1190
@@ -1156,7 +1194,8 @@ impl PushAuthorizationTests {
1156 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1194 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1157 Ok(r) => r, 1195 Ok(r) => r,
1158 Err(e) => { 1196 Err(e) => {
1159 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)); 1197 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1198 .fail(format!("{}", e));
1160 } 1199 }
1161 }; 1200 };
1162 1201
@@ -1233,7 +1272,8 @@ impl PushAuthorizationTests {
1233 { 1272 {
1234 Ok(e) => e, 1273 Ok(e) => e,
1235 Err(e) => { 1274 Err(e) => {
1236 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)); 1275 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1276 .fail(format!("{}", e));
1237 } 1277 }
1238 }; 1278 };
1239 1279
@@ -1243,7 +1283,8 @@ impl PushAuthorizationTests {
1243 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1283 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1244 Ok(r) => r, 1284 Ok(r) => r,
1245 Err(e) => { 1285 Err(e) => {
1246 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)); 1286 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1287 .fail(format!("{}", e));
1247 } 1288 }
1248 }; 1289 };
1249 1290
diff --git a/src/config.rs b/src/config.rs
index 8c6de05..7834a3f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -77,11 +77,19 @@ pub struct Config {
77 pub metrics_enabled: bool, 77 pub metrics_enabled: bool,
78 78
79 /// Connections per IP before flagging as potential abuse in metrics (display only, no rate limiting) 79 /// Connections per IP before flagging as potential abuse in metrics (display only, no rate limiting)
80 #[arg(long = "metrics-connection-per-ip-abuse-threshold", env = "NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD", default_value_t = 10)] 80 #[arg(
81 long = "metrics-connection-per-ip-abuse-threshold",
82 env = "NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD",
83 default_value_t = 10
84 )]
81 pub metrics_connection_per_ip_abuse_threshold: u32, 85 pub metrics_connection_per_ip_abuse_threshold: u32,
82 86
83 /// Number of top bandwidth repos to track in metrics 87 /// Number of top bandwidth repos to track in metrics
84 #[arg(long = "metrics-top-n-repos", env = "NGIT_METRICS_TOP_N_REPOS", default_value_t = 10)] 88 #[arg(
89 long = "metrics-top-n-repos",
90 env = "NGIT_METRICS_TOP_N_REPOS",
91 default_value_t = 10
92 )]
85 pub metrics_top_n_repos: usize, 93 pub metrics_top_n_repos: usize,
86 94
87 /// URL of bootstrap relay to sync from on startup (optional) 95 /// URL of bootstrap relay to sync from on startup (optional)
@@ -95,7 +103,11 @@ pub struct Config {
95 103
96 /// Interval in seconds for checking disconnected relays and attempting reconnection (default: 60) 104 /// Interval in seconds for checking disconnected relays and attempting reconnection (default: 60)
97 /// Set to lower value for faster reconnection testing 105 /// Set to lower value for faster reconnection testing
98 #[arg(long, env = "NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", default_value_t = 60)] 106 #[arg(
107 long,
108 env = "NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS",
109 default_value_t = 60
110 )]
99 pub sync_disconnect_check_interval_secs: u64, 111 pub sync_disconnect_check_interval_secs: u64,
100 112
101 /// Base backoff time in seconds for relay reconnection (default: 5) 113 /// Base backoff time in seconds for relay reconnection (default: 5)
diff --git a/src/http/landing.rs b/src/http/landing.rs
index 8ab4a68..5fc1e6e 100644
--- a/src/http/landing.rs
+++ b/src/http/landing.rs
@@ -341,10 +341,7 @@ fn generate_hero_tags(nip11: &RelayInformationDocument) -> String {
341 341
342 // Add GRASP tags 342 // Add GRASP tags
343 for grasp in &nip11.supported_grasps { 343 for grasp in &nip11.supported_grasps {
344 html.push_str(&format!( 344 html.push_str(&format!(r#"<span class="tag tag-grasp">{}</span>"#, grasp));
345 r#"<span class="tag tag-grasp">{}</span>"#,
346 grasp
347 ));
348 html.push('\n'); 345 html.push('\n');
349 } 346 }
350 347
diff --git a/src/http/mod.rs b/src/http/mod.rs
index f584e03..91a6067 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -509,7 +509,13 @@ pub async fn run_server(
509 loop { 509 loop {
510 let (socket, addr) = listener.accept().await?; 510 let (socket, addr) = listener.accept().await?;
511 let io = TokioIo::new(socket); 511 let io = TokioIo::new(socket);
512 let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone(), metrics.clone()); 512 let service = HttpService::new(
513 relay.clone(),
514 config.clone(),
515 addr,
516 database.clone(),
517 metrics.clone(),
518 );
513 519
514 tokio::spawn(async move { 520 tokio::spawn(async move {
515 if let Err(e) = http1::Builder::new() 521 if let Err(e) = http1::Builder::new()
diff --git a/src/main.rs b/src/main.rs
index 97a14eb..6d8b4dd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -37,7 +37,9 @@ async fn main() -> Result<()> {
37 // Initialize metrics if enabled 37 // Initialize metrics if enabled
38 let metrics = if config.metrics_enabled { 38 let metrics = if config.metrics_enabled {
39 info!("Metrics enabled on /metrics endpoint"); 39 info!("Metrics enabled on /metrics endpoint");
40 Some(Arc::new(Metrics::new(config.metrics_connection_per_ip_abuse_threshold))) 40 Some(Arc::new(Metrics::new(
41 config.metrics_connection_per_ip_abuse_threshold,
42 )))
41 } else { 43 } else {
42 info!("Metrics disabled"); 44 info!("Metrics disabled");
43 None 45 None
@@ -65,7 +67,10 @@ async fn main() -> Result<()> {
65 ); 67 );
66 68
67 if config.sync_bootstrap_relay_url.is_some() { 69 if config.sync_bootstrap_relay_url.is_some() {
68 info!("Starting proactive sync with bootstrap relay: {:?}", config.sync_bootstrap_relay_url); 70 info!(
71 "Starting proactive sync with bootstrap relay: {:?}",
72 config.sync_bootstrap_relay_url
73 );
69 } else { 74 } else {
70 info!("Proactive sync enabled (will discover relays from stored announcements)"); 75 info!("Proactive sync enabled (will discover relays from stored announcements)");
71 } 76 }
diff --git a/src/metrics/bandwidth.rs b/src/metrics/bandwidth.rs
index d2c53e8..d51af12 100644
--- a/src/metrics/bandwidth.rs
+++ b/src/metrics/bandwidth.rs
@@ -80,7 +80,9 @@ impl BandwidthTracker {
80 &["repo"], 80 &["repo"],
81 ) 81 )
82 .unwrap(); 82 .unwrap();
83 registry.register(Box::new(top_repos_gauge.clone())).unwrap(); 83 registry
84 .register(Box::new(top_repos_gauge.clone()))
85 .unwrap();
84 86
85 Self { 87 Self {
86 all_repos: DashMap::new(), 88 all_repos: DashMap::new(),
@@ -120,7 +122,12 @@ impl BandwidthTracker {
120 // Try to update the timestamp atomically to prevent concurrent refreshes 122 // Try to update the timestamp atomically to prevent concurrent refreshes
121 if self 123 if self
122 .last_refresh_nanos 124 .last_refresh_nanos
123 .compare_exchange(last_refresh, elapsed_nanos, Ordering::SeqCst, Ordering::Relaxed) 125 .compare_exchange(
126 last_refresh,
127 elapsed_nanos,
128 Ordering::SeqCst,
129 Ordering::Relaxed,
130 )
124 .is_ok() 131 .is_ok()
125 { 132 {
126 self.refresh_top_n(); 133 self.refresh_top_n();
@@ -298,4 +305,4 @@ mod tests {
298 // Refresh should not panic on empty data 305 // Refresh should not panic on empty data
299 tracker.refresh_top_n(); 306 tracker.refresh_top_n();
300 } 307 }
301} \ No newline at end of file 308}
diff --git a/src/metrics/connection.rs b/src/metrics/connection.rs
index 6a7f406..2d42081 100644
--- a/src/metrics/connection.rs
+++ b/src/metrics/connection.rs
@@ -25,7 +25,8 @@ use tracing::warn;
25struct ConnectionInfo { 25struct ConnectionInfo {
26 /// Number of active connections from this IP 26 /// Number of active connections from this IP
27 count: u32, 27 count: u32,
28 /// When the first connection from this IP was established 28 /// When the first connection from this IP was established (for future rate limiting)
29 #[allow(dead_code)]
29 first_seen: Instant, 30 first_seen: Instant,
30 /// Whether this IP has been flagged as potentially abusive 31 /// Whether this IP has been flagged as potentially abusive
31 flagged_as_abuse: bool, 32 flagged_as_abuse: bool,
@@ -48,16 +49,16 @@ struct ConnectionInfo {
48pub struct ConnectionTracker { 49pub struct ConnectionTracker {
49 /// Active connections per IP (INTERNAL ONLY - never exposed to metrics) 50 /// Active connections per IP (INTERNAL ONLY - never exposed to metrics)
50 connections: DashMap<IpAddr, ConnectionInfo>, 51 connections: DashMap<IpAddr, ConnectionInfo>,
51 52
52 /// Threshold for abuse flagging (connections per IP) 53 /// Threshold for abuse flagging (connections per IP)
53 abuse_threshold: u32, 54 abuse_threshold: u32,
54 55
55 /// Prometheus gauge: total active connections 56 /// Prometheus gauge: total active connections
56 active_connections: IntGauge, 57 active_connections: IntGauge,
57 58
58 /// Prometheus gauge: number of unique IPs connected 59 /// Prometheus gauge: number of unique IPs connected
59 unique_ips: IntGauge, 60 unique_ips: IntGauge,
60 61
61 /// Prometheus gauge: number of IPs flagged as potential abusers 62 /// Prometheus gauge: number of IPs flagged as potential abusers
62 flagged_abusers: IntGauge, 63 flagged_abusers: IntGauge,
63} 64}
@@ -70,29 +71,30 @@ impl ConnectionTracker {
70 /// * `abuse_threshold` - Number of connections from a single IP before flagging 71 /// * `abuse_threshold` - Number of connections from a single IP before flagging
71 /// * `registry` - Prometheus registry to register metrics with 72 /// * `registry` - Prometheus registry to register metrics with
72 pub fn new(abuse_threshold: u32, registry: &Registry) -> Self { 73 pub fn new(abuse_threshold: u32, registry: &Registry) -> Self {
73 let active_connections = IntGauge::with_opts( 74 let active_connections = IntGauge::with_opts(Opts::new(
74 Opts::new( 75 "ngit_websocket_connections_active",
75 "ngit_websocket_connections_active", 76 "Current active WebSocket connections",
76 "Current active WebSocket connections", 77 ))
77 ) 78 .unwrap();
78 ).unwrap(); 79 registry
79 registry.register(Box::new(active_connections.clone())).unwrap(); 80 .register(Box::new(active_connections.clone()))
80 81 .unwrap();
81 let unique_ips = IntGauge::with_opts( 82
82 Opts::new( 83 let unique_ips = IntGauge::with_opts(Opts::new(
83 "ngit_websocket_unique_ips", 84 "ngit_websocket_unique_ips",
84 "Number of unique IP addresses connected (NOT the IPs themselves)", 85 "Number of unique IP addresses connected (NOT the IPs themselves)",
85 ) 86 ))
86 ).unwrap(); 87 .unwrap();
87 registry.register(Box::new(unique_ips.clone())).unwrap(); 88 registry.register(Box::new(unique_ips.clone())).unwrap();
88 89
89 let flagged_abusers = IntGauge::with_opts( 90 let flagged_abusers = IntGauge::with_opts(Opts::new(
90 Opts::new( 91 "ngit_websocket_flagged_abusers",
91 "ngit_websocket_flagged_abusers", 92 "Number of IPs exceeding connection threshold",
92 "Number of IPs exceeding connection threshold", 93 ))
93 ) 94 .unwrap();
94 ).unwrap(); 95 registry
95 registry.register(Box::new(flagged_abusers.clone())).unwrap(); 96 .register(Box::new(flagged_abusers.clone()))
97 .unwrap();
96 98
97 Self { 99 Self {
98 connections: DashMap::new(), 100 connections: DashMap::new(),
@@ -140,7 +142,7 @@ impl ConnectionTracker {
140 142
141 // Update Prometheus metrics (aggregate counts only) 143 // Update Prometheus metrics (aggregate counts only)
142 self.active_connections.inc(); 144 self.active_connections.inc();
143 145
144 if is_new_ip { 146 if is_new_ip {
145 self.unique_ips.inc(); 147 self.unique_ips.inc();
146 } 148 }
@@ -334,4 +336,4 @@ mod tests {
334 assert_eq!(tracker.active_connections(), 0); 336 assert_eq!(tracker.active_connections(), 0);
335 assert_eq!(tracker.unique_ip_count(), 0); 337 assert_eq!(tracker.unique_ip_count(), 0);
336 } 338 }
337} \ No newline at end of file 339}
diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs
index 736414f..5420dfd 100644
--- a/src/metrics/mod.rs
+++ b/src/metrics/mod.rs
@@ -87,7 +87,8 @@ struct MetricsInner {
87 // === System Health Metrics === 87 // === System Health Metrics ===
88 /// Server start time for uptime calculation 88 /// Server start time for uptime calculation
89 pub start_time: Instant, 89 pub start_time: Instant,
90 /// Build information gauge 90 /// Build information gauge (stored to prevent unregistration from Prometheus)
91 #[allow(dead_code)]
91 pub build_info: GaugeVec, 92 pub build_info: GaugeVec,
92} 93}
93 94
@@ -158,7 +159,10 @@ impl Metrics {
158 159
159 /// Start timing a git operation, returns a timer 160 /// Start timing a git operation, returns a timer
160 pub fn start_git_operation_timer(&self, operation: &str) -> GitOperationTimer { 161 pub fn start_git_operation_timer(&self, operation: &str) -> GitOperationTimer {
161 GitOperationTimer::new(self.inner.git_operation_duration.clone(), operation.to_string()) 162 GitOperationTimer::new(
163 self.inner.git_operation_duration.clone(),
164 operation.to_string(),
165 )
162 } 166 }
163 167
164 /// Record bytes transferred for a git operation 168 /// Record bytes transferred for a git operation
@@ -266,13 +270,14 @@ impl MetricsInner {
266 } 270 }
267 271
268 // WebSocket metrics 272 // WebSocket metrics
269 let websocket_connections_total = Counter::with_opts( 273 let websocket_connections_total = Counter::with_opts(Opts::new(
270 Opts::new( 274 "ngit_websocket_connections_total",
271 "ngit_websocket_connections_total", 275 "Total WebSocket connections since startup",
272 "Total WebSocket connections since startup", 276 ))
273 ) 277 .unwrap();
274 ).unwrap(); 278 REGISTRY
275 REGISTRY.register(Box::new(websocket_connections_total.clone())).unwrap(); 279 .register(Box::new(websocket_connections_total.clone()))
280 .unwrap();
276 281
277 let websocket_connection_duration = Histogram::with_opts( 282 let websocket_connection_duration = Histogram::with_opts(
278 HistogramOpts::new( 283 HistogramOpts::new(
@@ -280,8 +285,11 @@ impl MetricsInner {
280 "Duration of WebSocket connections", 285 "Duration of WebSocket connections",
281 ) 286 )
282 .buckets(vec![1.0, 5.0, 15.0, 30.0, 60.0, 300.0, 900.0, 3600.0]), 287 .buckets(vec![1.0, 5.0, 15.0, 30.0, 60.0, 300.0, 900.0, 3600.0]),
283 ).unwrap(); 288 )
284 REGISTRY.register(Box::new(websocket_connection_duration.clone())).unwrap(); 289 .unwrap();
290 REGISTRY
291 .register(Box::new(websocket_connection_duration.clone()))
292 .unwrap();
285 293
286 let websocket_messages_received = CounterVec::new( 294 let websocket_messages_received = CounterVec::new(
287 Opts::new( 295 Opts::new(
@@ -289,8 +297,11 @@ impl MetricsInner {
289 "WebSocket messages received by type", 297 "WebSocket messages received by type",
290 ), 298 ),
291 &["type"], 299 &["type"],
292 ).unwrap(); 300 )
293 REGISTRY.register(Box::new(websocket_messages_received.clone())).unwrap(); 301 .unwrap();
302 REGISTRY
303 .register(Box::new(websocket_messages_received.clone()))
304 .unwrap();
294 305
295 let websocket_messages_sent = CounterVec::new( 306 let websocket_messages_sent = CounterVec::new(
296 Opts::new( 307 Opts::new(
@@ -298,8 +309,11 @@ impl MetricsInner {
298 "WebSocket messages sent by type", 309 "WebSocket messages sent by type",
299 ), 310 ),
300 &["type"], 311 &["type"],
301 ).unwrap(); 312 )
302 REGISTRY.register(Box::new(websocket_messages_sent.clone())).unwrap(); 313 .unwrap();
314 REGISTRY
315 .register(Box::new(websocket_messages_sent.clone()))
316 .unwrap();
303 317
304 // Git operation metrics 318 // Git operation metrics
305 let git_operations_total = CounterVec::new( 319 let git_operations_total = CounterVec::new(
@@ -308,8 +322,11 @@ impl MetricsInner {
308 "Git operations by type and status", 322 "Git operations by type and status",
309 ), 323 ),
310 &["operation", "status"], 324 &["operation", "status"],
311 ).unwrap(); 325 )
312 REGISTRY.register(Box::new(git_operations_total.clone())).unwrap(); 326 .unwrap();
327 REGISTRY
328 .register(Box::new(git_operations_total.clone()))
329 .unwrap();
313 330
314 let git_operation_duration = HistogramVec::new( 331 let git_operation_duration = HistogramVec::new(
315 HistogramOpts::new( 332 HistogramOpts::new(
@@ -318,8 +335,11 @@ impl MetricsInner {
318 ) 335 )
319 .buckets(vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]), 336 .buckets(vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]),
320 &["operation"], 337 &["operation"],
321 ).unwrap(); 338 )
322 REGISTRY.register(Box::new(git_operation_duration.clone())).unwrap(); 339 .unwrap();
340 REGISTRY
341 .register(Box::new(git_operation_duration.clone()))
342 .unwrap();
323 343
324 let git_bytes_total = CounterVec::new( 344 let git_bytes_total = CounterVec::new(
325 Opts::new( 345 Opts::new(
@@ -327,8 +347,11 @@ impl MetricsInner {
327 "Total bytes transferred for git operations", 347 "Total bytes transferred for git operations",
328 ), 348 ),
329 &["direction"], 349 &["direction"],
330 ).unwrap(); 350 )
331 REGISTRY.register(Box::new(git_bytes_total.clone())).unwrap(); 351 .unwrap();
352 REGISTRY
353 .register(Box::new(git_bytes_total.clone()))
354 .unwrap();
332 355
333 let git_push_authorization = CounterVec::new( 356 let git_push_authorization = CounterVec::new(
334 Opts::new( 357 Opts::new(
@@ -336,8 +359,11 @@ impl MetricsInner {
336 "Push authorization results", 359 "Push authorization results",
337 ), 360 ),
338 &["result"], 361 &["result"],
339 ).unwrap(); 362 )
340 REGISTRY.register(Box::new(git_push_authorization.clone())).unwrap(); 363 .unwrap();
364 REGISTRY
365 .register(Box::new(git_push_authorization.clone()))
366 .unwrap();
341 367
342 // Nostr event metrics 368 // Nostr event metrics
343 let events_received_total = CounterVec::new( 369 let events_received_total = CounterVec::new(
@@ -346,8 +372,11 @@ impl MetricsInner {
346 "Nostr events received by kind", 372 "Nostr events received by kind",
347 ), 373 ),
348 &["kind"], 374 &["kind"],
349 ).unwrap(); 375 )
350 REGISTRY.register(Box::new(events_received_total.clone())).unwrap(); 376 .unwrap();
377 REGISTRY
378 .register(Box::new(events_received_total.clone()))
379 .unwrap();
351 380
352 let events_stored_total = CounterVec::new( 381 let events_stored_total = CounterVec::new(
353 Opts::new( 382 Opts::new(
@@ -355,8 +384,11 @@ impl MetricsInner {
355 "Nostr events successfully stored by kind", 384 "Nostr events successfully stored by kind",
356 ), 385 ),
357 &["kind"], 386 &["kind"],
358 ).unwrap(); 387 )
359 REGISTRY.register(Box::new(events_stored_total.clone())).unwrap(); 388 .unwrap();
389 REGISTRY
390 .register(Box::new(events_stored_total.clone()))
391 .unwrap();
360 392
361 let events_rejected_total = CounterVec::new( 393 let events_rejected_total = CounterVec::new(
362 Opts::new( 394 Opts::new(
@@ -364,31 +396,36 @@ impl MetricsInner {
364 "Nostr events rejected by kind and reason", 396 "Nostr events rejected by kind and reason",
365 ), 397 ),
366 &["kind", "reason"], 398 &["kind", "reason"],
367 ).unwrap(); 399 )
368 REGISTRY.register(Box::new(events_rejected_total.clone())).unwrap(); 400 .unwrap();
401 REGISTRY
402 .register(Box::new(events_rejected_total.clone()))
403 .unwrap();
369 404
370 // Repository metrics 405 // Repository metrics
371 let repositories_total = Gauge::with_opts( 406 let repositories_total = Gauge::with_opts(Opts::new(
372 Opts::new( 407 "ngit_repositories_total",
373 "ngit_repositories_total", 408 "Total repositories hosted",
374 "Total repositories hosted", 409 ))
375 ) 410 .unwrap();
376 ).unwrap(); 411 REGISTRY
377 REGISTRY.register(Box::new(repositories_total.clone())).unwrap(); 412 .register(Box::new(repositories_total.clone()))
413 .unwrap();
378 414
379 // Build info 415 // Build info
380 let build_info = GaugeVec::new( 416 let build_info = GaugeVec::new(
381 Opts::new( 417 Opts::new("ngit_build_info", "Build information"),
382 "ngit_build_info",
383 "Build information",
384 ),
385 &["version", "commit"], 418 &["version", "commit"],
386 ).unwrap(); 419 )
420 .unwrap();
387 REGISTRY.register(Box::new(build_info.clone())).unwrap(); 421 REGISTRY.register(Box::new(build_info.clone())).unwrap();
388 422
389 // Set build info gauge to 1 (it's just for labels) 423 // Set build info gauge to 1 (it's just for labels)
390 build_info 424 build_info
391 .with_label_values(&[env!("CARGO_PKG_VERSION"), option_env!("GIT_HASH").unwrap_or("unknown")]) 425 .with_label_values(&[
426 env!("CARGO_PKG_VERSION"),
427 option_env!("GIT_HASH").unwrap_or("unknown"),
428 ])
392 .set(1.0); 429 .set(1.0);
393 430
394 Self { 431 Self {
@@ -472,7 +509,7 @@ mod tests {
472 // Note: This test may fail if run with other tests due to global registry 509 // Note: This test may fail if run with other tests due to global registry
473 // In production, consider using a test-specific registry 510 // In production, consider using a test-specific registry
474 let metrics = Metrics::new(10); 511 let metrics = Metrics::new(10);
475 512
476 // Test that we can record metrics without panicking 513 // Test that we can record metrics without panicking
477 metrics.record_websocket_connection(); 514 metrics.record_websocket_connection();
478 metrics.record_message_received("REQ"); 515 metrics.record_message_received("REQ");
@@ -484,4 +521,4 @@ mod tests {
484 metrics.record_event_rejected(1, "invalid_signature"); 521 metrics.record_event_rejected(1, "invalid_signature");
485 metrics.set_repositories_total(5); 522 metrics.set_repositories_total(5);
486 } 523 }
487} \ No newline at end of file 524}
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 2284c18..c9bd1e1 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -16,8 +16,8 @@ use crate::nostr::events::{
16 KIND_REPOSITORY_STATE, 16 KIND_REPOSITORY_STATE,
17}; 17};
18use crate::nostr::policy::{ 18use crate::nostr::policy::{
19 AnnouncementPolicy, AnnouncementResult, PolicyContext, PrEventPolicy, RelatedEventPolicy, 19 AnnouncementPolicy, AnnouncementResult, PolicyContext, PrEventPolicy, ReferenceResult,
20 ReferenceResult, StatePolicy, StateResult, 20 RelatedEventPolicy, StatePolicy, StateResult,
21}; 21};
22 22
23/// Type alias for the shared database used by the relay 23/// Type alias for the shared database used by the relay
@@ -77,7 +77,9 @@ impl Nip34WritePolicy {
77 match RepositoryAnnouncement::from_event(event.clone()) { 77 match RepositoryAnnouncement::from_event(event.clone()) {
78 Ok(announcement) => { 78 Ok(announcement) => {
79 // Try to create bare repository if it doesn't exist 79 // Try to create bare repository if it doesn't exist
80 if let Err(e) = self.announcement_policy.ensure_bare_repository(&announcement) 80 if let Err(e) = self
81 .announcement_policy
82 .ensure_bare_repository(&announcement)
81 { 83 {
82 tracing::warn!( 84 tracing::warn!(
83 "Failed to create bare repository for {}: {}", 85 "Failed to create bare repository for {}: {}",
@@ -145,22 +147,14 @@ impl Nip34WritePolicy {
145 Ok(_state) => { 147 Ok(_state) => {
146 // Process state alignment asynchronously 148 // Process state alignment asynchronously
147 if let Err(e) = self.state_policy.process_state_event(event).await { 149 if let Err(e) = self.state_policy.process_state_event(event).await {
148 tracing::warn!( 150 tracing::warn!("Failed to process state event {}: {}", event_id_str, e);
149 "Failed to process state event {}: {}",
150 event_id_str,
151 e
152 );
153 } 151 }
154 152
155 tracing::debug!("Accepted repository state: {}", event_id_str); 153 tracing::debug!("Accepted repository state: {}", event_id_str);
156 PolicyResult::Accept 154 PolicyResult::Accept
157 } 155 }
158 Err(e) => { 156 Err(e) => {
159 tracing::warn!( 157 tracing::warn!("Failed to parse repository state {}: {}", event_id_str, e);
160 "Failed to parse repository state {}: {}",
161 event_id_str,
162 e
163 );
164 // Still accept the event even if we can't parse it 158 // Still accept the event even if we can't parse it
165 // The validation passed, so it's structurally valid 159 // The validation passed, so it's structurally valid
166 PolicyResult::Accept 160 PolicyResult::Accept
@@ -348,4 +342,4 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
348 database, 342 database,
349 write_policy, 343 write_policy,
350 }) 344 })
351} \ No newline at end of file 345}
diff --git a/src/nostr/policy/announcement.rs b/src/nostr/policy/announcement.rs
index 8d30baf..353738b 100644
--- a/src/nostr/policy/announcement.rs
+++ b/src/nostr/policy/announcement.rs
@@ -72,7 +72,10 @@ impl AnnouncementPolicy {
72 72
73 /// Create a bare git repository if it doesn't exist 73 /// Create a bare git repository if it doesn't exist
74 /// Path format: <git_data_path>/<npub>/<identifier>.git 74 /// Path format: <git_data_path>/<npub>/<identifier>.git
75 pub fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { 75 pub fn ensure_bare_repository(
76 &self,
77 announcement: &RepositoryAnnouncement,
78 ) -> Result<(), String> {
76 let repo_path = self.ctx.git_data_path.join(announcement.repo_path()); 79 let repo_path = self.ctx.git_data_path.join(announcement.repo_path());
77 80
78 // Check if repository already exists 81 // Check if repository already exists
@@ -154,4 +157,4 @@ impl AnnouncementPolicy {
154 157
155 Ok(false) 158 Ok(false)
156 } 159 }
157} \ No newline at end of file 160}
diff --git a/src/nostr/policy/mod.rs b/src/nostr/policy/mod.rs
index 6d67394..19db5f6 100644
--- a/src/nostr/policy/mod.rs
+++ b/src/nostr/policy/mod.rs
@@ -5,7 +5,6 @@
5/// - `StatePolicy` - State event validation + ref alignment 5/// - `StatePolicy` - State event validation + ref alignment
6/// - `PrEventPolicy` - PR/PR Update validation 6/// - `PrEventPolicy` - PR/PR Update validation
7/// - `RelatedEventPolicy` - Forward/backward reference checking 7/// - `RelatedEventPolicy` - Forward/backward reference checking
8
9mod announcement; 8mod announcement;
10mod pr_event; 9mod pr_event;
11mod related; 10mod related;
@@ -38,4 +37,4 @@ impl PolicyContext {
38 git_data_path: git_data_path.into(), 37 git_data_path: git_data_path.into(),
39 } 38 }
40 } 39 }
41} \ No newline at end of file 40}
diff --git a/src/nostr/policy/pr_event.rs b/src/nostr/policy/pr_event.rs
index fee9a2a..53da369 100644
--- a/src/nostr/policy/pr_event.rs
+++ b/src/nostr/policy/pr_event.rs
@@ -195,4 +195,4 @@ impl PrEventPolicy {
195 Ok(None) 195 Ok(None)
196 } 196 }
197 } 197 }
198} \ No newline at end of file 198}
diff --git a/src/nostr/policy/related.rs b/src/nostr/policy/related.rs
index 1937ca7..7ce87db 100644
--- a/src/nostr/policy/related.rs
+++ b/src/nostr/policy/related.rs
@@ -169,10 +169,7 @@ impl RelatedEventPolicy {
169 169
170 /// Check if any events exist in database 170 /// Check if any events exist in database
171 /// Returns the first matching event ID found, or None if none match 171 /// Returns the first matching event ID found, or None if none match
172 async fn find_accepted_event( 172 async fn find_accepted_event(&self, event_ids: &[EventId]) -> Result<Option<EventId>, String> {
173 &self,
174 event_ids: &[EventId],
175 ) -> Result<Option<EventId>, String> {
176 if event_ids.is_empty() { 173 if event_ids.is_empty() {
177 return Ok(None); 174 return Ok(None);
178 } 175 }
@@ -273,4 +270,4 @@ impl RelatedEventPolicy {
273 270
274 Ok(false) 271 Ok(false)
275 } 272 }
276} \ No newline at end of file 273}
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index 5692bd8..43349e2 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -239,7 +239,10 @@ impl StatePolicy {
239 } 239 }
240 240
241 // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git 241 // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git
242 let repo_path = self.ctx.git_data_path.join(announcement.repo_path().clone()); 242 let repo_path = self
243 .ctx
244 .git_data_path
245 .join(announcement.repo_path().clone());
243 owner_repos.push((announcement, repo_path)); 246 owner_repos.push((announcement, repo_path));
244 } 247 }
245 248
@@ -416,4 +419,4 @@ impl StatePolicy {
416 419
417 result 420 result
418 } 421 }
419} \ No newline at end of file 422}
diff --git a/src/sync/algorithms.rs b/src/sync/algorithms.rs
index 7d87411..3063516 100644
--- a/src/sync/algorithms.rs
+++ b/src/sync/algorithms.rs
@@ -65,9 +65,7 @@ pub fn derive_relay_targets(
65 65
66 for (repo_id, needs) in repo_index { 66 for (repo_id, needs) in repo_index {
67 for relay_url in &needs.relays { 67 for relay_url in &needs.relays {
68 let entry = relay_targets 68 let entry = relay_targets.entry(relay_url.clone()).or_default();
69 .entry(relay_url.clone())
70 .or_insert_with(RelaySyncNeeds::default);
71 69
72 entry.repos.insert(repo_id.clone()); 70 entry.repos.insert(repo_id.clone());
73 entry.root_events.extend(needs.root_events.iter().cloned()); 71 entry.root_events.extend(needs.root_events.iter().cloned());
@@ -586,4 +584,4 @@ mod tests {
586 ); 584 );
587 assert_eq!(actions[0].relay_url, "wss://new-relay.com"); 585 assert_eq!(actions[0].relay_url, "wss://new-relay.com");
588 } 586 }
589} \ No newline at end of file 587}
diff --git a/src/sync/filters.rs b/src/sync/filters.rs
index 02d580e..24e9bb2 100644
--- a/src/sync/filters.rs
+++ b/src/sync/filters.rs
@@ -337,4 +337,4 @@ mod tests {
337 337
338 assert_eq!(filters.len(), 6); 338 assert_eq!(filters.len(), 6);
339 } 339 }
340} \ No newline at end of file 340}
diff --git a/src/sync/health.rs b/src/sync/health.rs
index f9a5f3a..0ae7dee 100644
--- a/src/sync/health.rs
+++ b/src/sync/health.rs
@@ -206,11 +206,7 @@ impl RelayHealthTracker {
206 health.next_retry_at = Some(now + backoff); 206 health.next_retry_at = Some(now + backoff);
207 207
208 if old_state != HealthState::Degraded { 208 if old_state != HealthState::Degraded {
209 tracing::warn!( 209 tracing::warn!("Relay {} degraded, backoff {:?}", relay_url, backoff);
210 "Relay {} degraded, backoff {:?}",
211 relay_url,
212 backoff
213 );
214 } else { 210 } else {
215 tracing::debug!( 211 tracing::debug!(
216 "Relay {} failure #{}, backoff {:?}", 212 "Relay {} failure #{}, backoff {:?}",
@@ -308,12 +304,17 @@ impl RelayHealthTracker {
308 304
309 /// Get all tracked relay URLs 305 /// Get all tracked relay URLs
310 pub fn get_tracked_relays(&self) -> Vec<String> { 306 pub fn get_tracked_relays(&self) -> Vec<String> {
311 self.health.iter().map(|entry| entry.key().clone()).collect() 307 self.health
308 .iter()
309 .map(|entry| entry.key().clone())
310 .collect()
312 } 311 }
313 312
314 /// Get a clone of the health info for a relay 313 /// Get a clone of the health info for a relay
315 pub fn get_health(&self, relay_url: &str) -> Option<RelayHealth> { 314 pub fn get_health(&self, relay_url: &str) -> Option<RelayHealth> {
316 self.health.get(relay_url).map(|entry| entry.value().clone()) 315 self.health
316 .get(relay_url)
317 .map(|entry| entry.value().clone())
317 } 318 }
318} 319}
319 320
@@ -369,7 +370,7 @@ mod tests {
369 fn test_backoff_increases_exponentially() { 370 fn test_backoff_increases_exponentially() {
370 let base = DEFAULT_BASE_BACKOFF_SECS; // 5 seconds 371 let base = DEFAULT_BASE_BACKOFF_SECS; // 5 seconds
371 let max = 3600u64; 372 let max = 3600u64;
372 373
373 // failure 1: 5s (base * 2^0 = 5) 374 // failure 1: 5s (base * 2^0 = 5)
374 assert_eq!( 375 assert_eq!(
375 RelayHealthTracker::get_backoff_duration(1, base, max), 376 RelayHealthTracker::get_backoff_duration(1, base, max),
@@ -498,4 +499,4 @@ mod tests {
498 let health = tracker.get_health("wss://nonexistent.example.com"); 499 let health = tracker.get_health("wss://nonexistent.example.com");
499 assert!(health.is_none()); 500 assert!(health.is_none());
500 } 501 }
501} \ No newline at end of file 502}
diff --git a/src/sync/metrics.rs b/src/sync/metrics.rs
index 411ff63..d917dc0 100644
--- a/src/sync/metrics.rs
+++ b/src/sync/metrics.rs
@@ -207,7 +207,9 @@ impl SyncMetrics {
207 HealthState::Degraded => 2, 207 HealthState::Degraded => 2,
208 HealthState::Dead => 3, 208 HealthState::Dead => 3,
209 }; 209 };
210 self.relay_status.with_label_values(&[relay]).set(state_value); 210 self.relay_status
211 .with_label_values(&[relay])
212 .set(state_value);
211 } 213 }
212 214
213 /// Record relay failure count. 215 /// Record relay failure count.
@@ -259,9 +261,7 @@ impl SyncMetrics {
259 /// * `source` - The event source type (see [`record_event`](Self::record_event)) 261 /// * `source` - The event source type (see [`record_event`](Self::record_event))
260 /// * `count` - Number of events to record 262 /// * `count` - Number of events to record
261 pub fn record_events(&self, source: &str, count: u64) { 263 pub fn record_events(&self, source: &str, count: u64) {
262 self.events_total 264 self.events_total.with_label_values(&[source]).inc_by(count);
263 .with_label_values(&[source])
264 .inc_by(count);
265 } 265 }
266 266
267 /// Record a gap event filled during catchup. 267 /// Record a gap event filled during catchup.
@@ -451,4 +451,4 @@ mod tests {
451 let metrics2 = SyncMetrics::register(&registry); 451 let metrics2 = SyncMetrics::register(&registry);
452 assert!(metrics2.is_err()); 452 assert!(metrics2.is_err());
453 } 453 }
454} \ No newline at end of file 454}
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index c4c3c7f..fb59b3c 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -512,8 +512,8 @@ impl SyncManager {
512 }; 512 };
513 513
514 // Check if relay supports NIP-77 negentropy AND negentropy is not disabled 514 // Check if relay supports NIP-77 negentropy AND negentropy is not disabled
515 let use_negentropy = !self.config.sync_disable_negentropy 515 let use_negentropy =
516 && connection.supports_negentropy().await; 516 !self.config.sync_disable_negentropy && connection.supports_negentropy().await;
517 517
518 // Unsubscribe all current subscriptions 518 // Unsubscribe all current subscriptions
519 connection.unsubscribe_all().await; 519 connection.unsubscribe_all().await;
@@ -1657,12 +1657,12 @@ impl SyncManager {
1657 1657
1658 let layer1_filters = 1; 1658 let layer1_filters = 1;
1659 let layer2_filters = if repo_count > 0 { 1659 let layer2_filters = if repo_count > 0 {
1660 ((repo_count + 99) / 100) * 3 1660 repo_count.div_ceil(100) * 3
1661 } else { 1661 } else {
1662 0 1662 0
1663 }; 1663 };
1664 let layer3_filters = if event_count > 0 { 1664 let layer3_filters = if event_count > 0 {
1665 ((event_count + 99) / 100) * 3 1665 event_count.div_ceil(100) * 3
1666 } else { 1666 } else {
1667 0 1667 0
1668 }; 1668 };
diff --git a/src/sync/relay_connection.rs b/src/sync/relay_connection.rs
index fae179b..4167a0c 100644
--- a/src/sync/relay_connection.rs
+++ b/src/sync/relay_connection.rs
@@ -150,17 +150,21 @@ impl RelayConnection {
150 // 150 //
151 // See: nostr-sdk-0.44 Client::try_connect_relay documentation 151 // See: nostr-sdk-0.44 Client::try_connect_relay documentation
152 self.client 152 self.client
153 .try_connect_relay(&self.url, std::time::Duration::from_secs(connection_timeout_secs)) 153 .try_connect_relay(
154 &self.url,
155 std::time::Duration::from_secs(connection_timeout_secs),
156 )
154 .await 157 .await
155 .map_err(|e| format!("Failed to connect to relay {}: {}", self.url, e))?; 158 .map_err(|e| format!("Failed to connect to relay {}: {}", self.url, e))?;
156 159
157 // Subscribe to Layer 1 (announcements) 160 // Subscribe to Layer 1 (announcements)
158 let filter = build_announcement_filter(since); 161 let filter = build_announcement_filter(since);
159 let output = self 162 let output = self.client.subscribe(filter, None).await.map_err(|e| {
160 .client 163 format!(
161 .subscribe(filter, None) 164 "Failed to subscribe to announcements on {}: {}",
162 .await 165 self.url, e
163 .map_err(|e| format!("Failed to subscribe to announcements on {}: {}", self.url, e))?; 166 )
167 })?;
164 168
165 tracing::info!(url = %self.url, sub_id = %output.val, "Connected and subscribed to Layer 1 (announcements)"); 169 tracing::info!(url = %self.url, sub_id = %output.val, "Connected and subscribed to Layer 1 (announcements)");
166 Ok(output.val) 170 Ok(output.val)
@@ -250,7 +254,8 @@ impl RelayConnection {
250 } 254 }
251 RelayMessage::Closed { message: msg, .. } => { 255 RelayMessage::Closed { message: msg, .. } => {
252 tracing::info!(relay = %url, message = %msg, "Relay closed subscription"); 256 tracing::info!(relay = %url, message = %msg, "Relay closed subscription");
253 let _ = event_sender.send(RelayEvent::Closed(msg.to_string())).await; 257 let _ =
258 event_sender.send(RelayEvent::Closed(msg.to_string())).await;
254 break; 259 break;
255 } 260 }
256 _ => {} 261 _ => {}
@@ -421,7 +426,10 @@ impl RelayConnection {
421 /// - Relay doesn't actually support NIP-77 (despite claiming to) 426 /// - Relay doesn't actually support NIP-77 (despite claiming to)
422 /// - Network errors during reconciliation 427 /// - Network errors during reconciliation
423 /// - Timeout during sync 428 /// - Timeout during sync
424 pub async fn negentropy_sync_filter(&self, filter: Filter) -> Result<NegentropySyncResult, String> { 429 pub async fn negentropy_sync_filter(
430 &self,
431 filter: Filter,
432 ) -> Result<NegentropySyncResult, String> {
425 // Use nostr-sdk's sync method which handles the NEG-OPEN/NEG-MSG exchange 433 // Use nostr-sdk's sync method which handles the NEG-OPEN/NEG-MSG exchange
426 let sync_opts = SyncOptions::default(); 434 let sync_opts = SyncOptions::default();
427 435
diff --git a/src/sync/self_subscriber.rs b/src/sync/self_subscriber.rs
index f83b081..e29e45b 100644
--- a/src/sync/self_subscriber.rs
+++ b/src/sync/self_subscriber.rs
@@ -49,7 +49,12 @@ impl PendingUpdates {
49 } 49 }
50 50
51 /// Add or update a repo with its relays and root events 51 /// Add or update a repo with its relays and root events
52 fn add_repo(&mut self, repo_id: String, relays: HashSet<String>, root_events: HashSet<EventId>) { 52 fn add_repo(
53 &mut self,
54 repo_id: String,
55 relays: HashSet<String>,
56 root_events: HashSet<EventId>,
57 ) {
53 let entry = self.repos.entry(repo_id).or_insert_with(|| RepoSyncNeeds { 58 let entry = self.repos.entry(repo_id).or_insert_with(|| RepoSyncNeeds {
54 relays: HashSet::new(), 59 relays: HashSet::new(),
55 root_events: HashSet::new(), 60 root_events: HashSet::new(),
@@ -251,9 +256,9 @@ impl SelfSubscriber {
251 /// 256 ///
252 /// Returns true if any extracted relay URL contains our domain 257 /// Returns true if any extracted relay URL contains our domain
253 fn lists_our_relay(&self, event: &Event) -> bool { 258 fn lists_our_relay(&self, event: &Event) -> bool {
254 Self::extract_relay_urls(event).iter().any(|url| { 259 Self::extract_relay_urls(event)
255 url.contains(&self.relay_domain) || url == &self.own_relay_url 260 .iter()
256 }) 261 .any(|url| url.contains(&self.relay_domain) || url == &self.own_relay_url)
257 } 262 }
258 263
259 /// Main run loop 264 /// Main run loop
@@ -413,21 +418,21 @@ impl SelfSubscriber {
413 if let Some(repo_sync) = index.get_mut(&repo_ref) { 418 if let Some(repo_sync) = index.get_mut(&repo_ref) {
414 // Add event.id to root_events set in the index (immediate availability) 419 // Add event.id to root_events set in the index (immediate availability)
415 repo_sync.root_events.insert(event.id); 420 repo_sync.root_events.insert(event.id);
416 421
417 // Clone the relays before releasing the lock - Layer 3 filters need to be 422 // Clone the relays before releasing the lock - Layer 3 filters need to be
418 // sent to the same relays as Layer 2 filters for this repo 423 // sent to the same relays as Layer 2 filters for this repo
419 let relays = repo_sync.relays.clone(); 424 let relays = repo_sync.relays.clone();
420 425
421 // Release lock before modifying pending 426 // Release lock before modifying pending
422 drop(index); 427 drop(index);
423 428
424 // Also add root event to pending - this ensures batch processing runs 429 // Also add root event to pending - this ensures batch processing runs
425 // and creates Layer 3 filters for events referencing this root event. 430 // and creates Layer 3 filters for events referencing this root event.
426 // CRITICAL: Include relays so derive_relay_targets knows where to send filters! 431 // CRITICAL: Include relays so derive_relay_targets knows where to send filters!
427 let mut root_events = HashSet::new(); 432 let mut root_events = HashSet::new();
428 root_events.insert(event.id); 433 root_events.insert(event.id);
429 pending.add_repo(repo_ref.clone(), relays.clone(), root_events); 434 pending.add_repo(repo_ref.clone(), relays.clone(), root_events);
430 435
431 tracing::debug!( 436 tracing::debug!(
432 event_id = %event.id, 437 event_id = %event.id,
433 repo_ref = %repo_ref, 438 repo_ref = %repo_ref,
@@ -475,10 +480,12 @@ impl SelfSubscriber {
475 480
476 for (repo_id, needs) in updates { 481 for (repo_id, needs) in updates {
477 // Merge with existing entry or insert new 482 // Merge with existing entry or insert new
478 let entry = index.entry(repo_id.clone()).or_insert_with(|| RepoSyncNeeds { 483 let entry = index
479 relays: HashSet::new(), 484 .entry(repo_id.clone())
480 root_events: HashSet::new(), 485 .or_insert_with(|| RepoSyncNeeds {
481 }); 486 relays: HashSet::new(),
487 root_events: HashSet::new(),
488 });
482 entry.relays.extend(needs.relays); 489 entry.relays.extend(needs.relays);
483 entry.root_events.extend(needs.root_events); 490 entry.root_events.extend(needs.root_events);
484 491
@@ -556,7 +563,7 @@ fn clone_url_to_relay_url(clone_url: &str) -> Option<String> {
556 } else { 563 } else {
557 return None; 564 return None;
558 }; 565 };
559 566
560 // Extract just the host:port part (everything before the first /) 567 // Extract just the host:port part (everything before the first /)
561 let host_port = rest.split('/').next()?; 568 let host_port = rest.split('/').next()?;
562 Some(format!("{}{}", ws_scheme, host_port)) 569 Some(format!("{}{}", ws_scheme, host_port))
@@ -581,7 +588,7 @@ mod tests {
581 Some("ws://localhost:3000".to_string()) 588 Some("ws://localhost:3000".to_string())
582 ); 589 );
583 } 590 }
584 591
585 #[test] 592 #[test]
586 fn test_clone_url_to_relay_url_with_port() { 593 fn test_clone_url_to_relay_url_with_port() {
587 assert_eq!( 594 assert_eq!(
@@ -593,6 +600,9 @@ mod tests {
593 #[test] 600 #[test]
594 fn test_clone_url_to_relay_url_unsupported() { 601 fn test_clone_url_to_relay_url_unsupported() {
595 assert_eq!(clone_url_to_relay_url("git://example.com/repo.git"), None); 602 assert_eq!(clone_url_to_relay_url("git://example.com/repo.git"), None);
596 assert_eq!(clone_url_to_relay_url("ssh://git@example.com/repo.git"), None); 603 assert_eq!(
604 clone_url_to_relay_url("ssh://git@example.com/repo.git"),
605 None
606 );
597 } 607 }
598} \ No newline at end of file 608}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 9bbfb40..37ac3bb 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -1,4 +1,6 @@
1//! Common test utilities 1//! Common test utilities
2#![allow(dead_code)] // Test helpers may not be used in all test configurations
3#![allow(unused_imports)] // Re-exports may not be used in all test configurations
2 4
3pub mod relay; 5pub mod relay;
4pub mod sync_helpers; 6pub mod sync_helpers;
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
index 2dd526b..55cc18e 100644
--- a/tests/common/relay.rs
+++ b/tests/common/relay.rs
@@ -104,7 +104,11 @@ impl TestRelay {
104 } 104 }
105 105
106 /// Start relay with full options 106 /// Start relay with full options
107 async fn start_with_full_options(port: u16, bootstrap_relay_url: Option<String>, disable_negentropy: bool) -> Self { 107 async fn start_with_full_options(
108 port: u16,
109 bootstrap_relay_url: Option<String>,
110 disable_negentropy: bool,
111 ) -> Self {
108 let bind_address = format!("127.0.0.1:{}", port); 112 let bind_address = format!("127.0.0.1:{}", port);
109 let url = format!("ws://127.0.0.1:{}", port); 113 let url = format!("ws://127.0.0.1:{}", port);
110 114
diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs
index 531ebe1..7fa0393 100644
--- a/tests/common/sync_helpers.rs
+++ b/tests/common/sync_helpers.rs
@@ -173,7 +173,11 @@ impl TestClient {
173/// # Returns 173/// # Returns
174/// * `Ok(Event)` - Signed event ready to send 174/// * `Ok(Event)` - Signed event ready to send
175/// * `Err(String)` - If signing fails 175/// * `Err(String)` - If signing fails
176pub fn build_layer2_issue_event(keys: &Keys, repo_coord: &str, title: &str) -> Result<Event, String> { 176pub fn build_layer2_issue_event(
177 keys: &Keys,
178 repo_coord: &str,
179 title: &str,
180) -> Result<Event, String> {
177 build_layer2_issue_with_tag(keys, repo_coord, title, TagVariant::LowercaseA) 181 build_layer2_issue_with_tag(keys, repo_coord, title, TagVariant::LowercaseA)
178} 182}
179 183
@@ -256,10 +260,7 @@ pub fn build_layer3_comment_event(
256 // Choose tag based on kind (NIP-22 uses E, NIP-10 style uses e) 260 // Choose tag based on kind (NIP-22 uses E, NIP-10 style uses e)
257 let tag = if kind_num == KIND_COMMENT { 261 let tag = if kind_num == KIND_COMMENT {
258 // NIP-22 comment: uppercase 'E' tag 262 // NIP-22 comment: uppercase 'E' tag
259 Tag::custom( 263 Tag::custom(TagKind::custom("E"), vec![parent_event_id.to_hex()])
260 TagKind::custom("E"),
261 vec![parent_event_id.to_hex()],
262 )
263 } else { 264 } else {
264 // Kind 1 reply: lowercase 'e' tag with root marker (NIP-10) 265 // Kind 1 reply: lowercase 'e' tag with root marker (NIP-10)
265 Tag::custom( 266 Tag::custom(
@@ -299,10 +300,7 @@ pub fn build_layer3_comment_with_uppercase_e_tag(
299 parent_event_id: &EventId, 300 parent_event_id: &EventId,
300 content: &str, 301 content: &str,
301) -> Result<Event, String> { 302) -> Result<Event, String> {
302 let tag = Tag::custom( 303 let tag = Tag::custom(TagKind::custom("E"), vec![parent_event_id.to_hex()]);
303 TagKind::custom("E"),
304 vec![parent_event_id.to_hex()],
305 );
306 304
307 EventBuilder::new(Kind::Custom(KIND_COMMENT), content) 305 EventBuilder::new(Kind::Custom(KIND_COMMENT), content)
308 .tags(vec![tag]) 306 .tags(vec![tag])
@@ -316,10 +314,7 @@ pub fn build_layer3_quote_with_q_tag(
316 parent_event_id: &EventId, 314 parent_event_id: &EventId,
317 content: &str, 315 content: &str,
318) -> Result<Event, String> { 316) -> Result<Event, String> {
319 let tag = Tag::custom( 317 let tag = Tag::custom(TagKind::custom("q"), vec![parent_event_id.to_hex()]);
320 TagKind::custom("q"),
321 vec![parent_event_id.to_hex()],
322 );
323 318
324 EventBuilder::new(Kind::Custom(1), content) 319 EventBuilder::new(Kind::Custom(1), content)
325 .tags(vec![tag]) 320 .tags(vec![tag])
@@ -587,10 +582,7 @@ pub fn repo_coord(keys: &Keys, identifier: &str) -> String {
587/// ``` 582/// ```
588pub async fn fetch_metrics(relay_url: &str) -> Result<String, reqwest::Error> { 583pub async fn fetch_metrics(relay_url: &str) -> Result<String, reqwest::Error> {
589 // Convert ws:// URL to http:// for metrics endpoint 584 // Convert ws:// URL to http:// for metrics endpoint
590 let http_url = relay_url 585 let http_url = relay_url.replace("ws://", "http://").replace("/", "") + "/metrics";
591 .replace("ws://", "http://")
592 .replace("/", "")
593 + "/metrics";
594 586
595 reqwest::get(&http_url).await?.text().await 587 reqwest::get(&http_url).await?.text().await
596} 588}
@@ -888,8 +880,8 @@ mod tests {
888 let keys = Keys::generate(); 880 let keys = Keys::generate();
889 let coord = repo_coord(&keys, "my-repo"); 881 let coord = repo_coord(&keys, "my-repo");
890 882
891 let event = build_layer2_issue_event(&keys, &coord, "Test Issue") 883 let event =
892 .expect("Should create event"); 884 build_layer2_issue_event(&keys, &coord, "Test Issue").expect("Should create event");
893 885
894 // nostr-sdk 0.43: use field access 886 // nostr-sdk 0.43: use field access
895 assert_eq!(event.kind.as_u16(), KIND_ISSUE); 887 assert_eq!(event.kind.as_u16(), KIND_ISSUE);
@@ -937,8 +929,13 @@ mod tests {
937 let keys = Keys::generate(); 929 let keys = Keys::generate();
938 let parent_id = EventId::all_zeros(); 930 let parent_id = EventId::all_zeros();
939 931
940 let event = build_layer3_comment_event(&keys, &parent_id, "Test comment", Kind::Custom(KIND_COMMENT)) 932 let event = build_layer3_comment_event(
941 .expect("Should create event"); 933 &keys,
934 &parent_id,
935 "Test comment",
936 Kind::Custom(KIND_COMMENT),
937 )
938 .expect("Should create event");
942 939
943 assert_eq!(event.kind.as_u16(), KIND_COMMENT); 940 assert_eq!(event.kind.as_u16(), KIND_COMMENT);
944 941
@@ -980,8 +977,7 @@ mod tests {
980 977
981 let has_e_tag = event.tags.iter().any(|tag| { 978 let has_e_tag = event.tags.iter().any(|tag| {
982 let slice = tag.as_slice(); 979 let slice = tag.as_slice();
983 slice.first().is_some_and(|t| t == "e") && 980 slice.first().is_some_and(|t| t == "e") && slice.get(3).is_some_and(|m| m == "root")
984 slice.get(3).is_some_and(|m| m == "root")
985 }); 981 });
986 assert!(has_e_tag, "Should have 'e' tag with root marker"); 982 assert!(has_e_tag, "Should have 'e' tag with root marker");
987 } 983 }
@@ -1038,7 +1034,10 @@ mod tests {
1038 fn test_parse_gauge_without_labels() { 1034 fn test_parse_gauge_without_labels() {
1039 let text = r#"ngit_sync_relays_tracked_total 3"#; 1035 let text = r#"ngit_sync_relays_tracked_total 3"#;
1040 let metrics = ParsedMetrics::parse(text); 1036 let metrics = ParsedMetrics::parse(text);
1041 assert_eq!(metrics.gauge("ngit_sync_relays_tracked_total", &[]), Some(3)); 1037 assert_eq!(
1038 metrics.gauge("ngit_sync_relays_tracked_total", &[]),
1039 Some(3)
1040 );
1042 } 1041 }
1043 1042
1044 #[test] 1043 #[test]
@@ -1051,9 +1050,6 @@ mod tests {
1051 fn test_parse_metric_with_relay_url_label() { 1050 fn test_parse_metric_with_relay_url_label() {
1052 let text = r#"ngit_sync_relay_connected{relay="ws://127.0.0.1:12345"} 1"#; 1051 let text = r#"ngit_sync_relay_connected{relay="ws://127.0.0.1:12345"} 1"#;
1053 let metrics = ParsedMetrics::parse(text); 1052 let metrics = ParsedMetrics::parse(text);
1054 assert_eq!( 1053 assert_eq!(metrics.relay_connected("ws://127.0.0.1:12345"), Some(true));
1055 metrics.relay_connected("ws://127.0.0.1:12345"),
1056 Some(true)
1057 );
1058 } 1054 }
1059} \ No newline at end of file 1055}
diff --git a/tests/nip77_negentropy.rs b/tests/nip77_negentropy.rs
index c8e0b50..5293754 100644
--- a/tests/nip77_negentropy.rs
+++ b/tests/nip77_negentropy.rs
@@ -45,13 +45,13 @@ async fn test_nip77_negentropy_sync_finds_events() {
45 let keys = Keys::generate(); 45 let keys = Keys::generate();
46 46
47 // Create a repository announcement that will be accepted by the relay 47 // Create a repository announcement that will be accepted by the relay
48 let announcement = create_repo_announcement( 48 let announcement = create_repo_announcement(&keys, &[&relay.domain()], "test-repo-nip77");
49 &keys,
50 &[&relay.domain()],
51 "test-repo-nip77",
52 );
53 let event1_id = announcement.id; 49 let event1_id = announcement.id;
54 println!("Created event 1: {} (kind {})", event1_id, announcement.kind.as_u16()); 50 println!(
51 "Created event 1: {} (kind {})",
52 event1_id,
53 announcement.kind.as_u16()
54 );
55 55
56 // Create a second event (issue referencing the repo) 56 // Create a second event (issue referencing the repo)
57 let repo_coord = format!( 57 let repo_coord = format!(
@@ -63,7 +63,11 @@ async fn test_nip77_negentropy_sync_finds_events() {
63 let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for NIP-77") 63 let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for NIP-77")
64 .expect("Failed to build issue event"); 64 .expect("Failed to build issue event");
65 let event2_id = issue.id; 65 let event2_id = issue.id;
66 println!("Created event 2: {} (kind {})", event2_id, issue.kind.as_u16()); 66 println!(
67 "Created event 2: {} (kind {})",
68 event2_id,
69 issue.kind.as_u16()
70 );
67 71
68 // 3. Send events to relay using TestClient 72 // 3. Send events to relay using TestClient
69 let publish_client = TestClient::new(relay.url(), keys.clone()) 73 let publish_client = TestClient::new(relay.url(), keys.clone())
@@ -99,9 +103,10 @@ async fn test_nip77_negentropy_sync_finds_events() {
99 tokio::time::sleep(Duration::from_millis(500)).await; 103 tokio::time::sleep(Duration::from_millis(500)).await;
100 104
101 // 6. Perform negentropy sync with filter matching our events 105 // 6. Perform negentropy sync with filter matching our events
102 let filter = Filter::new() 106 let filter = Filter::new().author(keys.public_key()).kinds(vec![
103 .author(keys.public_key()) 107 Kind::Custom(KIND_REPOSITORY_STATE),
104 .kinds(vec![Kind::Custom(KIND_REPOSITORY_STATE), Kind::Custom(KIND_ISSUE)]); 108 Kind::Custom(KIND_ISSUE),
109 ]);
105 110
106 println!("Starting negentropy sync with filter: {:?}", filter); 111 println!("Starting negentropy sync with filter: {:?}", filter);
107 112
@@ -177,7 +182,7 @@ async fn test_nip77_negentropy_sync_empty_result() {
177 182
178 // 3. Sync with filter that won't match anything 183 // 3. Sync with filter that won't match anything
179 let filter = Filter::new() 184 let filter = Filter::new()
180 .author(keys.public_key()) // Random new key, no events exist 185 .author(keys.public_key()) // Random new key, no events exist
181 .kind(Kind::Custom(KIND_REPOSITORY_STATE)); 186 .kind(Kind::Custom(KIND_REPOSITORY_STATE));
182 187
183 println!("Starting negentropy sync with empty filter"); 188 println!("Starting negentropy sync with empty filter");
diff --git a/tests/sync.rs b/tests/sync.rs
index 5b6b752..2e09fb8 100644
--- a/tests/sync.rs
+++ b/tests/sync.rs
@@ -37,4 +37,4 @@ mod sync {
37 pub mod live_sync; 37 pub mod live_sync;
38 pub mod metrics; 38 pub mod metrics;
39 pub mod tag_variations; 39 pub mod tag_variations;
40} \ No newline at end of file 40}
diff --git a/tests/sync/bootstrap.rs b/tests/sync/bootstrap.rs
index 8a181c9..174fe28 100644
--- a/tests/sync/bootstrap.rs
+++ b/tests/sync/bootstrap.rs
@@ -167,7 +167,8 @@ async fn test_relay_replays_events_after_restart() {
167 .kind(Kind::Custom(KIND_REPOSITORY_STATE)) 167 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
168 .author(keys.public_key()); 168 .author(keys.public_key());
169 169
170 let synced_first = wait_for_event_on_relay(relay_b.url(), filter.clone(), Duration::from_secs(5)).await; 170 let synced_first =
171 wait_for_event_on_relay(relay_b.url(), filter.clone(), Duration::from_secs(5)).await;
171 println!("First sync check: {}", synced_first); 172 println!("First sync check: {}", synced_first);
172 173
173 // 8. Stop relay_b 174 // 8. Stop relay_b
@@ -193,7 +194,8 @@ async fn test_relay_replays_events_after_restart() {
193 // 12. Verify announcement is available on new relay_b 194 // 12. Verify announcement is available on new relay_b
194 // The announcement listed the OLD relay_b domain, but since relay_a still 195 // The announcement listed the OLD relay_b domain, but since relay_a still
195 // has the event, new relay_b should be able to sync it via bootstrap 196 // has the event, new relay_b should be able to sync it via bootstrap
196 let synced_after_restart = wait_for_event_on_relay(relay_b_new.url(), filter, Duration::from_secs(5)).await; 197 let synced_after_restart =
198 wait_for_event_on_relay(relay_b_new.url(), filter, Duration::from_secs(5)).await;
197 199
198 // 13. Cleanup 200 // 13. Cleanup
199 relay_b_new.stop().await; 201 relay_b_new.stop().await;
@@ -384,7 +386,8 @@ async fn test_history_sync_without_negentropy() {
384 relay_b_port, 386 relay_b_port,
385 Some(relay_a.url().into()), 387 Some(relay_a.url().into()),
386 true, // disable_negentropy = true 388 true, // disable_negentropy = true
387 ).await; 389 )
390 .await;
388 println!( 391 println!(
389 "relay_b started at {} (domain: {}) - negentropy DISABLED, will do HISTORY sync", 392 "relay_b started at {} (domain: {}) - negentropy DISABLED, will do HISTORY sync",
390 relay_b.url(), 393 relay_b.url(),
diff --git a/tests/sync/discovery.rs b/tests/sync/discovery.rs
index 9e27f9e..ed3e9bb 100644
--- a/tests/sync/discovery.rs
+++ b/tests/sync/discovery.rs
@@ -88,7 +88,8 @@ async fn test_discovers_layer3_via_layer2() {
88 ); 88 );
89 89
90 // 6. Create a patch event (Layer 2) that references the announcement 90 // 6. Create a patch event (Layer 2) that references the announcement
91 let patch = create_event_referencing_repo(&keys, &repo_coord, KIND_PATCH, "Test patch proposal"); 91 let patch =
92 create_event_referencing_repo(&keys, &repo_coord, KIND_PATCH, "Test patch proposal");
92 let patch_id = patch.id; 93 let patch_id = patch.id;
93 94
94 println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16()); 95 println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16());
@@ -252,7 +253,8 @@ async fn test_layer2_discovery_with_chain() {
252 let issue_filter = Filter::new() 253 let issue_filter = Filter::new()
253 .kind(Kind::Custom(KIND_ISSUE)) 254 .kind(Kind::Custom(KIND_ISSUE))
254 .author(keys.public_key()); 255 .author(keys.public_key());
255 let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await; 256 let issue_synced =
257 wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
256 258
257 println!("Sync result:"); 259 println!("Sync result:");
258 println!(" Issue {} synced: {}", issue_id, issue_synced); 260 println!(" Issue {} synced: {}", issue_id, issue_synced);
@@ -296,7 +298,7 @@ async fn test_layer2_discovery_with_chain() {
296#[tokio::test] 298#[tokio::test]
297async fn test_recursive_relay_discovery_syncs_announcement() { 299async fn test_recursive_relay_discovery_syncs_announcement() {
298 // 1. Start all three relays 300 // 1. Start all three relays
299 301
300 // relay_b - will be the bootstrap relay, has announcement_x 302 // relay_b - will be the bootstrap relay, has announcement_x
301 let relay_b = TestRelay::start().await; 303 let relay_b = TestRelay::start().await;
302 println!( 304 println!(
@@ -344,7 +346,10 @@ async fn test_recursive_relay_discovery_syncs_announcement() {
344 "repo-y-ac-only", 346 "repo-y-ac-only",
345 ); 347 );
346 let announcement_y_id = announcement_y.id; 348 let announcement_y_id = announcement_y.id;
347 println!("Created announcement_y {} listing A+C only", announcement_y_id); 349 println!(
350 "Created announcement_y {} listing A+C only",
351 announcement_y_id
352 );
348 for tag in announcement_y.tags.iter() { 353 for tag in announcement_y.tags.iter() {
349 println!(" Tag: {:?}", tag.as_slice()); 354 println!(" Tag: {:?}", tag.as_slice());
350 } 355 }
@@ -425,4 +430,4 @@ async fn test_recursive_relay_discovery_syncs_announcement() {
425 "announcement_y {} should have synced from discovered relay_c to relay_a (recursive discovery)", 430 "announcement_y {} should have synced from discovered relay_c to relay_a (recursive discovery)",
426 announcement_y_id 431 announcement_y_id
427 ); 432 );
428} \ No newline at end of file 433}
diff --git a/tests/sync/live_sync.rs b/tests/sync/live_sync.rs
index ebe1c0b..7fa08a0 100644
--- a/tests/sync/live_sync.rs
+++ b/tests/sync/live_sync.rs
@@ -229,7 +229,10 @@ async fn test_live_sync_layer3_events() {
229 .send_event(&comment) 229 .send_event(&comment)
230 .await 230 .await
231 .expect("Failed to send comment"); 231 .expect("Failed to send comment");
232 println!("Layer 3 comment {} sent to relay_a BEFORE Layer 3 subscription established", comment_id); 232 println!(
233 "Layer 3 comment {} sent to relay_a BEFORE Layer 3 subscription established",
234 comment_id
235 );
233 236
234 // 6. Now wait for issue to sync to relay_b (this triggers Layer 3 filter creation) 237 // 6. Now wait for issue to sync to relay_b (this triggers Layer 3 filter creation)
235 tokio::time::sleep(Duration::from_secs(2)).await; 238 tokio::time::sleep(Duration::from_secs(2)).await;
@@ -394,7 +397,7 @@ async fn test_live_sync_event_ordering() {
394 client_a 397 client_a
395 .send_event(&issue) 398 .send_event(&issue)
396 .await 399 .await
397 .expect(&format!("Failed to send issue {}", i)); 400 .unwrap_or_else(|_| panic!("Failed to send issue {}", i));
398 401
399 // Delay between events to ensure different timestamps 402 // Delay between events to ensure different timestamps
400 tokio::time::sleep(Duration::from_millis(150)).await; 403 tokio::time::sleep(Duration::from_millis(150)).await;
diff --git a/tests/sync/metrics.rs b/tests/sync/metrics.rs
index 26d379d..14e1dfd 100644
--- a/tests/sync/metrics.rs
+++ b/tests/sync/metrics.rs
@@ -32,7 +32,7 @@ async fn test_prometheus_format_valid() {
32 let relay = TestRelay::start().await; 32 let relay = TestRelay::start().await;
33 tokio::time::sleep(Duration::from_millis(500)).await; 33 tokio::time::sleep(Duration::from_millis(500)).await;
34 34
35 let metrics = fetch_metrics(&relay.url()) 35 let metrics = fetch_metrics(relay.url())
36 .await 36 .await
37 .expect("Failed to fetch metrics"); 37 .expect("Failed to fetch metrics");
38 38
@@ -67,7 +67,7 @@ async fn test_metrics_availability_during_sync() {
67 67
68 // Make multiple metrics requests while sync is active 68 // Make multiple metrics requests while sync is active
69 for i in 0..3 { 69 for i in 0..3 {
70 let metrics = fetch_metrics(&sync_relay.url()).await; 70 let metrics = fetch_metrics(sync_relay.url()).await;
71 assert!( 71 assert!(
72 metrics.is_ok(), 72 metrics.is_ok(),
73 "Metrics request {} should succeed during sync", 73 "Metrics request {} should succeed during sync",
@@ -135,7 +135,7 @@ async fn test_metric_values_are_numeric() {
135 let relay = TestRelay::start().await; 135 let relay = TestRelay::start().await;
136 tokio::time::sleep(Duration::from_millis(500)).await; 136 tokio::time::sleep(Duration::from_millis(500)).await;
137 137
138 let metrics = fetch_metrics(&relay.url()) 138 let metrics = fetch_metrics(relay.url())
139 .await 139 .await
140 .expect("Should fetch metrics"); 140 .expect("Should fetch metrics");
141 141
diff --git a/tests/sync/tag_variations.rs b/tests/sync/tag_variations.rs
index 273a573..41a6611 100644
--- a/tests/sync/tag_variations.rs
+++ b/tests/sync/tag_variations.rs
@@ -57,11 +57,8 @@ async fn test_layer2_sync_with_lowercase_a_tag() {
57 57
58 // 2. Create and send repository announcement to both relays 58 // 2. Create and send repository announcement to both relays
59 let repo_id = "test-repo-tag-8a"; 59 let repo_id = "test-repo-tag-8a";
60 let announcement = create_repo_announcement( 60 let announcement =
61 &keys, 61 create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
62 &[&relay_a.domain(), &relay_b.domain()],
63 repo_id,
64 );
65 62
66 let client_a = TestClient::new(relay_a.url(), keys.clone()) 63 let client_a = TestClient::new(relay_a.url(), keys.clone())
67 .await 64 .await
@@ -88,11 +85,16 @@ async fn test_layer2_sync_with_lowercase_a_tag() {
88 85
89 // 4. Create and send Layer 2 issue with lowercase 'a' tag 86 // 4. Create and send Layer 2 issue with lowercase 'a' tag
90 let repo_coordinate = repo_coord(&keys, repo_id); 87 let repo_coordinate = repo_coord(&keys, repo_id);
91 let issue = build_layer2_issue_event(&keys, &repo_coordinate, "Test Issue with lowercase a tag") 88 let issue =
92 .expect("Failed to create issue event"); 89 build_layer2_issue_event(&keys, &repo_coordinate, "Test Issue with lowercase a tag")
90 .expect("Failed to create issue event");
93 let issue_id = issue.id; 91 let issue_id = issue.id;
94 92
95 println!("Created issue {} (kind {}) with lowercase 'a' tag", issue_id, issue.kind.as_u16()); 93 println!(
94 "Created issue {} (kind {}) with lowercase 'a' tag",
95 issue_id,
96 issue.kind.as_u16()
97 );
96 for tag in issue.tags.iter() { 98 for tag in issue.tags.iter() {
97 println!(" Tag: {:?}", tag.as_slice()); 99 println!(" Tag: {:?}", tag.as_slice());
98 } 100 }
@@ -154,11 +156,8 @@ async fn test_layer2_sync_with_uppercase_a_tag() {
154 156
155 // 2. Create and send repository announcement to both relays 157 // 2. Create and send repository announcement to both relays
156 let repo_id = "test-repo-tag-8b"; 158 let repo_id = "test-repo-tag-8b";
157 let announcement = create_repo_announcement( 159 let announcement =
158 &keys, 160 create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
159 &[&relay_a.domain(), &relay_b.domain()],
160 repo_id,
161 );
162 161
163 let client_a = TestClient::new(relay_a.url(), keys.clone()) 162 let client_a = TestClient::new(relay_a.url(), keys.clone())
164 .await 163 .await
@@ -185,11 +184,19 @@ async fn test_layer2_sync_with_uppercase_a_tag() {
185 184
186 // 4. Create and send Layer 2 issue with uppercase 'A' tag 185 // 4. Create and send Layer 2 issue with uppercase 'A' tag
187 let repo_coordinate = repo_coord(&keys, repo_id); 186 let repo_coordinate = repo_coord(&keys, repo_id);
188 let issue = build_layer2_issue_with_uppercase_a_tag(&keys, &repo_coordinate, "Test Issue with uppercase A tag") 187 let issue = build_layer2_issue_with_uppercase_a_tag(
189 .expect("Failed to create issue event"); 188 &keys,
189 &repo_coordinate,
190 "Test Issue with uppercase A tag",
191 )
192 .expect("Failed to create issue event");
190 let issue_id = issue.id; 193 let issue_id = issue.id;
191 194
192 println!("Created issue {} (kind {}) with uppercase 'A' tag", issue_id, issue.kind.as_u16()); 195 println!(
196 "Created issue {} (kind {}) with uppercase 'A' tag",
197 issue_id,
198 issue.kind.as_u16()
199 );
193 for tag in issue.tags.iter() { 200 for tag in issue.tags.iter() {
194 println!(" Tag: {:?}", tag.as_slice()); 201 println!(" Tag: {:?}", tag.as_slice());
195 } 202 }
@@ -250,11 +257,8 @@ async fn test_layer2_sync_with_q_tag() {
250 257
251 // 2. Create and send repository announcement to both relays 258 // 2. Create and send repository announcement to both relays
252 let repo_id = "test-repo-tag-8c"; 259 let repo_id = "test-repo-tag-8c";
253 let announcement = create_repo_announcement( 260 let announcement =
254 &keys, 261 create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
255 &[&relay_a.domain(), &relay_b.domain()],
256 repo_id,
257 );
258 262
259 let client_a = TestClient::new(relay_a.url(), keys.clone()) 263 let client_a = TestClient::new(relay_a.url(), keys.clone())
260 .await 264 .await
@@ -285,7 +289,11 @@ async fn test_layer2_sync_with_q_tag() {
285 .expect("Failed to create issue event"); 289 .expect("Failed to create issue event");
286 let issue_id = issue.id; 290 let issue_id = issue.id;
287 291
288 println!("Created issue {} (kind {}) with 'q' tag", issue_id, issue.kind.as_u16()); 292 println!(
293 "Created issue {} (kind {}) with 'q' tag",
294 issue_id,
295 issue.kind.as_u16()
296 );
289 for tag in issue.tags.iter() { 297 for tag in issue.tags.iter() {
290 println!(" Tag: {:?}", tag.as_slice()); 298 println!(" Tag: {:?}", tag.as_slice());
291 } 299 }
@@ -350,11 +358,8 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
350 358
351 // 2. Create and send repository announcement to both relays 359 // 2. Create and send repository announcement to both relays
352 let repo_id = "test-repo-tag-9a"; 360 let repo_id = "test-repo-tag-9a";
353 let announcement = create_repo_announcement( 361 let announcement =
354 &keys, 362 create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
355 &[&relay_a.domain(), &relay_b.domain()],
356 repo_id,
357 );
358 363
359 let client_a = TestClient::new(relay_a.url(), keys.clone()) 364 let client_a = TestClient::new(relay_a.url(), keys.clone())
360 .await 365 .await
@@ -392,10 +397,9 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
392 println!("Layer 2 issue {} sent to relay_a", issue_id); 397 println!("Layer 2 issue {} sent to relay_a", issue_id);
393 398
394 // 5. Wait for issue to sync to relay_b 399 // 5. Wait for issue to sync to relay_b
395 let issue_filter = Filter::new() 400 let issue_filter = Filter::new().kind(Kind::Custom(KIND_ISSUE)).id(issue_id);
396 .kind(Kind::Custom(KIND_ISSUE)) 401 let issue_synced =
397 .id(issue_id); 402 wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
398 let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
399 println!("Issue synced to relay_b: {}", issue_synced); 403 println!("Issue synced to relay_b: {}", issue_synced);
400 assert!(issue_synced, "Layer 2 issue should sync first"); 404 assert!(issue_synced, "Layer 2 issue should sync first");
401 405
@@ -412,7 +416,11 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
412 .expect("Failed to create reply"); 416 .expect("Failed to create reply");
413 let reply_id = reply.id; 417 let reply_id = reply.id;
414 418
415 println!("Created reply {} (kind {}) with lowercase 'e' tag", reply_id, reply.kind.as_u16()); 419 println!(
420 "Created reply {} (kind {}) with lowercase 'e' tag",
421 reply_id,
422 reply.kind.as_u16()
423 );
416 for tag in reply.tags.iter() { 424 for tag in reply.tags.iter() {
417 println!(" Tag: {:?}", tag.as_slice()); 425 println!(" Tag: {:?}", tag.as_slice());
418 } 426 }
@@ -428,11 +436,12 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
428 436
429 // 7. Wait and verify reply syncs to relay_b 437 // 7. Wait and verify reply syncs to relay_b
430 let reply_filter = Filter::new() 438 let reply_filter = Filter::new()
431 .kind(Kind::TextNote) // Kind 1 439 .kind(Kind::TextNote) // Kind 1
432 .author(keys.public_key()) 440 .author(keys.public_key())
433 .id(reply_id); 441 .id(reply_id);
434 442
435 let reply_synced = wait_for_event_on_relay(relay_b.url(), reply_filter, Duration::from_secs(5)).await; 443 let reply_synced =
444 wait_for_event_on_relay(relay_b.url(), reply_filter, Duration::from_secs(5)).await;
436 445
437 println!("Reply {} synced to relay_b: {}", reply_id, reply_synced); 446 println!("Reply {} synced to relay_b: {}", reply_id, reply_synced);
438 447
@@ -473,11 +482,8 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
473 482
474 // 2. Create and send repository announcement to both relays 483 // 2. Create and send repository announcement to both relays
475 let repo_id = "test-repo-tag-9b"; 484 let repo_id = "test-repo-tag-9b";
476 let announcement = create_repo_announcement( 485 let announcement =
477 &keys, 486 create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
478 &[&relay_a.domain(), &relay_b.domain()],
479 repo_id,
480 );
481 487
482 let client_a = TestClient::new(relay_a.url(), keys.clone()) 488 let client_a = TestClient::new(relay_a.url(), keys.clone())
483 .await 489 .await
@@ -515,10 +521,9 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
515 println!("Layer 2 issue {} sent to relay_a", issue_id); 521 println!("Layer 2 issue {} sent to relay_a", issue_id);
516 522
517 // 5. Wait for issue to sync to relay_b 523 // 5. Wait for issue to sync to relay_b
518 let issue_filter = Filter::new() 524 let issue_filter = Filter::new().kind(Kind::Custom(KIND_ISSUE)).id(issue_id);
519 .kind(Kind::Custom(KIND_ISSUE)) 525 let issue_synced =
520 .id(issue_id); 526 wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
521 let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
522 println!("Issue synced to relay_b: {}", issue_synced); 527 println!("Issue synced to relay_b: {}", issue_synced);
523 assert!(issue_synced, "Layer 2 issue should sync first"); 528 assert!(issue_synced, "Layer 2 issue should sync first");
524 529
@@ -531,11 +536,16 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
531 tokio::time::sleep(Duration::from_millis(500)).await; 536 tokio::time::sleep(Duration::from_millis(500)).await;
532 537
533 // 6. Create and send Layer 3 comment with uppercase 'E' tag (kind 1111) 538 // 6. Create and send Layer 3 comment with uppercase 'E' tag (kind 1111)
534 let comment = build_layer3_comment_with_uppercase_e_tag(&keys, &issue_id, "Comment with uppercase E tag") 539 let comment =
535 .expect("Failed to create comment"); 540 build_layer3_comment_with_uppercase_e_tag(&keys, &issue_id, "Comment with uppercase E tag")
541 .expect("Failed to create comment");
536 let comment_id = comment.id; 542 let comment_id = comment.id;
537 543
538 println!("Created comment {} (kind {}) with uppercase 'E' tag", comment_id, comment.kind.as_u16()); 544 println!(
545 "Created comment {} (kind {}) with uppercase 'E' tag",
546 comment_id,
547 comment.kind.as_u16()
548 );
539 for tag in comment.tags.iter() { 549 for tag in comment.tags.iter() {
540 println!(" Tag: {:?}", tag.as_slice()); 550 println!(" Tag: {:?}", tag.as_slice());
541 } 551 }
@@ -551,13 +561,17 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
551 561
552 // 7. Wait and verify comment syncs to relay_b 562 // 7. Wait and verify comment syncs to relay_b
553 let comment_filter = Filter::new() 563 let comment_filter = Filter::new()
554 .kind(Kind::Custom(KIND_COMMENT)) // Kind 1111 564 .kind(Kind::Custom(KIND_COMMENT)) // Kind 1111
555 .author(keys.public_key()) 565 .author(keys.public_key())
556 .id(comment_id); 566 .id(comment_id);
557 567
558 let comment_synced = wait_for_event_on_relay(relay_b.url(), comment_filter, Duration::from_secs(5)).await; 568 let comment_synced =
569 wait_for_event_on_relay(relay_b.url(), comment_filter, Duration::from_secs(5)).await;
559 570
560 println!("Comment {} synced to relay_b: {}", comment_id, comment_synced); 571 println!(
572 "Comment {} synced to relay_b: {}",
573 comment_id, comment_synced
574 );
561 575
562 // 8. Cleanup 576 // 8. Cleanup
563 relay_b.stop().await; 577 relay_b.stop().await;
@@ -596,11 +610,8 @@ async fn test_layer3_sync_with_q_tag() {
596 610
597 // 2. Create and send repository announcement to both relays 611 // 2. Create and send repository announcement to both relays
598 let repo_id = "test-repo-tag-9c"; 612 let repo_id = "test-repo-tag-9c";
599 let announcement = create_repo_announcement( 613 let announcement =
600 &keys, 614 create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
601 &[&relay_a.domain(), &relay_b.domain()],
602 repo_id,
603 );
604 615
605 let client_a = TestClient::new(relay_a.url(), keys.clone()) 616 let client_a = TestClient::new(relay_a.url(), keys.clone())
606 .await 617 .await
@@ -638,10 +649,9 @@ async fn test_layer3_sync_with_q_tag() {
638 println!("Layer 2 issue {} sent to relay_a", issue_id); 649 println!("Layer 2 issue {} sent to relay_a", issue_id);
639 650
640 // 5. Wait for issue to sync to relay_b 651 // 5. Wait for issue to sync to relay_b
641 let issue_filter = Filter::new() 652 let issue_filter = Filter::new().kind(Kind::Custom(KIND_ISSUE)).id(issue_id);
642 .kind(Kind::Custom(KIND_ISSUE)) 653 let issue_synced =
643 .id(issue_id); 654 wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
644 let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
645 println!("Issue synced to relay_b: {}", issue_synced); 655 println!("Issue synced to relay_b: {}", issue_synced);
646 assert!(issue_synced, "Layer 2 issue should sync first"); 656 assert!(issue_synced, "Layer 2 issue should sync first");
647 657
@@ -658,7 +668,11 @@ async fn test_layer3_sync_with_q_tag() {
658 .expect("Failed to create quote"); 668 .expect("Failed to create quote");
659 let quote_id = quote.id; 669 let quote_id = quote.id;
660 670
661 println!("Created quote {} (kind {}) with 'q' tag", quote_id, quote.kind.as_u16()); 671 println!(
672 "Created quote {} (kind {}) with 'q' tag",
673 quote_id,
674 quote.kind.as_u16()
675 );
662 for tag in quote.tags.iter() { 676 for tag in quote.tags.iter() {
663 println!(" Tag: {:?}", tag.as_slice()); 677 println!(" Tag: {:?}", tag.as_slice());
664 } 678 }
@@ -674,11 +688,12 @@ async fn test_layer3_sync_with_q_tag() {
674 688
675 // 7. Wait and verify quote syncs to relay_b 689 // 7. Wait and verify quote syncs to relay_b
676 let quote_filter = Filter::new() 690 let quote_filter = Filter::new()
677 .kind(Kind::TextNote) // Kind 1 691 .kind(Kind::TextNote) // Kind 1
678 .author(keys.public_key()) 692 .author(keys.public_key())
679 .id(quote_id); 693 .id(quote_id);
680 694
681 let quote_synced = wait_for_event_on_relay(relay_b.url(), quote_filter, Duration::from_secs(5)).await; 695 let quote_synced =
696 wait_for_event_on_relay(relay_b.url(), quote_filter, Duration::from_secs(5)).await;
682 697
683 println!("Quote {} synced to relay_b: {}", quote_id, quote_synced); 698 println!("Quote {} synced to relay_b: {}", quote_id, quote_synced);
684 699
@@ -690,4 +705,4 @@ async fn test_layer3_sync_with_q_tag() {
690 quote_synced, 705 quote_synced,
691 "Layer 3 quote with 'q' tag should have synced to relay_b" 706 "Layer 3 quote with 'q' tag should have synced to relay_b"
692 ); 707 );
693} \ No newline at end of file 708}