don't
5
fork

Configure Feed

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

test(knot): add unit tests for XrpcRepository extractor

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

tjh 0ab31a46 26df3eb2

+112 -6
+91 -4
crates/gordian-knot/src/extract/repository.rs
··· 8 8 use crate::public::xrpc::XrpcQuery; 9 9 use crate::types::repository_spec::RepositoryKey; 10 10 11 - /// Extract a repository from the `repo` parameter of a `sh.tangled.repo.*` XRPC 12 - /// request. 11 + /// Extract a repository from the `repo` parameter of an XRPC request. 13 12 pub struct XrpcRepository(pub ThreadSafeRepository); 14 13 15 14 #[derive(Deserialize)] ··· 25 26 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 26 27 let XrpcQuery(Param { repo }) = XrpcQuery::from_request_parts(parts, state).await?; 27 28 28 - // Appview should always give use "did/name", let's just pretend its a 29 + // Appview should always pass repo as "did/name"; let's just pretend its a 29 30 // "did/rkey". 30 31 let key: RepositoryKey = repo.parse()?; 31 32 32 - // @review cardinality may be an issue. 33 33 let labels = [("did", key.owner.to_string()), ("name", key.rkey.clone())]; 34 34 metrics::counter!("knot_repo_requests_total", &labels).increment(1); 35 35 36 36 let repo = state.open_repository(key).await?; 37 37 38 38 Ok(Self(repo)) 39 + } 40 + } 41 + 42 + #[cfg(test)] 43 + mod tests { 44 + use crate::extract::repository::XrpcRepository; 45 + use crate::public::xrpc::XrpcError; 46 + use crate::test_helpers::Client; 47 + use crate::test_helpers::TestState; 48 + 49 + fn app() -> axum::Router { 50 + axum::Router::new() 51 + .route("/test", axum::routing::get(async |_: XrpcRepository| {})) 52 + .with_state(TestState::default()) 53 + } 54 + 55 + #[tokio::test] 56 + async fn can_extract_repo() { 57 + let client = Client::new(app()); 58 + let _ = client 59 + .get("/test?repo=did:plc:65gha4t3avpfpzmvpbwovss7/core") 60 + .await 61 + .ok(); 62 + } 63 + 64 + #[tokio::test] 65 + async fn deny_invalid_repo_with_invalid_request() { 66 + let client = Client::new(app()); 67 + for repo in [ 68 + "", 69 + "did:plc:65gha4t3avpfpzmvpbwovss7", 70 + "did:plc:65gha4t3avpfpzmvpbwovss7/", 71 + "did:plc:65gha4t3avpfpzmvpbwovss7/..", 72 + "../core", 73 + "../.", 74 + "/core", 75 + "core", 76 + ] { 77 + let XrpcError { error, .. } = client 78 + .get(&format!("/test?repo={repo}")) 79 + .await 80 + .bad_request() 81 + .json() 82 + .await; 83 + assert_eq!(error, "InvalidRequest"); 84 + } 85 + } 86 + 87 + #[tokio::test] 88 + async fn deny_non_did_owner_with_invalid_request() { 89 + let client = Client::new(app()); 90 + for repo in ["tjh.dev/core", "did.plc.asdf/core"] { 91 + let XrpcError { error, .. } = client 92 + .get(&format!("/test?repo={repo}")) 93 + .await 94 + .bad_request() 95 + .json() 96 + .await; 97 + assert_eq!(error, "InvalidRequest"); 98 + } 99 + } 100 + 101 + #[tokio::test] 102 + async fn deny_missing_repo_with_repo_not_found() { 103 + let client = Client::new(app()); 104 + for repo in [ 105 + "did:plc:65gha4t3avpfpzmvpbwovss7/coasdfre", 106 + "did:web:tjh.dev/core", 107 + ] { 108 + let XrpcError { error, .. } = client 109 + .get(&format!("/test?repo={repo}")) 110 + .await 111 + .not_found() 112 + .json() 113 + .await; 114 + assert_eq!(error, "RepoNotFound"); 115 + } 116 + } 117 + 118 + #[tokio::test] 119 + async fn deny_missing_parameter_with_invalid_request() { 120 + let client = Client::new(app()); 121 + let XrpcError { error, .. } = client 122 + .get(&format!("/test")) 123 + .await 124 + .bad_request() 125 + .json() 126 + .await; 127 + assert_eq!(error, "InvalidRequest"); 39 128 } 40 129 }
+21 -2
crates/gordian-knot/src/test_helpers/test_client.rs
··· 96 96 self 97 97 } 98 98 99 + /// Assert the response status is [`StatusCode::BAD_REQUEST`]. 100 + pub fn bad_request(self) -> Self { 101 + assert_eq!(self.status(), StatusCode::BAD_REQUEST); 102 + self 103 + } 104 + 105 + /// Assert the response status is [`StatusCode::NOT_FOUND`]. 106 + pub fn not_found(self) -> Self { 107 + assert_eq!(self.status(), StatusCode::NOT_FOUND); 108 + self 109 + } 110 + 99 111 pub fn status(&self) -> StatusCode { 100 112 self.0.status() 101 113 } 102 114 115 + pub async fn text(self) -> String { 116 + self.0.text().await.unwrap() 117 + } 118 + 103 119 pub async fn json<T: DeserializeOwned>(self) -> T { 104 - match self.0.json().await { 120 + let body = self.text().await; 121 + match serde_json::from_str(&body) { 105 122 Ok(value) => value, 106 123 Err(error) => { 107 124 let type_name = std::any::type_name::<T>(); 108 - panic!("response body should be valid JSON, deserializing to {type_name}: {error}"); 125 + panic!( 126 + "response body should be valid JSON, deserializing to {type_name}: {error}\nbody: {body}" 127 + ); 109 128 } 110 129 } 111 130 }