upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/grasp01/push_authorization.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 15:16:58 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 15:16:58 +0000
commit233feae6af4b291e4860a1ddf9df2ccf82e57c2f (patch)
tree569d798c72f3219d7d4a7035724045a624d0fc2a /grasp-audit/src/specs/grasp01/push_authorization.rs
parent3f18235e4d2b881b7bac543878cdf501abfe667e (diff)
fix(tests): update main project tests for grasp-audit API changes
Diffstat (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs')
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs606
1 files changed, 6 insertions, 600 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index cba9e69..d58247d 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -16,612 +16,18 @@
16//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test 16//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
17//! ``` 17//! ```
18 18
19use crate::{AuditClient, FixtureKind, TestContext, TestResult}; 19use crate::{
20 clone_repo, create_commit, setup_repo_for_maintainer, setup_repo_for_recursive_maintainer,
21 setup_repo_with_deterministic_commit, try_push, AuditClient, FixtureKind, TestContext,
22 TestResult,
23};
20use nostr_sdk::prelude::*; 24use nostr_sdk::prelude::*;
21use std::fs; 25use std::fs;
22use std::path::{Path, PathBuf}; 26use std::path::Path;
23use std::process::Command;
24 27
25/// Test suite for Push Authorization operations 28/// Test suite for Push Authorization operations
26pub struct PushAuthorizationTests; 29pub struct PushAuthorizationTests;
27 30
28/// Helper to clone a repository and return the path
29fn clone_repo(
30 relay_domain: &str,
31 npub: &str,
32 repo_id: &str,
33) -> Result<std::path::PathBuf, String> {
34 let temp_base = std::env::temp_dir();
35 let clone_dir_name = format!("grasp-push-test-{}", uuid::Uuid::new_v4());
36 let clone_path = temp_base.join(&clone_dir_name);
37 let _ = fs::remove_dir_all(&clone_path);
38
39 let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
40 let output = Command::new("git")
41 .args(["clone", &clone_url, clone_path.to_str().unwrap()])
42 .env("GIT_TERMINAL_PROMPT", "0")
43 .output()
44 .map_err(|e| format!("Failed to execute git clone: {}", e))?;
45
46 if !output.status.success() {
47 let stderr = String::from_utf8_lossy(&output.stderr);
48 return Err(format!("Git clone failed: {}", stderr));
49 }
50
51 // Configure git user
52 let _ = Command::new("git")
53 .args(["config", "user.email", "test@grasp-audit.local"])
54 .current_dir(&clone_path)
55 .output();
56 let _ = Command::new("git")
57 .args(["config", "user.name", "GRASP Audit Test"])
58 .current_dir(&clone_path)
59 .output();
60
61 Ok(clone_path)
62}
63
64/// Helper to create a commit and return the hash
65fn create_commit(clone_path: &Path, message: &str) -> Result<String, String> {
66 let test_file = clone_path.join(format!("test-{}.txt", uuid::Uuid::new_v4()));
67 fs::write(&test_file, message).map_err(|e| format!("Failed to write file: {}", e))?;
68
69 let filename = test_file.file_name().unwrap().to_str().unwrap();
70 let output = Command::new("git")
71 .args(["add", filename])
72 .current_dir(clone_path)
73 .output()
74 .map_err(|e| format!("Git add failed: {}", e))?;
75
76 if !output.status.success() {
77 return Err("Git add failed".to_string());
78 }
79
80 let output = Command::new("git")
81 .args(["commit", "-m", message])
82 .current_dir(clone_path)
83 .output()
84 .map_err(|e| format!("Git commit failed: {}", e))?;
85
86 if !output.status.success() {
87 return Err("Git commit failed".to_string());
88 }
89
90 let output = Command::new("git")
91 .args(["rev-parse", "HEAD"])
92 .current_dir(clone_path)
93 .output()
94 .map_err(|e| format!("Git rev-parse failed: {}", e))?;
95
96 if !output.status.success() {
97 return Err("Failed to get commit hash".to_string());
98 }
99
100 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
101}
102
103/// Variant of deterministic commit for different pubkey types
104/// Each variant produces a different but reproducible commit hash
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum CommitVariant {
107 /// Main pubkey variant - uses "Initial commit" content
108 Owner,
109 /// Maintainer pubkey variant - uses "Maintainer initial commit" content
110 Maintainer,
111 /// Recursive maintainer pubkey variant - uses "Recursive maintainer initial commit" content
112 RecursiveMaintainer,
113}
114
115impl CommitVariant {
116 /// Get the file content for this variant
117 pub fn file_content(&self) -> &'static str {
118 match self {
119 CommitVariant::Owner => "Initial commit",
120 CommitVariant::Maintainer => "Maintainer initial commit",
121 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit",
122 }
123 }
124
125 /// Get the commit message for this variant
126 pub fn commit_message(&self) -> &'static str {
127 match self {
128 CommitVariant::Owner => "Initial commit",
129 CommitVariant::Maintainer => "Maintainer initial commit",
130 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit",
131 }
132 }
133}
134
135/// Helper to create a deterministic commit (for fixtures)
136/// Uses fixed author/committer dates and disables GPG signing to ensure consistent hash
137///
138/// The variant parameter allows different commit hashes for different pubkey types:
139/// - Owner: uses the original DETERMINISTIC_COMMIT_HASH
140/// - Maintainer: uses MAINTAINER_DETERMINISTIC_COMMIT_HASH
141/// - RecursiveMaintainer: uses RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
142pub fn create_deterministic_commit_with_variant(clone_path: &Path, variant: CommitVariant) -> Result<String, String> {
143 let test_file = clone_path.join("test.txt");
144 let content = variant.file_content();
145 let message = variant.commit_message();
146
147 fs::write(&test_file, content).map_err(|e| format!("Failed to write file: {}", e))?;
148
149 let output = Command::new("git")
150 .args(["add", "test.txt"])
151 .current_dir(clone_path)
152 .output()
153 .map_err(|e| format!("Git add failed: {}", e))?;
154
155 if !output.status.success() {
156 return Err("Git add failed".to_string());
157 }
158
159 // Create deterministic commit with fixed dates and GPG disabled
160 let output = Command::new("git")
161 .args([
162 "-c", "commit.gpgsign=false",
163 "commit",
164 "-m", message,
165 ])
166 .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z")
167 .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z")
168 .current_dir(clone_path)
169 .output()
170 .map_err(|e| format!("Git commit failed: {}", e))?;
171
172 if !output.status.success() {
173 let stderr = String::from_utf8_lossy(&output.stderr);
174 return Err(format!("Git commit failed: {}", stderr));
175 }
176
177 let output = Command::new("git")
178 .args(["rev-parse", "HEAD"])
179 .current_dir(clone_path)
180 .output()
181 .map_err(|e| format!("Git rev-parse failed: {}", e))?;
182
183 if !output.status.success() {
184 return Err("Failed to get commit hash".to_string());
185 }
186
187 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
188}
189
190/// Helper to create a deterministic commit (for fixtures) - uses Owner variant
191/// Uses fixed author/committer dates and disables GPG signing to ensure consistent hash
192pub fn create_deterministic_commit(clone_path: &Path, _message: &str) -> Result<String, String> {
193 // Note: message parameter is ignored for backwards compatibility
194 // The Owner variant always uses "Initial commit"
195 create_deterministic_commit_with_variant(clone_path, CommitVariant::Owner)
196}
197
198/// Repository setup with deterministic commit
199/// This struct holds all the data needed for push authorization tests
200pub struct RepoSetup {
201 pub clone_path: PathBuf,
202 pub repo_id: String,
203 pub npub: String,
204 pub commit_hash: String,
205}
206
207impl Drop for RepoSetup {
208 fn drop(&mut self) {
209 let _ = fs::remove_dir_all(&self.clone_path);
210 }
211}
212
213/// Helper function to set up a repository with deterministic commit
214///
215/// This performs all the common setup steps needed for push authorization tests:
216/// 1. Gets RepoState fixture (repo announcement + state event with deterministic commit)
217/// 2. Extracts repo_id and npub
218/// 3. Verifies repo exists on disk
219/// 4. Clones the repository
220/// 5. Creates deterministic commit locally
221/// 6. Verifies commit hash matches expected
222/// 7. Creates and checks out main branch
223/// 8. Pushes the commit so the grasp server has the state in the state event
224///
225/// Returns RepoSetup which auto-cleans up the clone_path on drop
226pub async fn setup_repo_with_deterministic_commit(
227 client: &AuditClient,
228 git_data_dir: &Path,
229 relay_domain: &str,
230) -> Result<RepoSetup, String> {
231 use crate::DETERMINISTIC_COMMIT_HASH;
232
233 let ctx = TestContext::new(client);
234
235 // Get RepoState fixture (includes repo announcement and state event with deterministic commit)
236 let state_event = ctx.get_fixture(FixtureKind::RepoState).await
237 .map_err(|e| format!("Failed to create repo state fixture: {}", e))?;
238
239 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
240
241 // Extract repo_id from state event
242 let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d())
243 .and_then(|t| t.content())
244 .ok_or("Missing repo_id")?
245 .to_string();
246 let npub = state_event.pubkey.to_bech32()
247 .map_err(|e| format!("Failed to convert pubkey to bech32: {}", e))?;
248
249 // Verify repo exists
250 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
251 if !repo_path.exists() {
252 return Err(format!("Repo not found: {}", repo_path.display()));
253 }
254
255 // Clone repo
256 let clone_path = clone_repo(relay_domain, &npub, &repo_id)?;
257
258 // Create deterministic commit locally (this will be the root commit with no parent)
259 let commit_hash = create_deterministic_commit(&clone_path, "Initial commit")
260 .map_err(|e| {
261 let _ = fs::remove_dir_all(&clone_path);
262 e
263 })?;
264
265 // Verify commit hash matches expected deterministic hash
266 if commit_hash != DETERMINISTIC_COMMIT_HASH {
267 let _ = fs::remove_dir_all(&clone_path);
268 return Err(format!(
269 "Commit hash mismatch: got {}, expected {}",
270 commit_hash, DETERMINISTIC_COMMIT_HASH
271 ));
272 }
273
274 // Create main branch pointing to our deterministic commit
275 let branch_output = Command::new("git")
276 .args(["branch", "main"])
277 .current_dir(&clone_path)
278 .output()
279 .map_err(|e| {
280 let _ = fs::remove_dir_all(&clone_path);
281 format!("Failed to create main branch: {}", e)
282 })?;
283
284 if !branch_output.status.success() {
285 let _ = fs::remove_dir_all(&clone_path);
286 return Err(format!(
287 "Failed to create main branch: {}",
288 String::from_utf8_lossy(&branch_output.stderr)
289 ));
290 }
291
292 // Checkout main branch
293 let checkout_output = Command::new("git")
294 .args(["checkout", "main"])
295 .current_dir(&clone_path)
296 .output()
297 .map_err(|e| {
298 let _ = fs::remove_dir_all(&clone_path);
299 format!("Failed to checkout main branch: {}", e)
300 })?;
301
302 if !checkout_output.status.success() {
303 let _ = fs::remove_dir_all(&clone_path);
304 return Err(format!(
305 "Failed to checkout main branch: {}",
306 String::from_utf8_lossy(&checkout_output.stderr)
307 ));
308 }
309
310 // Push the commit to the server so the bare repo matches the state event
311 let push_output = Command::new("git")
312 .args(["push", "origin", "main"])
313 .current_dir(&clone_path)
314 .env("GIT_TERMINAL_PROMPT", "0")
315 .output()
316 .map_err(|e| {
317 let _ = fs::remove_dir_all(&clone_path);
318 format!("Failed to push to server: {}", e)
319 })?;
320
321 if !push_output.status.success() {
322 let _ = fs::remove_dir_all(&clone_path);
323 return Err(format!(
324 "Failed to push to server: {}",
325 String::from_utf8_lossy(&push_output.stderr)
326 ));
327 }
328
329 Ok(RepoSetup {
330 clone_path,
331 repo_id,
332 npub,
333 commit_hash,
334 })
335}
336
337/// Helper function to set up a maintainer repository with deterministic commit (state only)
338///
339/// This performs all the common setup steps needed for maintainer push authorization tests:
340/// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit)
341/// 2. Gets MaintainerState fixture (maintainer's state event ONLY - no announcement)
342/// 3. Extracts repo_id and owner npub
343/// 4. Verifies repo exists on disk
344/// 5. Clones the repository using owner's npub
345/// 6. Creates maintainer deterministic commit locally
346/// 7. Verifies commit hash matches expected
347/// 8. Creates and checks out main branch
348/// 9. Pushes the commit so the grasp server has the state in the state event
349///
350/// Note: This does NOT publish a maintainer announcement. For tests that need the
351/// maintainer announcement (like recursive maintainer tests), use setup_repo_for_recursive_maintainer
352/// which publishes MaintainerAnnouncement separately.
353///
354/// Returns RepoSetup which auto-cleans up the clone_path on drop
355pub async fn setup_repo_for_maintainer(
356 client: &AuditClient,
357 git_data_dir: &Path,
358 relay_domain: &str,
359) -> Result<RepoSetup, String> {
360 use crate::MAINTAINER_DETERMINISTIC_COMMIT_HASH;
361
362 let ctx = TestContext::new(client);
363
364 // Get RepoState fixture (includes owner's repo announcement and state event with owner's deterministic commit)
365 let state_event = ctx.get_fixture(FixtureKind::RepoState).await
366 .map_err(|e| format!("Failed to create repo state fixture: {}", e))?;
367
368 // Get MaintainerState fixture ONLY (no announcement - tests state-only authorization)
369 let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await
370 .map_err(|e| format!("Failed to create maintainer state fixture: {}", e))?;
371
372 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
373
374 // Extract repo_id from state event
375 let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d())
376 .and_then(|t| t.content())
377 .ok_or("Missing repo_id")?
378 .to_string();
379
380 // The npub is from the owner keys (the signer of the state event)
381 let npub = state_event.pubkey.to_bech32()
382 .map_err(|e| format!("Failed to convert owner pubkey to bech32: {}", e))?;
383
384 // Verify repo exists
385 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
386 if !repo_path.exists() {
387 return Err(format!("Owner repo not found: {}", repo_path.display()));
388 }
389
390 // Clone repo using owner's npub
391 let clone_path = clone_repo(relay_domain, &npub, &repo_id)?;
392
393 // Create maintainer deterministic commit locally (this will be the root commit with no parent)
394 let commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Maintainer)
395 .map_err(|e| {
396 let _ = fs::remove_dir_all(&clone_path);
397 e
398 })?;
399
400 // Verify commit hash matches expected maintainer deterministic hash
401 if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH {
402 let _ = fs::remove_dir_all(&clone_path);
403 return Err(format!(
404 "Maintainer commit hash mismatch: got {}, expected {}",
405 commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH
406 ));
407 }
408
409 // Create main branch pointing to our deterministic commit
410 let branch_output = Command::new("git")
411 .args(["branch", "main"])
412 .current_dir(&clone_path)
413 .output()
414 .map_err(|e| {
415 let _ = fs::remove_dir_all(&clone_path);
416 format!("Failed to create main branch: {}", e)
417 })?;
418
419 if !branch_output.status.success() {
420 let _ = fs::remove_dir_all(&clone_path);
421 return Err(format!(
422 "Failed to create main branch: {}",
423 String::from_utf8_lossy(&branch_output.stderr)
424 ));
425 }
426
427 // Checkout main branch
428 let checkout_output = Command::new("git")
429 .args(["checkout", "main"])
430 .current_dir(&clone_path)
431 .output()
432 .map_err(|e| {
433 let _ = fs::remove_dir_all(&clone_path);
434 format!("Failed to checkout main branch: {}", e)
435 })?;
436
437 if !checkout_output.status.success() {
438 let _ = fs::remove_dir_all(&clone_path);
439 return Err(format!(
440 "Failed to checkout main branch: {}",
441 String::from_utf8_lossy(&checkout_output.stderr)
442 ));
443 }
444
445 // Push the commit to the server so the bare repo matches the state event
446 let push_output = Command::new("git")
447 .args(["push", "origin", "main"])
448 .current_dir(&clone_path)
449 .env("GIT_TERMINAL_PROMPT", "0")
450 .output()
451 .map_err(|e| {
452 let _ = fs::remove_dir_all(&clone_path);
453 format!("Failed to push to server: {}", e)
454 })?;
455
456 if !push_output.status.success() {
457 let _ = fs::remove_dir_all(&clone_path);
458 return Err(format!(
459 "Failed to push to server: {}",
460 String::from_utf8_lossy(&push_output.stderr)
461 ));
462 }
463
464 Ok(RepoSetup {
465 clone_path,
466 repo_id,
467 npub,
468 commit_hash,
469 })
470}
471
472/// Helper function to set up a recursive maintainer repository with deterministic commit
473///
474/// This performs all the common setup steps needed for recursive maintainer push authorization tests:
475/// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit)
476/// 2. Gets MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag)
477/// 3. Gets MaintainerState fixture (maintainer's state event)
478/// 4. Gets RecursiveMaintainerRepoAndState fixture (recursive maintainer's repo - completes 3-level chain)
479/// 5. Extracts repo_id and owner npub
480/// 6. Verifies repo exists on disk
481/// 7. Clones the repository using owner's npub
482/// 8. Creates recursive maintainer deterministic commit locally
483/// 9. Verifies commit hash matches expected
484/// 10. Creates and checks out main branch
485/// 11. Pushes the commit so the grasp server has the state in the state event
486///
487/// Returns RepoSetup which auto-cleans up the clone_path on drop
488pub async fn setup_repo_for_recursive_maintainer(
489 client: &AuditClient,
490 git_data_dir: &Path,
491 relay_domain: &str,
492) -> Result<RepoSetup, String> {
493 use crate::RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH;
494
495 let ctx = TestContext::new(client);
496
497 // Get RepoState fixture (includes owner's repo announcement and state event)
498 let state_event = ctx.get_fixture(FixtureKind::RepoState).await
499 .map_err(|e| format!("Failed to create repo state fixture: {}", e))?;
500
501 // Get MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag)
502 let _maintainer_announcement = ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await
503 .map_err(|e| format!("Failed to create maintainer announcement fixture: {}", e))?;
504
505 // Get MaintainerState fixture (maintainer's state event)
506 let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await
507 .map_err(|e| format!("Failed to create maintainer state fixture: {}", e))?;
508
509 // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain)
510 let _recursive_maintainer_state = ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await
511 .map_err(|e| format!("Failed to create recursive maintainer repo state fixture: {}", e))?;
512
513 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
514
515 // Extract repo_id from owner's state event
516 let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d())
517 .and_then(|t| t.content())
518 .ok_or("Missing repo_id")?
519 .to_string();
520
521 // The npub is from the owner keys (the signer of the state event)
522 let npub = state_event.pubkey.to_bech32()
523 .map_err(|e| format!("Failed to convert owner pubkey to bech32: {}", e))?;
524
525 // Verify repo exists
526 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
527 if !repo_path.exists() {
528 return Err(format!("Owner repo not found: {}", repo_path.display()));
529 }
530
531 // Clone repo using owner's npub
532 let clone_path = clone_repo(relay_domain, &npub, &repo_id)?;
533
534 // Create recursive maintainer deterministic commit locally (this will be the root commit with no parent)
535 let commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::RecursiveMaintainer)
536 .map_err(|e| {
537 let _ = fs::remove_dir_all(&clone_path);
538 e
539 })?;
540
541 // Verify commit hash matches expected recursive maintainer deterministic hash
542 if commit_hash != RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH {
543 let _ = fs::remove_dir_all(&clone_path);
544 return Err(format!(
545 "Recursive maintainer commit hash mismatch: got {}, expected {}",
546 commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
547 ));
548 }
549
550 // Create main branch pointing to our deterministic commit
551 let branch_output = Command::new("git")
552 .args(["branch", "main"])
553 .current_dir(&clone_path)
554 .output()
555 .map_err(|e| {
556 let _ = fs::remove_dir_all(&clone_path);
557 format!("Failed to create main branch: {}", e)
558 })?;
559
560 if !branch_output.status.success() {
561 let _ = fs::remove_dir_all(&clone_path);
562 return Err(format!(
563 "Failed to create main branch: {}",
564 String::from_utf8_lossy(&branch_output.stderr)
565 ));
566 }
567
568 // Checkout main branch
569 let checkout_output = Command::new("git")
570 .args(["checkout", "main"])
571 .current_dir(&clone_path)
572 .output()
573 .map_err(|e| {
574 let _ = fs::remove_dir_all(&clone_path);
575 format!("Failed to checkout main branch: {}", e)
576 })?;
577
578 if !checkout_output.status.success() {
579 let _ = fs::remove_dir_all(&clone_path);
580 return Err(format!(
581 "Failed to checkout main branch: {}",
582 String::from_utf8_lossy(&checkout_output.stderr)
583 ));
584 }
585
586 // Push the commit to the server so the bare repo matches the state event
587 let push_output = Command::new("git")
588 .args(["push", "origin", "main"])
589 .current_dir(&clone_path)
590 .env("GIT_TERMINAL_PROMPT", "0")
591 .output()
592 .map_err(|e| {
593 let _ = fs::remove_dir_all(&clone_path);
594 format!("Failed to push to server: {}", e)
595 })?;
596
597 if !push_output.status.success() {
598 let _ = fs::remove_dir_all(&clone_path);
599 return Err(format!(
600 "Failed to push to server: {}",
601 String::from_utf8_lossy(&push_output.stderr)
602 ));
603 }
604
605 Ok(RepoSetup {
606 clone_path,
607 repo_id,
608 npub,
609 commit_hash,
610 })
611}
612
613/// Helper to attempt a push and return success/failure
614fn try_push(clone_path: &Path) -> Result<bool, String> {
615 let output = Command::new("git")
616 .args(["push", "origin", "main"])
617 .current_dir(clone_path)
618 .env("GIT_TERMINAL_PROMPT", "0")
619 .output()
620 .map_err(|e| format!("Failed to execute git push: {}", e))?;
621
622 Ok(output.status.success())
623}
624
625impl PushAuthorizationTests { 31impl PushAuthorizationTests {
626 /// Test that push is authorized when state event matches the commit 32 /// Test that push is authorized when state event matches the commit
627 /// 33 ///