upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/git_remote_nostr/list.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/git_remote_nostr/list.rs')
-rw-r--r--tests/git_remote_nostr/list.rs261
1 files changed, 261 insertions, 0 deletions
diff --git a/tests/git_remote_nostr/list.rs b/tests/git_remote_nostr/list.rs
index 88bd3f7..71e7114 100644
--- a/tests/git_remote_nostr/list.rs
+++ b/tests/git_remote_nostr/list.rs
@@ -151,6 +151,267 @@ mod with_state_announcement {
151 Ok(()) 151 Ok(())
152 } 152 }
153 } 153 }
154 mod when_state_event_references_oids_not_on_git_server {
155
156 use super::*;
157
158 /// Regression test for the bug where a state event published ahead of
159 /// the corresponding `git push` caused `git clone` / `git fetch` to
160 /// fail with missing-object errors.
161 ///
162 /// The fix walks per-relay state events newest-first and picks the
163 /// first one whose every OID is either present on a git server or
164 /// already available locally. When no such event exists it falls back
165 /// to the raw git-server state.
166 #[tokio::test]
167 #[serial]
168 async fn falls_back_to_git_server_state() -> Result<()> {
169 // Build a real git repo that acts as the git server.
170 let source_git_repo = prep_git_repo()?;
171 std::fs::write(source_git_repo.dir.join("initial.md"), "initial")?;
172 let main_commit_id = source_git_repo.stage_and_commit("initial.md")?;
173
174 // Craft a state event that claims main points at a commit that has
175 // NOT been pushed to the git server yet (a plausible OID that does
176 // not exist anywhere).
177 let fake_oid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
178 let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d";
179 let state_event = nostr::event::EventBuilder::new(STATE_KIND, "")
180 .tags([
181 nostr::Tag::identifier(format!("{root_commit}-consider-it-random")),
182 nostr::Tag::custom(
183 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")),
184 vec!["ref: refs/heads/main".to_string()],
185 ),
186 nostr::Tag::custom(
187 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")),
188 vec![fake_oid.to_string()],
189 ),
190 ])
191 .sign_with_keys(&TEST_KEY_1_KEYS)
192 .unwrap();
193
194 let git_repo = prep_git_repo()?;
195 let events = vec![
196 generate_test_key_1_metadata_event("fred"),
197 generate_test_key_1_relay_list_event(),
198 generate_repo_ref_event_with_git_server(vec![
199 source_git_repo.dir.to_str().unwrap().to_string(),
200 ]),
201 state_event,
202 ];
203 // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57)
204 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
205 Relay::new(8051, None, None),
206 Relay::new(8052, None, None),
207 Relay::new(8053, None, None),
208 Relay::new(8055, None, None),
209 Relay::new(8056, None, None),
210 Relay::new(8057, None, None),
211 );
212 r51.events = events.clone();
213 r55.events = events;
214
215 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
216 let mut p = cli_tester_after_fetch(&git_repo)?;
217 p.send_line("list")?;
218 p.expect("git servers: listing refs...\r\n")?;
219 let res = p.expect_eventually("\r\n\r\n")?;
220 p.exit()?;
221 for p in [51, 52, 53, 55, 56, 57] {
222 relay::shutdown_relay(8000 + p)?;
223 }
224 let lines: HashSet<String> = res
225 .split("\r\n")
226 .map(|e| e.to_string())
227 .filter(|s| {
228 !s.contains("remote: ")
229 && !s.contains("Receiving objects")
230 && !s.contains("Resolving deltas")
231 && !s.contains("fetching /")
232 })
233 .collect();
234 // The fake OID must NOT appear – the list must fall back to
235 // what the git server actually has.
236 assert!(
237 !lines.iter().any(|l| l.contains(fake_oid)),
238 "fake OID from unresolvable state event must not be advertised; got: {lines:?}"
239 );
240 // The real commit that IS on the git server must be advertised.
241 assert!(
242 lines.contains(&format!("{main_commit_id} refs/heads/main")),
243 "real git-server commit must be advertised; got: {lines:?}"
244 );
245 Ok(())
246 });
247 // launch relays
248 let _ = join!(
249 r51.listen_until_close(),
250 r52.listen_until_close(),
251 r53.listen_until_close(),
252 r55.listen_until_close(),
253 r56.listen_until_close(),
254 r57.listen_until_close(),
255 );
256 cli_tester_handle.join().unwrap()?;
257 Ok(())
258 }
259 }
260
261 mod when_newer_relay_state_has_missing_oid_but_older_relay_state_is_resolvable {
262
263 use super::*;
264
265 /// Two relays serve different state events; two git servers each have
266 /// different OIDs.
267 ///
268 /// - Relay 55 (repo relay A): **newer** state event → main = fake_oid
269 /// (not on any git server)
270 /// - Relay 56 (repo relay B): **older** state event → main = commit_a
271 /// (present on git_server_1)
272 /// - git_server_1: main = commit_a
273 /// - git_server_2: main = commit_b (a different real commit)
274 ///
275 /// Expected: `list` skips the newer unresolvable event and advertises
276 /// `commit_a` from the older-but-resolvable state event.
277 #[tokio::test]
278 #[serial]
279 async fn uses_older_resolvable_state_event() -> Result<()> {
280 // --- git_server_1: has commit_a on main ---
281 let git_server_1 = prep_git_repo()?;
282 std::fs::write(git_server_1.dir.join("server1.md"), "server1")?;
283 let commit_a = git_server_1.stage_and_commit("server1.md")?;
284 let bare_server_1 = GitTestRepo::recreate_as_bare(&git_server_1)?;
285
286 // --- git_server_2: has commit_b on main (different commit) ---
287 let git_server_2 = prep_git_repo()?;
288 std::fs::write(git_server_2.dir.join("server2.md"), "server2")?;
289 let commit_b = git_server_2.stage_and_commit("server2.md")?;
290 let bare_server_2 = GitTestRepo::recreate_as_bare(&git_server_2)?;
291
292 assert_ne!(commit_a, commit_b);
293
294 let fake_oid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
295 let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d";
296 let identifier = format!("{root_commit}-consider-it-random");
297
298 // Older state event: main = commit_a (resolvable via git_server_1)
299 let older_state_event = make_event_old_or_change_user(
300 nostr::event::EventBuilder::new(STATE_KIND, "")
301 .tags([
302 nostr::Tag::identifier(identifier.clone()),
303 nostr::Tag::custom(
304 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")),
305 vec!["ref: refs/heads/main".to_string()],
306 ),
307 nostr::Tag::custom(
308 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")),
309 vec![commit_a.to_string()],
310 ),
311 ])
312 .sign_with_keys(&TEST_KEY_1_KEYS)
313 .unwrap(),
314 &TEST_KEY_1_KEYS,
315 60, // 60 seconds old
316 );
317
318 // Newer state event: main = fake_oid (NOT on any git server)
319 let newer_state_event = nostr::event::EventBuilder::new(STATE_KIND, "")
320 .tags([
321 nostr::Tag::identifier(identifier.clone()),
322 nostr::Tag::custom(
323 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")),
324 vec!["ref: refs/heads/main".to_string()],
325 ),
326 nostr::Tag::custom(
327 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")),
328 vec![fake_oid.to_string()],
329 ),
330 ])
331 .sign_with_keys(&TEST_KEY_1_KEYS)
332 .unwrap();
333
334 let git_repo = prep_git_repo()?;
335
336 // Base events (metadata + relay list + repo ref) go on both relays.
337 let repo_ref_event = generate_repo_ref_event_with_git_server(vec![
338 bare_server_1.dir.to_str().unwrap().to_string(),
339 bare_server_2.dir.to_str().unwrap().to_string(),
340 ]);
341 let base_events = vec![
342 generate_test_key_1_metadata_event("fred"),
343 generate_test_key_1_relay_list_event(),
344 repo_ref_event,
345 ];
346
347 // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57)
348 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
349 Relay::new(8051, None, None),
350 Relay::new(8052, None, None),
351 Relay::new(8053, None, None),
352 Relay::new(8055, None, None),
353 Relay::new(8056, None, None),
354 Relay::new(8057, None, None),
355 );
356 r51.events = base_events.clone();
357 // r55 (repo relay A) serves the newer state event with the fake OID
358 r55.events = [base_events.clone(), vec![newer_state_event]].concat();
359 // r56 (repo relay B) serves the older state event with commit_a
360 r56.events = [base_events, vec![older_state_event]].concat();
361
362 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
363 let mut p = cli_tester_after_fetch(&git_repo)?;
364 p.send_line("list")?;
365 p.expect("git servers: listing refs...\r\n")?;
366 let res = p.expect_eventually("\r\n\r\n")?;
367 p.exit()?;
368 for p in [51, 52, 53, 55, 56, 57] {
369 relay::shutdown_relay(8000 + p)?;
370 }
371 let lines: HashSet<String> = res
372 .split("\r\n")
373 .map(|e| e.to_string())
374 .filter(|s| {
375 !s.contains("remote: ")
376 && !s.contains("Receiving objects")
377 && !s.contains("Resolving deltas")
378 && !s.contains("fetching /")
379 })
380 .collect();
381 // The fake OID from the newer-but-unresolvable state event must
382 // NOT appear.
383 assert!(
384 !lines.iter().any(|l| l.contains(fake_oid)),
385 "fake OID from newer unresolvable state event must not be advertised; got: {lines:?}"
386 );
387 // commit_a from the older-but-resolvable state event must be
388 // advertised for main.
389 assert!(
390 lines.contains(&format!("{commit_a} refs/heads/main")),
391 "commit_a from older resolvable state event must be advertised; got: {lines:?}"
392 );
393 // commit_b (only on git_server_2, not referenced by any state
394 // event) must NOT appear for main.
395 assert!(
396 !lines.contains(&format!("{commit_b} refs/heads/main")),
397 "commit_b from git_server_2 must not override the chosen state event; got: {lines:?}"
398 );
399 Ok(())
400 });
401 // launch relays
402 let _ = join!(
403 r51.listen_until_close(),
404 r52.listen_until_close(),
405 r53.listen_until_close(),
406 r55.listen_until_close(),
407 r56.listen_until_close(),
408 r57.listen_until_close(),
409 );
410 cli_tester_handle.join().unwrap()?;
411 Ok(())
412 }
413 }
414
154 mod when_announcement_doesnt_match_git_server { 415 mod when_announcement_doesnt_match_git_server {
155 416
156 use super::*; 417 use super::*;