upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests/archive_read_only.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/archive_read_only.rs')
-rw-r--r--tests/archive_read_only.rs368
1 files changed, 0 insertions, 368 deletions
diff --git a/tests/archive_read_only.rs b/tests/archive_read_only.rs
deleted file mode 100644
index be6959b..0000000
--- a/tests/archive_read_only.rs
+++ /dev/null
@@ -1,368 +0,0 @@
1//! Archive Read-Only Mode Integration Tests
2//!
3//! Tests that verify archive_read_only mode behavior:
4//! - Bare git repositories are created for announcements
5//! - Git data is synced via relay-to-relay sync (purgatory sync)
6//! - Git pushes are rejected (read-only mode)
7//!
8//! # Test Strategy
9//!
10//! These tests verify the GRASP-05 archive mode with read_only flag:
11//! 1. Source relay has full repository (announcement + state events + git data)
12//! 2. Archive relay syncs from source relay (relay-to-relay sync)
13//! 3. State events trigger purgatory sync which fetches git data
14//! 4. Git data is validated against Nostr state events
15//! 5. Git pushes are rejected (read-only enforcement)
16//!
17//! # Security Model
18//!
19//! Archive mode uses the existing purgatory sync infrastructure to ensure:
20//! - Git data is validated against Nostr state events
21//! - "Naughty git servers" can't provide incorrect state
22//! - Same security guarantees as normal relay operation
23//!
24//! # Running Tests
25//!
26//! ```bash
27//! # Run all archive read-only tests
28//! cargo test --test archive_read_only
29//!
30//! # Run specific test
31//! cargo test --test archive_read_only test_archive_read_only_creates_bare_repo
32//!
33//! # With output for debugging
34//! cargo test --test archive_read_only -- --nocapture
35//! ```
36
37mod common;
38
39use common::{
40 check_ref_at_commit, create_repo_announcement, create_state_event,
41 create_test_repo_with_commit, push_to_relay, wait_for_event_served, wait_for_sync_connection,
42 CommitVariant, TestRelay,
43};
44use nostr_sdk::prelude::*;
45use std::time::Duration;
46
47/// Test that archive_read_only mode creates bare git repositories and syncs data
48/// via relay-to-relay sync (purgatory sync infrastructure).
49///
50/// Scenario:
51/// 1. Start source relay with full repository (announcement + state + git data)
52/// 2. Start archive relay with archive_all=true, archive_read_only=true, syncing from source
53/// 3. Archive relay syncs announcement and state events from source
54/// 4. State events trigger purgatory sync which fetches git data from source's clone URL
55/// 5. Verify bare repository is created and git data is synced
56/// 6. Verify git pushes are rejected (read-only mode)
57#[tokio::test]
58async fn test_archive_read_only_creates_bare_repo() {
59 // 1. Start source relay
60 let source_relay = TestRelay::start().await;
61 let keys = Keys::generate();
62 let identifier = "archive-test-repo";
63
64 // Pre-allocate archive relay port so we can include it in announcement
65 let archive_port = TestRelay::find_free_port();
66 let archive_domain = format!("127.0.0.1:{}", archive_port);
67
68 // 2. Create test repository locally with deterministic commit
69 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
70 let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
71 .expect("Failed to create test repo");
72
73 let npub = keys.public_key().to_bech32().expect("Failed to get npub");
74
75 // 3. Create and send announcement listing BOTH relays
76 // This ensures the archive relay will accept the state event when it syncs
77 let announcement = create_repo_announcement(
78 &keys,
79 &[&source_relay.domain(), &archive_domain],
80 identifier,
81 );
82
83 let source_client = Client::new(keys.clone());
84 source_client
85 .add_relay(source_relay.url())
86 .await
87 .expect("Failed to add source relay");
88 source_client.connect().await;
89
90 // Wait for connection
91 tokio::time::sleep(Duration::from_millis(500)).await;
92
93 // Send announcement to source relay
94 source_client
95 .send_event(&announcement)
96 .await
97 .expect("Failed to send announcement to source");
98
99 tokio::time::sleep(Duration::from_millis(200)).await;
100
101 // 4. Create and send state event
102 let clone_urls = [
103 format!(
104 "http://{}/{}/{}.git",
105 source_relay.domain(),
106 npub,
107 identifier
108 ),
109 format!("http://{}/{}/{}.git", archive_domain, npub, identifier),
110 ];
111 let relay_urls = [
112 source_relay.url().to_string(),
113 format!("ws://{}", archive_domain),
114 ];
115
116 let state_event = create_state_event(
117 &keys,
118 identifier,
119 &[("main", &commit_hash)],
120 &[],
121 &[&clone_urls[0], &clone_urls[1]],
122 &[&relay_urls[0], &relay_urls[1]],
123 )
124 .expect("Failed to create state event");
125
126 let state_event_id = state_event.id;
127
128 // Send state event to source relay (goes to purgatory - no git data yet)
129 source_client
130 .send_event(&state_event)
131 .await
132 .expect("Failed to send state event to source");
133
134 tokio::time::sleep(Duration::from_millis(200)).await;
135
136 // 5. Push git data to source relay
137 // The state event in purgatory authorizes this push
138 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
139 .expect("Push to source should succeed");
140
141 // After push, state event should be released from purgatory on source relay
142 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
143 .await
144 .expect("State event should be served on source relay after push");
145
146 // 6. Start archive relay with archive_all=true, archive_read_only=true, syncing from source
147 let archive_relay = TestRelay::start_with_archive_and_sync(
148 archive_port,
149 Some(source_relay.url().to_string()),
150 false, // negentropy enabled
151 true, // archive_all
152 true, // archive_read_only
153 )
154 .await;
155
156 // Wait for sync connection to establish
157 wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5))
158 .await
159 .expect("Sync connection should establish");
160
161 // 7. Wait for state event to be released on archive relay
162 // The sync should:
163 // a) Fetch the announcement and state event from source relay
164 // b) Accept announcement (creates bare repo structure) - via archive mode
165 // c) Put state event in purgatory (git data missing on archive relay)
166 // d) Fetch git data from source relay's clone URL
167 // e) Release the state event from purgatory
168 let found = wait_for_event_served(
169 archive_relay.url(),
170 &state_event_id,
171 Duration::from_secs(30), // Allow time for sync + git fetch
172 )
173 .await;
174
175 assert!(
176 found.is_ok(),
177 "State event should be served after sync fetches git data: {:?}",
178 found.err()
179 );
180
181 // 8. Verify bare repository was created
182 let repo_path = archive_relay
183 .git_data_path()
184 .join(format!("{}/{}.git", npub, identifier));
185
186 assert!(
187 repo_path.exists(),
188 "Bare repository should be created at {:?} for archive announcement",
189 repo_path
190 );
191
192 // 9. Verify it's a bare repository (check for config file with bare = true)
193 let config_path = repo_path.join("config");
194 assert!(
195 config_path.exists(),
196 "Git config should exist at {:?}",
197 config_path
198 );
199
200 let config_content = tokio::fs::read_to_string(&config_path)
201 .await
202 .expect("Should read git config");
203 assert!(
204 config_content.contains("bare = true"),
205 "Repository at {:?} should be bare (config should contain 'bare = true')",
206 repo_path
207 );
208
209 // 10. Verify refs are correct on archive relay
210 let ref_correct = check_ref_at_commit(
211 &archive_domain,
212 &npub,
213 identifier,
214 "refs/heads/main",
215 &commit_hash,
216 )
217 .await
218 .expect("Failed to check ref");
219
220 assert!(ref_correct, "main branch should point to correct commit");
221
222 // 11. Verify git pushes are rejected (read-only mode)
223 // Create a new commit in the source repo
224 tokio::fs::write(temp_dir.path().join("new_file.txt"), "new content")
225 .await
226 .expect("Failed to write new file");
227
228 let output = tokio::process::Command::new("git")
229 .args(["add", "."])
230 .current_dir(temp_dir.path())
231 .output()
232 .await
233 .expect("Failed to git add");
234 assert!(output.status.success());
235
236 let output = tokio::process::Command::new("git")
237 .args(["commit", "-m", "New commit for push test"])
238 .current_dir(temp_dir.path())
239 .output()
240 .await
241 .expect("Failed to git commit");
242 assert!(output.status.success());
243
244 // Try to push to archive relay (should fail in read-only mode)
245 let push_url = format!("http://{}/{}/{}.git", archive_domain, npub, identifier);
246 let output = tokio::process::Command::new("git")
247 .args(["push", &push_url, "main"])
248 .current_dir(temp_dir.path())
249 .output()
250 .await
251 .expect("Failed to run git push");
252
253 assert!(
254 !output.status.success(),
255 "Git push should be rejected in archive_read_only mode. stderr: {}",
256 String::from_utf8_lossy(&output.stderr)
257 );
258
259 // Cleanup
260 source_client.disconnect().await;
261 archive_relay.stop().await;
262 source_relay.stop().await;
263}
264
265/// Test that archive mode without state events does NOT sync git data.
266///
267/// This verifies the security model: archive mode only syncs git data
268/// when there are state events to validate against.
269///
270/// Scenario:
271/// 1. Start source relay with announcement only (no state events)
272/// 2. Start archive relay syncing from source
273/// 3. Archive relay syncs announcement (creates bare repo)
274/// 4. Verify git data is NOT synced (no state events to trigger purgatory sync)
275#[tokio::test]
276async fn test_archive_without_state_events_does_not_sync_git() {
277 // 1. Start source relay
278 let source_relay = TestRelay::start().await;
279 let keys = Keys::generate();
280 let identifier = "archive-no-state-repo";
281
282 // Pre-allocate archive relay port
283 let archive_port = TestRelay::find_free_port();
284 let archive_domain = format!("127.0.0.1:{}", archive_port);
285
286 // 2. Create test repository locally
287 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
288 let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
289 .expect("Failed to create test repo");
290
291 let npub = keys.public_key().to_bech32().expect("Failed to get npub");
292
293 // 3. Create and send announcement listing BOTH relays (but NO state event)
294 let announcement = create_repo_announcement(
295 &keys,
296 &[&source_relay.domain(), &archive_domain],
297 identifier,
298 );
299
300 let source_client = Client::new(keys.clone());
301 source_client
302 .add_relay(source_relay.url())
303 .await
304 .expect("Failed to add source relay");
305 source_client.connect().await;
306
307 tokio::time::sleep(Duration::from_millis(500)).await;
308
309 // Send announcement to source relay
310 source_client
311 .send_event(&announcement)
312 .await
313 .expect("Failed to send announcement to source");
314
315 tokio::time::sleep(Duration::from_millis(200)).await;
316
317 // 4. Push git data to source relay (but no state event to authorize it)
318 // This push will fail because there's no state event in purgatory
319 // That's expected - we're testing that archive mode doesn't blindly fetch git data
320
321 // 5. Start archive relay
322 let archive_relay = TestRelay::start_with_archive_and_sync(
323 archive_port,
324 Some(source_relay.url().to_string()),
325 false,
326 true,
327 true,
328 )
329 .await;
330
331 // Wait for sync
332 wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5))
333 .await
334 .expect("Sync connection should establish");
335
336 // Give time for any potential git sync to happen
337 tokio::time::sleep(Duration::from_secs(3)).await;
338
339 // 6. Verify bare repository was created (announcement was accepted)
340 let repo_path = archive_relay
341 .git_data_path()
342 .join(format!("{}/{}.git", npub, identifier));
343
344 assert!(
345 repo_path.exists(),
346 "Bare repository should be created for archive announcement"
347 );
348
349 // 7. Verify git data was NOT synced (no state events to trigger purgatory sync)
350 // Check that the commit does NOT exist in the archive relay's repo
351 let output = tokio::process::Command::new("git")
352 .args(["cat-file", "-t", &commit_hash])
353 .current_dir(&repo_path)
354 .output()
355 .await;
356
357 let commit_exists = output.map(|o| o.status.success()).unwrap_or(false);
358
359 assert!(
360 !commit_exists,
361 "Git data should NOT be synced without state events (security: validates against Nostr state)"
362 );
363
364 // Cleanup
365 source_client.disconnect().await;
366 archive_relay.stop().await;
367 source_relay.stop().await;
368}