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:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-19 14:25:27 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-19 15:04:00 +0000
commit9372ad649b6c438b1e4645f1dbe95c0f648bb80d (patch)
treea2f95431711bde64713aeb72f3a7dcc65ffe58cc /tests/archive_read_only.rs
parent16833501a1004a5a661a729e4fd2dbcbeaecd1d5 (diff)
fix: archive_read_only creates bare repos for archived announcements
Combined Accept and AcceptArchive match arms in builder.rs to ensure bare repositories are created for both cases. Previously AcceptArchive had duplicate code that didn't call ensure_bare_repository(). Also includes: - Config fix: effective_git_data_path() respects explicit paths with memory backend - TestRelay: Added git_data_path() and archive config support for testing - Integration tests for archive_read_only behavior
Diffstat (limited to 'tests/archive_read_only.rs')
-rw-r--r--tests/archive_read_only.rs368
1 files changed, 368 insertions, 0 deletions
diff --git a/tests/archive_read_only.rs b/tests/archive_read_only.rs
new file mode 100644
index 0000000..be6959b
--- /dev/null
+++ b/tests/archive_read_only.rs
@@ -0,0 +1,368 @@
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}