funes 0.3.0

Funes is a server to mock HTTP responses.
Documentation
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use reqwest::Client;

use crate::{error, fetch, format, io, latency, statics, utils};

pub async fn get(
    api: web::Path<String>,
    client: web::Data<Client>,
    req: HttpRequest,
) -> Result<HttpResponse, error::FunesError> {
    let api = api.into_inner();
    if api == "favicon.ico" {
        return Ok(HttpResponse::Ok().finish());
    }

    let qs = req.query_string();
    let resource = format::resource(&api, qs, "");
    let file_content = io::read(&resource);

    latency::sleep(&api, &statics::LATENCY_COLLECTION).await?;

    match file_content {
        Ok(value) => Ok(HttpResponse::Ok().body(value)),
        Err(_) => {
            statics::ENVS.allow_externals_calls()?;

            let url = format::url(&api, qs);
            let res = fetch::get(&client, &url)
                .await
                .unwrap_or_else(|_| serde_json::json!({}));
            let file_content = serde_json::to_string(&res).unwrap_or_else(|_| "".to_string());

            io::write(&resource, file_content)?;

            Ok(HttpResponse::Created().json(res))
        }
    }
}

pub async fn post(
    api: web::Path<String>,
    client: web::Data<Client>,
    payload: web::Json<serde_json::Value>,
    req: HttpRequest,
) -> Result<HttpResponse, error::FunesError> {
    let api = api.into_inner();
    let payload = payload.into_inner();
    let hash = utils::hash(&utils::HashValue(&payload));

    let qs = req.query_string();
    let resource = format::resource(&api, qs, &hash.to_string());
    let file_content = io::read(&resource);

    latency::sleep(&api, &statics::LATENCY_COLLECTION).await?;

    match file_content {
        Ok(value) => Ok(HttpResponse::Ok().body(value)),
        Err(_) => {
            statics::ENVS.allow_externals_calls()?;

            let url = format::url(&api, qs);
            let res = fetch::post(&client, &url, &payload)
                .await
                .unwrap_or_else(|_| serde_json::json!({}));
            let file_content = serde_json::to_string(&res).unwrap_or_else(|_| "".to_string());

            io::write(&resource, file_content)?;

            Ok(HttpResponse::Created().json(res))
        }
    }
}

pub async fn ok() -> Result<impl Responder, error::FunesError> {
    Ok(HttpResponse::Ok().body("Ok".to_string()))
}

#[cfg(test)]
mod tests {
    use actix_web::{http, http::header, middleware, web, App};
    use dotenv::dotenv;

    use super::*;

    fn initial_setup() -> Result<(), error::FunesError> {
        io::mock_dir()?;
        Ok(())
    }

    #[actix_rt::test]
    async fn test_health() {
        dotenv().ok();
        initial_setup().ok();

        let srv = actix_test::start(|| {
            App::new().service(web::resource("/health").route(web::get().to(ok)))
        });

        assert_eq!(
            srv.get("/health").send().await.unwrap().status(),
            http::StatusCode::OK,
            "Test /health route."
        );
    }

    #[actix_rt::test]
    async fn test_resource_status() {
        dotenv().ok();
        initial_setup().ok();

        let srv = actix_test::start(|| {
            App::new().service(web::resource("/resource-status").route(web::get().to(ok)))
        });

        assert_eq!(
            srv.get("/resource-status").send().await.unwrap().status(),
            http::StatusCode::OK,
            "Test /resource-status route."
        );
    }

    #[actix_rt::test]
    async fn get_api() {
        dotenv().ok();
        initial_setup().ok();

        let srv = actix_test::start(move || {
            App::new()
                .app_data(web::Data::new(statics::CLIENT.clone()))
                .wrap(
                    middleware::DefaultHeaders::new()
                        .header(header::SERVER, &statics::ENVS.h_server)
                        .header(header::ACCEPT_CHARSET, mime::UTF_8.to_string())
                        .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()),
                )
                .service(
                    web::resource("/{api:.+}")
                        .route(web::post().to(post))
                        .route(web::get().to(get)),
                )
        });

        let mut res = srv
            .get("jsonplaceholder.typicode.com/todos/1")
            .send()
            .await
            .unwrap();
        assert!(res.status().is_success(),);

        let data = r#"{"completed":false,"id":1,"title":"delectus aut autem","userId":1}"#;
        let json: serde_json::Value = serde_json::from_str(data).unwrap();
        let res_json: serde_json::Value = serde_json::from_str(
            &String::from_utf8(res.body().limit(20_000_000).await.unwrap().to_vec()).unwrap(),
        )
        .unwrap();
        assert_eq!(res_json, json);
    }

    #[actix_rt::test]
    async fn post_api() {
        dotenv().ok();
        initial_setup().ok();

        let srv = actix_test::start(move || {
            App::new()
                .app_data(web::Data::new(statics::CLIENT.clone()))
                .wrap(
                    middleware::DefaultHeaders::new()
                        .header(header::SERVER, &statics::ENVS.h_server)
                        .header(header::ACCEPT_CHARSET, mime::UTF_8.to_string())
                        .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()),
                )
                .service(
                    web::resource("/{api:.+}")
                        .route(web::post().to(post))
                        .route(web::get().to(get)),
                )
        });

        let data = r#"{"userId":1,"id":101,"title":"title","body":"body"}"#;
        let json: serde_json::Value = serde_json::from_str(data).unwrap();
        let res = srv
            .post("jsonplaceholder.typicode.com/posts")
            .insert_header((header::CONTENT_TYPE, "application/json"))
            .send_json(&json)
            .await
            .unwrap()
            .body()
            .limit(20_000_000)
            .await
            .unwrap();
        let res_json: serde_json::Value =
            serde_json::from_str(&String::from_utf8(res.to_vec()).unwrap()).unwrap();

        assert_eq!(res_json, json);
    }
}