mod support; use axum::{ body::{Body, to_bytes}, http::{Request, StatusCode, header}, }; use serde_json::Value; use support::TestApp; #[tokio::test] async fn server_errors_do_not_disclose_internal_diagnostics() { use axum::response::IntoResponse as _; use cpds::error::Error; for error in [ Error::Internal("private repository details".to_owned()), Error::Configuration("private configuration details".to_owned()), Error::Io(std::io::Error::other("private filesystem path")), Error::Database(rusqlite::Error::InvalidParameterName( "private SQL".to_owned(), )), Error::Upstream("private upstream URL".to_owned()), ] { let response = error.into_response(); assert!(response.status().is_server_error()); let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); let body: Value = serde_json::from_slice(&body).unwrap(); assert!(!body["message"].as_str().unwrap().contains("private")); } } #[tokio::test] async fn unknown_xrpc_routes_and_wrong_methods_use_json_errors() { let app = TestApp::new(); let missing = app .send( Request::get("/xrpc/com.example.unknown") .body(Body::empty()) .unwrap(), ) .await; assert_error(missing, StatusCode::NOT_FOUND, "MethodNotFound").await; let wrong_method = app .send( Request::put("/xrpc/com.atproto.server.describeServer") .body(Body::empty()) .unwrap(), ) .await; assert_eq!(wrong_method.headers()[header::ALLOW], "GET,HEAD"); assert_error( wrong_method, StatusCode::METHOD_NOT_ALLOWED, "MethodNotAllowed", ) .await; let missing_page = app .send(Request::get("/missing").body(Body::empty()).unwrap()) .await; assert_error(missing_page, StatusCode::NOT_FOUND, "NotFound").await; } #[tokio::test] async fn extractor_rejections_use_the_xrpc_error_contract() { let app = TestApp::new(); let malformed_json = app .send( Request::post("/xrpc/com.atproto.repo.createRecord") .header(header::CONTENT_TYPE, "application/json") .body(Body::from("{")) .unwrap(), ) .await; assert_error(malformed_json, StatusCode::BAD_REQUEST, "InvalidRequest").await; let malformed_query = app .send( Request::get("/xrpc/com.atproto.repo.getRecord?repo=did%3Aweb%3Aalice.test") .body(Body::empty()) .unwrap(), ) .await; assert_error(malformed_query, StatusCode::BAD_REQUEST, "InvalidRequest").await; } #[tokio::test] async fn representative_successes_keep_their_declared_content_types() { let app = TestApp::new(); let json = app .send( Request::get("/xrpc/com.atproto.server.describeServer") .body(Body::empty()) .unwrap(), ) .await; assert_eq!(json.status(), StatusCode::OK); assert_eq!(json.headers()[header::CONTENT_TYPE], "application/json"); let html = app .send(Request::get("/").body(Body::empty()).unwrap()) .await; assert_eq!(html.status(), StatusCode::OK); assert_eq!( html.headers()[header::CONTENT_TYPE], "text/html; charset=utf-8" ); } async fn assert_error(response: axum::response::Response, status: StatusCode, name: &str) { assert_eq!(response.status(), status); assert_eq!(response.headers()[header::CONTENT_TYPE], "application/json"); let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); let body: Value = serde_json::from_slice(&body).unwrap(); assert_eq!(body["error"], name); assert!( body["message"] .as_str() .is_some_and(|value| !value.is_empty()) ); }