recipes/backend/tests/http.rs

496 lines
15 KiB
Rust

use std::{error::Error, sync::Arc};
use axum::http;
use axum_test::TestServer;
use common::ron_api;
use cookie::Cookie;
use scraper::{ElementRef, Html, Selector};
use serde::Serialize;
use sscanf::sscanf;
use recipes::{app, email};
mod utils;
#[tokio::test]
async fn homepage() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = utils::common_state().await?;
let user_id =
utils::create_user(&state.db_connection, "president@spaceball.planet", "12345").await?;
let _recipe_id = utils::create_recipe(&state.db_connection, user_id, "spaghetti").await?;
let server = TestServer::new(app::make_service(state))?;
// Act.
let response = server.get("/").await;
// Assert.
response.assert_status_ok();
let document = Html::parse_document(&response.text());
if !document.errors.is_empty() {
panic!("{:?}", document.errors);
}
let first_recipe_title =
Selector::parse("#recipes-list .recipes-list-public .recipe-item").unwrap();
let elements: Vec<ElementRef> = document.select(&first_recipe_title).collect();
assert_eq!(elements.len(), 1);
assert_eq!(elements[0].inner_html(), "spaghetti");
Ok(())
}
#[tokio::test]
async fn recipe_view() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = utils::common_state().await?;
let user_id =
utils::create_user(&state.db_connection, "president@spaceball.planet", "12345").await?;
let recipe_id = utils::create_recipe(&state.db_connection, user_id, "spaghetti").await?;
let server = TestServer::new(app::make_service(state))?;
// Act.
let response = server.get(&format!("/recipe/view/{}", recipe_id)).await;
// Assert.
response.assert_status_ok();
let document = Html::parse_document(&response.text());
if !document.errors.is_empty() {
panic!("{:?}", document.errors);
}
let recipe_title = Selector::parse("#recipe-view .recipe-title").unwrap();
let elements: Vec<ElementRef> = document.select(&recipe_title).collect();
assert_eq!(elements.len(), 1);
assert_eq!(elements[0].inner_html(), "spaghetti");
Ok(())
}
#[tokio::test]
async fn user_edit() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = utils::common_state().await?;
let _user_id =
utils::create_user(&state.db_connection, "president@spaceball.planet", "12345").await?;
let token = utils::sign_in(&state.db_connection, "president@spaceball.planet", "12345").await?;
let server = TestServer::new(app::make_service(state))?;
// Act.
let response = server
.get("/user/edit")
.add_cookie(Cookie::new("auth_token", token))
.await;
// Assert.
response.assert_status_ok();
let document = Html::parse_document(&response.text());
if !document.errors.is_empty() {
panic!("{:?}", document.errors);
}
let user_email = Selector::parse("#input-email").unwrap();
let elements: Vec<ElementRef> = document.select(&user_email).collect();
assert_eq!(elements.len(), 1);
assert_eq!(
elements[0].attr("value").unwrap(),
"president@spaceball.planet"
);
Ok(())
}
#[derive(Serialize, Debug)]
pub struct SignUpFormData {
email: String,
password_1: String,
password_2: String,
}
#[tokio::test]
async fn sign_up() -> Result<(), Box<dyn Error>> {
use std::{cell::RefCell, rc::Rc};
// Arrange.
let validation_url: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
let validation_url_clone = validation_url.clone();
let mut mock_email_service = utils::mock_email::MockEmailService::new();
mock_email_service
.expect_send_email()
.withf(|_email_sender, email_receiver, _title, _message| {
email_receiver == "president@spaceball.planet"
})
.times(1)
.returning_st(move |_email_sender, _email_receiver, _title, message| {
let url = sscanf!(
message,
"Follow this link to confirm your inscription, http://127.0.0.1:8000{String}"
)
.unwrap();
*validation_url_clone.borrow_mut() = url;
Ok(())
});
let state = utils::common_state_with_email_service(Arc::new(mock_email_service)).await?;
let server = TestServer::new(app::make_service(state))?;
let tr = recipes::translation::Tr::new("en");
// Sign up page.
{
// Act.
let response = server
.post("/signup")
.form(&SignUpFormData {
email: "president@spaceball.planet".into(),
password_1: "12345678".into(),
password_2: "12345678".into(),
})
.await;
// Assert.
response.assert_status_ok();
let document = Html::parse_document(&response.text());
if !document.errors.is_empty() {
panic!("{:?}", document.errors);
}
let message_selector = Selector::parse("#message").unwrap();
let element = document.select(&message_selector).next().unwrap();
assert_eq!(
element.inner_html(),
tr.t(common::translation::Sentence::SignUpEmailSent)
);
}
// Validation page.
{
// Act.
let response = server.get(&validation_url.borrow());
let response = response.await;
// Assert.
response.assert_status_ok();
let document = Html::parse_document(&response.text());
if !document.errors.is_empty() {
panic!("{:?}", document.errors);
}
let message_selector = Selector::parse("#message").unwrap();
let element = document.select(&message_selector).next().unwrap();
assert_eq!(
element.inner_html(),
tr.t(common::translation::Sentence::SignUpEmailValidationSuccess)
);
}
Ok(())
}
#[derive(Serialize, Debug)]
pub struct SignInFormData {
email: String,
password: String,
}
#[tokio::test]
async fn sign_in() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = utils::common_state().await?;
let _user_id = utils::create_user(
&state.db_connection,
"president@spaceball.planet",
"12345678",
)
.await?;
let server = TestServer::new(app::make_service(state))?;
// Act.
let response = server
.post("/signin")
.form(&SignInFormData {
email: "president@spaceball.planet".into(),
password: "12345678".into(),
})
.await;
// Assert.
response.assert_status_see_other(); // Redirection after successful sign in.
response.assert_text("");
response.assert_header("location", "/?user_message=16&user_message_icon=0");
Ok(())
}
#[tokio::test]
async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = utils::common_state().await?;
let _user_id = utils::create_user(
&state.db_connection,
"president@spaceball.planet",
"12345678",
)
.await?;
let token = utils::sign_in(
&state.db_connection,
"president@spaceball.planet",
"12345678",
)
.await?;
let server = TestServer::new(app::make_service(state))?;
// Act.
let cookie = Cookie::new("auth_token", token);
let response = server.get("/recipe/new").add_cookie(cookie.clone()).await;
response.assert_status_see_other();
let location = response.header("location").to_str().unwrap().to_string();
let recipe_id = sscanf!(&location, "/en/recipe/edit/{i64}").unwrap();
let response = server.get(&location).add_cookie(cookie.clone()).await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/title")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeTitle {
recipe_id,
title: "AAA".into(),
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/description")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeDescription {
recipe_id,
description: "BBB".into(),
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/servings")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeServings {
recipe_id,
servings: Some(42),
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/estimated_time")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeEstimatedTime {
recipe_id,
estimated_time: Some(420),
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/difficulty")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeDifficulty {
recipe_id,
difficulty: ron_api::Difficulty::Hard,
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/language")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeLanguage {
recipe_id,
lang: "fr".into(),
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.patch("/ron-api/recipe/is_public")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::SetRecipeIsPublic {
recipe_id,
is_public: true,
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
// Assert.
let response = server
.get(&format!("/recipe/edit/{}", recipe_id))
.add_cookie(cookie.clone())
.await;
response.assert_status_ok();
let document = Html::parse_document(&response.text());
if !document.errors.is_empty() {
panic!("{:?}", document.errors);
}
let title_selector = Selector::parse("#input-title").unwrap();
let element_title = document.select(&title_selector).next().unwrap();
assert_eq!(element_title.attr("value").unwrap(), "AAA");
let description_selector = Selector::parse("#text-area-description").unwrap();
let element_description = document.select(&description_selector).next().unwrap();
assert_eq!(element_description.inner_html(), "BBB");
let servings_selector = Selector::parse("#input-servings").unwrap();
let element_servings = document.select(&servings_selector).next().unwrap();
assert_eq!(element_servings.attr("value").unwrap(), "42");
let estimated_time_selector = Selector::parse("#input-estimated-time").unwrap();
let element_estimated_time = document.select(&estimated_time_selector).next().unwrap();
assert_eq!(element_estimated_time.attr("value").unwrap(), "420");
let selected_difficulty_selector =
Selector::parse("#select-difficulty option[selected]").unwrap();
let element_selected_difficulty = document
.select(&selected_difficulty_selector)
.next()
.unwrap();
assert_eq!(element_selected_difficulty.inner_html(), "Hard");
let selected_language_selector = Selector::parse("#select-language option[selected]").unwrap();
let element_selected_language = document.select(&selected_language_selector).next().unwrap();
assert_eq!(element_selected_language.inner_html(), "Français");
let is_public_selector = Selector::parse("#input-is-public").unwrap();
let element_is_public = document.select(&is_public_selector).next().unwrap();
assert!(element_is_public.attr("checked").is_some());
Ok(())
}
#[tokio::test]
async fn recipe_tags() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = utils::common_state().await?;
let user_id = utils::create_user(
&state.db_connection,
"president@spaceball.planet",
"12345678",
)
.await?;
let token = utils::sign_in(
&state.db_connection,
"president@spaceball.planet",
"12345678",
)
.await?;
let recipe_id = utils::create_recipe(&state.db_connection, user_id, "spaghetti").await?;
let server = TestServer::new(app::make_service(state))?;
let cookie = Cookie::new("auth_token", token);
// Act.
// Tags list must be empty.
let response = server
.get("/ron-api/recipe/tags")
.add_cookie(cookie.clone())
.add_query_param("id", recipe_id)
.await;
response.assert_status_ok();
let tags: ron_api::Tags = ron::de::from_bytes(response.as_bytes()).unwrap();
assert!(tags.tags.is_empty());
// Add some tags.
let response = server
.post("/ron-api/recipe/tags")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::Tags {
recipe_id,
tags: vec!["ABC".into(), "xyz".into()],
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.get("/ron-api/recipe/tags")
.add_cookie(cookie.clone())
.add_query_param("id", recipe_id)
.await;
response.assert_status_ok();
let tags: ron_api::Tags = ron::de::from_bytes(response.as_bytes()).unwrap();
assert_eq!(tags.tags.len(), 2);
assert!(tags.tags.contains(&"abc".to_string())); // Tags are in lower case.
assert!(tags.tags.contains(&"xyz".to_string()));
// Remove some tags.
let response = server
.delete("/ron-api/recipe/tags")
.add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(
ron_api::to_string(ron_api::Tags {
recipe_id,
tags: vec!["XYZ".into(), "qwe".into()],
})
.unwrap()
.into(),
)
.await;
response.assert_status_ok();
let response = server
.get("/ron-api/recipe/tags")
.add_cookie(cookie.clone())
.add_query_param("id", recipe_id)
.await;
response.assert_status_ok();
let tags: ron_api::Tags = ron::de::from_bytes(response.as_bytes()).unwrap();
assert_eq!(tags.tags.len(), 1);
assert_eq!(tags.tags[0], "abc".to_string());
Ok(())
}