mirror of
https://github.com/rwf2/Rocket.git
synced 2024-12-28 21:32:36 +00:00
3616f25c0b
Also reexport 'time' from the crate root.
940 lines
29 KiB
Rust
940 lines
29 KiB
Rust
use std::net::{IpAddr, SocketAddr};
|
|
use std::collections::{BTreeMap, HashMap};
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use rocket::UriDisplayQuery;
|
|
use rocket::http::uri::fmt::{UriDisplay, Query};
|
|
use rocket::form::{self, Form, Strict, FromForm, FromFormField, Errors};
|
|
use rocket::form::error::{ErrorKind, Entity};
|
|
use rocket::serde::json::Json;
|
|
|
|
fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Result<T, Errors<'f>> {
|
|
Form::<Strict<T>>::parse(string).map(|s| s.into_inner())
|
|
}
|
|
|
|
fn lenient<'f, T: FromForm<'f>>(string: &'f str) -> Result<T, Errors<'f>> {
|
|
Form::<T>::parse(string)
|
|
}
|
|
|
|
fn strict_encoded<T: 'static>(string: &str) -> Result<T, Errors<'static>>
|
|
where for<'a> T: FromForm<'a>
|
|
{
|
|
Form::<Strict<T>>::parse_encoded(string.into()).map(|s| s.into_inner())
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct TodoTask {
|
|
description: String,
|
|
completed: bool
|
|
}
|
|
|
|
#[test]
|
|
fn simple() {
|
|
// Same number of arguments: simple case.
|
|
let task: Option<TodoTask> = strict("description=Hello&completed=on").ok();
|
|
assert_eq!(task, Some(TodoTask {
|
|
description: "Hello".to_string(),
|
|
completed: true
|
|
}));
|
|
|
|
// Argument in string but not in form.
|
|
let task: Option<TodoTask> = strict("other=a&description=Hello&completed=on").ok();
|
|
assert!(task.is_none());
|
|
|
|
let task: Option<TodoTask> = lenient("other=a&description=Hello&completed=on").ok();
|
|
assert_eq!(task, Some(TodoTask {
|
|
description: "Hello".to_string(),
|
|
completed: true
|
|
}));
|
|
|
|
// Ensure _method isn't required.
|
|
let task: Option<TodoTask> = strict("_method=patch&description=Hello&completed=off").ok();
|
|
assert_eq!(task, Some(TodoTask {
|
|
description: "Hello".to_string(),
|
|
completed: false
|
|
}));
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromFormField)]
|
|
enum FormOption {
|
|
A, B, C
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct FormInput<'r> {
|
|
checkbox: bool,
|
|
number: usize,
|
|
radio: FormOption,
|
|
password: &'r str,
|
|
textarea: String,
|
|
select: FormOption,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct DefaultInput<'r> {
|
|
arg: Option<&'r str>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct ManualMethod<'r> {
|
|
_method: Option<&'r str>,
|
|
done: bool
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct UnpresentCheckbox {
|
|
checkbox: bool
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct UnpresentCheckboxTwo<'r> {
|
|
checkbox: bool,
|
|
something: &'r str
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct FieldNamedV<'r> {
|
|
v: &'r str,
|
|
}
|
|
|
|
#[test]
|
|
fn base_conditions() {
|
|
let form_string = &[
|
|
"password=testing", "checkbox=off", "number=10", "textarea=",
|
|
"select=a", "radio=c",
|
|
].join("&");
|
|
|
|
let input: Result<FormInput<'_>, _> = strict(&form_string);
|
|
assert_eq!(input, Ok(FormInput {
|
|
checkbox: false,
|
|
number: 10,
|
|
radio: FormOption::C,
|
|
password: "testing".into(),
|
|
textarea: "".to_string(),
|
|
select: FormOption::A,
|
|
}));
|
|
|
|
// Argument not in string with default in form.
|
|
let default: Option<DefaultInput<'_>> = strict("").ok();
|
|
assert_eq!(default, Some(DefaultInput {
|
|
arg: None
|
|
}));
|
|
|
|
// Ensure _method can be captured if desired.
|
|
let manual: Option<ManualMethod<'_>> = strict("_method=put&done=true").ok();
|
|
assert_eq!(manual, Some(ManualMethod {
|
|
_method: Some("put".into()),
|
|
done: true
|
|
}));
|
|
|
|
let manual: Option<ManualMethod<'_>> = lenient("_method=put&done=true").ok();
|
|
assert_eq!(manual, Some(ManualMethod {
|
|
_method: Some("put".into()),
|
|
done: true
|
|
}));
|
|
|
|
// And ignored when not present.
|
|
let manual: Option<ManualMethod<'_>> = strict("done=true").ok();
|
|
assert_eq!(manual, Some(ManualMethod {
|
|
_method: None,
|
|
done: true
|
|
}));
|
|
|
|
// Check that a `bool` value that isn't in the form is marked as `false`.
|
|
let manual: Option<UnpresentCheckbox> = lenient("").ok();
|
|
assert_eq!(manual, Some(UnpresentCheckbox {
|
|
checkbox: false
|
|
}));
|
|
|
|
// Check that a `bool` value that isn't in the form is marked as `false`.
|
|
let manual: Option<UnpresentCheckboxTwo<'_>> = lenient("something=hello").ok();
|
|
assert_eq!(manual, Some(UnpresentCheckboxTwo {
|
|
checkbox: false,
|
|
something: "hello".into()
|
|
}));
|
|
|
|
// Check that a structure with one field `v` parses correctly.
|
|
let manual: Option<FieldNamedV<'_>> = strict("v=abc").ok();
|
|
assert_eq!(manual, Some(FieldNamedV {
|
|
v: "abc".into()
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn lenient_parsing() {
|
|
// Check that a structure with one field `v` parses correctly (lenient).
|
|
let manual: Option<FieldNamedV<'_>> = lenient("v=abc").ok();
|
|
assert_eq!(manual, Some(FieldNamedV { v: "abc".into() }));
|
|
|
|
let manual: Option<FieldNamedV<'_>> = lenient("v=abc&a=123").ok();
|
|
assert_eq!(manual, Some(FieldNamedV { v: "abc".into() }));
|
|
|
|
let manual: Option<FieldNamedV<'_>> = lenient("c=abcddef&v=abc&a=123").ok();
|
|
assert_eq!(manual, Some(FieldNamedV { v: "abc".into() }));
|
|
|
|
// Check default values (bool) with lenient parsing.
|
|
let manual: Option<UnpresentCheckboxTwo<'_>> = lenient("something=hello").ok();
|
|
assert_eq!(manual, Some(UnpresentCheckboxTwo {
|
|
checkbox: false,
|
|
something: "hello".into()
|
|
}));
|
|
|
|
let manual: Option<UnpresentCheckboxTwo<'_>> = lenient("hi=hi&something=hello").ok();
|
|
assert_eq!(manual, Some(UnpresentCheckboxTwo {
|
|
checkbox: false,
|
|
something: "hello".into()
|
|
}));
|
|
|
|
// Check that a missing field doesn't parse, even leniently.
|
|
let manual: Option<FieldNamedV<'_>> = lenient("a=abc").ok();
|
|
assert!(manual.is_none());
|
|
|
|
let manual: Option<FieldNamedV<'_>> = lenient("_method=abc").ok();
|
|
assert!(manual.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn field_renaming() {
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct RenamedForm {
|
|
single: usize,
|
|
#[field(name = "camelCase")]
|
|
camel_case: String,
|
|
#[field(name = "TitleCase")]
|
|
title_case: String,
|
|
#[field(name = "type")]
|
|
field_type: isize,
|
|
#[field(name = "DOUBLE")]
|
|
double: String,
|
|
#[field(name = "a:b")]
|
|
colon: isize,
|
|
}
|
|
|
|
let form_string = &[
|
|
"single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2",
|
|
"DOUBLE=bing_bong", "a:b=123"
|
|
].join("&");
|
|
|
|
let form: Option<RenamedForm> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(RenamedForm {
|
|
single: 100,
|
|
camel_case: "helloThere".into(),
|
|
title_case: "HiHi".into(),
|
|
field_type: -2,
|
|
double: "bing_bong".into(),
|
|
colon: 123,
|
|
}));
|
|
|
|
let form_string = &[
|
|
"single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2",
|
|
"DOUBLE=bing_bong", "colon=123"
|
|
].join("&");
|
|
|
|
let form: Option<RenamedForm> = strict(&form_string).ok();
|
|
assert!(form.is_none());
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct MultiName<'r> {
|
|
single: usize,
|
|
#[field(name = "SomeCase")]
|
|
#[field(name = "some_case")]
|
|
some_case: &'r str,
|
|
}
|
|
|
|
let form_string = &["single=123", "some_case=hi_im_here"].join("&");
|
|
let form: Option<MultiName> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(MultiName { single: 123, some_case: "hi_im_here", }));
|
|
|
|
let form_string = &["single=123", "SomeCase=HiImHere"].join("&");
|
|
let form: Option<MultiName> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(MultiName { single: 123, some_case: "HiImHere", }));
|
|
|
|
let form_string = &["single=123", "some_case=hi_im_here", "SomeCase=HiImHere"].join("&");
|
|
let form: Option<MultiName> = strict(&form_string).ok();
|
|
assert!(form.is_none());
|
|
|
|
let form_string = &["single=123", "some_case=hi_im_here", "SomeCase=HiImHere"].join("&");
|
|
let form: Option<MultiName> = lenient(&form_string).ok();
|
|
assert_eq!(form, Some(MultiName { single: 123, some_case: "hi_im_here", }));
|
|
|
|
let form_string = &["single=123", "SomeCase=HiImHere", "some_case=hi_im_here"].join("&");
|
|
let form: Option<MultiName> = lenient(&form_string).ok();
|
|
assert_eq!(form, Some(MultiName { single: 123, some_case: "HiImHere", }));
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct CaseInsensitive<'r> {
|
|
#[field(name = uncased("SomeCase"))]
|
|
#[field(name = "some_case")]
|
|
some_case: &'r str,
|
|
|
|
#[field(name = uncased("hello"))]
|
|
hello: usize,
|
|
}
|
|
|
|
let form_string = &["HeLLO=123", "sOMECASe=hi_im_here"].join("&");
|
|
let form: Option<CaseInsensitive> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(CaseInsensitive { hello: 123, some_case: "hi_im_here", }));
|
|
|
|
let form_string = &["hello=456", "SomeCase=HiImHere"].join("&");
|
|
let form: Option<CaseInsensitive> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(CaseInsensitive { hello: 456, some_case: "HiImHere", }));
|
|
|
|
let form_string = &["helLO=789", "some_case=hi_there"].join("&");
|
|
let form: Option<CaseInsensitive> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(CaseInsensitive { hello: 789, some_case: "hi_there", }));
|
|
|
|
let form_string = &["hello=123", "SOme_case=hi_im_here"].join("&");
|
|
let form: Option<CaseInsensitive> = strict(&form_string).ok();
|
|
assert!(form.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn generics() {
|
|
#[derive(FromForm, Debug, PartialEq)]
|
|
struct Oops<A, B, C> {
|
|
base: String,
|
|
a: A,
|
|
b: B,
|
|
c: C,
|
|
}
|
|
|
|
#[derive(FromForm, Debug, PartialEq)]
|
|
struct YetOneMore<'f, T> {
|
|
string: &'f str,
|
|
other: T,
|
|
}
|
|
|
|
let form_string = &[
|
|
"string=hello", "other=00128"
|
|
].join("&");
|
|
|
|
let form: Option<YetOneMore<'_, usize>> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(YetOneMore {
|
|
string: "hello".into(),
|
|
other: 128,
|
|
}));
|
|
|
|
let form: Option<YetOneMore<'_, u8>> = strict(&form_string).ok();
|
|
assert_eq!(form, Some(YetOneMore {
|
|
string: "hello".into(),
|
|
other: 128,
|
|
}));
|
|
|
|
let form: Option<YetOneMore<'_, i8>> = strict(&form_string).ok();
|
|
assert!(form.is_none());
|
|
|
|
let form_string = "base=just%20a%20test&a=hey%20there&b=a&c=811";
|
|
let form: Option<Oops<String, FormOption, usize>> = strict_encoded(&form_string).ok();
|
|
assert_eq!(form, Some(Oops {
|
|
base: "just a test".into(),
|
|
a: "hey there".into(),
|
|
b: FormOption::A,
|
|
c: 811,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn form_errors() {
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct WhoopsForm {
|
|
complete: bool,
|
|
other: usize,
|
|
}
|
|
|
|
let form: Result<WhoopsForm, _> = strict("complete=true&other=781");
|
|
assert_eq!(form, Ok(WhoopsForm { complete: true, other: 781 }));
|
|
|
|
let errors = strict::<WhoopsForm>("complete=true&other=unknown").unwrap_err();
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "other"
|
|
&& e.value.as_deref() == Some("unknown")
|
|
&& matches!(e.kind, ErrorKind::Int(..))
|
|
}));
|
|
|
|
let errors = strict::<WhoopsForm>("complete=unknown&other=unknown").unwrap_err();
|
|
assert!(errors.iter().any(|e| {
|
|
"complete" == e.name.as_ref().unwrap()
|
|
&& e.value.as_deref() == Some("unknown")
|
|
&& matches!(e.kind, ErrorKind::Bool(..))
|
|
}));
|
|
|
|
let errors = strict::<WhoopsForm>("complete=true&other=1&extra=foo").unwrap_err();
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "extra"
|
|
&& e.value.as_deref() == Some("foo")
|
|
&& matches!(e.kind, ErrorKind::Unexpected)
|
|
}));
|
|
|
|
let errors = strict::<WhoopsForm>("complete=unknown&unknown=!").unwrap_err();
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "complete"
|
|
&& e.value.as_deref() == Some("unknown")
|
|
&& matches!(e.kind, ErrorKind::Bool(..))
|
|
}));
|
|
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "unknown"
|
|
&& e.value.as_deref() == Some("!")
|
|
&& matches!(e.kind, ErrorKind::Unexpected)
|
|
}));
|
|
|
|
let errors = strict::<WhoopsForm>("unknown=!").unwrap_err();
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "unknown"
|
|
&& e.value.as_deref() == Some("!")
|
|
&& matches!(e.kind, ErrorKind::Unexpected)
|
|
}));
|
|
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "complete"
|
|
&& e.value.is_none()
|
|
&& e.entity == Entity::Field
|
|
&& matches!(e.kind, ErrorKind::Missing)
|
|
}));
|
|
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "other"
|
|
&& e.value.is_none()
|
|
&& e.entity == Entity::Field
|
|
&& matches!(e.kind, ErrorKind::Missing)
|
|
}));
|
|
|
|
let errors = strict::<WhoopsForm>("complete=true").unwrap_err();
|
|
assert!(errors.iter().any(|e| {
|
|
e.name.as_ref().unwrap() == "other"
|
|
&& e.value.is_none()
|
|
&& e.entity == Entity::Field
|
|
&& matches!(e.kind, ErrorKind::Missing)
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn raw_ident_form() {
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct RawIdentForm {
|
|
r#type: String,
|
|
}
|
|
|
|
let form: Result<RawIdentForm, _> = strict("type=a");
|
|
assert_eq!(form, Ok(RawIdentForm { r#type: "a".into() }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi() {
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct Multi<'r> {
|
|
checks: Vec<bool>,
|
|
names: Vec<&'r str>,
|
|
news: Vec<String>,
|
|
dogs: HashMap<String, Dog>,
|
|
#[field(name = "more:dogs")]
|
|
more_dogs: HashMap<&'r str, Dog>,
|
|
}
|
|
|
|
let multi: Multi = strict("checks=true&checks=false&checks=false\
|
|
&names=Sam&names[]=Smith&names[]=Bob\
|
|
&news[]=Here&news[]=also here\
|
|
&dogs[fido].barks=true&dogs[George].barks=false\
|
|
&dogs[fido].trained=on&dogs[George].trained=yes\
|
|
&dogs[bob boo].trained=no&dogs[bob boo].barks=off\
|
|
&more:dogs[k:0]=My Dog&more:dogs[v:0].barks=true&more:dogs[v:0].trained=yes\
|
|
").unwrap();
|
|
assert_eq!(multi, Multi {
|
|
checks: vec![true, false, false],
|
|
names: vec!["Sam".into(), "Smith".into(), "Bob".into()],
|
|
news: vec!["Here".into(), "also here".into()],
|
|
dogs: {
|
|
let mut map = HashMap::new();
|
|
map.insert("fido".into(), Dog { barks: true, trained: true });
|
|
map.insert("George".into(), Dog { barks: false, trained: true });
|
|
map.insert("bob boo".into(), Dog { barks: false, trained: false });
|
|
map
|
|
},
|
|
more_dogs: {
|
|
let mut map = HashMap::new();
|
|
map.insert("My Dog".into(), Dog { barks: true, trained: true });
|
|
map
|
|
}
|
|
});
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct MultiOwned {
|
|
names: Vec<String>,
|
|
}
|
|
|
|
let raw = "names=Sam&names%5B%5D=Smith&names%5B%5D=Bob%20Smith%3F";
|
|
let multi: MultiOwned = strict_encoded(raw).unwrap();
|
|
assert_eq!(multi, MultiOwned {
|
|
names: vec!["Sam".into(), "Smith".into(), "Bob Smith?".into()],
|
|
});
|
|
}
|
|
|
|
#[derive(Debug, FromForm, PartialEq)]
|
|
struct Dog {
|
|
barks: bool,
|
|
trained: bool,
|
|
}
|
|
|
|
#[derive(Debug, FromForm, PartialEq)]
|
|
struct Cat<'r> {
|
|
nip: &'r str,
|
|
meows: bool
|
|
}
|
|
|
|
#[derive(Debug, FromForm, PartialEq)]
|
|
struct Pet<'r, T> {
|
|
pet: T,
|
|
name: &'r str,
|
|
age: u8
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct Person<'r> {
|
|
dogs: Vec<Pet<'r, Dog>>,
|
|
cats: Vec<Pet<'r, Cat<'r>>>,
|
|
sitting: Dog,
|
|
}
|
|
|
|
#[test]
|
|
fn test_nested_multi() {
|
|
let person: Person = strict("sitting.barks=true&sitting.trained=true").unwrap();
|
|
assert_eq!(person, Person {
|
|
sitting: Dog { barks: true, trained: true },
|
|
cats: vec![],
|
|
dogs: vec![],
|
|
});
|
|
|
|
let person: Person = strict("sitting.barks=true&sitting.trained=true\
|
|
&dogs[0].name=fido&dogs[0].pet.trained=yes&dogs[0].age=7&dogs[0].pet.barks=no\
|
|
").unwrap();
|
|
assert_eq!(person, Person {
|
|
sitting: Dog { barks: true, trained: true },
|
|
cats: vec![],
|
|
dogs: vec![Pet {
|
|
pet: Dog { barks: false, trained: true },
|
|
name: "fido".into(),
|
|
age: 7
|
|
}]
|
|
});
|
|
|
|
let person: Person = strict("sitting.trained=no&sitting.barks=true\
|
|
&dogs[0].name=fido&dogs[0].pet.trained=yes&dogs[0].age=7&dogs[0].pet.barks=no\
|
|
&dogs[1].pet.barks=true&dogs[1].name=Bob&dogs[1].pet.trained=no&dogs[1].age=1\
|
|
").unwrap();
|
|
assert_eq!(person, Person {
|
|
sitting: Dog { barks: true, trained: false },
|
|
cats: vec![],
|
|
dogs: vec![
|
|
Pet {
|
|
pet: Dog { barks: false, trained: true },
|
|
name: "fido".into(),
|
|
age: 7
|
|
},
|
|
Pet {
|
|
pet: Dog { barks: true, trained: false },
|
|
name: "Bob".into(),
|
|
age: 1
|
|
},
|
|
]
|
|
});
|
|
|
|
let person: Person = strict("sitting.barks=true&sitting.trained=no\
|
|
&dogs[0].name=fido&dogs[0].pet.trained=yes&dogs[0].age=7&dogs[0].pet.barks=no\
|
|
&dogs[1].pet.barks=true&dogs[1].name=Bob&dogs[1].pet.trained=no&dogs[1].age=1\
|
|
&cats[george].pet.nip=paws&cats[george].name=George&cats[george].age=2\
|
|
&cats[george].pet.meows=yes\
|
|
").unwrap();
|
|
assert_eq!(person, Person {
|
|
sitting: Dog { barks: true, trained: false },
|
|
cats: vec![
|
|
Pet {
|
|
pet: Cat { nip: "paws".into(), meows: true },
|
|
name: "George".into(),
|
|
age: 2
|
|
}
|
|
],
|
|
dogs: vec![
|
|
Pet {
|
|
pet: Dog { barks: false, trained: true },
|
|
name: "fido".into(),
|
|
age: 7
|
|
},
|
|
Pet {
|
|
pet: Dog { barks: true, trained: false },
|
|
name: "Bob".into(),
|
|
age: 1
|
|
},
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_multipart() {
|
|
use rocket::http::ContentType;
|
|
use rocket::local::blocking::Client;
|
|
use rocket::fs::TempFile;
|
|
|
|
#[derive(FromForm)]
|
|
struct MyForm<'r> {
|
|
names: Vec<&'r str>,
|
|
file: TempFile<'r>,
|
|
}
|
|
|
|
#[rocket::post("/", data = "<form>")]
|
|
fn form(form: Form<MyForm>) {
|
|
assert_eq!(form.names, &["abcd", "123"]);
|
|
assert_eq!(form.file.name(), Some("foo"));
|
|
}
|
|
|
|
let client = Client::debug_with(rocket::routes![form]).unwrap();
|
|
let ct = "multipart/form-data; boundary=X-BOUNDARY"
|
|
.parse::<ContentType>()
|
|
.unwrap();
|
|
|
|
let body = &[
|
|
"--X-BOUNDARY",
|
|
r#"Content-Disposition: form-data; name="names[]""#,
|
|
"",
|
|
"abcd",
|
|
"--X-BOUNDARY",
|
|
r#"Content-Disposition: form-data; name="names[]""#,
|
|
"",
|
|
"123",
|
|
"--X-BOUNDARY",
|
|
r#"Content-Disposition: form-data; name="file"; filename="foo.txt""#,
|
|
"Content-Type: text/plain",
|
|
"",
|
|
"hi there",
|
|
"--X-BOUNDARY--",
|
|
"",
|
|
].join("\r\n");
|
|
|
|
let response = client.post("/")
|
|
.header(ct)
|
|
.body(body)
|
|
.dispatch();
|
|
|
|
assert!(response.status().class().is_success());
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_removed() {
|
|
#[derive(FromForm, PartialEq, Debug)]
|
|
struct FormNoDefault {
|
|
#[field(default = None)]
|
|
field1: bool,
|
|
#[field(default = None)]
|
|
field2: Option<usize>,
|
|
#[field(default = None)]
|
|
field3: Option<Option<usize>>,
|
|
#[field(default_with = None)]
|
|
field4: bool,
|
|
}
|
|
|
|
let form_string = &["field1=false", "field2=10", "field3=23", "field4"].join("&");
|
|
let form1: Option<FormNoDefault> = lenient(&form_string).ok();
|
|
assert_eq!(form1, Some(FormNoDefault {
|
|
field1: false,
|
|
field2: Some(10),
|
|
field3: Some(Some(23)),
|
|
field4: true,
|
|
}));
|
|
|
|
let form_string = &["field1=true", "field2=10", "field3=23", "field4"].join("&");
|
|
let form1: Option<FormNoDefault> = lenient(&form_string).ok();
|
|
assert_eq!(form1, Some(FormNoDefault {
|
|
field1: true,
|
|
field2: Some(10),
|
|
field3: Some(Some(23)),
|
|
field4: true,
|
|
}));
|
|
|
|
// Field 1 missing.
|
|
let form_string = &["field2=20", "field3=10", "field4"].join("&");
|
|
assert!(lenient::<FormNoDefault>(&form_string).is_err());
|
|
|
|
// Field 2 missing.
|
|
let form_string = &["field1=true", "field3=10", "field4"].join("&");
|
|
assert!(lenient::<FormNoDefault>(&form_string).is_err());
|
|
|
|
// Field 3 missing.
|
|
let form_string = &["field1=true", "field2=10", "field4=false"].join("&");
|
|
assert!(lenient::<FormNoDefault>(&form_string).is_err());
|
|
|
|
// Field 4 missing.
|
|
let form_string = &["field1=true", "field2=10", "field3=23"].join("&");
|
|
assert!(lenient::<FormNoDefault>(&form_string).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_defaults() {
|
|
fn test_hashmap() -> HashMap<&'static str, &'static str> {
|
|
let mut map = HashMap::new();
|
|
map.insert("key", "value");
|
|
map.insert("one-more", "good-value");
|
|
map
|
|
}
|
|
|
|
fn test_btreemap() -> BTreeMap<Vec<usize>, &'static str> {
|
|
let mut map = BTreeMap::new();
|
|
map.insert(vec![], "empty");
|
|
map.insert(vec![1, 2], "one-and-two");
|
|
map.insert(vec![3, 7, 9], "prime");
|
|
map
|
|
}
|
|
|
|
#[derive(FromForm, UriDisplayQuery, PartialEq, Debug)]
|
|
struct FormWithDefaults<'a> {
|
|
field2: i128,
|
|
field5: bool,
|
|
|
|
#[field(default = 100)]
|
|
field1: usize,
|
|
#[field(default = true)]
|
|
field3: bool,
|
|
#[field(default = false)]
|
|
field4: bool,
|
|
#[field(default = 254 + 1)]
|
|
field6: u8,
|
|
#[field(default = Some(true))]
|
|
opt1: Option<bool>,
|
|
#[field(default = false)]
|
|
opt2: Option<bool>,
|
|
#[field(default = Ok("hello".into()))]
|
|
res: form::Result<'a, String>,
|
|
#[field(default = Ok("hello"))]
|
|
res2: form::Result<'a, &'a str>,
|
|
#[field(default = vec![1, 2, 3])]
|
|
vec_num: Vec<usize>,
|
|
#[field(default = vec!["wow", "a", "string", "nice"])]
|
|
vec_str: Vec<&'a str>,
|
|
#[field(default = test_hashmap())]
|
|
hashmap: HashMap<&'a str, &'a str>,
|
|
#[field(default = test_btreemap())]
|
|
btreemap: BTreeMap<Vec<usize>, &'a str>,
|
|
#[field(default_with = Some(false))]
|
|
boolean: bool,
|
|
#[field(default_with = (|| Some(777))())]
|
|
unsigned: usize,
|
|
#[field(default = std::num::NonZeroI32::new(3).unwrap())]
|
|
nonzero: std::num::NonZeroI32,
|
|
#[field(default_with = std::num::NonZeroI32::new(9001))]
|
|
nonzero2: std::num::NonZeroI32,
|
|
#[field(default = 3.0)]
|
|
float: f64,
|
|
#[field(default = "wow")]
|
|
str_ref: &'a str,
|
|
#[field(default = "wowie")]
|
|
string: String,
|
|
#[field(default = [192u8, 168, 1, 0])]
|
|
ip: IpAddr,
|
|
#[field(default = ([192u8, 168, 1, 0], 20))]
|
|
addr: SocketAddr,
|
|
#[field(default = time::macros::date!(2021-05-27))]
|
|
date: time::Date,
|
|
#[field(default = time::macros::time!(01:15:00))]
|
|
time: time::Time,
|
|
#[field(default = time::PrimitiveDateTime::new(
|
|
time::macros::date!(2021-05-27),
|
|
time::macros::time!(01:15:00),
|
|
))]
|
|
datetime: time::PrimitiveDateTime,
|
|
}
|
|
|
|
// `field2` has no default.
|
|
assert!(lenient::<FormWithDefaults>("").is_err());
|
|
|
|
// every other field should.
|
|
let form_string = &["field2=102"].join("&");
|
|
let form1: Option<FormWithDefaults> = lenient(&form_string).ok();
|
|
assert_eq!(form1, Some(FormWithDefaults {
|
|
field1: 100,
|
|
field2: 102,
|
|
field3: true,
|
|
field4: false,
|
|
field5: false,
|
|
field6: 255,
|
|
opt1: Some(true),
|
|
opt2: Some(false),
|
|
res: Ok("hello".into()),
|
|
res2: Ok("hello"),
|
|
vec_num: vec![1, 2, 3],
|
|
vec_str: vec!["wow", "a", "string", "nice"],
|
|
hashmap: test_hashmap(),
|
|
btreemap: test_btreemap(),
|
|
boolean: false,
|
|
unsigned: 777,
|
|
nonzero: std::num::NonZeroI32::new(3).unwrap(),
|
|
nonzero2: std::num::NonZeroI32::new(9001).unwrap(),
|
|
float: 3.0,
|
|
str_ref: "wow",
|
|
string: "wowie".to_string(),
|
|
ip: [192u8, 168, 1, 0].into(),
|
|
addr: ([192u8, 168, 1, 0], 20).into(),
|
|
date: time::macros::date!(2021-05-27),
|
|
time: time::macros::time!(01:15:00),
|
|
datetime: time::PrimitiveDateTime::new(
|
|
time::macros::date!(2021-05-27),
|
|
time::macros::time!(01:15:00)
|
|
),
|
|
}));
|
|
|
|
let form2: Option<FormWithDefaults> = strict(&form_string).ok();
|
|
assert!(form2.is_none());
|
|
|
|
// Ensure actual form field values take precedence.
|
|
let form_string = &["field1=101", "field2=102", "field3=true", "field5=true"].join("&");
|
|
let form3: Option<FormWithDefaults> = lenient(&form_string).ok();
|
|
assert_eq!(form3, Some(FormWithDefaults {
|
|
field1: 101,
|
|
field2: 102,
|
|
field3: true,
|
|
field4: false,
|
|
field5: true,
|
|
..form1.unwrap()
|
|
}));
|
|
|
|
// And that strict parsing still works.
|
|
let form = form3.unwrap();
|
|
let form_string = format!("{}", &form as &dyn UriDisplay<Query>);
|
|
let form4: form::Result<'_, FormWithDefaults> = strict(&form_string);
|
|
assert_eq!(form4, Ok(form));
|
|
|
|
#[derive(FromForm, UriDisplayQuery, PartialEq, Debug)]
|
|
struct OwnedFormWithDefaults {
|
|
#[field(default = {
|
|
let mut map = BTreeMap::new();
|
|
map.insert(vec![], "empty!/!? neat".into());
|
|
map.insert(vec![1, 2], "one/ and+two".into());
|
|
map.insert(vec![3, 7, 9], "prime numbers".into());
|
|
map
|
|
})]
|
|
btreemap: BTreeMap<Vec<usize>, String>,
|
|
}
|
|
|
|
// And that strict parsing still works even when encoded.
|
|
let form5: Option<OwnedFormWithDefaults> = lenient("").ok();
|
|
assert_eq!(form5, Some(OwnedFormWithDefaults {
|
|
btreemap: {
|
|
let mut map = BTreeMap::new();
|
|
map.insert(vec![3, 7, 9], "prime numbers".into());
|
|
map.insert(vec![1, 2], "one/ and+two".into());
|
|
map.insert(vec![], "empty!/!? neat".into());
|
|
map
|
|
}
|
|
}));
|
|
|
|
let form = form5.unwrap();
|
|
let form_string = format!("{}", &form as &dyn UriDisplay<Query>);
|
|
let form6: form::Result<'_, OwnedFormWithDefaults> = strict_encoded(&form_string);
|
|
assert_eq!(form6, Ok(form));
|
|
}
|
|
|
|
#[test]
|
|
fn test_lazy_default() {
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
static VAL: AtomicUsize = AtomicUsize::new(40);
|
|
|
|
fn f() -> usize {
|
|
VAL.fetch_add(1, Ordering::Relaxed)
|
|
}
|
|
|
|
fn opt_f() -> Option<usize> {
|
|
Some(f())
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct MyForm {
|
|
#[field(default = f())]
|
|
missing0: usize,
|
|
#[field(default = VAL.load(Ordering::Relaxed))]
|
|
missing1: usize,
|
|
#[field(default_with = opt_f())]
|
|
missing2: usize,
|
|
#[field(default_with = opt_f())]
|
|
a: usize,
|
|
#[field(default = f())]
|
|
b: usize,
|
|
#[field(default = VAL.load(Ordering::Relaxed))]
|
|
missing3: usize,
|
|
}
|
|
|
|
// Ensure actual form field values take precedence.
|
|
let form_string = &["a=100", "b=300"].join("&");
|
|
let form3: Option<MyForm> = lenient(&form_string).ok();
|
|
assert_eq!(form3, Some(MyForm {
|
|
missing0: 40,
|
|
missing1: 41,
|
|
missing2: 41,
|
|
a: 100,
|
|
b: 300,
|
|
missing3: 42
|
|
}));
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm, UriDisplayQuery)]
|
|
#[field(validate = len(3..), default = "some default hello")]
|
|
struct Token<'r>(&'r str);
|
|
|
|
#[derive(Debug, PartialEq, FromForm, UriDisplayQuery)]
|
|
#[field(validate = try_with(|s| s.parse::<usize>()), default = "123456")]
|
|
struct TokenOwned(String);
|
|
|
|
#[test]
|
|
fn wrapper_works() {
|
|
let form: Option<Token> = lenient("").ok();
|
|
assert_eq!(form, Some(Token("some default hello")));
|
|
|
|
let form: Option<TokenOwned> = lenient("").ok();
|
|
assert_eq!(form, Some(TokenOwned("123456".into())));
|
|
|
|
let errors = strict::<Token>("").unwrap_err();
|
|
assert!(errors.iter().any(|e| matches!(e.kind, ErrorKind::Missing)));
|
|
|
|
let form: Option<Token> = lenient("=hi there").ok();
|
|
assert_eq!(form, Some(Token("hi there")));
|
|
|
|
let form: Option<TokenOwned> = strict_encoded("=2318").ok();
|
|
assert_eq!(form, Some(TokenOwned("2318".into())));
|
|
|
|
let errors = lenient::<Token>("=hi").unwrap_err();
|
|
assert!(errors.iter().any(|e| matches!(e.kind, ErrorKind::InvalidLength { .. })));
|
|
|
|
let errors = lenient::<TokenOwned>("=hellothere").unwrap_err();
|
|
assert!(errors.iter().any(|e| matches!(e.kind, ErrorKind::Validation { .. })));
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm, UriDisplayQuery)]
|
|
struct JsonToken<T>(Json<T>);
|
|
|
|
#[test]
|
|
fn json_wrapper_works() {
|
|
let form: JsonToken<String> = lenient("=\"hello\"").unwrap();
|
|
assert_eq!(form, JsonToken(Json("hello".into())));
|
|
|
|
let form: JsonToken<usize> = lenient("=10").unwrap();
|
|
assert_eq!(form, JsonToken(Json(10)));
|
|
|
|
let form: JsonToken<()> = lenient("=null").unwrap();
|
|
assert_eq!(form, JsonToken(Json(())));
|
|
|
|
let form: JsonToken<Vec<usize>> = lenient("=[1, 4, 3, 9]").unwrap();
|
|
assert_eq!(form, JsonToken(Json(vec![1, 4, 3, 9])));
|
|
|
|
let string = String::from("=\"foo bar\"");
|
|
let form: JsonToken<&str> = lenient(&string).unwrap();
|
|
assert_eq!(form, JsonToken(Json("foo bar")));
|
|
}
|
|
|
|
// FIXME: https://github.com/rust-lang/rust/issues/86706
|
|
#[allow(private_in_public)]
|
|
struct Q<T>(T);
|
|
|
|
// This is here to ensure we don't warn, which we can't test with trybuild.
|
|
#[derive(FromForm)]
|
|
pub struct JsonTokenBad<T>(Q<T>);
|