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> { // 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 = 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> { // 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 = 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> { // 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 = 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> { use std::{cell::RefCell, rc::Rc}; // Arrange. let validation_url: Rc> = 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> { // 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> { // 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> { // 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(()) }