mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
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
This commit is contained in:
@@ -17,4 +17,4 @@ fn main() {
|
||||
// Re-run if HEAD changes (new commits)
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs/heads/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +585,9 @@ mod tests {
|
||||
"Missing 'grasp-audit-test-event' tag"
|
||||
);
|
||||
assert!(
|
||||
tag_contents.iter().any(|t| t.starts_with("audit-isolated-")),
|
||||
tag_contents
|
||||
.iter()
|
||||
.any(|t| t.starts_with("audit-isolated-")),
|
||||
"Missing 'audit-isolated-*' tag"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -1973,11 +1973,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_context_mode_from_audit_mode() {
|
||||
assert_eq!(ContextMode::from(AuditMode::Isolated), ContextMode::Isolated);
|
||||
assert_eq!(
|
||||
ContextMode::from(AuditMode::Shared),
|
||||
ContextMode::Shared
|
||||
ContextMode::from(AuditMode::Isolated),
|
||||
ContextMode::Isolated
|
||||
);
|
||||
assert_eq!(ContextMode::from(AuditMode::Shared), ContextMode::Shared);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -38,7 +38,9 @@ fn parse_spec_lines(spec_ref: &str) -> Vec<u32> {
|
||||
if line_part.contains('-') {
|
||||
let range_parts: Vec<&str> = line_part.split('-').collect();
|
||||
if range_parts.len() == 2 {
|
||||
if let (Ok(start), Ok(end)) = (range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>()) {
|
||||
if let (Ok(start), Ok(end)) =
|
||||
(range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>())
|
||||
{
|
||||
return (start..=end).collect();
|
||||
}
|
||||
}
|
||||
@@ -162,10 +164,19 @@ impl AuditResult {
|
||||
/// Print a detailed report aligned to GRASP-01 specification
|
||||
pub fn print_report(&self) {
|
||||
println!();
|
||||
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
||||
println!(
|
||||
"{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
|
||||
BOLD, RESET
|
||||
);
|
||||
println!("{}GRASP-01 Compliance Report{}", BOLD, RESET);
|
||||
println!("Source: github.com/nostr-protocol/grasp (commit: {})", GRASP_COMMIT_ID);
|
||||
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
||||
println!(
|
||||
"Source: github.com/nostr-protocol/grasp (commit: {})",
|
||||
GRASP_COMMIT_ID
|
||||
);
|
||||
println!(
|
||||
"{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
|
||||
BOLD, RESET
|
||||
);
|
||||
|
||||
// Build a map of spec line -> tests that cover it
|
||||
let mut tests_by_line: BTreeMap<u32, Vec<&TestResult>> = BTreeMap::new();
|
||||
@@ -185,7 +196,10 @@ impl AuditResult {
|
||||
println!();
|
||||
println!("{}{}## {}{}", CYAN, BOLD, section, RESET);
|
||||
|
||||
for req in GRASP_01_REQUIREMENTS.iter().filter(|r| r.section == section) {
|
||||
for req in GRASP_01_REQUIREMENTS
|
||||
.iter()
|
||||
.filter(|r| r.section == section)
|
||||
{
|
||||
println!();
|
||||
// Print spec requirement in blue
|
||||
println!("{}📘 Line {}: {}{}", BLUE, req.line, req.text, RESET);
|
||||
@@ -218,7 +232,10 @@ impl AuditResult {
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
||||
println!(
|
||||
"{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
|
||||
BOLD, RESET
|
||||
);
|
||||
|
||||
// Summary statistics
|
||||
let passed = self.passed_count();
|
||||
@@ -252,7 +269,10 @@ impl AuditResult {
|
||||
"{}Test results: {}/{} tests passed ({:.1}%){}",
|
||||
summary_color, passed, total_tests, pass_rate, RESET
|
||||
);
|
||||
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
||||
println!(
|
||||
"{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
|
||||
BOLD, RESET
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
@@ -313,7 +333,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_spec_lines_range() {
|
||||
assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7-9"), vec![7, 8, 9]);
|
||||
assert_eq!(parse_spec_lines("GRASP-01:cors:44-47"), vec![44, 45, 46, 47]);
|
||||
assert_eq!(
|
||||
parse_spec_lines("GRASP-01:cors:44-47"),
|
||||
vec![44, 45, 46, 47]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -327,4 +350,4 @@ mod tests {
|
||||
assert_eq!(parse_spec_lines("GRASP-01:invalid"), Vec::<u32>::new());
|
||||
assert_eq!(parse_spec_lines("GRASP-01:test:abc"), Vec::<u32>::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,6 @@ pub use nip11_document::Nip11DocumentTests;
|
||||
pub use push_authorization::PushAuthorizationTests;
|
||||
pub use repository_creation::RepositoryCreationTests;
|
||||
pub use spec_requirements::{
|
||||
get_requirement, get_requirements_for_section, get_sections, RequirementLevel,
|
||||
SpecRequirement, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID,
|
||||
get_requirement, get_requirements_for_section, get_sections, RequirementLevel, SpecRequirement,
|
||||
GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID,
|
||||
};
|
||||
|
||||
@@ -163,23 +163,27 @@ impl Nip01SmokeTests {
|
||||
/// Spec: NIP-01 CLOSE message
|
||||
/// Requirement: Relay MUST support CLOSE to end subscriptions
|
||||
pub async fn test_close_subscription(client: &AuditClient) -> TestResult {
|
||||
TestResult::new("close_subscription", "GRASP-01:nostr-relay:7", "Can close subscriptions")
|
||||
.run(|| async {
|
||||
// For now, we just verify we can query events
|
||||
// Full subscription management with CLOSE would require
|
||||
// lower-level WebSocket access
|
||||
TestResult::new(
|
||||
"close_subscription",
|
||||
"GRASP-01:nostr-relay:7",
|
||||
"Can close subscriptions",
|
||||
)
|
||||
.run(|| async {
|
||||
// For now, we just verify we can query events
|
||||
// Full subscription management with CLOSE would require
|
||||
// lower-level WebSocket access
|
||||
|
||||
let filter = Filter::new().kind(Kind::TextNote).limit(1);
|
||||
let filter = Filter::new().kind(Kind::TextNote).limit(1);
|
||||
|
||||
let _events = client
|
||||
.subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to subscribe: {}", e))?;
|
||||
let _events = client
|
||||
.subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to subscribe: {}", e))?;
|
||||
|
||||
// If we got here, subscription worked
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
// If we got here, subscription worked
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Test 5: Rejects events with invalid signatures
|
||||
|
||||
@@ -407,8 +407,12 @@ impl PushAuthorizationTests {
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
|
||||
.fail(format!("Failed to create repo: {}", e))
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push rejected without state event",
|
||||
)
|
||||
.fail(format!("Failed to create repo: {}", e))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -427,8 +431,12 @@ impl PushAuthorizationTests {
|
||||
let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
|
||||
.fail(&e)
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push rejected without state event",
|
||||
)
|
||||
.fail(&e)
|
||||
}
|
||||
};
|
||||
let cleanup = || {
|
||||
@@ -437,8 +445,12 @@ impl PushAuthorizationTests {
|
||||
|
||||
if let Err(e) = create_commit(&clone_path, "Unauthorized commit") {
|
||||
cleanup();
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
|
||||
.fail(&e);
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push rejected without state event",
|
||||
)
|
||||
.fail(&e);
|
||||
}
|
||||
|
||||
// Do NOT publish state event - push should be rejected
|
||||
@@ -446,14 +458,24 @@ impl PushAuthorizationTests {
|
||||
cleanup();
|
||||
|
||||
match push_result {
|
||||
Ok(false) => {
|
||||
TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event").pass()
|
||||
}
|
||||
Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
|
||||
.fail("Push accepted but should be rejected"),
|
||||
Err(e) => {
|
||||
TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event").fail(&e)
|
||||
}
|
||||
Ok(false) => TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push rejected without state event",
|
||||
)
|
||||
.pass(),
|
||||
Ok(true) => TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push rejected without state event",
|
||||
)
|
||||
.fail("Push accepted but should be rejected"),
|
||||
Err(e) => TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push rejected without state event",
|
||||
)
|
||||
.fail(&e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,11 +502,18 @@ impl PushAuthorizationTests {
|
||||
// The OwnerStateDataPushed fixture handles all stages:
|
||||
// Generate → Send → Verify → DataPush
|
||||
match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await {
|
||||
Ok(_state_event) => {
|
||||
TestResult::new(test_name, "GRASP-01:git-http:30", "Push authorized with matching state").pass()
|
||||
}
|
||||
Err(e) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push authorized with matching state")
|
||||
.fail(format!("{}", e)),
|
||||
Ok(_state_event) => TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push authorized with matching state",
|
||||
)
|
||||
.pass(),
|
||||
Err(e) => TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Push authorized with matching state",
|
||||
)
|
||||
.fail(format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,8 +897,12 @@ impl PushAuthorizationTests {
|
||||
// Send the rogue state event using the raw client to bypass AuditClient's key check
|
||||
if let Err(e) = client.client().send_event(&rogue_state).await {
|
||||
cleanup();
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:30", "Non-maintainer state events ignored")
|
||||
.fail(format!("Failed to send rogue state event: {}", e));
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01:git-http:30",
|
||||
"Non-maintainer state events ignored",
|
||||
)
|
||||
.fail(format!("Failed to send rogue state event: {}", e));
|
||||
}
|
||||
|
||||
// Wait for event to propagate
|
||||
@@ -1036,7 +1069,9 @@ impl PushAuthorizationTests {
|
||||
.await
|
||||
{
|
||||
Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass(),
|
||||
Err(e) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)),
|
||||
Err(e) => {
|
||||
TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,7 +1097,8 @@ impl PushAuthorizationTests {
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
|
||||
.fail(format!("{}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1072,7 +1108,8 @@ impl PushAuthorizationTests {
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
|
||||
.fail(format!("{}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1146,7 +1183,8 @@ impl PushAuthorizationTests {
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
|
||||
.fail(format!("{}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1156,7 +1194,8 @@ impl PushAuthorizationTests {
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
|
||||
.fail(format!("{}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1233,7 +1272,8 @@ impl PushAuthorizationTests {
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
|
||||
.fail(format!("{}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1243,7 +1283,8 @@ impl PushAuthorizationTests {
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
|
||||
return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
|
||||
.fail(format!("{}", e));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+15
-3
@@ -77,11 +77,19 @@ pub struct Config {
|
||||
pub metrics_enabled: bool,
|
||||
|
||||
/// Connections per IP before flagging as potential abuse in metrics (display only, no rate limiting)
|
||||
#[arg(long = "metrics-connection-per-ip-abuse-threshold", env = "NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD", default_value_t = 10)]
|
||||
#[arg(
|
||||
long = "metrics-connection-per-ip-abuse-threshold",
|
||||
env = "NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD",
|
||||
default_value_t = 10
|
||||
)]
|
||||
pub metrics_connection_per_ip_abuse_threshold: u32,
|
||||
|
||||
/// Number of top bandwidth repos to track in metrics
|
||||
#[arg(long = "metrics-top-n-repos", env = "NGIT_METRICS_TOP_N_REPOS", default_value_t = 10)]
|
||||
#[arg(
|
||||
long = "metrics-top-n-repos",
|
||||
env = "NGIT_METRICS_TOP_N_REPOS",
|
||||
default_value_t = 10
|
||||
)]
|
||||
pub metrics_top_n_repos: usize,
|
||||
|
||||
/// URL of bootstrap relay to sync from on startup (optional)
|
||||
@@ -95,7 +103,11 @@ pub struct Config {
|
||||
|
||||
/// Interval in seconds for checking disconnected relays and attempting reconnection (default: 60)
|
||||
/// Set to lower value for faster reconnection testing
|
||||
#[arg(long, env = "NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", default_value_t = 60)]
|
||||
#[arg(
|
||||
long,
|
||||
env = "NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS",
|
||||
default_value_t = 60
|
||||
)]
|
||||
pub sync_disconnect_check_interval_secs: u64,
|
||||
|
||||
/// Base backoff time in seconds for relay reconnection (default: 5)
|
||||
|
||||
+1
-4
@@ -341,10 +341,7 @@ fn generate_hero_tags(nip11: &RelayInformationDocument) -> String {
|
||||
|
||||
// Add GRASP tags
|
||||
for grasp in &nip11.supported_grasps {
|
||||
html.push_str(&format!(
|
||||
r#"<span class="tag tag-grasp">{}</span>"#,
|
||||
grasp
|
||||
));
|
||||
html.push_str(&format!(r#"<span class="tag tag-grasp">{}</span>"#, grasp));
|
||||
html.push('\n');
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -509,7 +509,13 @@ pub async fn run_server(
|
||||
loop {
|
||||
let (socket, addr) = listener.accept().await?;
|
||||
let io = TokioIo::new(socket);
|
||||
let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone(), metrics.clone());
|
||||
let service = HttpService::new(
|
||||
relay.clone(),
|
||||
config.clone(),
|
||||
addr,
|
||||
database.clone(),
|
||||
metrics.clone(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = http1::Builder::new()
|
||||
|
||||
+7
-2
@@ -37,7 +37,9 @@ async fn main() -> Result<()> {
|
||||
// Initialize metrics if enabled
|
||||
let metrics = if config.metrics_enabled {
|
||||
info!("Metrics enabled on /metrics endpoint");
|
||||
Some(Arc::new(Metrics::new(config.metrics_connection_per_ip_abuse_threshold)))
|
||||
Some(Arc::new(Metrics::new(
|
||||
config.metrics_connection_per_ip_abuse_threshold,
|
||||
)))
|
||||
} else {
|
||||
info!("Metrics disabled");
|
||||
None
|
||||
@@ -65,7 +67,10 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
if config.sync_bootstrap_relay_url.is_some() {
|
||||
info!("Starting proactive sync with bootstrap relay: {:?}", config.sync_bootstrap_relay_url);
|
||||
info!(
|
||||
"Starting proactive sync with bootstrap relay: {:?}",
|
||||
config.sync_bootstrap_relay_url
|
||||
);
|
||||
} else {
|
||||
info!("Proactive sync enabled (will discover relays from stored announcements)");
|
||||
}
|
||||
|
||||
@@ -80,7 +80,9 @@ impl BandwidthTracker {
|
||||
&["repo"],
|
||||
)
|
||||
.unwrap();
|
||||
registry.register(Box::new(top_repos_gauge.clone())).unwrap();
|
||||
registry
|
||||
.register(Box::new(top_repos_gauge.clone()))
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
all_repos: DashMap::new(),
|
||||
@@ -120,7 +122,12 @@ impl BandwidthTracker {
|
||||
// Try to update the timestamp atomically to prevent concurrent refreshes
|
||||
if self
|
||||
.last_refresh_nanos
|
||||
.compare_exchange(last_refresh, elapsed_nanos, Ordering::SeqCst, Ordering::Relaxed)
|
||||
.compare_exchange(
|
||||
last_refresh,
|
||||
elapsed_nanos,
|
||||
Ordering::SeqCst,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
self.refresh_top_n();
|
||||
@@ -298,4 +305,4 @@ mod tests {
|
||||
// Refresh should not panic on empty data
|
||||
tracker.refresh_top_n();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-27
@@ -25,7 +25,8 @@ use tracing::warn;
|
||||
struct ConnectionInfo {
|
||||
/// Number of active connections from this IP
|
||||
count: u32,
|
||||
/// When the first connection from this IP was established
|
||||
/// When the first connection from this IP was established (for future rate limiting)
|
||||
#[allow(dead_code)]
|
||||
first_seen: Instant,
|
||||
/// Whether this IP has been flagged as potentially abusive
|
||||
flagged_as_abuse: bool,
|
||||
@@ -48,16 +49,16 @@ struct ConnectionInfo {
|
||||
pub struct ConnectionTracker {
|
||||
/// Active connections per IP (INTERNAL ONLY - never exposed to metrics)
|
||||
connections: DashMap<IpAddr, ConnectionInfo>,
|
||||
|
||||
|
||||
/// Threshold for abuse flagging (connections per IP)
|
||||
abuse_threshold: u32,
|
||||
|
||||
|
||||
/// Prometheus gauge: total active connections
|
||||
active_connections: IntGauge,
|
||||
|
||||
|
||||
/// Prometheus gauge: number of unique IPs connected
|
||||
unique_ips: IntGauge,
|
||||
|
||||
|
||||
/// Prometheus gauge: number of IPs flagged as potential abusers
|
||||
flagged_abusers: IntGauge,
|
||||
}
|
||||
@@ -70,29 +71,30 @@ impl ConnectionTracker {
|
||||
/// * `abuse_threshold` - Number of connections from a single IP before flagging
|
||||
/// * `registry` - Prometheus registry to register metrics with
|
||||
pub fn new(abuse_threshold: u32, registry: &Registry) -> Self {
|
||||
let active_connections = IntGauge::with_opts(
|
||||
Opts::new(
|
||||
"ngit_websocket_connections_active",
|
||||
"Current active WebSocket connections",
|
||||
)
|
||||
).unwrap();
|
||||
registry.register(Box::new(active_connections.clone())).unwrap();
|
||||
let active_connections = IntGauge::with_opts(Opts::new(
|
||||
"ngit_websocket_connections_active",
|
||||
"Current active WebSocket connections",
|
||||
))
|
||||
.unwrap();
|
||||
registry
|
||||
.register(Box::new(active_connections.clone()))
|
||||
.unwrap();
|
||||
|
||||
let unique_ips = IntGauge::with_opts(
|
||||
Opts::new(
|
||||
"ngit_websocket_unique_ips",
|
||||
"Number of unique IP addresses connected (NOT the IPs themselves)",
|
||||
)
|
||||
).unwrap();
|
||||
let unique_ips = IntGauge::with_opts(Opts::new(
|
||||
"ngit_websocket_unique_ips",
|
||||
"Number of unique IP addresses connected (NOT the IPs themselves)",
|
||||
))
|
||||
.unwrap();
|
||||
registry.register(Box::new(unique_ips.clone())).unwrap();
|
||||
|
||||
let flagged_abusers = IntGauge::with_opts(
|
||||
Opts::new(
|
||||
"ngit_websocket_flagged_abusers",
|
||||
"Number of IPs exceeding connection threshold",
|
||||
)
|
||||
).unwrap();
|
||||
registry.register(Box::new(flagged_abusers.clone())).unwrap();
|
||||
let flagged_abusers = IntGauge::with_opts(Opts::new(
|
||||
"ngit_websocket_flagged_abusers",
|
||||
"Number of IPs exceeding connection threshold",
|
||||
))
|
||||
.unwrap();
|
||||
registry
|
||||
.register(Box::new(flagged_abusers.clone()))
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
connections: DashMap::new(),
|
||||
@@ -140,7 +142,7 @@ impl ConnectionTracker {
|
||||
|
||||
// Update Prometheus metrics (aggregate counts only)
|
||||
self.active_connections.inc();
|
||||
|
||||
|
||||
if is_new_ip {
|
||||
self.unique_ips.inc();
|
||||
}
|
||||
@@ -334,4 +336,4 @@ mod tests {
|
||||
assert_eq!(tracker.active_connections(), 0);
|
||||
assert_eq!(tracker.unique_ip_count(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+82
-45
@@ -87,7 +87,8 @@ struct MetricsInner {
|
||||
// === System Health Metrics ===
|
||||
/// Server start time for uptime calculation
|
||||
pub start_time: Instant,
|
||||
/// Build information gauge
|
||||
/// Build information gauge (stored to prevent unregistration from Prometheus)
|
||||
#[allow(dead_code)]
|
||||
pub build_info: GaugeVec,
|
||||
}
|
||||
|
||||
@@ -158,7 +159,10 @@ impl Metrics {
|
||||
|
||||
/// Start timing a git operation, returns a timer
|
||||
pub fn start_git_operation_timer(&self, operation: &str) -> GitOperationTimer {
|
||||
GitOperationTimer::new(self.inner.git_operation_duration.clone(), operation.to_string())
|
||||
GitOperationTimer::new(
|
||||
self.inner.git_operation_duration.clone(),
|
||||
operation.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Record bytes transferred for a git operation
|
||||
@@ -266,13 +270,14 @@ impl MetricsInner {
|
||||
}
|
||||
|
||||
// WebSocket metrics
|
||||
let websocket_connections_total = Counter::with_opts(
|
||||
Opts::new(
|
||||
"ngit_websocket_connections_total",
|
||||
"Total WebSocket connections since startup",
|
||||
)
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(websocket_connections_total.clone())).unwrap();
|
||||
let websocket_connections_total = Counter::with_opts(Opts::new(
|
||||
"ngit_websocket_connections_total",
|
||||
"Total WebSocket connections since startup",
|
||||
))
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(websocket_connections_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
let websocket_connection_duration = Histogram::with_opts(
|
||||
HistogramOpts::new(
|
||||
@@ -280,8 +285,11 @@ impl MetricsInner {
|
||||
"Duration of WebSocket connections",
|
||||
)
|
||||
.buckets(vec![1.0, 5.0, 15.0, 30.0, 60.0, 300.0, 900.0, 3600.0]),
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(websocket_connection_duration.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(websocket_connection_duration.clone()))
|
||||
.unwrap();
|
||||
|
||||
let websocket_messages_received = CounterVec::new(
|
||||
Opts::new(
|
||||
@@ -289,8 +297,11 @@ impl MetricsInner {
|
||||
"WebSocket messages received by type",
|
||||
),
|
||||
&["type"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(websocket_messages_received.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(websocket_messages_received.clone()))
|
||||
.unwrap();
|
||||
|
||||
let websocket_messages_sent = CounterVec::new(
|
||||
Opts::new(
|
||||
@@ -298,8 +309,11 @@ impl MetricsInner {
|
||||
"WebSocket messages sent by type",
|
||||
),
|
||||
&["type"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(websocket_messages_sent.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(websocket_messages_sent.clone()))
|
||||
.unwrap();
|
||||
|
||||
// Git operation metrics
|
||||
let git_operations_total = CounterVec::new(
|
||||
@@ -308,8 +322,11 @@ impl MetricsInner {
|
||||
"Git operations by type and status",
|
||||
),
|
||||
&["operation", "status"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(git_operations_total.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(git_operations_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
let git_operation_duration = HistogramVec::new(
|
||||
HistogramOpts::new(
|
||||
@@ -318,8 +335,11 @@ impl MetricsInner {
|
||||
)
|
||||
.buckets(vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]),
|
||||
&["operation"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(git_operation_duration.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(git_operation_duration.clone()))
|
||||
.unwrap();
|
||||
|
||||
let git_bytes_total = CounterVec::new(
|
||||
Opts::new(
|
||||
@@ -327,8 +347,11 @@ impl MetricsInner {
|
||||
"Total bytes transferred for git operations",
|
||||
),
|
||||
&["direction"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(git_bytes_total.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(git_bytes_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
let git_push_authorization = CounterVec::new(
|
||||
Opts::new(
|
||||
@@ -336,8 +359,11 @@ impl MetricsInner {
|
||||
"Push authorization results",
|
||||
),
|
||||
&["result"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(git_push_authorization.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(git_push_authorization.clone()))
|
||||
.unwrap();
|
||||
|
||||
// Nostr event metrics
|
||||
let events_received_total = CounterVec::new(
|
||||
@@ -346,8 +372,11 @@ impl MetricsInner {
|
||||
"Nostr events received by kind",
|
||||
),
|
||||
&["kind"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(events_received_total.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(events_received_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
let events_stored_total = CounterVec::new(
|
||||
Opts::new(
|
||||
@@ -355,8 +384,11 @@ impl MetricsInner {
|
||||
"Nostr events successfully stored by kind",
|
||||
),
|
||||
&["kind"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(events_stored_total.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(events_stored_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
let events_rejected_total = CounterVec::new(
|
||||
Opts::new(
|
||||
@@ -364,31 +396,36 @@ impl MetricsInner {
|
||||
"Nostr events rejected by kind and reason",
|
||||
),
|
||||
&["kind", "reason"],
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(events_rejected_total.clone())).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(events_rejected_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
// Repository metrics
|
||||
let repositories_total = Gauge::with_opts(
|
||||
Opts::new(
|
||||
"ngit_repositories_total",
|
||||
"Total repositories hosted",
|
||||
)
|
||||
).unwrap();
|
||||
REGISTRY.register(Box::new(repositories_total.clone())).unwrap();
|
||||
let repositories_total = Gauge::with_opts(Opts::new(
|
||||
"ngit_repositories_total",
|
||||
"Total repositories hosted",
|
||||
))
|
||||
.unwrap();
|
||||
REGISTRY
|
||||
.register(Box::new(repositories_total.clone()))
|
||||
.unwrap();
|
||||
|
||||
// Build info
|
||||
let build_info = GaugeVec::new(
|
||||
Opts::new(
|
||||
"ngit_build_info",
|
||||
"Build information",
|
||||
),
|
||||
Opts::new("ngit_build_info", "Build information"),
|
||||
&["version", "commit"],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
REGISTRY.register(Box::new(build_info.clone())).unwrap();
|
||||
|
||||
|
||||
// Set build info gauge to 1 (it's just for labels)
|
||||
build_info
|
||||
.with_label_values(&[env!("CARGO_PKG_VERSION"), option_env!("GIT_HASH").unwrap_or("unknown")])
|
||||
.with_label_values(&[
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
option_env!("GIT_HASH").unwrap_or("unknown"),
|
||||
])
|
||||
.set(1.0);
|
||||
|
||||
Self {
|
||||
@@ -472,7 +509,7 @@ mod tests {
|
||||
// Note: This test may fail if run with other tests due to global registry
|
||||
// In production, consider using a test-specific registry
|
||||
let metrics = Metrics::new(10);
|
||||
|
||||
|
||||
// Test that we can record metrics without panicking
|
||||
metrics.record_websocket_connection();
|
||||
metrics.record_message_received("REQ");
|
||||
@@ -484,4 +521,4 @@ mod tests {
|
||||
metrics.record_event_rejected(1, "invalid_signature");
|
||||
metrics.set_repositories_total(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-14
@@ -16,8 +16,8 @@ use crate::nostr::events::{
|
||||
KIND_REPOSITORY_STATE,
|
||||
};
|
||||
use crate::nostr::policy::{
|
||||
AnnouncementPolicy, AnnouncementResult, PolicyContext, PrEventPolicy, RelatedEventPolicy,
|
||||
ReferenceResult, StatePolicy, StateResult,
|
||||
AnnouncementPolicy, AnnouncementResult, PolicyContext, PrEventPolicy, ReferenceResult,
|
||||
RelatedEventPolicy, StatePolicy, StateResult,
|
||||
};
|
||||
|
||||
/// Type alias for the shared database used by the relay
|
||||
@@ -77,7 +77,9 @@ impl Nip34WritePolicy {
|
||||
match RepositoryAnnouncement::from_event(event.clone()) {
|
||||
Ok(announcement) => {
|
||||
// Try to create bare repository if it doesn't exist
|
||||
if let Err(e) = self.announcement_policy.ensure_bare_repository(&announcement)
|
||||
if let Err(e) = self
|
||||
.announcement_policy
|
||||
.ensure_bare_repository(&announcement)
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to create bare repository for {}: {}",
|
||||
@@ -145,22 +147,14 @@ impl Nip34WritePolicy {
|
||||
Ok(_state) => {
|
||||
// Process state alignment asynchronously
|
||||
if let Err(e) = self.state_policy.process_state_event(event).await {
|
||||
tracing::warn!(
|
||||
"Failed to process state event {}: {}",
|
||||
event_id_str,
|
||||
e
|
||||
);
|
||||
tracing::warn!("Failed to process state event {}: {}", event_id_str, e);
|
||||
}
|
||||
|
||||
tracing::debug!("Accepted repository state: {}", event_id_str);
|
||||
PolicyResult::Accept
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to parse repository state {}: {}",
|
||||
event_id_str,
|
||||
e
|
||||
);
|
||||
tracing::warn!("Failed to parse repository state {}: {}", event_id_str, e);
|
||||
// Still accept the event even if we can't parse it
|
||||
// The validation passed, so it's structurally valid
|
||||
PolicyResult::Accept
|
||||
@@ -348,4 +342,4 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
|
||||
database,
|
||||
write_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,10 @@ impl AnnouncementPolicy {
|
||||
|
||||
/// Create a bare git repository if it doesn't exist
|
||||
/// Path format: <git_data_path>/<npub>/<identifier>.git
|
||||
pub fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> {
|
||||
pub fn ensure_bare_repository(
|
||||
&self,
|
||||
announcement: &RepositoryAnnouncement,
|
||||
) -> Result<(), String> {
|
||||
let repo_path = self.ctx.git_data_path.join(announcement.repo_path());
|
||||
|
||||
// Check if repository already exists
|
||||
@@ -154,4 +157,4 @@ impl AnnouncementPolicy {
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
/// - `StatePolicy` - State event validation + ref alignment
|
||||
/// - `PrEventPolicy` - PR/PR Update validation
|
||||
/// - `RelatedEventPolicy` - Forward/backward reference checking
|
||||
|
||||
mod announcement;
|
||||
mod pr_event;
|
||||
mod related;
|
||||
@@ -38,4 +37,4 @@ impl PolicyContext {
|
||||
git_data_path: git_data_path.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,4 +195,4 @@ impl PrEventPolicy {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,10 +169,7 @@ impl RelatedEventPolicy {
|
||||
|
||||
/// Check if any events exist in database
|
||||
/// Returns the first matching event ID found, or None if none match
|
||||
async fn find_accepted_event(
|
||||
&self,
|
||||
event_ids: &[EventId],
|
||||
) -> Result<Option<EventId>, String> {
|
||||
async fn find_accepted_event(&self, event_ids: &[EventId]) -> Result<Option<EventId>, String> {
|
||||
if event_ids.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -273,4 +270,4 @@ impl RelatedEventPolicy {
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,10 @@ impl StatePolicy {
|
||||
}
|
||||
|
||||
// Build repository path: <git_data_path>/<owner_npub>/<identifier>.git
|
||||
let repo_path = self.ctx.git_data_path.join(announcement.repo_path().clone());
|
||||
let repo_path = self
|
||||
.ctx
|
||||
.git_data_path
|
||||
.join(announcement.repo_path().clone());
|
||||
owner_repos.push((announcement, repo_path));
|
||||
}
|
||||
|
||||
@@ -416,4 +419,4 @@ impl StatePolicy {
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,7 @@ pub fn derive_relay_targets(
|
||||
|
||||
for (repo_id, needs) in repo_index {
|
||||
for relay_url in &needs.relays {
|
||||
let entry = relay_targets
|
||||
.entry(relay_url.clone())
|
||||
.or_insert_with(RelaySyncNeeds::default);
|
||||
let entry = relay_targets.entry(relay_url.clone()).or_default();
|
||||
|
||||
entry.repos.insert(repo_id.clone());
|
||||
entry.root_events.extend(needs.root_events.iter().cloned());
|
||||
@@ -586,4 +584,4 @@ mod tests {
|
||||
);
|
||||
assert_eq!(actions[0].relay_url, "wss://new-relay.com");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -337,4 +337,4 @@ mod tests {
|
||||
|
||||
assert_eq!(filters.len(), 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -206,11 +206,7 @@ impl RelayHealthTracker {
|
||||
health.next_retry_at = Some(now + backoff);
|
||||
|
||||
if old_state != HealthState::Degraded {
|
||||
tracing::warn!(
|
||||
"Relay {} degraded, backoff {:?}",
|
||||
relay_url,
|
||||
backoff
|
||||
);
|
||||
tracing::warn!("Relay {} degraded, backoff {:?}", relay_url, backoff);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Relay {} failure #{}, backoff {:?}",
|
||||
@@ -308,12 +304,17 @@ impl RelayHealthTracker {
|
||||
|
||||
/// Get all tracked relay URLs
|
||||
pub fn get_tracked_relays(&self) -> Vec<String> {
|
||||
self.health.iter().map(|entry| entry.key().clone()).collect()
|
||||
self.health
|
||||
.iter()
|
||||
.map(|entry| entry.key().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a clone of the health info for a relay
|
||||
pub fn get_health(&self, relay_url: &str) -> Option<RelayHealth> {
|
||||
self.health.get(relay_url).map(|entry| entry.value().clone())
|
||||
self.health
|
||||
.get(relay_url)
|
||||
.map(|entry| entry.value().clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +370,7 @@ mod tests {
|
||||
fn test_backoff_increases_exponentially() {
|
||||
let base = DEFAULT_BASE_BACKOFF_SECS; // 5 seconds
|
||||
let max = 3600u64;
|
||||
|
||||
|
||||
// failure 1: 5s (base * 2^0 = 5)
|
||||
assert_eq!(
|
||||
RelayHealthTracker::get_backoff_duration(1, base, max),
|
||||
@@ -498,4 +499,4 @@ mod tests {
|
||||
let health = tracker.get_health("wss://nonexistent.example.com");
|
||||
assert!(health.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -207,7 +207,9 @@ impl SyncMetrics {
|
||||
HealthState::Degraded => 2,
|
||||
HealthState::Dead => 3,
|
||||
};
|
||||
self.relay_status.with_label_values(&[relay]).set(state_value);
|
||||
self.relay_status
|
||||
.with_label_values(&[relay])
|
||||
.set(state_value);
|
||||
}
|
||||
|
||||
/// Record relay failure count.
|
||||
@@ -259,9 +261,7 @@ impl SyncMetrics {
|
||||
/// * `source` - The event source type (see [`record_event`](Self::record_event))
|
||||
/// * `count` - Number of events to record
|
||||
pub fn record_events(&self, source: &str, count: u64) {
|
||||
self.events_total
|
||||
.with_label_values(&[source])
|
||||
.inc_by(count);
|
||||
self.events_total.with_label_values(&[source]).inc_by(count);
|
||||
}
|
||||
|
||||
/// Record a gap event filled during catchup.
|
||||
@@ -451,4 +451,4 @@ mod tests {
|
||||
let metrics2 = SyncMetrics::register(®istry);
|
||||
assert!(metrics2.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -512,8 +512,8 @@ impl SyncManager {
|
||||
};
|
||||
|
||||
// Check if relay supports NIP-77 negentropy AND negentropy is not disabled
|
||||
let use_negentropy = !self.config.sync_disable_negentropy
|
||||
&& connection.supports_negentropy().await;
|
||||
let use_negentropy =
|
||||
!self.config.sync_disable_negentropy && connection.supports_negentropy().await;
|
||||
|
||||
// Unsubscribe all current subscriptions
|
||||
connection.unsubscribe_all().await;
|
||||
@@ -1657,12 +1657,12 @@ impl SyncManager {
|
||||
|
||||
let layer1_filters = 1;
|
||||
let layer2_filters = if repo_count > 0 {
|
||||
((repo_count + 99) / 100) * 3
|
||||
repo_count.div_ceil(100) * 3
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let layer3_filters = if event_count > 0 {
|
||||
((event_count + 99) / 100) * 3
|
||||
event_count.div_ceil(100) * 3
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
@@ -150,17 +150,21 @@ impl RelayConnection {
|
||||
//
|
||||
// See: nostr-sdk-0.44 Client::try_connect_relay documentation
|
||||
self.client
|
||||
.try_connect_relay(&self.url, std::time::Duration::from_secs(connection_timeout_secs))
|
||||
.try_connect_relay(
|
||||
&self.url,
|
||||
std::time::Duration::from_secs(connection_timeout_secs),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to relay {}: {}", self.url, e))?;
|
||||
|
||||
// Subscribe to Layer 1 (announcements)
|
||||
let filter = build_announcement_filter(since);
|
||||
let output = self
|
||||
.client
|
||||
.subscribe(filter, None)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to subscribe to announcements on {}: {}", self.url, e))?;
|
||||
let output = self.client.subscribe(filter, None).await.map_err(|e| {
|
||||
format!(
|
||||
"Failed to subscribe to announcements on {}: {}",
|
||||
self.url, e
|
||||
)
|
||||
})?;
|
||||
|
||||
tracing::info!(url = %self.url, sub_id = %output.val, "Connected and subscribed to Layer 1 (announcements)");
|
||||
Ok(output.val)
|
||||
@@ -250,7 +254,8 @@ impl RelayConnection {
|
||||
}
|
||||
RelayMessage::Closed { message: msg, .. } => {
|
||||
tracing::info!(relay = %url, message = %msg, "Relay closed subscription");
|
||||
let _ = event_sender.send(RelayEvent::Closed(msg.to_string())).await;
|
||||
let _ =
|
||||
event_sender.send(RelayEvent::Closed(msg.to_string())).await;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
@@ -421,7 +426,10 @@ impl RelayConnection {
|
||||
/// - Relay doesn't actually support NIP-77 (despite claiming to)
|
||||
/// - Network errors during reconciliation
|
||||
/// - Timeout during sync
|
||||
pub async fn negentropy_sync_filter(&self, filter: Filter) -> Result<NegentropySyncResult, String> {
|
||||
pub async fn negentropy_sync_filter(
|
||||
&self,
|
||||
filter: Filter,
|
||||
) -> Result<NegentropySyncResult, String> {
|
||||
// Use nostr-sdk's sync method which handles the NEG-OPEN/NEG-MSG exchange
|
||||
let sync_opts = SyncOptions::default();
|
||||
|
||||
|
||||
+26
-16
@@ -49,7 +49,12 @@ impl PendingUpdates {
|
||||
}
|
||||
|
||||
/// Add or update a repo with its relays and root events
|
||||
fn add_repo(&mut self, repo_id: String, relays: HashSet<String>, root_events: HashSet<EventId>) {
|
||||
fn add_repo(
|
||||
&mut self,
|
||||
repo_id: String,
|
||||
relays: HashSet<String>,
|
||||
root_events: HashSet<EventId>,
|
||||
) {
|
||||
let entry = self.repos.entry(repo_id).or_insert_with(|| RepoSyncNeeds {
|
||||
relays: HashSet::new(),
|
||||
root_events: HashSet::new(),
|
||||
@@ -251,9 +256,9 @@ impl SelfSubscriber {
|
||||
///
|
||||
/// Returns true if any extracted relay URL contains our domain
|
||||
fn lists_our_relay(&self, event: &Event) -> bool {
|
||||
Self::extract_relay_urls(event).iter().any(|url| {
|
||||
url.contains(&self.relay_domain) || url == &self.own_relay_url
|
||||
})
|
||||
Self::extract_relay_urls(event)
|
||||
.iter()
|
||||
.any(|url| url.contains(&self.relay_domain) || url == &self.own_relay_url)
|
||||
}
|
||||
|
||||
/// Main run loop
|
||||
@@ -413,21 +418,21 @@ impl SelfSubscriber {
|
||||
if let Some(repo_sync) = index.get_mut(&repo_ref) {
|
||||
// Add event.id to root_events set in the index (immediate availability)
|
||||
repo_sync.root_events.insert(event.id);
|
||||
|
||||
|
||||
// Clone the relays before releasing the lock - Layer 3 filters need to be
|
||||
// sent to the same relays as Layer 2 filters for this repo
|
||||
let relays = repo_sync.relays.clone();
|
||||
|
||||
|
||||
// Release lock before modifying pending
|
||||
drop(index);
|
||||
|
||||
|
||||
// Also add root event to pending - this ensures batch processing runs
|
||||
// and creates Layer 3 filters for events referencing this root event.
|
||||
// CRITICAL: Include relays so derive_relay_targets knows where to send filters!
|
||||
let mut root_events = HashSet::new();
|
||||
root_events.insert(event.id);
|
||||
pending.add_repo(repo_ref.clone(), relays.clone(), root_events);
|
||||
|
||||
|
||||
tracing::debug!(
|
||||
event_id = %event.id,
|
||||
repo_ref = %repo_ref,
|
||||
@@ -475,10 +480,12 @@ impl SelfSubscriber {
|
||||
|
||||
for (repo_id, needs) in updates {
|
||||
// Merge with existing entry or insert new
|
||||
let entry = index.entry(repo_id.clone()).or_insert_with(|| RepoSyncNeeds {
|
||||
relays: HashSet::new(),
|
||||
root_events: HashSet::new(),
|
||||
});
|
||||
let entry = index
|
||||
.entry(repo_id.clone())
|
||||
.or_insert_with(|| RepoSyncNeeds {
|
||||
relays: HashSet::new(),
|
||||
root_events: HashSet::new(),
|
||||
});
|
||||
entry.relays.extend(needs.relays);
|
||||
entry.root_events.extend(needs.root_events);
|
||||
|
||||
@@ -556,7 +563,7 @@ fn clone_url_to_relay_url(clone_url: &str) -> Option<String> {
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
|
||||
// Extract just the host:port part (everything before the first /)
|
||||
let host_port = rest.split('/').next()?;
|
||||
Some(format!("{}{}", ws_scheme, host_port))
|
||||
@@ -581,7 +588,7 @@ mod tests {
|
||||
Some("ws://localhost:3000".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_clone_url_to_relay_url_with_port() {
|
||||
assert_eq!(
|
||||
@@ -593,6 +600,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_clone_url_to_relay_url_unsupported() {
|
||||
assert_eq!(clone_url_to_relay_url("git://example.com/repo.git"), None);
|
||||
assert_eq!(clone_url_to_relay_url("ssh://git@example.com/repo.git"), None);
|
||||
assert_eq!(
|
||||
clone_url_to_relay_url("ssh://git@example.com/repo.git"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Common test utilities
|
||||
#![allow(dead_code)] // Test helpers may not be used in all test configurations
|
||||
#![allow(unused_imports)] // Re-exports may not be used in all test configurations
|
||||
|
||||
pub mod relay;
|
||||
pub mod sync_helpers;
|
||||
|
||||
@@ -104,7 +104,11 @@ impl TestRelay {
|
||||
}
|
||||
|
||||
/// Start relay with full options
|
||||
async fn start_with_full_options(port: u16, bootstrap_relay_url: Option<String>, disable_negentropy: bool) -> Self {
|
||||
async fn start_with_full_options(
|
||||
port: u16,
|
||||
bootstrap_relay_url: Option<String>,
|
||||
disable_negentropy: bool,
|
||||
) -> Self {
|
||||
let bind_address = format!("127.0.0.1:{}", port);
|
||||
let url = format!("ws://127.0.0.1:{}", port);
|
||||
|
||||
|
||||
@@ -173,7 +173,11 @@ impl TestClient {
|
||||
/// # Returns
|
||||
/// * `Ok(Event)` - Signed event ready to send
|
||||
/// * `Err(String)` - If signing fails
|
||||
pub fn build_layer2_issue_event(keys: &Keys, repo_coord: &str, title: &str) -> Result<Event, String> {
|
||||
pub fn build_layer2_issue_event(
|
||||
keys: &Keys,
|
||||
repo_coord: &str,
|
||||
title: &str,
|
||||
) -> Result<Event, String> {
|
||||
build_layer2_issue_with_tag(keys, repo_coord, title, TagVariant::LowercaseA)
|
||||
}
|
||||
|
||||
@@ -256,10 +260,7 @@ pub fn build_layer3_comment_event(
|
||||
// Choose tag based on kind (NIP-22 uses E, NIP-10 style uses e)
|
||||
let tag = if kind_num == KIND_COMMENT {
|
||||
// NIP-22 comment: uppercase 'E' tag
|
||||
Tag::custom(
|
||||
TagKind::custom("E"),
|
||||
vec![parent_event_id.to_hex()],
|
||||
)
|
||||
Tag::custom(TagKind::custom("E"), vec![parent_event_id.to_hex()])
|
||||
} else {
|
||||
// Kind 1 reply: lowercase 'e' tag with root marker (NIP-10)
|
||||
Tag::custom(
|
||||
@@ -299,10 +300,7 @@ pub fn build_layer3_comment_with_uppercase_e_tag(
|
||||
parent_event_id: &EventId,
|
||||
content: &str,
|
||||
) -> Result<Event, String> {
|
||||
let tag = Tag::custom(
|
||||
TagKind::custom("E"),
|
||||
vec![parent_event_id.to_hex()],
|
||||
);
|
||||
let tag = Tag::custom(TagKind::custom("E"), vec![parent_event_id.to_hex()]);
|
||||
|
||||
EventBuilder::new(Kind::Custom(KIND_COMMENT), content)
|
||||
.tags(vec![tag])
|
||||
@@ -316,10 +314,7 @@ pub fn build_layer3_quote_with_q_tag(
|
||||
parent_event_id: &EventId,
|
||||
content: &str,
|
||||
) -> Result<Event, String> {
|
||||
let tag = Tag::custom(
|
||||
TagKind::custom("q"),
|
||||
vec![parent_event_id.to_hex()],
|
||||
);
|
||||
let tag = Tag::custom(TagKind::custom("q"), vec![parent_event_id.to_hex()]);
|
||||
|
||||
EventBuilder::new(Kind::Custom(1), content)
|
||||
.tags(vec![tag])
|
||||
@@ -587,10 +582,7 @@ pub fn repo_coord(keys: &Keys, identifier: &str) -> String {
|
||||
/// ```
|
||||
pub async fn fetch_metrics(relay_url: &str) -> Result<String, reqwest::Error> {
|
||||
// Convert ws:// URL to http:// for metrics endpoint
|
||||
let http_url = relay_url
|
||||
.replace("ws://", "http://")
|
||||
.replace("/", "")
|
||||
+ "/metrics";
|
||||
let http_url = relay_url.replace("ws://", "http://").replace("/", "") + "/metrics";
|
||||
|
||||
reqwest::get(&http_url).await?.text().await
|
||||
}
|
||||
@@ -888,8 +880,8 @@ mod tests {
|
||||
let keys = Keys::generate();
|
||||
let coord = repo_coord(&keys, "my-repo");
|
||||
|
||||
let event = build_layer2_issue_event(&keys, &coord, "Test Issue")
|
||||
.expect("Should create event");
|
||||
let event =
|
||||
build_layer2_issue_event(&keys, &coord, "Test Issue").expect("Should create event");
|
||||
|
||||
// nostr-sdk 0.43: use field access
|
||||
assert_eq!(event.kind.as_u16(), KIND_ISSUE);
|
||||
@@ -937,8 +929,13 @@ mod tests {
|
||||
let keys = Keys::generate();
|
||||
let parent_id = EventId::all_zeros();
|
||||
|
||||
let event = build_layer3_comment_event(&keys, &parent_id, "Test comment", Kind::Custom(KIND_COMMENT))
|
||||
.expect("Should create event");
|
||||
let event = build_layer3_comment_event(
|
||||
&keys,
|
||||
&parent_id,
|
||||
"Test comment",
|
||||
Kind::Custom(KIND_COMMENT),
|
||||
)
|
||||
.expect("Should create event");
|
||||
|
||||
assert_eq!(event.kind.as_u16(), KIND_COMMENT);
|
||||
|
||||
@@ -980,8 +977,7 @@ mod tests {
|
||||
|
||||
let has_e_tag = event.tags.iter().any(|tag| {
|
||||
let slice = tag.as_slice();
|
||||
slice.first().is_some_and(|t| t == "e") &&
|
||||
slice.get(3).is_some_and(|m| m == "root")
|
||||
slice.first().is_some_and(|t| t == "e") && slice.get(3).is_some_and(|m| m == "root")
|
||||
});
|
||||
assert!(has_e_tag, "Should have 'e' tag with root marker");
|
||||
}
|
||||
@@ -1038,7 +1034,10 @@ mod tests {
|
||||
fn test_parse_gauge_without_labels() {
|
||||
let text = r#"ngit_sync_relays_tracked_total 3"#;
|
||||
let metrics = ParsedMetrics::parse(text);
|
||||
assert_eq!(metrics.gauge("ngit_sync_relays_tracked_total", &[]), Some(3));
|
||||
assert_eq!(
|
||||
metrics.gauge("ngit_sync_relays_tracked_total", &[]),
|
||||
Some(3)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1051,9 +1050,6 @@ mod tests {
|
||||
fn test_parse_metric_with_relay_url_label() {
|
||||
let text = r#"ngit_sync_relay_connected{relay="ws://127.0.0.1:12345"} 1"#;
|
||||
let metrics = ParsedMetrics::parse(text);
|
||||
assert_eq!(
|
||||
metrics.relay_connected("ws://127.0.0.1:12345"),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(metrics.relay_connected("ws://127.0.0.1:12345"), Some(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-11
@@ -45,13 +45,13 @@ async fn test_nip77_negentropy_sync_finds_events() {
|
||||
let keys = Keys::generate();
|
||||
|
||||
// Create a repository announcement that will be accepted by the relay
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay.domain()],
|
||||
"test-repo-nip77",
|
||||
);
|
||||
let announcement = create_repo_announcement(&keys, &[&relay.domain()], "test-repo-nip77");
|
||||
let event1_id = announcement.id;
|
||||
println!("Created event 1: {} (kind {})", event1_id, announcement.kind.as_u16());
|
||||
println!(
|
||||
"Created event 1: {} (kind {})",
|
||||
event1_id,
|
||||
announcement.kind.as_u16()
|
||||
);
|
||||
|
||||
// Create a second event (issue referencing the repo)
|
||||
let repo_coord = format!(
|
||||
@@ -63,7 +63,11 @@ async fn test_nip77_negentropy_sync_finds_events() {
|
||||
let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for NIP-77")
|
||||
.expect("Failed to build issue event");
|
||||
let event2_id = issue.id;
|
||||
println!("Created event 2: {} (kind {})", event2_id, issue.kind.as_u16());
|
||||
println!(
|
||||
"Created event 2: {} (kind {})",
|
||||
event2_id,
|
||||
issue.kind.as_u16()
|
||||
);
|
||||
|
||||
// 3. Send events to relay using TestClient
|
||||
let publish_client = TestClient::new(relay.url(), keys.clone())
|
||||
@@ -99,9 +103,10 @@ async fn test_nip77_negentropy_sync_finds_events() {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// 6. Perform negentropy sync with filter matching our events
|
||||
let filter = Filter::new()
|
||||
.author(keys.public_key())
|
||||
.kinds(vec![Kind::Custom(KIND_REPOSITORY_STATE), Kind::Custom(KIND_ISSUE)]);
|
||||
let filter = Filter::new().author(keys.public_key()).kinds(vec![
|
||||
Kind::Custom(KIND_REPOSITORY_STATE),
|
||||
Kind::Custom(KIND_ISSUE),
|
||||
]);
|
||||
|
||||
println!("Starting negentropy sync with filter: {:?}", filter);
|
||||
|
||||
@@ -177,7 +182,7 @@ async fn test_nip77_negentropy_sync_empty_result() {
|
||||
|
||||
// 3. Sync with filter that won't match anything
|
||||
let filter = Filter::new()
|
||||
.author(keys.public_key()) // Random new key, no events exist
|
||||
.author(keys.public_key()) // Random new key, no events exist
|
||||
.kind(Kind::Custom(KIND_REPOSITORY_STATE));
|
||||
|
||||
println!("Starting negentropy sync with empty filter");
|
||||
|
||||
+1
-1
@@ -37,4 +37,4 @@ mod sync {
|
||||
pub mod live_sync;
|
||||
pub mod metrics;
|
||||
pub mod tag_variations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,8 @@ async fn test_relay_replays_events_after_restart() {
|
||||
.kind(Kind::Custom(KIND_REPOSITORY_STATE))
|
||||
.author(keys.public_key());
|
||||
|
||||
let synced_first = wait_for_event_on_relay(relay_b.url(), filter.clone(), Duration::from_secs(5)).await;
|
||||
let synced_first =
|
||||
wait_for_event_on_relay(relay_b.url(), filter.clone(), Duration::from_secs(5)).await;
|
||||
println!("First sync check: {}", synced_first);
|
||||
|
||||
// 8. Stop relay_b
|
||||
@@ -193,7 +194,8 @@ async fn test_relay_replays_events_after_restart() {
|
||||
// 12. Verify announcement is available on new relay_b
|
||||
// The announcement listed the OLD relay_b domain, but since relay_a still
|
||||
// has the event, new relay_b should be able to sync it via bootstrap
|
||||
let synced_after_restart = wait_for_event_on_relay(relay_b_new.url(), filter, Duration::from_secs(5)).await;
|
||||
let synced_after_restart =
|
||||
wait_for_event_on_relay(relay_b_new.url(), filter, Duration::from_secs(5)).await;
|
||||
|
||||
// 13. Cleanup
|
||||
relay_b_new.stop().await;
|
||||
@@ -384,7 +386,8 @@ async fn test_history_sync_without_negentropy() {
|
||||
relay_b_port,
|
||||
Some(relay_a.url().into()),
|
||||
true, // disable_negentropy = true
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
println!(
|
||||
"relay_b started at {} (domain: {}) - negentropy DISABLED, will do HISTORY sync",
|
||||
relay_b.url(),
|
||||
|
||||
+10
-5
@@ -88,7 +88,8 @@ async fn test_discovers_layer3_via_layer2() {
|
||||
);
|
||||
|
||||
// 6. Create a patch event (Layer 2) that references the announcement
|
||||
let patch = create_event_referencing_repo(&keys, &repo_coord, KIND_PATCH, "Test patch proposal");
|
||||
let patch =
|
||||
create_event_referencing_repo(&keys, &repo_coord, KIND_PATCH, "Test patch proposal");
|
||||
let patch_id = patch.id;
|
||||
|
||||
println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16());
|
||||
@@ -252,7 +253,8 @@ async fn test_layer2_discovery_with_chain() {
|
||||
let issue_filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_ISSUE))
|
||||
.author(keys.public_key());
|
||||
let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
let issue_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
|
||||
println!("Sync result:");
|
||||
println!(" Issue {} synced: {}", issue_id, issue_synced);
|
||||
@@ -296,7 +298,7 @@ async fn test_layer2_discovery_with_chain() {
|
||||
#[tokio::test]
|
||||
async fn test_recursive_relay_discovery_syncs_announcement() {
|
||||
// 1. Start all three relays
|
||||
|
||||
|
||||
// relay_b - will be the bootstrap relay, has announcement_x
|
||||
let relay_b = TestRelay::start().await;
|
||||
println!(
|
||||
@@ -344,7 +346,10 @@ async fn test_recursive_relay_discovery_syncs_announcement() {
|
||||
"repo-y-ac-only",
|
||||
);
|
||||
let announcement_y_id = announcement_y.id;
|
||||
println!("Created announcement_y {} listing A+C only", announcement_y_id);
|
||||
println!(
|
||||
"Created announcement_y {} listing A+C only",
|
||||
announcement_y_id
|
||||
);
|
||||
for tag in announcement_y.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -425,4 +430,4 @@ async fn test_recursive_relay_discovery_syncs_announcement() {
|
||||
"announcement_y {} should have synced from discovered relay_c to relay_a (recursive discovery)",
|
||||
announcement_y_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,10 @@ async fn test_live_sync_layer3_events() {
|
||||
.send_event(&comment)
|
||||
.await
|
||||
.expect("Failed to send comment");
|
||||
println!("Layer 3 comment {} sent to relay_a BEFORE Layer 3 subscription established", comment_id);
|
||||
println!(
|
||||
"Layer 3 comment {} sent to relay_a BEFORE Layer 3 subscription established",
|
||||
comment_id
|
||||
);
|
||||
|
||||
// 6. Now wait for issue to sync to relay_b (this triggers Layer 3 filter creation)
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
@@ -394,7 +397,7 @@ async fn test_live_sync_event_ordering() {
|
||||
client_a
|
||||
.send_event(&issue)
|
||||
.await
|
||||
.expect(&format!("Failed to send issue {}", i));
|
||||
.unwrap_or_else(|_| panic!("Failed to send issue {}", i));
|
||||
|
||||
// Delay between events to ensure different timestamps
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
|
||||
@@ -32,7 +32,7 @@ async fn test_prometheus_format_valid() {
|
||||
let relay = TestRelay::start().await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
let metrics = fetch_metrics(&relay.url())
|
||||
let metrics = fetch_metrics(relay.url())
|
||||
.await
|
||||
.expect("Failed to fetch metrics");
|
||||
|
||||
@@ -67,7 +67,7 @@ async fn test_metrics_availability_during_sync() {
|
||||
|
||||
// Make multiple metrics requests while sync is active
|
||||
for i in 0..3 {
|
||||
let metrics = fetch_metrics(&sync_relay.url()).await;
|
||||
let metrics = fetch_metrics(sync_relay.url()).await;
|
||||
assert!(
|
||||
metrics.is_ok(),
|
||||
"Metrics request {} should succeed during sync",
|
||||
@@ -135,7 +135,7 @@ async fn test_metric_values_are_numeric() {
|
||||
let relay = TestRelay::start().await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
let metrics = fetch_metrics(&relay.url())
|
||||
let metrics = fetch_metrics(relay.url())
|
||||
.await
|
||||
.expect("Should fetch metrics");
|
||||
|
||||
|
||||
@@ -57,11 +57,8 @@ async fn test_layer2_sync_with_lowercase_a_tag() {
|
||||
|
||||
// 2. Create and send repository announcement to both relays
|
||||
let repo_id = "test-repo-tag-8a";
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay_a.domain(), &relay_b.domain()],
|
||||
repo_id,
|
||||
);
|
||||
let announcement =
|
||||
create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
|
||||
|
||||
let client_a = TestClient::new(relay_a.url(), keys.clone())
|
||||
.await
|
||||
@@ -88,11 +85,16 @@ async fn test_layer2_sync_with_lowercase_a_tag() {
|
||||
|
||||
// 4. Create and send Layer 2 issue with lowercase 'a' tag
|
||||
let repo_coordinate = repo_coord(&keys, repo_id);
|
||||
let issue = build_layer2_issue_event(&keys, &repo_coordinate, "Test Issue with lowercase a tag")
|
||||
.expect("Failed to create issue event");
|
||||
let issue =
|
||||
build_layer2_issue_event(&keys, &repo_coordinate, "Test Issue with lowercase a tag")
|
||||
.expect("Failed to create issue event");
|
||||
let issue_id = issue.id;
|
||||
|
||||
println!("Created issue {} (kind {}) with lowercase 'a' tag", issue_id, issue.kind.as_u16());
|
||||
println!(
|
||||
"Created issue {} (kind {}) with lowercase 'a' tag",
|
||||
issue_id,
|
||||
issue.kind.as_u16()
|
||||
);
|
||||
for tag in issue.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -154,11 +156,8 @@ async fn test_layer2_sync_with_uppercase_a_tag() {
|
||||
|
||||
// 2. Create and send repository announcement to both relays
|
||||
let repo_id = "test-repo-tag-8b";
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay_a.domain(), &relay_b.domain()],
|
||||
repo_id,
|
||||
);
|
||||
let announcement =
|
||||
create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
|
||||
|
||||
let client_a = TestClient::new(relay_a.url(), keys.clone())
|
||||
.await
|
||||
@@ -185,11 +184,19 @@ async fn test_layer2_sync_with_uppercase_a_tag() {
|
||||
|
||||
// 4. Create and send Layer 2 issue with uppercase 'A' tag
|
||||
let repo_coordinate = repo_coord(&keys, repo_id);
|
||||
let issue = build_layer2_issue_with_uppercase_a_tag(&keys, &repo_coordinate, "Test Issue with uppercase A tag")
|
||||
.expect("Failed to create issue event");
|
||||
let issue = build_layer2_issue_with_uppercase_a_tag(
|
||||
&keys,
|
||||
&repo_coordinate,
|
||||
"Test Issue with uppercase A tag",
|
||||
)
|
||||
.expect("Failed to create issue event");
|
||||
let issue_id = issue.id;
|
||||
|
||||
println!("Created issue {} (kind {}) with uppercase 'A' tag", issue_id, issue.kind.as_u16());
|
||||
println!(
|
||||
"Created issue {} (kind {}) with uppercase 'A' tag",
|
||||
issue_id,
|
||||
issue.kind.as_u16()
|
||||
);
|
||||
for tag in issue.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -250,11 +257,8 @@ async fn test_layer2_sync_with_q_tag() {
|
||||
|
||||
// 2. Create and send repository announcement to both relays
|
||||
let repo_id = "test-repo-tag-8c";
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay_a.domain(), &relay_b.domain()],
|
||||
repo_id,
|
||||
);
|
||||
let announcement =
|
||||
create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
|
||||
|
||||
let client_a = TestClient::new(relay_a.url(), keys.clone())
|
||||
.await
|
||||
@@ -285,7 +289,11 @@ async fn test_layer2_sync_with_q_tag() {
|
||||
.expect("Failed to create issue event");
|
||||
let issue_id = issue.id;
|
||||
|
||||
println!("Created issue {} (kind {}) with 'q' tag", issue_id, issue.kind.as_u16());
|
||||
println!(
|
||||
"Created issue {} (kind {}) with 'q' tag",
|
||||
issue_id,
|
||||
issue.kind.as_u16()
|
||||
);
|
||||
for tag in issue.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -350,11 +358,8 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
|
||||
|
||||
// 2. Create and send repository announcement to both relays
|
||||
let repo_id = "test-repo-tag-9a";
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay_a.domain(), &relay_b.domain()],
|
||||
repo_id,
|
||||
);
|
||||
let announcement =
|
||||
create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
|
||||
|
||||
let client_a = TestClient::new(relay_a.url(), keys.clone())
|
||||
.await
|
||||
@@ -392,10 +397,9 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
|
||||
println!("Layer 2 issue {} sent to relay_a", issue_id);
|
||||
|
||||
// 5. Wait for issue to sync to relay_b
|
||||
let issue_filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_ISSUE))
|
||||
.id(issue_id);
|
||||
let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
let issue_filter = Filter::new().kind(Kind::Custom(KIND_ISSUE)).id(issue_id);
|
||||
let issue_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
println!("Issue synced to relay_b: {}", issue_synced);
|
||||
assert!(issue_synced, "Layer 2 issue should sync first");
|
||||
|
||||
@@ -412,7 +416,11 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
|
||||
.expect("Failed to create reply");
|
||||
let reply_id = reply.id;
|
||||
|
||||
println!("Created reply {} (kind {}) with lowercase 'e' tag", reply_id, reply.kind.as_u16());
|
||||
println!(
|
||||
"Created reply {} (kind {}) with lowercase 'e' tag",
|
||||
reply_id,
|
||||
reply.kind.as_u16()
|
||||
);
|
||||
for tag in reply.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -428,11 +436,12 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
|
||||
|
||||
// 7. Wait and verify reply syncs to relay_b
|
||||
let reply_filter = Filter::new()
|
||||
.kind(Kind::TextNote) // Kind 1
|
||||
.kind(Kind::TextNote) // Kind 1
|
||||
.author(keys.public_key())
|
||||
.id(reply_id);
|
||||
|
||||
let reply_synced = wait_for_event_on_relay(relay_b.url(), reply_filter, Duration::from_secs(5)).await;
|
||||
let reply_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), reply_filter, Duration::from_secs(5)).await;
|
||||
|
||||
println!("Reply {} synced to relay_b: {}", reply_id, reply_synced);
|
||||
|
||||
@@ -473,11 +482,8 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
|
||||
|
||||
// 2. Create and send repository announcement to both relays
|
||||
let repo_id = "test-repo-tag-9b";
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay_a.domain(), &relay_b.domain()],
|
||||
repo_id,
|
||||
);
|
||||
let announcement =
|
||||
create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
|
||||
|
||||
let client_a = TestClient::new(relay_a.url(), keys.clone())
|
||||
.await
|
||||
@@ -515,10 +521,9 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
|
||||
println!("Layer 2 issue {} sent to relay_a", issue_id);
|
||||
|
||||
// 5. Wait for issue to sync to relay_b
|
||||
let issue_filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_ISSUE))
|
||||
.id(issue_id);
|
||||
let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
let issue_filter = Filter::new().kind(Kind::Custom(KIND_ISSUE)).id(issue_id);
|
||||
let issue_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
println!("Issue synced to relay_b: {}", issue_synced);
|
||||
assert!(issue_synced, "Layer 2 issue should sync first");
|
||||
|
||||
@@ -531,11 +536,16 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// 6. Create and send Layer 3 comment with uppercase 'E' tag (kind 1111)
|
||||
let comment = build_layer3_comment_with_uppercase_e_tag(&keys, &issue_id, "Comment with uppercase E tag")
|
||||
.expect("Failed to create comment");
|
||||
let comment =
|
||||
build_layer3_comment_with_uppercase_e_tag(&keys, &issue_id, "Comment with uppercase E tag")
|
||||
.expect("Failed to create comment");
|
||||
let comment_id = comment.id;
|
||||
|
||||
println!("Created comment {} (kind {}) with uppercase 'E' tag", comment_id, comment.kind.as_u16());
|
||||
println!(
|
||||
"Created comment {} (kind {}) with uppercase 'E' tag",
|
||||
comment_id,
|
||||
comment.kind.as_u16()
|
||||
);
|
||||
for tag in comment.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -551,13 +561,17 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
|
||||
|
||||
// 7. Wait and verify comment syncs to relay_b
|
||||
let comment_filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_COMMENT)) // Kind 1111
|
||||
.kind(Kind::Custom(KIND_COMMENT)) // Kind 1111
|
||||
.author(keys.public_key())
|
||||
.id(comment_id);
|
||||
|
||||
let comment_synced = wait_for_event_on_relay(relay_b.url(), comment_filter, Duration::from_secs(5)).await;
|
||||
let comment_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), comment_filter, Duration::from_secs(5)).await;
|
||||
|
||||
println!("Comment {} synced to relay_b: {}", comment_id, comment_synced);
|
||||
println!(
|
||||
"Comment {} synced to relay_b: {}",
|
||||
comment_id, comment_synced
|
||||
);
|
||||
|
||||
// 8. Cleanup
|
||||
relay_b.stop().await;
|
||||
@@ -596,11 +610,8 @@ async fn test_layer3_sync_with_q_tag() {
|
||||
|
||||
// 2. Create and send repository announcement to both relays
|
||||
let repo_id = "test-repo-tag-9c";
|
||||
let announcement = create_repo_announcement(
|
||||
&keys,
|
||||
&[&relay_a.domain(), &relay_b.domain()],
|
||||
repo_id,
|
||||
);
|
||||
let announcement =
|
||||
create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id);
|
||||
|
||||
let client_a = TestClient::new(relay_a.url(), keys.clone())
|
||||
.await
|
||||
@@ -638,10 +649,9 @@ async fn test_layer3_sync_with_q_tag() {
|
||||
println!("Layer 2 issue {} sent to relay_a", issue_id);
|
||||
|
||||
// 5. Wait for issue to sync to relay_b
|
||||
let issue_filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_ISSUE))
|
||||
.id(issue_id);
|
||||
let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
let issue_filter = Filter::new().kind(Kind::Custom(KIND_ISSUE)).id(issue_id);
|
||||
let issue_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await;
|
||||
println!("Issue synced to relay_b: {}", issue_synced);
|
||||
assert!(issue_synced, "Layer 2 issue should sync first");
|
||||
|
||||
@@ -658,7 +668,11 @@ async fn test_layer3_sync_with_q_tag() {
|
||||
.expect("Failed to create quote");
|
||||
let quote_id = quote.id;
|
||||
|
||||
println!("Created quote {} (kind {}) with 'q' tag", quote_id, quote.kind.as_u16());
|
||||
println!(
|
||||
"Created quote {} (kind {}) with 'q' tag",
|
||||
quote_id,
|
||||
quote.kind.as_u16()
|
||||
);
|
||||
for tag in quote.tags.iter() {
|
||||
println!(" Tag: {:?}", tag.as_slice());
|
||||
}
|
||||
@@ -674,11 +688,12 @@ async fn test_layer3_sync_with_q_tag() {
|
||||
|
||||
// 7. Wait and verify quote syncs to relay_b
|
||||
let quote_filter = Filter::new()
|
||||
.kind(Kind::TextNote) // Kind 1
|
||||
.kind(Kind::TextNote) // Kind 1
|
||||
.author(keys.public_key())
|
||||
.id(quote_id);
|
||||
|
||||
let quote_synced = wait_for_event_on_relay(relay_b.url(), quote_filter, Duration::from_secs(5)).await;
|
||||
let quote_synced =
|
||||
wait_for_event_on_relay(relay_b.url(), quote_filter, Duration::from_secs(5)).await;
|
||||
|
||||
println!("Quote {} synced to relay_b: {}", quote_id, quote_synced);
|
||||
|
||||
@@ -690,4 +705,4 @@ async fn test_layer3_sync_with_q_tag() {
|
||||
quote_synced,
|
||||
"Layer 3 quote with 'q' tag should have synced to relay_b"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user