upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 20:02:40 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 20:02:40 +0000
commite7e61d1abfb3609c6818e6040294c6be19ba805f (patch)
tree529a9feddd97071d6db005018683a437d1e02bd5 /tests
parente22021f0b248ebcf3bd09210d59b2cdb4701032f (diff)
refactor: move archive_read_only test to archive_grasp_services and remove redundant test
Diffstat (limited to 'tests')
-rw-r--r--tests/archive_grasp_services.rs225
-rw-r--r--tests/archive_read_only.rs417
2 files changed, 224 insertions, 418 deletions
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 @@
29 29
30mod common; 30mod common;
31 31
32use common::TestRelay; 32use common::{
33 check_ref_at_commit, create_repo_announcement, create_state_event,
34 create_test_repo_with_commit, push_to_relay, wait_for_event_served, wait_for_sync_connection,
35 CommitVariant, TestRelay,
36};
33use nostr_sdk::prelude::*; 37use nostr_sdk::prelude::*;
34use std::path::PathBuf; 38use std::path::PathBuf;
35use std::process::{Child, Command, Stdio}; 39use std::process::{Child, Command, Stdio};
@@ -376,3 +380,222 @@ async fn test_archive_multiple_grasp_services() {
376 let _ = process.kill(); 380 let _ = process.kill();
377 let _ = process.wait(); 381 let _ = process.wait();
378} 382}
383
384/// Test that archive_read_only mode creates bare git repositories and syncs data
385/// via relay-to-relay sync (purgatory sync infrastructure).
386///
387/// Scenario:
388/// 1. Start source relay with full repository (announcement + state + git data)
389/// 2. Start archive relay with archive_all=true, archive_read_only=true, syncing from source
390/// 3. Archive relay syncs announcement and state events from source
391/// 4. State events trigger purgatory sync which fetches git data from source's clone URL
392/// 5. Verify bare repository is created and git data is synced
393/// 6. Verify git pushes are rejected (read-only mode)
394#[tokio::test]
395async fn test_archive_read_only_creates_bare_repo() {
396 // 1. Start source relay
397 let source_relay = TestRelay::start().await;
398 let keys = Keys::generate();
399 let identifier = "archive-test-repo";
400
401 // Pre-allocate archive relay port so we can include it in announcement
402 let archive_port = TestRelay::find_free_port();
403 let archive_domain = format!("127.0.0.1:{}", archive_port);
404
405 // 2. Create test repository locally with deterministic commit
406 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
407 let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
408 .expect("Failed to create test repo");
409
410 let npub = keys.public_key().to_bech32().expect("Failed to get npub");
411
412 // 3. Create and send announcement listing BOTH relays
413 // This ensures the archive relay will accept the state event when it syncs
414 let announcement = create_repo_announcement(
415 &keys,
416 &[&source_relay.domain(), &archive_domain],
417 identifier,
418 );
419
420 let source_client = Client::new(keys.clone());
421 source_client
422 .add_relay(source_relay.url())
423 .await
424 .expect("Failed to add source relay");
425 source_client.connect().await;
426
427 // Wait for connection
428 tokio::time::sleep(Duration::from_millis(500)).await;
429
430 // Send announcement to source relay
431 source_client
432 .send_event(&announcement)
433 .await
434 .expect("Failed to send announcement to source");
435
436 tokio::time::sleep(Duration::from_millis(200)).await;
437
438 // 4. Create and send state event
439 let clone_urls = [
440 format!(
441 "http://{}/{}/{}.git",
442 source_relay.domain(),
443 npub,
444 identifier
445 ),
446 format!("http://{}/{}/{}.git", archive_domain, npub, identifier),
447 ];
448 let relay_urls = [
449 source_relay.url().to_string(),
450 format!("ws://{}", archive_domain),
451 ];
452
453 let state_event = create_state_event(
454 &keys,
455 identifier,
456 &[("main", &commit_hash)],
457 &[],
458 &[&clone_urls[0], &clone_urls[1]],
459 &[&relay_urls[0], &relay_urls[1]],
460 )
461 .expect("Failed to create state event");
462
463 let state_event_id = state_event.id;
464
465 // Send state event to source relay (goes to purgatory - no git data yet)
466 source_client
467 .send_event(&state_event)
468 .await
469 .expect("Failed to send state event to source");
470
471 tokio::time::sleep(Duration::from_millis(200)).await;
472
473 // 5. Push git data to source relay
474 // The state event in purgatory authorizes this push
475 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
476 .expect("Push to source should succeed");
477
478 // After push, state event should be released from purgatory on source relay
479 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
480 .await
481 .expect("State event should be served on source relay after push");
482
483 // 6. Start archive relay with archive_all=true, archive_read_only=true, syncing from source
484 let archive_relay = TestRelay::start_with_archive_and_sync(
485 archive_port,
486 Some(source_relay.url().to_string()),
487 false, // negentropy enabled
488 true, // archive_all
489 true, // archive_read_only
490 )
491 .await;
492
493 // Wait for sync connection to establish
494 wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5))
495 .await
496 .expect("Sync connection should establish");
497
498 // 7. Wait for state event to be released on archive relay
499 // The sync should:
500 // a) Fetch the announcement and state event from source relay
501 // b) Accept announcement (creates bare repo structure) - via archive mode
502 // c) Put state event in purgatory (git data missing on archive relay)
503 // d) Fetch git data from source relay's clone URL
504 // e) Release the state event from purgatory
505
506 let found = wait_for_event_served(
507 archive_relay.url(),
508 &state_event_id,
509 Duration::from_secs(30), // Allow time for sync + git fetch
510 )
511 .await;
512
513 assert!(
514 found.is_ok(),
515 "State event should be served after sync fetches git data: {:?}",
516 found.err()
517 );
518
519 // 8. Verify bare repository was created
520 let repo_path = archive_relay
521 .git_data_path()
522 .join(format!("{}/{}.git", npub, identifier));
523
524 assert!(
525 repo_path.exists(),
526 "Bare repository should be created at {:?} for archive announcement",
527 repo_path
528 );
529
530 // 9. Verify it's a bare repository (check for config file with bare = true)
531 let config_path = repo_path.join("config");
532 assert!(
533 config_path.exists(),
534 "Git config should exist at {:?}",
535 config_path
536 );
537
538 let config_content = tokio::fs::read_to_string(&config_path)
539 .await
540 .expect("Should read git config");
541 assert!(
542 config_content.contains("bare = true"),
543 "Repository at {:?} should be bare (config should contain 'bare = true')",
544 repo_path
545 );
546
547 // 10. Verify refs are correct on archive relay
548 let ref_correct = check_ref_at_commit(
549 &archive_domain,
550 &npub,
551 identifier,
552 "refs/heads/main",
553 &commit_hash,
554 )
555 .await
556 .expect("Failed to check ref");
557
558 assert!(ref_correct, "main branch should point to correct commit");
559
560 // 11. Verify git pushes are rejected (read-only mode)
561 // Create a new commit in the source repo
562 tokio::fs::write(temp_dir.path().join("new_file.txt"), "new content")
563 .await
564 .expect("Failed to write new file");
565
566 let output = tokio::process::Command::new("git")
567 .args(["add", "."])
568 .current_dir(temp_dir.path())
569 .output()
570 .await
571 .expect("Failed to git add");
572 assert!(output.status.success());
573
574 let output = tokio::process::Command::new("git")
575 .args(["commit", "-m", "New commit for push test"])
576 .current_dir(temp_dir.path())
577 .output()
578 .await
579 .expect("Failed to git commit");
580 assert!(output.status.success());
581
582 // Try to push to archive relay (should fail in read-only mode)
583 let push_url = format!("http://{}/{}/{}.git", archive_domain, npub, identifier);
584 let output = tokio::process::Command::new("git")
585 .args(["push", &push_url, "main"])
586 .current_dir(temp_dir.path())
587 .output()
588 .await
589 .expect("Failed to run git push");
590
591 assert!(
592 !output.status.success(),
593 "Git push should be rejected in archive_read_only mode. stderr: {}",
594 String::from_utf8_lossy(&output.stderr)
595 );
596
597 // Cleanup
598 source_client.disconnect().await;
599 archive_relay.stop().await;
600 source_relay.stop().await;
601}
diff --git a/tests/archive_read_only.rs b/tests/archive_read_only.rs
deleted file mode 100644
index 069b3b7..0000000
--- a/tests/archive_read_only.rs
+++ /dev/null
@@ -1,417 +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
169 let found = wait_for_event_served(
170 archive_relay.url(),
171 &state_event_id,
172 Duration::from_secs(30), // Allow time for sync + git fetch
173 )
174 .await;
175
176 assert!(
177 found.is_ok(),
178 "State event should be served after sync fetches git data: {:?}",
179 found.err()
180 );
181
182 // 8. Verify bare repository was created
183 let repo_path = archive_relay
184 .git_data_path()
185 .join(format!("{}/{}.git", npub, identifier));
186
187 assert!(
188 repo_path.exists(),
189 "Bare repository should be created at {:?} for archive announcement",
190 repo_path
191 );
192
193 // 9. Verify it's a bare repository (check for config file with bare = true)
194 let config_path = repo_path.join("config");
195 assert!(
196 config_path.exists(),
197 "Git config should exist at {:?}",
198 config_path
199 );
200
201 let config_content = tokio::fs::read_to_string(&config_path)
202 .await
203 .expect("Should read git config");
204 assert!(
205 config_content.contains("bare = true"),
206 "Repository at {:?} should be bare (config should contain 'bare = true')",
207 repo_path
208 );
209
210 // 10. Verify refs are correct on archive relay
211 let ref_correct = check_ref_at_commit(
212 &archive_domain,
213 &npub,
214 identifier,
215 "refs/heads/main",
216 &commit_hash,
217 )
218 .await
219 .expect("Failed to check ref");
220
221 assert!(ref_correct, "main branch should point to correct commit");
222
223 // 11. Verify git pushes are rejected (read-only mode)
224 // Create a new commit in the source repo
225 tokio::fs::write(temp_dir.path().join("new_file.txt"), "new content")
226 .await
227 .expect("Failed to write new file");
228
229 let output = tokio::process::Command::new("git")
230 .args(["add", "."])
231 .current_dir(temp_dir.path())
232 .output()
233 .await
234 .expect("Failed to git add");
235 assert!(output.status.success());
236
237 let output = tokio::process::Command::new("git")
238 .args(["commit", "-m", "New commit for push test"])
239 .current_dir(temp_dir.path())
240 .output()
241 .await
242 .expect("Failed to git commit");
243 assert!(output.status.success());
244
245 // Try to push to archive relay (should fail in read-only mode)
246 let push_url = format!("http://{}/{}/{}.git", archive_domain, npub, identifier);
247 let output = tokio::process::Command::new("git")
248 .args(["push", &push_url, "main"])
249 .current_dir(temp_dir.path())
250 .output()
251 .await
252 .expect("Failed to run git push");
253
254 assert!(
255 !output.status.success(),
256 "Git push should be rejected in archive_read_only mode. stderr: {}",
257 String::from_utf8_lossy(&output.stderr)
258 );
259
260 // Cleanup
261 source_client.disconnect().await;
262 archive_relay.stop().await;
263 source_relay.stop().await;
264}
265
266/// Test that archive mode proactively syncs state events and git data
267/// when the source relay has state events available.
268///
269/// With StateOnly sync now implemented, purgatory announcements subscribe
270/// to state events from the relays listed in the announcement. This means
271/// the archive relay will:
272/// 1. Sync the announcement → purgatory → register as StateOnly in repo_sync_index
273/// 2. Subscribe to state events (kind 30618) on source relay
274/// 3. Receive the state event → purgatory sync triggered
275/// 4. Fetch git data from source relay's clone URL
276///
277/// This test verifies the full sync chain works end-to-end for archive mode.
278#[tokio::test]
279async fn test_archive_syncs_state_events_and_git_data_via_state_only_subscription() {
280 // 1. Start source relay
281 let source_relay = TestRelay::start().await;
282 let keys = Keys::generate();
283 let identifier = "archive-state-only-sync-repo";
284
285 // Pre-allocate archive relay port
286 let archive_port = TestRelay::find_free_port();
287 let archive_domain = format!("127.0.0.1:{}", archive_port);
288
289 // 2. Create test repository locally
290 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
291 let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
292 .expect("Failed to create test repo");
293
294 let npub = keys.public_key().to_bech32().expect("Failed to get npub");
295
296 // 3. Create and send announcement listing BOTH relays
297 // The archive relay will subscribe to state events on BOTH listed relays
298 let announcement = create_repo_announcement(
299 &keys,
300 &[&source_relay.domain(), &archive_domain],
301 identifier,
302 );
303
304 let source_client = Client::new(keys.clone());
305 source_client
306 .add_relay(source_relay.url())
307 .await
308 .expect("Failed to add source relay");
309 source_client.connect().await;
310
311 tokio::time::sleep(Duration::from_millis(500)).await;
312
313 // Send announcement to source relay (goes to purgatory)
314 source_client
315 .send_event(&announcement)
316 .await
317 .expect("Failed to send announcement to source");
318
319 tokio::time::sleep(Duration::from_millis(200)).await;
320
321 // 4. Create and send state event to source relay (goes to purgatory)
322 let clone_url = format!(
323 "http://{}/{}/{}.git",
324 source_relay.domain(),
325 npub,
326 identifier
327 );
328 let relay_url = source_relay.url().to_string();
329
330 let state_event = create_state_event(
331 &keys,
332 identifier,
333 &[("main", &commit_hash)],
334 &[],
335 &[&clone_url],
336 &[&relay_url],
337 )
338 .expect("Failed to create state event");
339
340 let state_event_id = state_event.id;
341
342 source_client
343 .send_event(&state_event)
344 .await
345 .expect("Failed to send state event to source");
346
347 tokio::time::sleep(Duration::from_millis(200)).await;
348
349 // 5. Push git data to source relay (promotes announcement and state event)
350 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
351 .expect("Push to source should succeed");
352
353 // Wait for state event to be promoted on source relay
354 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
355 .await
356 .expect("State event should be served on source relay after push");
357
358 // 6. Start archive relay - StateOnly subscription will proactively fetch state events
359 let archive_relay = TestRelay::start_with_archive_and_sync(
360 archive_port,
361 Some(source_relay.url().to_string()),
362 false,
363 true,
364 true,
365 )
366 .await;
367
368 // Wait for sync connection
369 wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5))
370 .await
371 .expect("Sync connection should establish");
372
373 // 7. Wait for state event to be served on archive relay
374 // The StateOnly subscription fetches the state event from source relay,
375 // which then triggers purgatory sync and git data fetch.
376 let found = wait_for_event_served(
377 archive_relay.url(),
378 &state_event_id,
379 Duration::from_secs(30), // Allow time for sync + git fetch
380 )
381 .await;
382
383 assert!(
384 found.is_ok(),
385 "State event should be served on archive after StateOnly subscription fetches it: {:?}",
386 found.err()
387 );
388
389 // 8. Verify bare repository was created
390 let repo_path = archive_relay
391 .git_data_path()
392 .join(format!("{}/{}.git", npub, identifier));
393
394 assert!(
395 repo_path.exists(),
396 "Bare repository should be created for archive announcement"
397 );
398
399 // 9. Verify git data was synced via the state event chain
400 let output = tokio::process::Command::new("git")
401 .args(["cat-file", "-t", &commit_hash])
402 .current_dir(&repo_path)
403 .output()
404 .await;
405
406 let commit_exists = output.map(|o| o.status.success()).unwrap_or(false);
407
408 assert!(
409 commit_exists,
410 "Git data should be synced via StateOnly subscription → state event → git fetch chain"
411 );
412
413 // Cleanup
414 source_client.disconnect().await;
415 archive_relay.stop().await;
416 source_relay.stop().await;
417}