don't
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(knot): refactor test helpers

Signed-off-by: tjh <x@tjh.dev>

tjh 26df3eb2 9fc8316c

+280 -199
+75 -89
crates/gordian-knot/src/public/xrpc/sh_tangled/repo/branches.rs
··· 92 92 93 93 #[cfg(test)] 94 94 mod tests { 95 - use std::fs; 96 - use std::path::Path; 97 - use std::process::Command; 98 - 99 - use gordian_lexicon::extra::objectid::ObjectId; 100 - use gordian_lexicon::sh_tangled::repo::refs::Signature; 101 - 102 95 use super::Branch; 103 96 use super::Output; 104 - use crate::model::knot_state::RepositoryProvider; 105 - use crate::test_helpers::TestClient; 106 - use crate::types::repository_spec::RepositoryKey; 107 - 108 - fn setup() { 109 - let test_repos = Path::new(env!("CARGO_MANIFEST_DIR")).join("test-repositories"); 110 - if !fs::exists(&test_repos).unwrap() { 111 - fs::create_dir_all(&test_repos).unwrap(); 112 - } 113 - 114 - let core = test_repos.join("did:plc:wshs7t2adsemcrrd4snkeqli/core"); 115 - if !fs::exists(&core).unwrap() { 116 - assert!( 117 - Command::new("git") 118 - .args(["clone", "--bare", "https://tangled.org/tangled.org/core"]) 119 - .arg(&core) 120 - .status() 121 - .unwrap() 122 - .success(), 123 - "failed to setup test repository" 124 - ); 125 - } 126 - 127 - eprintln!("resetting {core:?}"); 128 - assert!( 129 - Command::new("git") 130 - .args(["-C"]) 131 - .arg(core) 132 - .args(["reset", "--soft", "v1.12.0-alpha"]) 133 - .status() 134 - .unwrap() 135 - .success() 136 - ); 137 - } 97 + use crate::lexicon::extra::objectid::ObjectId; 98 + use crate::lexicon::sh_tangled::repo::refs::Signature; 99 + use crate::test_helpers::Client; 100 + use crate::test_helpers::TestState; 138 101 139 102 fn app() -> axum::Router { 140 - #[derive(Clone)] 141 - struct TestState; 142 - 143 - impl RepositoryProvider for TestState { 144 - fn path_for_repository(&self, key: &RepositoryKey) -> std::path::PathBuf { 145 - Path::new(env!("CARGO_MANIFEST_DIR")) 146 - .join("test-repositories") 147 - .join(key.owner_str()) 148 - .join(key.rkey()) 149 - } 150 - } 151 - 152 - setup(); 153 103 axum::Router::new() 154 104 .route(super::LXM, axum::routing::get(super::handle)) 155 - .with_state(TestState) 105 + .with_state(TestState::default()) 156 106 } 157 107 158 108 #[tokio::test] 159 109 async fn can_list_branches() { 160 - let client = TestClient::new(app()); 161 - 110 + let client = Client::new(app()); 162 111 let response = client 163 - .get("/sh.tangled.repo.branches?repo=did:plc:wshs7t2adsemcrrd4snkeqli/core&limit=500") 112 + .get("/sh.tangled.repo.branches?repo=did:plc:65gha4t3avpfpzmvpbwovss7/core&limit=500") 164 113 .await 165 114 .ok(); 166 115 167 116 let Output { branches } = response.json().await; 168 - let Branch { 169 - reference, 170 - commit, 171 - is_default, 172 - } = branches 173 - .iter() 174 - .find(|branch| branch.reference.name == "push-mozrrovxmlou") 175 - .expect("branch 'push-mozrrovxmlou' should exist in test repo"); 176 117 177 - let commit_oid: ObjectId = "214dc688ec8cccc0b58c4418c629ce16954d8442" 178 - .parse::<gix::ObjectId>() 179 - .unwrap() 180 - .into(); 118 + // Check the default branch. 119 + { 120 + let Branch { 121 + reference, 122 + commit, 123 + is_default, 124 + } = branches 125 + .iter() 126 + .find(|Branch { is_default, .. }| *is_default) 127 + .expect("default branch should exist in test repo"); 181 128 182 - assert_eq!(reference.hash, commit_oid); 183 - assert_eq!(commit.hash, commit_oid); 129 + let oid: ObjectId = "8f7e61bf51373c417c6f98339f4c7becb560d299".parse().unwrap(); 130 + let tree: ObjectId = "a4a26f1d63f4a92ce0d058d9341c07b4ce4a2dbf".parse().unwrap(); 131 + let parent: ObjectId = "5186fbfb4942f96f1e33f007127fd529b8b84b65".parse().unwrap(); 184 132 185 - assert_eq!( 186 - commit.author, 187 - Signature { 188 - name: "Anirudh Oppiliappan".to_string(), 189 - email: "anirudh@tangled.org".to_string(), 190 - when: time::macros::datetime!(2025-09-29 14:01:13 UTC) 191 - } 192 - ); 133 + assert!(is_default); 134 + assert_eq!(reference.name, "master"); 135 + assert_eq!(reference.hash, oid); 136 + assert_eq!( 137 + commit.author, 138 + Signature { 139 + name: "oppiliappan".to_string(), 140 + email: "me@oppi.li".to_string(), 141 + when: time::macros::datetime!(2026-03-02 09:18:45 UTC) 142 + } 143 + ); 144 + assert_eq!( 145 + commit.committer, 146 + Signature { 147 + name: "oppiliappan".to_string(), 148 + email: "me@oppi.li".to_string(), 149 + when: time::macros::datetime!(2026-03-02 09:22:43 UTC) 150 + } 151 + ); 152 + assert_eq!(commit.tree_hash, tree); 153 + assert_eq!(commit.parent_hashes.len(), 1); 154 + assert_eq!(commit.parent_hashes[0], parent); 155 + } 193 156 194 - assert_eq!( 195 - commit.committer, 196 - Signature { 197 - name: "Anirudh Oppiliappan".to_string(), 198 - email: "anirudh@tangled.org".to_string(), 199 - when: time::macros::datetime!(2025-09-29 17:28:35 +03) 200 - } 201 - ); 157 + // Pick a random branch to check. 158 + { 159 + let Branch { 160 + reference, 161 + commit, 162 + is_default, 163 + } = branches 164 + .iter() 165 + .find(|branch| branch.reference.name == "push-mozrrovxmlou") 166 + .expect("branch 'push-mozrrovxmlou' should exist in test repo"); 202 167 203 - assert!(!is_default); 168 + let commit_oid: ObjectId = "214dc688ec8cccc0b58c4418c629ce16954d8442".parse().unwrap(); 169 + 170 + assert!(!is_default); 171 + assert_eq!(reference.hash, commit_oid); 172 + assert_eq!(commit.hash, commit_oid); 173 + assert_eq!( 174 + commit.author, 175 + Signature { 176 + name: "Anirudh Oppiliappan".to_string(), 177 + email: "anirudh@tangled.org".to_string(), 178 + when: time::macros::datetime!(2025-09-29 14:01:13 UTC) 179 + } 180 + ); 181 + assert_eq!( 182 + commit.committer, 183 + Signature { 184 + name: "Anirudh Oppiliappan".to_string(), 185 + email: "anirudh@tangled.org".to_string(), 186 + when: time::macros::datetime!(2025-09-29 17:28:35 +03) 187 + } 188 + ); 189 + } 204 190 } 205 191 }
+4 -110
crates/gordian-knot/src/test_helpers.rs
··· 1 - //! Helpers for testing routes. 2 - //! 3 - //! This is mostly just stolen from: <https://github.com/tokio-rs/axum/blob/main/axum/src/test_helpers/test_client.rs> 4 - use std::convert::Infallible; 5 - use std::net::SocketAddr; 6 - use std::ops; 1 + mod test_client; 2 + mod test_state; 7 3 8 - use axum::extract::Request; 9 - use axum::response::Response; 10 - use futures_util::future::BoxFuture; 11 - use reqwest::StatusCode; 12 - use serde::de::DeserializeOwned; 13 - use tokio::net::TcpListener; 14 - use tower::Service; 15 - 16 - pub fn spawn_service<S>(service: S) -> SocketAddr 17 - where 18 - S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static, 19 - S::Future: Send, 20 - { 21 - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); 22 - listener.set_nonblocking(true).unwrap(); 23 - 24 - let addr = listener.local_addr().unwrap(); 25 - let listener = TcpListener::from_std(listener).unwrap(); 26 - 27 - tokio::spawn(async move { 28 - axum::serve(listener, tower::make::Shared::new(service)) 29 - .await 30 - .unwrap(); 31 - }); 32 - 33 - addr 34 - } 35 - 36 - pub struct TestClient { 37 - client: reqwest::Client, 38 - addr: SocketAddr, 39 - } 40 - 41 - impl TestClient { 42 - pub fn new<S>(service: S) -> Self 43 - where 44 - S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static, 45 - S::Future: Send, 46 - { 47 - let addr = spawn_service(service); 48 - let client = reqwest::Client::builder() 49 - .redirect(reqwest::redirect::Policy::none()) 50 - .build() 51 - .unwrap(); 52 - 53 - Self { client, addr } 54 - } 55 - 56 - pub fn get(&self, url: &str) -> RequestBuilder { 57 - self.client.get(format!("http://{}{url}", self.addr)).into() 58 - } 59 - } 60 - 61 - #[must_use] 62 - pub struct RequestBuilder(reqwest::RequestBuilder); 63 - 64 - impl RequestBuilder { 65 - async fn send(self) -> TestResponse { 66 - self.0.send().await.unwrap().into() 67 - } 68 - } 69 - 70 - impl From<reqwest::RequestBuilder> for RequestBuilder { 71 - fn from(value: reqwest::RequestBuilder) -> Self { 72 - Self(value) 73 - } 74 - } 75 - 76 - impl IntoFuture for RequestBuilder { 77 - type Output = TestResponse; 78 - 79 - type IntoFuture = BoxFuture<'static, Self::Output>; 80 - 81 - fn into_future(self) -> Self::IntoFuture { 82 - Box::pin(async { self.send().await }) 83 - } 84 - } 85 - 86 - pub struct TestResponse(reqwest::Response); 87 - 88 - impl TestResponse { 89 - pub fn ok(self) -> Self { 90 - assert_eq!(self.status(), StatusCode::OK); 91 - self 92 - } 93 - 94 - pub async fn json<T: DeserializeOwned>(self) -> T { 95 - self.0.json().await.unwrap() 96 - } 97 - } 98 - 99 - impl From<reqwest::Response> for TestResponse { 100 - fn from(value: reqwest::Response) -> Self { 101 - Self(value) 102 - } 103 - } 104 - 105 - impl ops::Deref for TestResponse { 106 - type Target = reqwest::Response; 107 - 108 - fn deref(&self) -> &Self::Target { 109 - &self.0 110 - } 111 - } 4 + pub use test_client::Client; 5 + pub use test_state::TestState;
+118
crates/gordian-knot/src/test_helpers/test_client.rs
··· 1 + //! This is mostly just stolen from: <https://github.com/tokio-rs/axum/blob/main/axum/src/test_helpers/test_client.rs> 2 + 3 + use std::convert::Infallible; 4 + use std::net::SocketAddr; 5 + 6 + use axum::extract::Request; 7 + use futures_util::future::BoxFuture; 8 + use reqwest::StatusCode; 9 + use serde::de::DeserializeOwned; 10 + use tokio::net::TcpListener; 11 + use tower::Service; 12 + 13 + pub fn spawn_service<S>(service: S) -> SocketAddr 14 + where 15 + S: Service<Request, Response = axum::response::Response, Error = Infallible> 16 + + Clone 17 + + Send 18 + + 'static, 19 + S::Future: Send, 20 + { 21 + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); 22 + listener.set_nonblocking(true).unwrap(); 23 + 24 + let addr = listener.local_addr().unwrap(); 25 + let listener = TcpListener::from_std(listener).unwrap(); 26 + 27 + tokio::spawn(async move { 28 + axum::serve(listener, tower::make::Shared::new(service)) 29 + .await 30 + .unwrap(); 31 + }); 32 + 33 + addr 34 + } 35 + 36 + pub struct Client { 37 + client: reqwest::Client, 38 + addr: SocketAddr, 39 + } 40 + 41 + impl Client { 42 + pub fn new<S>(service: S) -> Self 43 + where 44 + S: Service<Request, Response = axum::response::Response, Error = Infallible> 45 + + Clone 46 + + Send 47 + + 'static, 48 + S::Future: Send, 49 + { 50 + let addr = spawn_service(service); 51 + let client = reqwest::Client::builder() 52 + .redirect(reqwest::redirect::Policy::none()) 53 + .build() 54 + .unwrap(); 55 + 56 + Self { client, addr } 57 + } 58 + 59 + pub fn get(&self, url: &str) -> RequestBuilder { 60 + self.client.get(format!("http://{}{url}", self.addr)).into() 61 + } 62 + } 63 + 64 + #[must_use] 65 + pub struct RequestBuilder(reqwest::RequestBuilder); 66 + 67 + impl RequestBuilder { 68 + async fn send(self) -> Response { 69 + self.0.send().await.unwrap().into() 70 + } 71 + } 72 + 73 + impl From<reqwest::RequestBuilder> for RequestBuilder { 74 + fn from(value: reqwest::RequestBuilder) -> Self { 75 + Self(value) 76 + } 77 + } 78 + 79 + impl IntoFuture for RequestBuilder { 80 + type Output = Response; 81 + 82 + type IntoFuture = BoxFuture<'static, Self::Output>; 83 + 84 + fn into_future(self) -> Self::IntoFuture { 85 + Box::pin(async { self.send().await }) 86 + } 87 + } 88 + 89 + #[must_use] 90 + pub struct Response(reqwest::Response); 91 + 92 + impl Response { 93 + /// Assert the response status is [`StatusCode::OK`]. 94 + pub fn ok(self) -> Self { 95 + assert_eq!(self.status(), StatusCode::OK); 96 + self 97 + } 98 + 99 + pub fn status(&self) -> StatusCode { 100 + self.0.status() 101 + } 102 + 103 + pub async fn json<T: DeserializeOwned>(self) -> T { 104 + match self.0.json().await { 105 + Ok(value) => value, 106 + Err(error) => { 107 + let type_name = std::any::type_name::<T>(); 108 + panic!("response body should be valid JSON, deserializing to {type_name}: {error}"); 109 + } 110 + } 111 + } 112 + } 113 + 114 + impl From<reqwest::Response> for Response { 115 + fn from(value: reqwest::Response) -> Self { 116 + Self(value) 117 + } 118 + }
+83
crates/gordian-knot/src/test_helpers/test_state.rs
··· 1 + use std::borrow::Cow; 2 + use std::fs; 3 + use std::io; 4 + use std::path; 5 + use std::process; 6 + 7 + use url::Url; 8 + 9 + use crate::model::knot_state::RepositoryProvider; 10 + use crate::types::repository_spec::RepositoryKey; 11 + 12 + fn test_repositories_base() -> Cow<'static, path::Path> { 13 + Cow::Borrowed(path::Path::new(env!("OUT_DIR"))) 14 + } 15 + 16 + /// Clone a respository into `base_dir` and reset to the specified 17 + /// `reference`. 18 + /// 19 + /// The path for the repository is extracted from the path of the clone URL, 20 + /// and will not be cloned if it already exists. 21 + fn setup_test_repository(base_dir: &path::Path, source_url: &str, reference: &str) { 22 + let url = Url::parse(source_url).expect("repo source must be a valid URL"); 23 + let repo = url.path().trim_start_matches('/'); 24 + 25 + if let Err(error) = fs::create_dir_all(&base_dir) 26 + && error.kind() != io::ErrorKind::AlreadyExists 27 + { 28 + panic!("could not create test repository base directory: {error}"); 29 + } 30 + 31 + let path = base_dir.join(repo); 32 + if !fs::exists(&path).unwrap() { 33 + assert!( 34 + process::Command::new("git") 35 + .args(["clone", "--bare", url.as_str()]) 36 + .arg(&path) 37 + .stderr(process::Stdio::inherit()) 38 + .status() 39 + .expect("git command must be available") 40 + .success(), 41 + "could not clone test repository '{url}'" 42 + ); 43 + } 44 + 45 + // Reset HEAD to the specified reference, so we always have a known state to 46 + // compare to. 47 + assert!( 48 + process::Command::new("git") 49 + .args(["-C"]) 50 + .arg(&path) 51 + .args(["reset", "--soft", reference]) 52 + .stderr(process::Stdio::inherit()) 53 + .status() 54 + .expect("git command must be available") 55 + .success(), 56 + "could not reset test repository '{url}' to '{reference}" 57 + ); 58 + } 59 + 60 + #[derive(Clone)] 61 + pub struct TestState(path::PathBuf); 62 + 63 + impl Default for TestState { 64 + fn default() -> Self { 65 + fn setup() -> path::PathBuf { 66 + let base_dir = test_repositories_base(); 67 + setup_test_repository( 68 + &base_dir, 69 + "https://knot.tjh.dev/did:plc:65gha4t3avpfpzmvpbwovss7/core", 70 + "v1.12.0-alpha", 71 + ); 72 + base_dir.to_path_buf() 73 + } 74 + 75 + Self(setup()) 76 + } 77 + } 78 + 79 + impl RepositoryProvider for TestState { 80 + fn path_for_repository(&self, key: &RepositoryKey) -> path::PathBuf { 81 + self.0.join(key.owner_str()).join(key.rkey()) 82 + } 83 + }