From e7e61d1abfb3609c6818e6040294c6be19ba805f Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 18 Feb 2026 20:02:40 +0000 Subject: refactor: move archive_read_only test to archive_grasp_services and remove redundant test --- tests/archive_grasp_services.rs | 225 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 224 insertions(+), 1 deletion(-) (limited to 'tests/archive_grasp_services.rs') diff --git a/tests/archive_grasp_services.rs b/tests/archive_grasp_services.rs index a47fc55..9f13d2a 100644 --- a/tests/archive_grasp_services.rs +++ b/tests/archive_grasp_services.rs @@ -29,7 +29,11 @@ mod common; -use common::TestRelay; +use common::{ + check_ref_at_commit, create_repo_announcement, create_state_event, + create_test_repo_with_commit, push_to_relay, wait_for_event_served, wait_for_sync_connection, + CommitVariant, TestRelay, +}; use nostr_sdk::prelude::*; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; @@ -376,3 +380,222 @@ async fn test_archive_multiple_grasp_services() { let _ = process.kill(); let _ = process.wait(); } + +/// Test that archive_read_only mode creates bare git repositories and syncs data +/// via relay-to-relay sync (purgatory sync infrastructure). +/// +/// Scenario: +/// 1. Start source relay with full repository (announcement + state + git data) +/// 2. Start archive relay with archive_all=true, archive_read_only=true, syncing from source +/// 3. Archive relay syncs announcement and state events from source +/// 4. State events trigger purgatory sync which fetches git data from source's clone URL +/// 5. Verify bare repository is created and git data is synced +/// 6. Verify git pushes are rejected (read-only mode) +#[tokio::test] +async fn test_archive_read_only_creates_bare_repo() { + // 1. Start source relay + let source_relay = TestRelay::start().await; + let keys = Keys::generate(); + let identifier = "archive-test-repo"; + + // Pre-allocate archive relay port so we can include it in announcement + let archive_port = TestRelay::find_free_port(); + let archive_domain = format!("127.0.0.1:{}", archive_port); + + // 2. Create test repository locally with deterministic commit + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) + .expect("Failed to create test repo"); + + let npub = keys.public_key().to_bech32().expect("Failed to get npub"); + + // 3. Create and send announcement listing BOTH relays + // This ensures the archive relay will accept the state event when it syncs + let announcement = create_repo_announcement( + &keys, + &[&source_relay.domain(), &archive_domain], + identifier, + ); + + let source_client = Client::new(keys.clone()); + source_client + .add_relay(source_relay.url()) + .await + .expect("Failed to add source relay"); + source_client.connect().await; + + // Wait for connection + tokio::time::sleep(Duration::from_millis(500)).await; + + // Send announcement to source relay + source_client + .send_event(&announcement) + .await + .expect("Failed to send announcement to source"); + + tokio::time::sleep(Duration::from_millis(200)).await; + + // 4. Create and send state event + let clone_urls = [ + format!( + "http://{}/{}/{}.git", + source_relay.domain(), + npub, + identifier + ), + format!("http://{}/{}/{}.git", archive_domain, npub, identifier), + ]; + let relay_urls = [ + source_relay.url().to_string(), + format!("ws://{}", archive_domain), + ]; + + let state_event = create_state_event( + &keys, + identifier, + &[("main", &commit_hash)], + &[], + &[&clone_urls[0], &clone_urls[1]], + &[&relay_urls[0], &relay_urls[1]], + ) + .expect("Failed to create state event"); + + let state_event_id = state_event.id; + + // Send state event to source relay (goes to purgatory - no git data yet) + source_client + .send_event(&state_event) + .await + .expect("Failed to send state event to source"); + + tokio::time::sleep(Duration::from_millis(200)).await; + + // 5. Push git data to source relay + // The state event in purgatory authorizes this push + push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) + .expect("Push to source should succeed"); + + // After push, state event should be released from purgatory on source relay + wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) + .await + .expect("State event should be served on source relay after push"); + + // 6. Start archive relay with archive_all=true, archive_read_only=true, syncing from source + let archive_relay = TestRelay::start_with_archive_and_sync( + archive_port, + Some(source_relay.url().to_string()), + false, // negentropy enabled + true, // archive_all + true, // archive_read_only + ) + .await; + + // Wait for sync connection to establish + wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5)) + .await + .expect("Sync connection should establish"); + + // 7. Wait for state event to be released on archive relay + // The sync should: + // a) Fetch the announcement and state event from source relay + // b) Accept announcement (creates bare repo structure) - via archive mode + // c) Put state event in purgatory (git data missing on archive relay) + // d) Fetch git data from source relay's clone URL + // e) Release the state event from purgatory + + let found = wait_for_event_served( + archive_relay.url(), + &state_event_id, + Duration::from_secs(30), // Allow time for sync + git fetch + ) + .await; + + assert!( + found.is_ok(), + "State event should be served after sync fetches git data: {:?}", + found.err() + ); + + // 8. Verify bare repository was created + let repo_path = archive_relay + .git_data_path() + .join(format!("{}/{}.git", npub, identifier)); + + assert!( + repo_path.exists(), + "Bare repository should be created at {:?} for archive announcement", + repo_path + ); + + // 9. Verify it's a bare repository (check for config file with bare = true) + let config_path = repo_path.join("config"); + assert!( + config_path.exists(), + "Git config should exist at {:?}", + config_path + ); + + let config_content = tokio::fs::read_to_string(&config_path) + .await + .expect("Should read git config"); + assert!( + config_content.contains("bare = true"), + "Repository at {:?} should be bare (config should contain 'bare = true')", + repo_path + ); + + // 10. Verify refs are correct on archive relay + let ref_correct = check_ref_at_commit( + &archive_domain, + &npub, + identifier, + "refs/heads/main", + &commit_hash, + ) + .await + .expect("Failed to check ref"); + + assert!(ref_correct, "main branch should point to correct commit"); + + // 11. Verify git pushes are rejected (read-only mode) + // Create a new commit in the source repo + tokio::fs::write(temp_dir.path().join("new_file.txt"), "new content") + .await + .expect("Failed to write new file"); + + let output = tokio::process::Command::new("git") + .args(["add", "."]) + .current_dir(temp_dir.path()) + .output() + .await + .expect("Failed to git add"); + assert!(output.status.success()); + + let output = tokio::process::Command::new("git") + .args(["commit", "-m", "New commit for push test"]) + .current_dir(temp_dir.path()) + .output() + .await + .expect("Failed to git commit"); + assert!(output.status.success()); + + // Try to push to archive relay (should fail in read-only mode) + let push_url = format!("http://{}/{}/{}.git", archive_domain, npub, identifier); + let output = tokio::process::Command::new("git") + .args(["push", &push_url, "main"]) + .current_dir(temp_dir.path()) + .output() + .await + .expect("Failed to run git push"); + + assert!( + !output.status.success(), + "Git push should be rejected in archive_read_only mode. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Cleanup + source_client.disconnect().await; + archive_relay.stop().await; + source_relay.stop().await; +} -- cgit v1.2.3