upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 16:44:14 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 16:44:14 +0000
commit35199693690345039b2d2db2070bbd652e25328c (patch)
treee82465943fcece3d6d24f15f694f85d4df207bfd /grasp-audit
parentd2ac69816567f092fe0d4661723bc43778cb481b (diff)
test: test_head_set_after_state_event_with_existing_commit
currently failing as branch isn't pushed (we should auto create this branch as we have the ref)
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs353
1 files changed, 353 insertions, 0 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 24eae1d..a4731ae 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -390,6 +390,67 @@ fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String>
390 Ok(push_output.status.success()) 390 Ok(push_output.status.success())
391} 391}
392 392
393/// Queries the git smart HTTP info/refs endpoint to determine the default branch.
394///
395/// This parses the git-upload-pack service response to find the symref=HEAD capability
396/// which indicates what branch HEAD points to (i.e., the default branch).
397///
398/// # Arguments
399/// * `relay_domain` - The relay domain (e.g., "localhost:7000")
400/// * `npub` - The owner's npub (bech32 public key)
401/// * `repo_id` - The repository identifier
402///
403/// # Returns
404/// * `Ok(String)` - The default branch ref (e.g., "refs/heads/main")
405/// * `Err(String)` - Error message if request or parsing failed
406async fn get_default_branch_from_info_refs(
407 relay_domain: &str,
408 npub: &str,
409 repo_id: &str,
410) -> Result<String, String> {
411 let info_refs_url = format!(
412 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
413 relay_domain, npub, repo_id
414 );
415
416 let http_client = reqwest::Client::new();
417 let response = http_client
418 .get(&info_refs_url)
419 .send()
420 .await
421 .map_err(|e| format!("HTTP request failed: {}", e))?;
422
423 if !response.status().is_success() {
424 return Err(format!(
425 "info/refs returned status {} for URL: {}",
426 response.status(),
427 info_refs_url
428 ));
429 }
430
431 let body = response
432 .text()
433 .await
434 .map_err(|e| format!("Failed to read response body: {}", e))?;
435
436 // Parse the git smart HTTP response to find symref=HEAD:refs/heads/xxx
437 // The format is: capabilities are space-separated after the first NUL byte
438 // Example line: 0000000000000000000000000000000000000000 capabilities^{}\0symref=HEAD:refs/heads/master ...
439 for line in body.lines() {
440 if let Some(caps_start) = line.find('\0') {
441 let caps = &line[caps_start + 1..];
442 for cap in caps.split(' ') {
443 if cap.starts_with("symref=HEAD:") {
444 let branch = cap.trim_start_matches("symref=HEAD:");
445 return Ok(branch.to_string());
446 }
447 }
448 }
449 }
450
451 Err("No symref=HEAD capability found in info/refs response".to_string())
452}
453
393/// Checks if a ref exists on the remote. 454/// Checks if a ref exists on the remote.
394#[allow(dead_code)] 455#[allow(dead_code)]
395fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> { 456fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
@@ -450,6 +511,9 @@ impl PushAuthorizationTests {
450 ) 511 )
451 .await, 512 .await,
452 ); 513 );
514 results.add(
515 Self::test_head_set_after_state_event_with_existing_commit(client, relay_domain).await,
516 );
453 517
454 results 518 results
455 } 519 }
@@ -1883,6 +1947,295 @@ impl PushAuthorizationTests {
1883 1947
1884 TestResult::new(test_name, "GRASP-01", desc).pass() 1948 TestResult::new(test_name, "GRASP-01", desc).pass()
1885 } 1949 }
1950
1951 /// Test that HEAD is set after a state event is published with an existing commit
1952 ///
1953 /// GRASP-01: "MUST set repository HEAD per repository state announcement
1954 /// as soon as the git data related to that branch has been received."
1955 ///
1956 /// This test verifies the HEAD-setting behavior when:
1957 /// 1. A maintainer commit is pushed to the relay (git data exists)
1958 /// 2. A state event is published pointing to that commit with HEAD="refs/heads/develop"
1959 /// 3. The relay should update the repository's default branch to "develop"
1960 ///
1961 /// ## Fixture-First Pattern
1962 ///
1963 /// 1. **Generate**: Create TestContext and get RepoState + MaintainerState fixtures
1964 /// (both commits are pushed as part of the fixture setup)
1965 /// 2. **Send**: Push maintainer commit to relay first, then publish state event with HEAD=develop
1966 /// 3. **Verify**: Query info/refs to verify HEAD symref points to refs/heads/develop
1967 pub async fn test_head_set_after_state_event_with_existing_commit(
1968 client: &AuditClient,
1969 relay_domain: &str,
1970 ) -> TestResult {
1971 use std::process::Command;
1972
1973 let test_name = "test_head_set_after_state_event_with_existing_commit";
1974 let desc = "HEAD is set when state event published with existing commit";
1975
1976 // ============================================================
1977 // Step 1: GENERATE - Create TestContext and get fixtures
1978 // ============================================================
1979 let ctx = TestContext::new(client);
1980
1981 // Get RepoState fixture (owner's repo announcement + state event)
1982 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
1983 Ok(e) => e,
1984 Err(e) => {
1985 return TestResult::new(test_name, "GRASP-01", desc)
1986 .fail(format!("Failed to create RepoState fixture: {}", e));
1987 }
1988 };
1989
1990 // Extract repo_id and npub from owner's state event
1991 let repo_id = match state_event
1992 .tags
1993 .iter()
1994 .find(|t| t.kind() == TagKind::d())
1995 .and_then(|t| t.content())
1996 {
1997 Some(id) => id.to_string(),
1998 None => {
1999 return TestResult::new(test_name, "GRASP-01", desc)
2000 .fail("Missing repo_id in state event");
2001 }
2002 };
2003
2004 let npub = match state_event.pubkey.to_bech32() {
2005 Ok(n) => n,
2006 Err(e) => {
2007 return TestResult::new(test_name, "GRASP-01", desc)
2008 .fail(format!("Failed to convert pubkey to bech32: {}", e));
2009 }
2010 };
2011
2012 let _maintainer_ann_event = match ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await
2013 {
2014 Ok(e) => e,
2015 Err(e) => {
2016 return TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2017 "Failed to create MaintainerAnnouncement fixture: {}",
2018 e
2019 ));
2020 }
2021 };
2022
2023 let _maintainer_state_event = match ctx.get_fixture(FixtureKind::MaintainerState).await {
2024 Ok(e) => e,
2025 Err(e) => {
2026 return TestResult::new(test_name, "GRASP-01", desc)
2027 .fail(format!("Failed to create MaintainerState fixture: {}", e));
2028 }
2029 };
2030
2031 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2032
2033 // ============================================================
2034 // Step 2: SEND - First push maintainer commit so relay has the git data
2035 // ============================================================
2036 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
2037 Ok(p) => p,
2038 Err(e) => {
2039 return TestResult::new(test_name, "GRASP-01", desc)
2040 .fail(format!("Failed to clone repo: {}", e));
2041 }
2042 };
2043
2044 let cleanup = || {
2045 let _ = fs::remove_dir_all(&clone_path);
2046 };
2047
2048 // TODO - this should be pushed inside the MaintainerState fixture.
2049
2050 // Reset to orphan state and create deterministic root commit
2051 // Step 1: Create orphan branch (removes all history)
2052 let _ = Command::new("git")
2053 .args(["checkout", "--orphan", "main"])
2054 .current_dir(&clone_path)
2055 .output();
2056
2057 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
2058 let _ = Command::new("git")
2059 .args(["rm", "-rf", "--cached", "."])
2060 .current_dir(&clone_path)
2061 .output();
2062
2063 // Step 3: Create deterministic commit using Maintainer variant
2064 let commit_hash = match create_deterministic_commit_with_variant(
2065 &clone_path,
2066 CommitVariant::Maintainer,
2067 ) {
2068 Ok(h) => h,
2069 Err(e) => {
2070 cleanup();
2071 return TestResult::new(test_name, "GRASP-01", desc)
2072 .fail(format!("Failed to create maintainer commit: {}", e));
2073 }
2074 };
2075
2076 // Verify commit hash matches expected
2077 if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH {
2078 cleanup();
2079 return TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2080 "Maintainer commit hash mismatch: got {}, expected {}",
2081 commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH
2082 ));
2083 }
2084
2085 // Push the develop branch with the maintainer commit
2086 let push_output = Command::new("git")
2087 .args(["push", "origin", "main"])
2088 .current_dir(&clone_path)
2089 .env("GIT_TERMINAL_PROMPT", "0")
2090 .output();
2091
2092 match push_output {
2093 Err(e) => {
2094 cleanup();
2095 return TestResult::new(test_name, "GRASP-01", desc)
2096 .fail(format!("Failed to push develop branch: {}", e));
2097 }
2098 Ok(output) if !output.status.success() => {
2099 // this will fail when not in isolation - as the Recusive state will be the authorised state
2100 // but we need to do it here so the grasp server has the oid
2101 }
2102 _ => {}
2103 }
2104
2105 let _recursive_maintainer_ann_event = match ctx
2106 .get_fixture(FixtureKind::RecursiveMaintainerAnnouncement)
2107 .await
2108 {
2109 Ok(e) => e,
2110 Err(e) => {
2111 return TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2112 "Failed to create RecursiveMaintainerAnnouncement fixture: {}",
2113 e
2114 ));
2115 }
2116 };
2117
2118 let _recursive_maintainer_state_event =
2119 match ctx.get_fixture(FixtureKind::RecursiveMaintainerState).await {
2120 Ok(e) => e,
2121 Err(e) => {
2122 return TestResult::new(test_name, "GRASP-01", desc)
2123 .fail(format!("Failed to create MaintainerState fixture: {}", e));
2124 }
2125 };
2126
2127 // Verify commit hash matches expected
2128 if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH {
2129 cleanup();
2130 return TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2131 "Maintainer commit hash mismatch: got {}, expected {}",
2132 commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH
2133 ));
2134 }
2135
2136 // Reset to orphan state and create deterministic root commit
2137 // Step 1: Create orphan branch (removes all history)
2138 let _ = Command::new("git")
2139 .args(["checkout", "--orphan", "develop"])
2140 .current_dir(&clone_path)
2141 .output();
2142
2143 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
2144 let _ = Command::new("git")
2145 .args(["rm", "-rf", "--cached", "."])
2146 .current_dir(&clone_path)
2147 .output();
2148
2149 // ============================================================
2150 // Step 3: Publish state event with HEAD pointing to develop branch
2151 // ============================================================
2152
2153 // Create state event with HEAD=refs/heads/develop and develop branch pointing to maintainer commit
2154 let state_event = match client
2155 .event_builder(Kind::Custom(30618), "")
2156 .tag(Tag::identifier(&repo_id))
2157 .tag(Tag::custom(
2158 TagKind::custom("HEAD"),
2159 vec!["refs/heads/develop".to_string()],
2160 ))
2161 .tag(Tag::custom(
2162 TagKind::custom("refs/heads/develop"),
2163 vec![MAINTAINER_DETERMINISTIC_COMMIT_HASH.to_string()],
2164 ))
2165 .build(client.maintainer_keys())
2166 {
2167 Ok(e) => e,
2168 Err(e) => {
2169 cleanup();
2170 return TestResult::new(test_name, "GRASP-01", desc)
2171 .fail(format!("Failed to build state event: {}", e));
2172 }
2173 };
2174
2175 // Send the state event
2176 if let Err(e) = client.client().send_event(&state_event).await {
2177 cleanup();
2178 return TestResult::new(test_name, "GRASP-01", desc)
2179 .fail(format!("Failed to send state event: {}", e));
2180 }
2181
2182 // Wait for relay to process the state event
2183 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2184
2185 // // Now that state event is published, try pushing again if previous push failed
2186 // let push_output = Command::new("git")
2187 // .args(["push", "-f", "origin", "develop"])
2188 // .current_dir(&clone_path)
2189 // .env("GIT_TERMINAL_PROMPT", "0")
2190 // .output();
2191
2192 // match push_output {
2193 // Err(e) => {
2194 // cleanup();
2195 // return TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2196 // "Failed to push develop branch after state event: {}",
2197 // e
2198 // ));
2199 // }
2200 // Ok(output) if !output.status.success() => {
2201 // cleanup();
2202 // return TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2203 // "Push of develop branch rejected after state event: {}",
2204 // String::from_utf8_lossy(&output.stderr)
2205 // ));
2206 // }
2207 // _ => {}
2208 // }
2209
2210 cleanup();
2211
2212 // Wait a bit more for HEAD to be updated
2213 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
2214
2215 // ============================================================
2216 // Step 4: VERIFY - Query info/refs to check the default branch
2217 // ============================================================
2218 let default_branch =
2219 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await {
2220 Ok(branch) => branch,
2221 Err(e) => {
2222 return TestResult::new(test_name, "GRASP-01", desc)
2223 .fail(format!("Failed to get default branch: {}", e));
2224 }
2225 };
2226
2227 // Verify HEAD points to refs/heads/develop
2228 if default_branch == "refs/heads/develop" {
2229 TestResult::new(test_name, "GRASP-01", desc).pass()
2230 } else {
2231 TestResult::new(test_name, "GRASP-01", desc).fail(format!(
2232 "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \
2233 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \
2234 as soon as the git data related to that branch has been received.'",
2235 default_branch
2236 ))
2237 }
2238 }
1886} 2239}
1887 2240
1888#[cfg(test)] 2241#[cfg(test)]