Something went wrong. Try again.
Chris’s PDS implementation
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294//! Standard extractor tuples keep request parsing in Axum's declared order.
use axum::{ Json, extract::{OriginalUri, Query, State}, http::HeaderMap,};
use crate::AppState;
pub(crate) type JsonRequest<T> = (State<AppState>, HeaderMap, Json<T>);pub(crate) type HeaderQuery<T> = (State<AppState>, HeaderMap, Query<T>);pub(crate) type QueryRequest<T> = (State<AppState>, Query<T>);pub(crate) type SpaceQuery<T> = (State<AppState>, OriginalUri, HeaderMap, Query<T>);pub(crate) type SpaceJson<T> = (State<AppState>, OriginalUri, HeaderMap, Json<T>);
#[cfg(test)]mod tests { use std::{ sync::{ Arc, atomic::{AtomicUsize, Ordering}, }, time::Duration, };
use axum::{ Extension, Router, body::{Body, to_bytes}, extract::DefaultBodyLimit, http::{Request, StatusCode, header::CONTENT_TYPE}, routing::{get, post}, }; use serde::Deserialize; use serde_json::{Value, json}; use tower::ServiceExt as _;
use super::*; use crate::{ blob_store::MemoryBlobStore, config::{AccountConfig, Config, DeploymentMode, HttpOrigin}, database::Database, events::NoopEventSink, };
type Calls = Arc<AtomicUsize>;
#[derive(Deserialize)] struct Input { number: u8, }
#[tokio::test] async fn extractor_aliases_preserve_native_values_rejections_and_handler_entry() { let calls = Arc::new(AtomicUsize::new(0)); let state = state(); let routers = [ Router::new() .route("/json", post(native_json)) .route("/header-query", get(native_header_query)) .route("/query", get(native_query)) .route("/space-query", get(native_space_query)) .route("/space-json", post(native_space_json)), Router::new() .route("/json", post(aliased_json)) .route("/header-query", get(aliased_header_query)) .route("/query", get(aliased_query)) .route("/space-query", get(aliased_space_query)) .route("/space-json", post(aliased_space_json)), ] .map(|router| { Router::new() .nest("/nested", router) .layer(DefaultBodyLimit::max(32)) .layer(Extension(calls.clone())) .with_state(state.clone()) });
for path in ["json", "space-json"] { for (body, content_type, status) in [ (r#"{"number":7}"#, Some("application/json"), StatusCode::OK), ("{", Some("application/json"), StatusCode::BAD_REQUEST), ( r#"{"number":"wrong"}"#, Some("application/json"), StatusCode::UNPROCESSABLE_ENTITY, ), (r#"{"number":7}"#, None, StatusCode::UNSUPPORTED_MEDIA_TYPE), ( r#"{"number":7,"padding":"abcdefghijklmnopqrstuvwxyz"}"#, Some("application/json"), StatusCode::PAYLOAD_TOO_LARGE, ), ] { let mut request = Request::post(format!("/nested/{path}?trace=original")) .header("x-test", "preserved"); if let Some(content_type) = content_type { request = request.header(CONTENT_TYPE, content_type); } assert_equivalent(&routers, &calls, request.body(body).unwrap(), status).await; } } for path in ["query", "header-query", "space-query"] { for (query, status) in [ ("number=7", StatusCode::OK), ("number=wrong", StatusCode::BAD_REQUEST), ("other=7", StatusCode::BAD_REQUEST), ] { let request = Request::get(format!("/nested/{path}?{query}")) .header("x-test", "preserved") .body("") .unwrap(); assert_equivalent(&routers, &calls, request, status).await; } } }
async fn assert_equivalent( routers: &[Router; 2], calls: &AtomicUsize, request: Request<&'static str>, status: StatusCode, ) { let before = calls.load(Ordering::SeqCst); let mut responses = Vec::new(); for (index, router) in routers.iter().enumerate() { let response = router .clone() .oneshot(request.clone().map(Body::from)) .await .unwrap(); assert_eq!(response.status(), status); let content_type = response.headers().get(CONTENT_TYPE).cloned(); let bytes = to_bytes(response.into_body(), 1024).await.unwrap(); assert_eq!( calls.load(Ordering::SeqCst), before + (index + 1) * usize::from(status.is_success()) ); responses.push((content_type, bytes)); } assert_eq!(responses[0], responses[1]); if !status.is_success() { return; } let body: Value = serde_json::from_slice(&responses[0].1).unwrap(); assert_eq!(body["did"], "did:web:alice.test"); assert_eq!(body["number"], 7); assert_eq!( body["header"], if request.uri().path() == "/nested/query" { Value::Null } else { json!("preserved") } ); assert_eq!( body["uri"], if request.uri().path().contains("space-") { json!(request.uri().to_string()) } else { Value::Null } ); }
fn observed( state: &AppState, headers: Option<HeaderMap>, uri: Option<OriginalUri>, number: u8, calls: &AtomicUsize, ) -> Json<Value> { calls.fetch_add(1, Ordering::SeqCst); Json(json!({ "did": state.config.account.did, "header": headers.and_then(|headers| headers.get("x-test").and_then(|header| header.to_str().ok()).map(str::to_owned)), "uri": uri.map(|uri| uri.0.to_string()), "number": number, })) }
async fn native_json( Extension(calls): Extension<Calls>, State(state): State<AppState>, headers: HeaderMap, Json(input): Json<Input>, ) -> Json<Value> { observed(&state, Some(headers), None, input.number, &calls) }
async fn aliased_json( Extension(calls): Extension<Calls>, (State(state), headers, Json(input)): JsonRequest<Input>, ) -> Json<Value> { observed(&state, Some(headers), None, input.number, &calls) }
async fn native_header_query( Extension(calls): Extension<Calls>, State(state): State<AppState>, headers: HeaderMap, Query(input): Query<Input>, ) -> Json<Value> { observed(&state, Some(headers), None, input.number, &calls) }
async fn aliased_header_query( Extension(calls): Extension<Calls>, (State(state), headers, Query(input)): HeaderQuery<Input>, ) -> Json<Value> { observed(&state, Some(headers), None, input.number, &calls) }
async fn native_query( Extension(calls): Extension<Calls>, State(state): State<AppState>, Query(input): Query<Input>, ) -> Json<Value> { observed(&state, None, None, input.number, &calls) }
async fn aliased_query( Extension(calls): Extension<Calls>, (State(state), Query(input)): QueryRequest<Input>, ) -> Json<Value> { observed(&state, None, None, input.number, &calls) }
async fn native_space_query( Extension(calls): Extension<Calls>, State(state): State<AppState>, uri: OriginalUri, headers: HeaderMap, Query(input): Query<Input>, ) -> Json<Value> { observed(&state, Some(headers), Some(uri), input.number, &calls) }
async fn aliased_space_query( Extension(calls): Extension<Calls>, (State(state), uri, headers, Query(input)): SpaceQuery<Input>, ) -> Json<Value> { observed(&state, Some(headers), Some(uri), input.number, &calls) }
async fn native_space_json( Extension(calls): Extension<Calls>, State(state): State<AppState>, uri: OriginalUri, headers: HeaderMap, Json(input): Json<Input>, ) -> Json<Value> { observed(&state, Some(headers), Some(uri), input.number, &calls) }
async fn aliased_space_json( Extension(calls): Extension<Calls>, (State(state), uri, headers, Json(input)): SpaceJson<Input>, ) -> Json<Value> { observed(&state, Some(headers), Some(uri), input.number, &calls) }
fn state() -> AppState { let config = Config { deployment_mode: DeploymentMode::Local, listen_address: "127.0.0.1:3000".parse().unwrap(), public_origin: HttpOrigin::parse_public("http://pds.test/", DeploymentMode::Local) .unwrap(), database_path: ":memory:".into(), blob_directory: "unused".into(), blob_orphan_ttl: Duration::from_secs(86_400), blob_gc_interval: Duration::from_secs(3_600), identity_revalidation_interval: Duration::from_secs(3_600), repo_event_retention: 100_000, repo_stream_send_timeout: Duration::from_secs(10), relay_origins: Vec::new(), account: AccountConfig { did: "did:web:alice.test".to_owned(), handle: "alice.test".to_owned(), password: "correct horse battery staple".to_owned(), signing_key: [7; 32], }, oauth_clients: Vec::new(), }; AppState::assemble( config, Database::open_in_memory().unwrap(), Arc::new(MemoryBlobStore::default()), Arc::new(NoopEventSink), ) .unwrap() }}