A better Rust ATProto crate
1use axum::{Json, Router, response::IntoResponse}; 2use axum_test::TestServer; 3use jacquard_axum::{ExtractXrpc, IntoRouter}; 4use jacquard_common::types::string::Did; 5use serde::{Deserialize, Serialize}; 6use std::collections::BTreeMap; 7 8// Mock XRPC endpoint for testing 9#[derive(Debug, Clone, Serialize, Deserialize)] 10struct TestQueryRequest<'a> { 11 #[serde(borrow)] 12 did: Did<'a>, 13 #[serde(default)] 14 limit: Option<u32>, 15} 16 17impl 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)] 29struct 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 36impl 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")] 49struct TestError; 50 51impl jacquard::IntoStatic for TestError { 52 type Output = TestError; 53 54 fn into_static(self) -> Self::Output { 55 self 56 } 57} 58 59impl 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 66impl 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 72impl 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 79async 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] 87async 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] 107async 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] 127async 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}