#![cfg(feature = "validation")]
use ::axum::{
Form, Router,
body::Body,
extract::{Path, Query},
http::Request,
routing::post,
};
use ::serde::Deserialize;
use ::tower::ServiceExt;
use ::vespera::{Schema, ValidatePayload, Validated};
async fn body_to_string(body: Body) -> String {
let bytes = ::axum::body::to_bytes(body, usize::MAX).await.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[derive(Deserialize, Schema)]
#[allow(dead_code)]
struct LoginForm {
#[schema(min_length = 3, max_length = 32)]
username: String,
#[schema(min_length = 8)]
password: String,
}
async fn login(Validated(Form(_p)): Validated<Form<LoginForm>>) -> &'static str {
"ok"
}
fn form_router() -> Router {
Router::new().route("/login", post(login))
}
#[tokio::test]
async fn validated_form_valid_payload_returns_200() {
let req = Request::builder()
.method("POST")
.uri("/login")
.header("content-type", "application/x-www-form-urlencoded")
.body(Body::from("username=alice&password=correcthorse"))
.unwrap();
let res = form_router().oneshot(req).await.unwrap();
assert_eq!(res.status(), 200);
}
#[tokio::test]
async fn validated_form_short_password_returns_422() {
let req = Request::builder()
.method("POST")
.uri("/login")
.header("content-type", "application/x-www-form-urlencoded")
.body(Body::from("username=alice&password=short"))
.unwrap();
let res = form_router().oneshot(req).await.unwrap();
assert_eq!(res.status(), 422);
let body: ::serde_json::Value =
::serde_json::from_str(&body_to_string(res.into_body()).await).unwrap();
let errors = body["errors"].as_array().expect("errors");
assert!(
errors
.iter()
.any(|e| e["path"].as_str() == Some("password")),
"expected `password` error, got {body:#}"
);
}
#[derive(Deserialize, Schema)]
#[allow(dead_code)]
struct SearchParams {
#[schema(min_length = 1, max_length = 100)]
q: String,
#[schema(minimum = 1, maximum = 100)]
limit: u32,
}
#[test]
fn validated_query_payload_exposes_inner_ref() {
use ::garde::Validate;
let q = Query(SearchParams {
q: "hello".into(),
limit: 10,
});
let inner: &SearchParams = ValidatePayload::payload(&q);
assert!(inner.validate().is_ok());
assert_eq!(inner.q, "hello");
assert_eq!(inner.limit, 10);
}
#[test]
fn validated_query_payload_invalid_inner_value_is_rejected_by_garde() {
use ::garde::Validate;
let q = Query(SearchParams {
q: String::new(), limit: 999, });
let inner: &SearchParams = ValidatePayload::payload(&q);
let err = inner.validate().expect_err("invalid query rejects");
let paths: Vec<String> = err.iter().map(|(p, _)| p.to_string()).collect();
assert!(paths.contains(&"q".to_string()));
assert!(paths.contains(&"limit".to_string()));
}
#[derive(Deserialize, Schema)]
#[allow(dead_code)]
struct UserPath {
#[schema(min_length = 3, max_length = 32, pattern = "^[a-z0-9_]+$")]
username: String,
}
#[test]
fn validated_path_payload_exposes_inner_ref() {
use ::garde::Validate;
let p = Path(UserPath {
username: "alice_99".into(),
});
let inner: &UserPath = ValidatePayload::payload(&p);
assert!(inner.validate().is_ok());
}
#[test]
fn validated_path_payload_invalid_inner_value_is_rejected_by_garde() {
use ::garde::Validate;
let p = Path(UserPath {
username: "BAD".into(), });
let inner: &UserPath = ValidatePayload::payload(&p);
let err = inner.validate().expect_err("uppercase rejects");
let paths: Vec<String> = err.iter().map(|(p, _)| p.to_string()).collect();
assert!(paths.contains(&"username".to_string()));
}
#[test]
fn validated_form_payload_exposes_inner_ref() {
use ::garde::Validate;
let f = Form(LoginForm {
username: "alice".into(),
password: "correcthorse".into(),
});
let inner: &LoginForm = ValidatePayload::payload(&f);
assert!(inner.validate().is_ok());
}
#[tokio::test]
async fn validated_form_inner_extractor_rejection_is_forwarded() {
let req = Request::builder()
.method("POST")
.uri("/login")
.body(Body::from("username=alice&password=correcthorse"))
.unwrap();
let res = form_router().oneshot(req).await.unwrap();
assert_ne!(res.status(), 422, "must not synthesize a 422 envelope");
assert_ne!(res.status(), 200, "must not pass through to handler");
}