A better Rust ATProto crate

added some basic extractor tests, verified deserialization changes removes need for extractor url encoding workaround.

Orual d478b2fb 4b657b11

Changed files
+146 -17
crates
jacquard-axum
+2 -17
crates/jacquard-axum/src/lib.rs
··· 106 106 } 107 107 XrpcMethod::Query => { 108 108 if let Some(path_query) = req.uri().path_and_query() { 109 - // TODO: see if we can eliminate this now that we've fixed the deserialize impls for string types 110 - let query = 111 - urlencoding::decode(path_query.query().unwrap_or("")).map_err(|e| { 112 - ( 113 - StatusCode::BAD_REQUEST, 114 - [( 115 - header::CONTENT_TYPE, 116 - HeaderValue::from_static("application/json"), 117 - )], 118 - Json(json!({ 119 - "error": "InvalidRequest", 120 - "message": format!("failed to decode request: {}", e) 121 - })), 122 - ) 123 - .into_response() 124 - })?; 109 + let query = path_query.query().unwrap_or(""); 125 110 let value: R::Request<'_> = serde_html_form::from_str::<R::Request<'_>>( 126 - query.as_ref(), 111 + query, 127 112 ) 128 113 .map_err(|e| { 129 114 (
+144
crates/jacquard-axum/tests/extractor_tests.rs
··· 1 + use axum::{Json, Router, response::IntoResponse}; 2 + use axum_test::TestServer; 3 + use jacquard_axum::{ExtractXrpc, IntoRouter}; 4 + use jacquard_common::types::string::Did; 5 + use serde::{Deserialize, Serialize}; 6 + use std::collections::BTreeMap; 7 + 8 + // Mock XRPC endpoint for testing 9 + #[derive(Debug, Clone, Serialize, Deserialize)] 10 + struct TestQueryRequest<'a> { 11 + #[serde(borrow)] 12 + did: Did<'a>, 13 + #[serde(default)] 14 + limit: Option<u32>, 15 + } 16 + 17 + impl jacquard::IntoStatic for TestQueryRequest<'_> { 18 + type Output = TestQueryRequest<'static>; 19 + 20 + fn into_static(self) -> Self::Output { 21 + TestQueryRequest { 22 + did: self.did.into_static(), 23 + limit: self.limit, 24 + } 25 + } 26 + } 27 + 28 + #[derive(Debug, Clone, Serialize, Deserialize)] 29 + struct TestQueryResponse<'a> { 30 + #[serde(borrow)] 31 + did: Did<'a>, 32 + #[serde(skip_serializing_if = "BTreeMap::is_empty", default)] 33 + extra_data: BTreeMap<String, serde_json::Value>, 34 + } 35 + 36 + impl jacquard::IntoStatic for TestQueryResponse<'_> { 37 + type Output = TestQueryResponse<'static>; 38 + 39 + fn into_static(self) -> Self::Output { 40 + TestQueryResponse { 41 + did: self.did.into_static(), 42 + extra_data: self.extra_data, 43 + } 44 + } 45 + } 46 + 47 + #[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)] 48 + #[error("test error")] 49 + struct TestError; 50 + 51 + impl jacquard::IntoStatic for TestError { 52 + type Output = TestError; 53 + 54 + fn into_static(self) -> Self::Output { 55 + self 56 + } 57 + } 58 + 59 + impl jacquard::xrpc::XrpcResp for TestQueryResponse<'_> { 60 + const NSID: &'static str = "com.example.test.query"; 61 + const ENCODING: &'static str = "application/json"; 62 + type Output<'a> = TestQueryResponse<'a>; 63 + type Err<'a> = TestError; 64 + } 65 + 66 + impl jacquard::xrpc::XrpcRequest for TestQueryRequest<'_> { 67 + const NSID: &'static str = "com.example.test.query"; 68 + const METHOD: jacquard::xrpc::XrpcMethod = jacquard::xrpc::XrpcMethod::Query; 69 + type Response = TestQueryResponse<'static>; 70 + } 71 + 72 + impl jacquard::xrpc::XrpcEndpoint for TestQueryRequest<'_> { 73 + const PATH: &'static str = "/xrpc/com.example.test.query"; 74 + const METHOD: jacquard::xrpc::XrpcMethod = jacquard::xrpc::XrpcMethod::Query; 75 + type Request<'a> = TestQueryRequest<'a>; 76 + type Response = TestQueryResponse<'static>; 77 + } 78 + 79 + async fn test_handler(ExtractXrpc(req): ExtractXrpc<TestQueryRequest<'_>>) -> impl IntoResponse { 80 + Json(TestQueryResponse { 81 + did: req.did, 82 + extra_data: BTreeMap::new(), 83 + }) 84 + } 85 + 86 + #[tokio::test] 87 + async fn test_url_encoded_did_in_query_params() { 88 + let app = Router::new().merge(TestQueryRequest::into_router(test_handler)); 89 + 90 + let server = TestServer::new(app).unwrap(); 91 + 92 + // Test with URL-encoded DID (colons should be encoded as %3A) 93 + let response = server 94 + .get("/xrpc/com.example.test.query?did=did%3Aplc%3A123abc") 95 + .await; 96 + 97 + response.assert_status_ok(); 98 + 99 + let body_text = response.text(); 100 + println!("URL-encoded test response: {}", body_text); 101 + let body: TestQueryResponse = serde_json::from_str(&body_text).unwrap(); 102 + println!("Parsed DID: {}", body.did.as_str()); 103 + assert_eq!(body.did.as_str(), "did:plc:123abc"); 104 + } 105 + 106 + #[tokio::test] 107 + async fn test_unencoded_did_in_query_params() { 108 + let app = Router::new().merge(TestQueryRequest::into_router(test_handler)); 109 + 110 + let server = TestServer::new(app).unwrap(); 111 + 112 + // Test with unencoded DID (some clients might send it unencoded) 113 + let response = server 114 + .get("/xrpc/com.example.test.query?did=did:plc:123abc") 115 + .await; 116 + 117 + response.assert_status_ok(); 118 + 119 + let body_text = response.text(); 120 + println!("Unencoded test response: {}", body_text); 121 + let body: TestQueryResponse = serde_json::from_str(&body_text).unwrap(); 122 + println!("Parsed DID: {}", body.did.as_str()); 123 + assert_eq!(body.did.as_str(), "did:plc:123abc"); 124 + } 125 + 126 + #[tokio::test] 127 + async fn test_multiple_params_with_encoded_did() { 128 + let app = Router::new().merge(TestQueryRequest::into_router(test_handler)); 129 + 130 + let server = TestServer::new(app).unwrap(); 131 + 132 + // Test with multiple params including URL-encoded DID 133 + let response = server 134 + .get("/xrpc/com.example.test.query?did=did%3Aweb%3Aexample.com&limit=50") 135 + .await; 136 + 137 + response.assert_status_ok(); 138 + 139 + let body_text = response.text(); 140 + println!("Multiple params test response: {}", body_text); 141 + let body: TestQueryResponse = serde_json::from_str(&body_text).unwrap(); 142 + println!("Parsed DID: {}", body.did.as_str()); 143 + assert_eq!(body.did.as_str(), "did:web:example.com"); 144 + }