don't
5
fork

Configure Feed

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

feat(knot): impl rejection, test, and docs for GitProtocol extractor

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

tjh cfe13e59 732b018d

+197 -28
+187 -18
crates/gordian-knot/src/extractors/git_protocol.rs
··· 1 - use std::ops; 1 + use std::ffi; 2 2 3 + use axum::extract::FromRequestParts; 3 4 use axum::extract::OptionalFromRequestParts; 5 + use axum::http::HeaderName; 6 + use axum::http::StatusCode; 7 + use axum::http::header::ToStrError; 4 8 use axum::http::request::Parts; 5 9 use axum::response::IntoResponse; 6 - use reqwest::header::ToStrError; 7 10 8 - /// Extract the "Git-Protocol" header from a request. 11 + const GIT_PROTOCOL: HeaderName = HeaderName::from_static("git-protocol"); 12 + 13 + /// Extractor for the "Git-Protocol" header. 14 + /// 15 + /// ```rust,no_run 16 + /// # use axum::Router; 17 + /// # use axum::routing::get; 18 + /// # use gordian_knot::extractors::GitProtocol; 19 + /// async fn handler(git_protocol: GitProtocol) { 20 + /// // ... 21 + /// } 22 + /// 23 + /// let app = Router::new().route("/", get(handler)); 24 + /// # let _: Router = app; 25 + /// ``` 26 + /// 27 + /// If the header is missing or cannot be converted to a [`String`] it will 28 + /// reject the request with a `400 Bad Request` response. 29 + /// 30 + /// Use `Option<GitProtocol>` to make the extractor optional. 31 + /// 32 + /// ```rust,no_run 33 + /// # use axum::Router; 34 + /// # use axum::routing::get; 35 + /// # use gordian_knot::extractors::GitProtocol; 36 + /// async fn handler(git_protocol: Option<GitProtocol>) { 37 + /// // ... 38 + /// } 39 + /// 40 + /// let app = Router::new().route("/", get(handler)); 41 + /// # let _: Router = app; 42 + /// ``` 43 + /// 44 + /// As an optional extractor it will still reject the request with a `400 Bad 45 + /// Request` response if the header value cannot be converted. 9 46 #[derive(Debug)] 10 47 pub struct GitProtocol(pub String); 11 48 12 - impl GitProtocol { 13 - pub fn as_str(&self) -> &str { 49 + impl AsRef<str> for GitProtocol { 50 + fn as_ref(&self) -> &str { 14 51 &self.0 15 52 } 16 53 } 17 54 18 - impl ops::Deref for GitProtocol { 19 - type Target = str; 20 - fn deref(&self) -> &Self::Target { 21 - &self.0 55 + // Impl AsRef<ffi::OsStr> so we can pass an `Option<GitProtocol>` to 56 + // `CommandExt::option_env()` without any awkward `Option::as_deref()` nonsense. 57 + impl AsRef<ffi::OsStr> for GitProtocol { 58 + fn as_ref(&self) -> &ffi::OsStr { 59 + ffi::OsStr::new(&self.0) 22 60 } 23 61 } 24 62 25 - impl<S> OptionalFromRequestParts<S> for GitProtocol 26 - where 27 - S: Send + Sync, 28 - { 63 + impl<S: Send + Sync> FromRequestParts<S> for GitProtocol { 64 + type Rejection = GitProtocolRejection; 65 + 66 + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> { 67 + let header_value = parts 68 + .headers 69 + .get(GIT_PROTOCOL) 70 + .map(|header_value| header_value.to_str()) 71 + .transpose()? 72 + .ok_or_else(GitProtocolRejection::missing)?; 73 + 74 + Ok(Self(header_value.to_string())) 75 + } 76 + } 77 + 78 + impl<S: Send + Sync> OptionalFromRequestParts<S> for GitProtocol { 29 79 type Rejection = GitProtocolRejection; 30 80 31 81 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>, Self::Rejection> { 32 82 let header_value = parts 33 83 .headers 34 - .get("Git-Protocol") 84 + .get(GIT_PROTOCOL) 35 85 .map(|header_value| header_value.to_str()) 36 - .transpose() 37 - .map_err(GitProtocolRejection)?; 86 + .transpose()?; 38 87 39 88 Ok(header_value.map(|value| Self(value.to_string()))) 40 89 } 41 90 } 42 91 43 92 #[derive(Debug)] 44 - pub struct GitProtocolRejection(ToStrError); 93 + pub struct GitProtocolRejection { 94 + message: &'static str, 95 + } 96 + 97 + impl GitProtocolRejection { 98 + const fn missing() -> Self { 99 + Self { 100 + message: r#"missing "Git-Protocol" header"#, 101 + } 102 + } 103 + } 104 + 105 + impl From<ToStrError> for GitProtocolRejection { 106 + fn from(_: ToStrError) -> Self { 107 + Self { 108 + message: r#"failed to convert "Git-Protocol" header to a str"#, 109 + } 110 + } 111 + } 45 112 46 113 impl IntoResponse for GitProtocolRejection { 47 114 fn into_response(self) -> axum::response::Response { 48 - todo!() 115 + (StatusCode::BAD_REQUEST, self.message).into_response() 116 + } 117 + } 118 + 119 + #[cfg(test)] 120 + mod tests { 121 + use core::error; 122 + 123 + use axum::Router; 124 + use axum::body::Body; 125 + use axum::http::Request; 126 + use axum::http::StatusCode; 127 + use axum::response::IntoResponse; 128 + use axum::response::Response; 129 + use tower::ServiceExt; 130 + 131 + use super::GIT_PROTOCOL; 132 + use super::GitProtocol; 133 + 134 + async fn optional_handler(git_protocol: Option<GitProtocol>) -> impl IntoResponse { 135 + git_protocol 136 + .map(|protocol| protocol.0.clone()) 137 + .unwrap_or_default() 138 + } 139 + 140 + async fn handler(git_protocol: GitProtocol) -> impl IntoResponse { 141 + git_protocol.0 142 + } 143 + 144 + fn app() -> Router { 145 + Router::new() 146 + .route("/requires_protocol", axum::routing::get(handler)) 147 + .route("/optional_protocol", axum::routing::get(optional_handler)) 148 + } 149 + 150 + fn request(path: &str, protocol: Option<&str>) -> Request<Body> { 151 + match protocol { 152 + Some(value) => Request::get(path).header(GIT_PROTOCOL, value), 153 + None => Request::get(path), 154 + } 155 + .body(Body::empty()) 156 + .unwrap() 157 + } 158 + 159 + async fn response_text(response: Response) -> String { 160 + String::from_utf8( 161 + axum::body::to_bytes(response.into_body(), 1024) 162 + .await 163 + .unwrap() 164 + .to_vec(), 165 + ) 166 + .unwrap() 167 + } 168 + 169 + #[tokio::test] 170 + async fn can_extract_required_header() -> Result<(), Box<dyn error::Error>> { 171 + let response = app() 172 + .oneshot(request("/requires_protocol", Some("version=2"))) 173 + .await?; 174 + assert_eq!(response.status(), StatusCode::OK); 175 + assert_eq!(response_text(response).await, "version=2"); 176 + Ok(()) 177 + } 178 + 179 + #[tokio::test] 180 + async fn rejects_missing_required_header() -> Result<(), Box<dyn error::Error>> { 181 + let response = app().oneshot(request("/requires_protocol", None)).await?; 182 + assert_eq!(response.status(), StatusCode::BAD_REQUEST); 183 + assert_eq!( 184 + response_text(response).await, 185 + r#"missing "Git-Protocol" header"# 186 + ); 187 + Ok(()) 188 + } 189 + 190 + #[tokio::test] 191 + async fn can_extract_header_optional() -> Result<(), Box<dyn error::Error>> { 192 + let response = app() 193 + .oneshot(request("/optional_protocol", Some("version=2"))) 194 + .await?; 195 + assert_eq!(response.status(), StatusCode::OK); 196 + assert_eq!(response_text(response).await, "version=2"); 197 + Ok(()) 198 + } 199 + 200 + #[tokio::test] 201 + async fn accepts_optional_header_missing() -> Result<(), Box<dyn error::Error>> { 202 + let response = app().oneshot(request("/optional_protocol", None)).await?; 203 + assert_eq!(response.status(), StatusCode::OK); 204 + assert_eq!(response_text(response).await, ""); 205 + Ok(()) 206 + } 207 + 208 + #[test] 209 + fn works_with_commandext_option_env() { 210 + fn test<C: crate::command::CommandExt>(mut command: C) { 211 + let val = GitProtocol("version=2".to_string()); 212 + command.option_env("GIT_PROTOCOL", Some(&val)); 213 + command.option_env("GIT_PROTOCOL", Some(val)); 214 + } 215 + 216 + test(std::process::Command::new("git")); 217 + test(tokio::process::Command::new("git")); 49 218 } 50 219 }
+4 -4
crates/gordian-knot/src/public/git/receive_pack.rs
··· 30 30 31 31 pub async fn advertise_receive_pack( 32 32 State(knot): State<Knot>, 33 - protocol: Option<GitProtocol>, 33 + git_protocol: Option<GitProtocol>, 34 34 request_id: Option<RequestId>, 35 35 request: Request, 36 36 ) -> Result<impl IntoResponse, super::Error> { ··· 57 57 58 58 let mut command: tokio::process::Command = repository.git().into(); 59 59 command 60 - .option_env("GIT_PROTOCOL", protocol.as_deref()) 60 + .option_env("GIT_PROTOCOL", git_protocol) 61 61 .option_env("X_REQUEST_ID", request_id) 62 62 .env(private::ENV_USER_DID, auth.iss.as_str()) 63 63 .args([ ··· 86 86 87 87 pub async fn receive_pack( 88 88 State(knot): State<Knot>, 89 - protocol: Option<GitProtocol>, 89 + git_protocol: Option<GitProtocol>, 90 90 request_id: Option<RequestId>, 91 91 request: Request, 92 92 ) -> Result<impl IntoResponse, super::Error> { ··· 119 119 120 120 let mut command: tokio::process::Command = repository.git().into(); 121 121 command 122 - .option_env("GIT_PROTOCOL", protocol.as_deref()) 122 + .option_env("GIT_PROTOCOL", git_protocol) 123 123 .option_env("X_REQUEST_ID", request_id) 124 124 .env(private::ENV_USER_DID, auth.iss.as_str()) 125 125 .args(["-c", &nonce_seed, "-c"])
+2 -2
crates/gordian-knot/src/public/git/upload_archive.rs
··· 19 19 20 20 pub async fn upload_archive( 21 21 State(knot): State<Knot>, 22 - protocol: Option<GitProtocol>, 22 + git_protocol: Option<GitProtocol>, 23 23 request_id: Option<RequestId>, 24 24 request: Request, 25 25 ) -> Result<impl IntoResponse, super::Error> { ··· 34 34 35 35 let mut command: tokio::process::Command = repository.git().into(); 36 36 command 37 - .option_env("GIT_PROTOCOL", protocol.as_deref()) 37 + .option_env("GIT_PROTOCOL", git_protocol) 38 38 .option_env("X_REQUEST_ID", request_id) 39 39 .args(["upload-archive"]) 40 40 .arg(repository.path())
+4 -4
crates/gordian-knot/src/public/git/upload_pack.rs
··· 27 27 /// operation. 28 28 pub async fn advertise_upload_pack( 29 29 State(knot): State<Knot>, 30 - protocol: Option<GitProtocol>, 30 + git_protocol: Option<GitProtocol>, 31 31 request_id: Option<RequestId>, 32 32 request: Request, 33 33 ) -> Result<impl IntoResponse, super::Error> { ··· 41 41 42 42 let mut command: tokio::process::Command = repository.git().into(); 43 43 command 44 - .option_env("GIT_PROTOCOL", protocol.as_deref()) 44 + .option_env("GIT_PROTOCOL", git_protocol) 45 45 .option_env("X_REQUEST_ID", request_id) 46 46 .args([ 47 47 "upload-pack", ··· 69 69 70 70 pub async fn upload_pack( 71 71 State(knot): State<Knot>, 72 - protocol: Option<GitProtocol>, 72 + git_protocol: Option<GitProtocol>, 73 73 request_id: Option<RequestId>, 74 74 request: Request, 75 75 ) -> Result<impl IntoResponse, super::Error> { ··· 84 84 85 85 let mut command: tokio::process::Command = repository.git().into(); 86 86 command 87 - .option_env("GIT_PROTOCOL", protocol.as_deref()) 87 + .option_env("GIT_PROTOCOL", git_protocol) 88 88 .option_env("X_REQUEST_ID", request_id) 89 89 .args(["upload-pack", "--strict", "--stateless-rpc"]) 90 90 .arg(repository.path())