Skip to content

Add unit-testing example for POST requests #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wbprice opened this issue Dec 21, 2018 · 12 comments
Closed

Add unit-testing example for POST requests #1

wbprice opened this issue Dec 21, 2018 · 12 comments

Comments

@wbprice
Copy link

wbprice commented Dec 21, 2018

Hi,

I'm trying to write a unit test for a POST request handler that echos a JSON payload.

I'm expecting to be able to set the method, headers, etc (similar to the request builder as described in the http crate documentation) but I get error messages like the following:

error[E0599]: no method named `header` found for type `http::request::Request<lambda_http::body::Body>` in the current scope
  --> src/create.rs:43:14
   |
43 |             .header("Content-Type", "application/json");
   |              ^^^^^^
   |
   = help: did you mean `headers`?

Can you point me to any documentation that may help with this?

My code attached for background:

#[macro_use]
extern crate serde_derive;

use lambda_http::{lambda, IntoResponse, Request, Body};
use lambda_runtime::{error::HandlerError, Context};

use serde;
use serde_json::{json, Value, Error};
use serde_dynamodb;
use uuid;
use rusoto_core;
use rusoto_dynamodb;

mod rsvp;

fn main() {
    lambda!(create_handler)
}

fn create_handler(
    request: Request,
    _context: Context,
) -> Result<impl IntoResponse, HandlerError> {

    println!("{:?}", request);

    return Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_handler() {

        let body = "{'hello': 'world'}";

        let request = Request::new(Body::from(body.clone()))
            .header("Content-Type", "application/json");

        let response = create_handler(request, Context::default())
            .expect("expected Ok(_) value")
            .into_response();

        println!("{:?}", response);
    }
}
@softprops
Copy link
Owner

hi @wbprice sorry I just now noticed this. There are few ways of creating requests with the http crate. the new fn and the builder fn. The new method a request that doesn't support builder methods like header. I think you want one of the builder flavored methods, post takes a uri but if you don't need a uri you can just as easily use builder

Here's a quick sketch

let body = "{'hello': 'world'}";

let request = Request::builder()
            .method("POST")
            .header("Content-Type", "application/json")
            .body(Body::from(body.clone()))
            .expect("failed to build request");

I have an example of a unit test here if you are looking for inspiration.

@wbprice
Copy link
Author

wbprice commented Jan 24, 2019

Thanks! I eventually figured something out. The tricky thing was that Request doesn't have a builder static method but the following is close enough:

        let payload = r#"[
            {
                "email_address": "[email protected]",
                "name": "Firstname Lastname"
            },
            {
                "email_address": "[email protected]",
                "name": "Firstname Lastname"
            }
        ]"#;

        let request = Request::new(Body::from(payload));

        handler(request, Context::default()).expect("expected Ok(_) value");

When I write better tests I'll open a PR to this or the parent repo.

@softprops
Copy link
Owner

Thanks @wbprice. good to hear.

I'm curious. What was the error you got with Request::builder()?

@wbprice
Copy link
Author

wbprice commented Jan 24, 2019

It's worth noting that I'm using Request exported by lambda_http:

With a handler and test written like so:

extern crate log;
extern crate simple_logger;

use lambda_http::{lambda, IntoResponse, Request, Body};
use lambda_runtime::{error::HandlerError, Context};
use serde_json::{json, Value};
use url::{Url, ParseError};
use std::ops::Deref;
use log::{info, error};
use uuid::Uuid;
use std::collections::HashMap;
mod rsvp;

fn main() {
    simple_logger::init_with_level(log::Level::Info).unwrap();
    lambda!(handler)
}

fn handler(
    request: Request,
    _: Context,
) -> Result<impl IntoResponse, HandlerError> {

    let body = request.body().deref();
    let payload: HashMap<String, bool> = serde_json::from_slice(body).unwrap_or_else(|e| {
        panic!("Do better! {:?}", e);
    });

    match Url::parse(&request.uri().to_string()) {
        Ok(uri) => {
            match uri.path_segments().map(|c| c.collect::<Vec<_>>()) {
                Some(path_segments) => {
                    match Uuid::parse_str(&path_segments[1].to_string()) {
                        Ok(uuid) => {
                            match rsvp::RSVP::patch(uuid, payload) {
                                Ok(rsvps) => Ok(json!(rsvps)),
                                Err(_) => Ok(json!({"message": "Failed to retrieve RSVPs"}))
                            }
                        },
                        Err(_) => Ok(json!({"message": "UUID not provided"}))
                    }
                },
                None => Ok(json!({"message": "No URI segments found."}))
            }
        },
        Err(_) => Ok(json!({"message": "Couldn't parse the URI"}))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn patch_handler_handles() {

        let payload = r#"[
            "attending": true,
            "invitation_submitted": true,
            "reminder_submitted": true
        ]"#;

        let request = Request::builder()
            .method("PUT")
            .header("Content-Type", "application/json")
            .body(Body::from(payload.clone()))
            .expect("failed to build request");

        handler(request, Context::default()).expect("Expected an OK response");
    }
}

I get the following error at compilation.

error[E0599]: no function or associated item named `builder` found for type `http::request::Request<lambda_http::body::Body>` in the current scope
  --> src/rsvp-patch.rs:63:23
   |
63 |         let request = Request::builder()
   |                       ^^^^^^^^^^^^^^^^ function or associated item not found in `http::request::Request<lambda_http::body::Body>`

@softprops
Copy link
Owner

Ah bingo! The lambda-http crate also re-exports the http crate types under lambda_http::http

Try lambda_http::http::Request

@softprops
Copy link
Owner

Also I see you are trying to gain access to something like a path parameter. You can gain access to request.path_parameters() by importing lambda_http::RequestExt

@softprops
Copy link
Owner

also what you're doing with body parsing couldn't request.payload. that should handle both form post and application/json request bodies, deserializing to a serde deserliaizable type ( including HashMap<String, bool> )

@wbprice
Copy link
Author

wbprice commented Jan 27, 2019

Thanks, that's really helpful!

extern crate log;
extern crate simple_logger;

use lambda_http::{lambda, IntoResponse, Request, RequestExt, Body, http};
use lambda_runtime::{error::HandlerError, Context};
use serde_json::{json, Value};
use url::{Url, ParseError};
use std::ops::Deref;
use log::{info, error};
use uuid::Uuid;
use std::collections::HashMap;
mod rsvp;

fn main() {
    simple_logger::init_with_level(log::Level::Info).unwrap();
    lambda!(handler)
}

fn handler(
    request: Request,
    context: Context,
) -> Result<impl IntoResponse, HandlerError> {

    let path_parameters = request.path_parameters();
    let payload : HashMap<String, bool> = request.payload()
        .unwrap()
        .unwrap();

    dbg!(path_parameters);
    dbg!(payload);

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn patch_handler_handles() {

        let payload = r#"{
            "attending": true,
            "invitation_submitted": true,
            "reminder_submitted": true
        }"#;

        let request = http::Request::builder()
            .uri("api.com/ac242e6f-269c-498b-aa5c-4b0535bd9366")
            .method("PUT")
            .header("Content-Type", "application/json")
            .body(Body::from(payload.clone()))
            .expect("failed to build request");

        let context = http::Context

        handler(request, Context::default()).expect("Expected an OK response");
    }
}

In testing, this logs:

[src/rsvp-patch.rs:29] path_parameters = StrMap(
    {}
)
[src/rsvp-patch.rs:30] payload = {
    "invitation_submitted": true,
    "attending": true,
    "reminder_submitted": true
}

I'm guessing the behavior for getting the path variable from the URI would normally be configured by Serverless in API Gateway. Is there a way to mock this behavior in a unit test? I'm guessing I should start looking at creating a custom context for the request?

@softprops
Copy link
Owner

I like where you're taking this. I don't have an immediate answer but I'll get back to you. I just wanted to let you know I at least saw your question.

@softprops
Copy link
Owner

So methods of populating extension methods that extend from api gateway events are not currently exposed (intentionally) as public methods because they may change in the future and aren't meant for public consumption. In a unit test scenario you do have some options though

The extension interface is defined as a trait which just happens to be implemented by Request<Body> could do something like this in a unit testing scenario to take advantage of that

Define a method that your handler calls to extract what fields you need from path parameters

fn extract_path_params<R>(request: R) -> WhatYouNeed where R: RequestExt {
  let params = request.path_parameters();
  extractWhatYouNeed(params)
}

Note path_parameters (and other ext methods) return a StrMap type. You can construct these from a HashMap<String, Vec> at the moment.

inside the handler you could pass the request to extract_path_params as it implements RequestExt. In a unit test scenario you could pass another "mock" type which also implements RequestExt

struct MockPathParams(HashMap<String, Vec<String>>);

impl RequestExt for MockPathParams {
  fn path_parameters(&self) -> StrMap {
    self.0.into()
  }
  
 fn query_string_parameters(&self) -> StrMap {
   unreachable!()
  }

 // unreachable! impls for other RequestExt methods as well.
}

then in a unit test where you would normally pass the request to extract_path_params you could pass an instance of MockPathParams.

It's understandable this is a bit awkward for now but I'm going to see if I can take this usecase and think about it a bit more. For now this solution may get you unblocked.

@softprops
Copy link
Owner

alternatively you could extract what info you need from the handler to a method to another method which is then a bit more straightforward to drive a unit tests

@wbprice
Copy link
Author

wbprice commented Feb 3, 2019

Thanks for your help with this! I don't completely understand the workaround but I think I'll make a note to study up on traits and revisit this later. In the meantime your comments have been really helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants