rwf2/Rocket

Ease Testing Forms w/`LocalForm`

Open

#1,591 opened on Mar 25, 2021

View on GitHub
 (7 comments) (6 reactions) (2 assignees)Rust (25,738 stars) (1,645 forks)batch import
enhancementhelp wanted

Description

Testing forms in Rocket is currently an entirely manual process:

let client = Client::tracked(rocket).unwrap();

let response = client.post("/")
    .header(ContentType::Form)
    .body("field=value&is+it=a+cat%3F")
    .dispatch();

With the addition of multipart form support, form testing verbosity is even further exacerbated:

let client = Client::tracked(rocket).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();

Rocket should make testing applications with forms a much simpler task.

Here's how we might hope testing the above forms would look:

let client = Client::tracked(rocket).unwrap();
let response = client.post("/")
    .form(&[("field", "value"), ("is it", "a cat?")])
    .dispatch();

let client = Client::tracked(rocket).unwrap();
let response = client.post("/")
    .form(LocalForm::new()
        .field("names[]", "abcd")
        .field("names[]", "123")
        .file("foo.txt", ContentType::Plain, "hi there"))
    .dispatch();

An API enabling this might resemble:

impl Client {
    pub fn form<F: Into<LocalForm>>(&mut self, form: F) -> &mut Self {
        let form = form.into();
        self.set_header(form.content_type());
        self.set_body(form.body_data());
        self
    }
}

struct LocalForm { /* .. */ }

impl LocalForm {
    pub fn new() -> Self;

    /// A percent-decoded `name` and `value`.
    pub fn field<'v, N, V>(mut self, name: N, value: V) -> Self
        where N: Into<NameBuf<'v>>, V: AsRef<str>;

    /// Adds all of the `fields` to `self`.
    pub fn fields<'v, I, F>(mut self, fields: I) -> Self
        where I: Iterator<Item = F>, F: Into<ValueField<'v>>;

    /// A percent-encoded `name` and `value`.
    pub fn raw_field(mut self, name: &'v RawStr, value: &'v RawStr) -> Self;

    /// Adds a field for the file with name `file_name`, content-type `ct`, and
    /// file contents `data`.
    pub fn file<'v, N, V>(mut self, file_name: N, ct: ContentType, data: V) -> Self
        where N: Into<Option<&'v str>>, V: AsRef<[u8]>;

    /// Add a data field with content-type `ct` and binary `data`.
    pub fn data<'v, V>(mut self, ct: ContentType, data: V) -> Self
        where V: AsRef<[u8]>;

    /// The full content-type for this form.
    pub fn content_type(&self) -> ContentType;

    /// The full body data for this form.
    pub fn body_data(&self) -> Vec<u8>;
}

impl<'v, F: Into<ValueField<'v>>, I: Iterator<Item = F>> From<I> for LocalForm { /* .. */ }

Contributor guide