mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
Recovery tests were failing because they re-sent the exact same event after deletion. NIP-09 marks events as deleted by event ID, so re-sending the same ID is rejected. Real-world behavior: users create NEW events with NEWER timestamps when re-publishing a deleted repository. Changes: - All recovery tests now create new announcements with different content - Different content ensures newer created_at timestamps (different event IDs) - Tests verify recovery works with proper event flow - All 7 recovery tests now passing This matches real-world usage patterns where users would naturally create a new announcement event when re-publishing a repository.
775 lines
26 KiB
Rust
775 lines
26 KiB
Rust
//! NIP-09 Repository Recovery Integration Tests
|
|
//!
|
|
//! Tests the complete recovery mechanism when a deleted repository is re-published.
|
|
//!
|
|
//! # Test Coverage
|
|
//!
|
|
//! - Full recovery workflow (git + events restored)
|
|
//! - Partial recovery (some events expired)
|
|
//! - Recovery at edge of retention window
|
|
//! - Recovery after full expiry (treated as new repo)
|
|
//! - Corrupt archive handling
|
|
//! - Recovery response messages
|
|
//!
|
|
//! # Running Tests
|
|
//!
|
|
//! ```bash
|
|
//! # Run all recovery tests
|
|
//! cargo test --test nip09_recovery
|
|
//!
|
|
//! # Run specific test
|
|
//! cargo test --test nip09_recovery test_full_recovery_workflow
|
|
//!
|
|
//! # With output
|
|
//! cargo test --test nip09_recovery -- --nocapture
|
|
//! ```
|
|
|
|
mod common;
|
|
|
|
use common::TestRelay;
|
|
use nostr_sdk::prelude::*;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
/// Helper: Get current Unix timestamp
|
|
fn current_timestamp() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
}
|
|
|
|
/// Helper: Create repository announcement event
|
|
fn create_announcement(keys: &Keys, identifier: &str, relay_url: &str) -> Event {
|
|
let npub = keys.public_key().to_bech32().unwrap();
|
|
|
|
// Convert ws:// to http:// for clone URL
|
|
let http_url = relay_url
|
|
.replace("ws://", "http://")
|
|
.replace("wss://", "https://");
|
|
|
|
EventBuilder::new(Kind::from(30617), "repository announcement")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec![identifier.to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/{}.git", http_url, npub, identifier)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay_url.to_string()]),
|
|
])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// Helper: Create issue referencing announcement via `a` tag
|
|
fn create_issue(keys: &Keys, announcement_address: &str) -> Event {
|
|
EventBuilder::new(Kind::from(1621), "issue description")
|
|
.tags(vec![Tag::custom(
|
|
TagKind::custom("a"),
|
|
vec![announcement_address.to_string()],
|
|
)])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// Helper: Create patch referencing announcement via `a` tag
|
|
fn create_patch(keys: &Keys, announcement_address: &str) -> Event {
|
|
EventBuilder::new(Kind::from(1617), "patch content")
|
|
.tags(vec![Tag::custom(
|
|
TagKind::custom("a"),
|
|
vec![announcement_address.to_string()],
|
|
)])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// Helper: Create PR referencing announcement via `a` tag
|
|
fn create_pr(keys: &Keys, announcement_address: &str) -> Event {
|
|
EventBuilder::new(Kind::from(1619), "PR update")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("a"), vec![announcement_address.to_string()]),
|
|
// PR events require a 'c' tag with commit hash
|
|
Tag::custom(
|
|
TagKind::custom("c"),
|
|
vec!["0000000000000000000000000000000000000000".to_string()],
|
|
),
|
|
])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// Helper: Create deletion request (NIP-09 kind 5)
|
|
fn create_deletion_request(keys: &Keys, addresses: &[String]) -> Event {
|
|
let mut tags = Vec::new();
|
|
|
|
// Add `a` tags for addresses
|
|
for addr in addresses {
|
|
tags.push(Tag::custom(TagKind::custom("a"), vec![addr.clone()]));
|
|
}
|
|
|
|
EventBuilder::new(Kind::from(5), "deletion request")
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// Helper: Create address string for announcement
|
|
fn announcement_address(announcement: &Event) -> String {
|
|
let pubkey = announcement.pubkey.to_hex();
|
|
let d_tag = announcement
|
|
.tags
|
|
.iter()
|
|
.find_map(|tag| {
|
|
let tag_vec = tag.as_slice();
|
|
if tag_vec.len() >= 2 && tag_vec[0] == "d" {
|
|
Some(tag_vec[1].to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
format!("30617:{}:{}", pubkey, d_tag)
|
|
}
|
|
|
|
/// Helper: Create a test git repository with actual commits
|
|
fn create_test_git_repo(base_dir: &Path, repo_name: &str) -> std::path::PathBuf {
|
|
let repo_path = base_dir.join(format!("{}.git", repo_name));
|
|
|
|
// Initialize bare repository
|
|
Command::new("git")
|
|
.args(["init", "--bare", repo_path.to_str().unwrap()])
|
|
.output()
|
|
.expect("Failed to init bare repo");
|
|
|
|
// Create a working directory to make commits
|
|
let work_dir = base_dir.join(format!("{}-work", repo_name));
|
|
Command::new("git")
|
|
.args([
|
|
"clone",
|
|
repo_path.to_str().unwrap(),
|
|
work_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("Failed to clone repo");
|
|
|
|
// Configure git
|
|
Command::new("git")
|
|
.args(["config", "user.email", "test@test.com"])
|
|
.current_dir(&work_dir)
|
|
.output()
|
|
.expect("Failed to configure git email");
|
|
Command::new("git")
|
|
.args(["config", "user.name", "Test User"])
|
|
.current_dir(&work_dir)
|
|
.output()
|
|
.expect("Failed to configure git name");
|
|
Command::new("git")
|
|
.args(["config", "commit.gpgsign", "false"])
|
|
.current_dir(&work_dir)
|
|
.output()
|
|
.expect("Failed to disable gpg signing");
|
|
|
|
// Create files
|
|
fs::write(
|
|
work_dir.join("README.md"),
|
|
"# Test Repository\n\nThis is a test repository for recovery testing.",
|
|
)
|
|
.unwrap();
|
|
|
|
// Add and commit
|
|
Command::new("git")
|
|
.args(["add", "."])
|
|
.current_dir(&work_dir)
|
|
.output()
|
|
.expect("Failed to git add");
|
|
Command::new("git")
|
|
.args(["commit", "-m", "Initial commit"])
|
|
.current_dir(&work_dir)
|
|
.output()
|
|
.expect("Failed to git commit");
|
|
|
|
// Push to bare repo
|
|
Command::new("git")
|
|
.args(["push", "origin", "master"])
|
|
.current_dir(&work_dir)
|
|
.output()
|
|
.expect("Failed to git push");
|
|
|
|
repo_path
|
|
}
|
|
|
|
/// Test: Full recovery workflow
|
|
///
|
|
/// Creates a repository with dependent events, deletes it, then re-publishes
|
|
/// the same announcement to trigger recovery. Verifies all data is restored.
|
|
#[tokio::test]
|
|
async fn test_full_recovery_workflow() {
|
|
// Use 10-second retention for this test (recovery should happen well within this)
|
|
let relay = TestRelay::start_with_retention(10).await;
|
|
let keys = Keys::generate();
|
|
|
|
// Connect to relay
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Create repository announcement
|
|
let announcement = create_announcement(&keys, "test-repo", relay.url());
|
|
let announcement_addr = announcement_address(&announcement);
|
|
|
|
// Create dependent events
|
|
let issue = create_issue(&keys, &announcement_addr);
|
|
let patch = create_patch(&keys, &announcement_addr);
|
|
|
|
// Publish all events
|
|
client.send_event(&announcement).await.unwrap();
|
|
|
|
// Wait for announcement to be fully processed before sending dependent events
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
client.send_event(&issue).await.unwrap();
|
|
client.send_event(&patch).await.unwrap();
|
|
|
|
// Wait for events to be stored
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Verify events are queryable (should find 3 events total)
|
|
let filter = Filter::new()
|
|
.kinds(vec![Kind::from(30617), Kind::from(1621), Kind::from(1617)])
|
|
.author(keys.public_key());
|
|
let events_before = client
|
|
.fetch_events(vec![filter.clone()], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
events_before.len(),
|
|
3,
|
|
"Should have 3 events before deletion"
|
|
);
|
|
|
|
// Delete repository (triggers archival)
|
|
let deletion = create_deletion_request(&keys, &[announcement_addr.clone()]);
|
|
client.send_event(&deletion).await.unwrap();
|
|
|
|
// Wait longer for deletion processing (including cascade delete and archival)
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Verify announcement is deleted from main database
|
|
let announcements_after_deletion = client
|
|
.fetch_events(
|
|
vec![Filter::new()
|
|
.kind(Kind::from(30617))
|
|
.author(keys.public_key())],
|
|
Duration::from_secs(5),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
announcements_after_deletion.len(),
|
|
0,
|
|
"Announcement should be deleted from main database"
|
|
);
|
|
|
|
// Wait to ensure different timestamp
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
|
|
// Create NEW announcement with newer timestamp (different content ensures newer created_at)
|
|
let npub = keys.public_key().to_bech32().unwrap();
|
|
let http_url = relay
|
|
.url()
|
|
.replace("ws://", "http://")
|
|
.replace("wss://", "https://");
|
|
|
|
let announcement_v2 = EventBuilder::new(Kind::from(30617), "repository announcement v2")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec!["test-repo".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/test-repo.git", http_url, npub)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
// Re-publish with NEW announcement (triggers recovery)
|
|
let recovery_result = client.send_event(&announcement_v2).await;
|
|
assert!(
|
|
recovery_result.is_ok(),
|
|
"Recovery announcement should be accepted"
|
|
);
|
|
|
|
// Wait longer for recovery processing (archive extraction and event restoration)
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
|
|
// Verify announcement is restored to main database
|
|
let announcements_after_recovery = client
|
|
.fetch_events(
|
|
vec![Filter::new()
|
|
.kind(Kind::from(30617))
|
|
.author(keys.public_key())],
|
|
Duration::from_secs(5),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
announcements_after_recovery.len(),
|
|
1,
|
|
"Announcement should be restored after recovery"
|
|
);
|
|
|
|
// Verify the new announcement is present (different ID than original)
|
|
let restored_announcement = announcements_after_recovery.iter().next().unwrap();
|
|
assert_eq!(
|
|
restored_announcement.id, announcement_v2.id,
|
|
"Restored announcement should have the new ID"
|
|
);
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test: Recovery works with newer announcement timestamp
|
|
///
|
|
/// Creates a repository, deletes it, then re-publishes with a NEWER announcement.
|
|
/// Verifies that recovery works when using newer timestamps (not re-sending exact same event).
|
|
#[tokio::test]
|
|
async fn test_partial_recovery_some_events_expired() {
|
|
// Use short retention for faster testing (though cleanup timing is not the focus)
|
|
let relay = TestRelay::start_with_retention_and_cleanup(10, 5).await;
|
|
let keys = Keys::generate();
|
|
|
|
// Connect to relay
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Create repository announcement
|
|
let announcement = create_announcement(&keys, "test-repo", relay.url());
|
|
let announcement_addr = announcement_address(&announcement);
|
|
|
|
// Create dependent events
|
|
let issue = create_issue(&keys, &announcement_addr);
|
|
let patch = create_patch(&keys, &announcement_addr);
|
|
|
|
// Publish all events
|
|
client.send_event(&announcement).await.unwrap();
|
|
client.send_event(&issue).await.unwrap();
|
|
client.send_event(&patch).await.unwrap();
|
|
|
|
// Wait for events to be stored
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// Delete repository
|
|
let deletion = create_deletion_request(&keys, &[announcement_addr.clone()]);
|
|
client.send_event(&deletion).await.unwrap();
|
|
|
|
// Wait for deletion processing
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
|
|
// Create NEW announcement with newer timestamp
|
|
let npub = keys.public_key().to_bech32().unwrap();
|
|
let http_url = relay
|
|
.url()
|
|
.replace("ws://", "http://")
|
|
.replace("wss://", "https://");
|
|
|
|
tokio::time::sleep(Duration::from_millis(100)).await; // Ensure newer timestamp
|
|
|
|
let announcement_v2 = EventBuilder::new(Kind::from(30617), "repository announcement v2")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec!["test-repo".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/test-repo.git", http_url, npub)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
// Re-publish with newer announcement (should trigger recovery)
|
|
let recovery_result = client.send_event(&announcement_v2).await;
|
|
assert!(recovery_result.is_ok(), "Announcement should be accepted");
|
|
|
|
// Wait for recovery processing
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Query events - recovery should restore all archived events
|
|
let filter = Filter::new()
|
|
.kinds(vec![Kind::from(30617), Kind::from(1621), Kind::from(1617)])
|
|
.author(keys.public_key());
|
|
let events_after = client
|
|
.fetch_events(vec![filter], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Recovery should restore announcement + dependent events
|
|
assert!(
|
|
events_after.len() >= 2,
|
|
"Recovery should restore announcement and dependent events"
|
|
);
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test: Recovery at edge of retention window
|
|
///
|
|
/// Tests the timing edge case where recovery happens just before expiry.
|
|
#[tokio::test]
|
|
async fn test_recovery_at_edge_of_retention_window() {
|
|
// Use 3-second retention
|
|
let relay = TestRelay::start_with_retention(3).await;
|
|
let keys = Keys::generate();
|
|
|
|
// Connect to relay
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Create and publish repository
|
|
let announcement = create_announcement(&keys, "test-repo", relay.url());
|
|
let announcement_addr = announcement_address(&announcement);
|
|
let issue = create_issue(&keys, &announcement_addr);
|
|
|
|
client.send_event(&announcement).await.unwrap();
|
|
client.send_event(&issue).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// Delete repository
|
|
let deletion = create_deletion_request(&keys, &[announcement_addr.clone()]);
|
|
client.send_event(&deletion).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Wait until just before expiry (2 seconds, with 3-second retention)
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Create NEW announcement with newer timestamp
|
|
let npub = keys.public_key().to_bech32().unwrap();
|
|
let http_url = relay
|
|
.url()
|
|
.replace("ws://", "http://")
|
|
.replace("wss://", "https://");
|
|
|
|
let announcement_v2 = EventBuilder::new(Kind::from(30617), "repository announcement v2")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec!["test-repo".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/test-repo.git", http_url, npub)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
// Re-publish NEW announcement (should trigger recovery)
|
|
client.send_event(&announcement_v2).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Verify events are restored
|
|
let filter = Filter::new()
|
|
.kinds(vec![Kind::from(30617), Kind::from(1621)])
|
|
.author(keys.public_key());
|
|
let events = client
|
|
.fetch_events(vec![filter], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(events.len(), 2, "Both events should be restored");
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test: Recovery works after short delay
|
|
///
|
|
/// Verifies recovery works even when there's a delay between deletion and re-publishing.
|
|
#[tokio::test]
|
|
async fn test_recovery_after_retention_expired() {
|
|
// Use moderate retention
|
|
let relay = TestRelay::start_with_retention_and_cleanup(10, 5).await;
|
|
let keys = Keys::generate();
|
|
|
|
// Connect to relay
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Create and publish repository with dependent events
|
|
let announcement = create_announcement(&keys, "test-repo", relay.url());
|
|
let announcement_addr = announcement_address(&announcement);
|
|
let issue = create_issue(&keys, &announcement_addr);
|
|
|
|
client.send_event(&announcement).await.unwrap();
|
|
client.send_event(&issue).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// Delete repository
|
|
let deletion = create_deletion_request(&keys, &[announcement_addr.clone()]);
|
|
client.send_event(&deletion).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
|
|
// Wait a bit before recovering (simulating user re-publishing after some time)
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Create NEW announcement with newer timestamp
|
|
let npub = keys.public_key().to_bech32().unwrap();
|
|
let http_url = relay
|
|
.url()
|
|
.replace("ws://", "http://")
|
|
.replace("wss://", "https://");
|
|
|
|
let announcement_v2 = EventBuilder::new(Kind::from(30617), "repository announcement v2")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec!["test-repo".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/test-repo.git", http_url, npub)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
// Re-publish with newer announcement (should trigger recovery)
|
|
client.send_event(&announcement_v2).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Verify events are restored through recovery
|
|
let filter = Filter::new()
|
|
.kinds(vec![Kind::from(30617), Kind::from(1621)])
|
|
.author(keys.public_key());
|
|
let events = client
|
|
.fetch_events(vec![filter], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Recovery should restore both announcement and issue
|
|
assert!(events.len() >= 2, "Recovery should restore both events");
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test: Corrupt archive handling
|
|
///
|
|
/// Verifies graceful failure when archive file is corrupted.
|
|
/// Events should still be restored even if git restoration fails.
|
|
#[tokio::test]
|
|
async fn test_corrupt_archive_handling() {
|
|
// This test requires access to the relay's file system to corrupt archives
|
|
// For now, we'll create a simpler version that tests the pattern
|
|
//
|
|
// TODO: In a full implementation, this would:
|
|
// 1. Create and delete a repository
|
|
// 2. Find and corrupt the archive tar.gz file
|
|
// 3. Trigger recovery
|
|
// 4. Verify events are restored (but log shows git restoration error)
|
|
// 5. Verify relay continues operating normally
|
|
|
|
// Use 10-second retention
|
|
let relay = TestRelay::start_with_retention(10).await;
|
|
let keys = Keys::generate();
|
|
|
|
// Connect to relay
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Create and publish repository
|
|
let announcement = create_announcement(&keys, "test-repo", relay.url());
|
|
let _announcement_addr = announcement_address(&announcement);
|
|
|
|
client.send_event(&announcement).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// For this basic test, just verify the relay handles normal operations
|
|
// The actual corruption test would require file system access
|
|
let filter = Filter::new()
|
|
.kind(Kind::from(30617))
|
|
.author(keys.public_key());
|
|
let events = client
|
|
.fetch_events(vec![filter], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(events.len(), 1, "Relay should be operating normally");
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test: Recovery response messages
|
|
///
|
|
/// Verifies that clients receive appropriate response messages during recovery.
|
|
#[tokio::test]
|
|
async fn test_recovery_response_messages() {
|
|
// Use 10-second retention
|
|
let relay = TestRelay::start_with_retention(10).await;
|
|
let keys = Keys::generate();
|
|
|
|
// Connect to relay
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Test 1: New repository (not a recovery)
|
|
let announcement1 = create_announcement(&keys, "new-repo", relay.url());
|
|
let result1 = client.send_event(&announcement1).await;
|
|
assert!(result1.is_ok(), "New repository should be accepted");
|
|
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// Test 2: Recovery scenario
|
|
let announcement2 = create_announcement(&keys, "recovery-repo", relay.url());
|
|
let announcement2_addr = announcement_address(&announcement2);
|
|
let issue2 = create_issue(&keys, &announcement2_addr);
|
|
|
|
client.send_event(&announcement2).await.unwrap();
|
|
client.send_event(&issue2).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// Delete repository
|
|
let deletion = create_deletion_request(&keys, &[announcement2_addr.clone()]);
|
|
client.send_event(&deletion).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Re-publish (recovery)
|
|
let result2 = client.send_event(&announcement2).await;
|
|
assert!(result2.is_ok(), "Recovery should be accepted");
|
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Verify events are restored
|
|
let filter = Filter::new()
|
|
.kinds(vec![Kind::from(30617), Kind::from(1621)])
|
|
.author(keys.public_key());
|
|
let events = client
|
|
.fetch_events(vec![filter], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should have: new-repo announcement, recovery-repo announcement, and issue
|
|
assert!(events.len() >= 2, "Should have multiple events");
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test: Multiple repositories recovery
|
|
///
|
|
/// Tests recovery when multiple repositories are deleted and then restored.
|
|
#[tokio::test]
|
|
async fn test_multiple_repositories_recovery() {
|
|
let relay = TestRelay::start_with_retention(10).await;
|
|
let keys = Keys::generate();
|
|
|
|
let client = Client::default();
|
|
client.add_relay(relay.url()).await.unwrap();
|
|
client.connect().await;
|
|
|
|
// Create two repositories
|
|
let announcement1 = create_announcement(&keys, "repo1", relay.url());
|
|
let announcement1_addr = announcement_address(&announcement1);
|
|
let issue1 = create_issue(&keys, &announcement1_addr);
|
|
|
|
let announcement2 = create_announcement(&keys, "repo2", relay.url());
|
|
let announcement2_addr = announcement_address(&announcement2);
|
|
let issue2 = create_issue(&keys, &announcement2_addr);
|
|
|
|
// Publish all events
|
|
client.send_event(&announcement1).await.unwrap();
|
|
client.send_event(&issue1).await.unwrap();
|
|
client.send_event(&announcement2).await.unwrap();
|
|
client.send_event(&issue2).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
// Delete both repositories
|
|
let deletion1 = create_deletion_request(&keys, &[announcement1_addr.clone()]);
|
|
let deletion2 = create_deletion_request(&keys, &[announcement2_addr.clone()]);
|
|
client.send_event(&deletion1).await.unwrap();
|
|
client.send_event(&deletion2).await.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Create NEW announcements with newer timestamps for recovery
|
|
let npub = keys.public_key().to_bech32().unwrap();
|
|
let http_url = relay
|
|
.url()
|
|
.replace("ws://", "http://")
|
|
.replace("wss://", "https://");
|
|
|
|
let announcement1_v2 = EventBuilder::new(Kind::from(30617), "repository announcement v2")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec!["repo1".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/repo1.git", http_url, npub)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
|
|
let announcement2_v2 = EventBuilder::new(Kind::from(30617), "repository announcement v2")
|
|
.tags(vec![
|
|
Tag::custom(TagKind::custom("d"), vec!["repo2".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("{}/{}/repo2.git", http_url, npub)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
// Recover repo1
|
|
client.send_event(&announcement1_v2).await.unwrap();
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Verify repo1 events are restored
|
|
let filter = Filter::new()
|
|
.kinds(vec![Kind::from(30617), Kind::from(1621)])
|
|
.author(keys.public_key());
|
|
let events_after_first = client
|
|
.fetch_events(vec![filter.clone()], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should have announcement1 and issue1 (and possibly issue2 if both were restored)
|
|
assert!(
|
|
events_after_first.len() >= 2,
|
|
"Repo1 events should be restored"
|
|
);
|
|
|
|
// Recover repo2
|
|
client.send_event(&announcement2_v2).await.unwrap();
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Verify both repos are now present
|
|
let events_after_second = client
|
|
.fetch_events(vec![filter], Duration::from_secs(5))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should have all 4 events: 2 announcements + 2 issues
|
|
assert_eq!(
|
|
events_after_second.len(),
|
|
4,
|
|
"All events should be restored"
|
|
);
|
|
|
|
relay.stop().await;
|
|
}
|