diff --git a/Cargo.toml b/Cargo.toml index 48a938a2f..7b7fe14be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ default = [ ] auth_jwt = ["dep:jsonwebtoken"] cli = ["dep:clap"] -testing = ["dep:axum-test"] +testing = ["dep:axum-test", "dep:scraper"] with-db = ["dep:sea-orm", "dep:sea-orm-migration", "loco-gen/with-db"] # Storage features all_storage = ["storage_aws_s3", "storage_azure", "storage_gcp"] @@ -143,6 +143,8 @@ ulid = { version = "1", optional = true } rusty-sidekiq = { version = "0.11.0", default-features = false, optional = true } bb8 = { version = "0.8.1", optional = true } +scraper = { version = "0.21.0", optional = true } + [workspace.dependencies] chrono = { version = "0.4", features = ["serde"] } diff --git a/docs-site/content/docs/infrastructure/storage.md b/docs-site/content/docs/infrastructure/storage.md index 6eee6aa04..7a0f5a596 100644 --- a/docs-site/content/docs/infrastructure/storage.md +++ b/docs-site/content/docs/infrastructure/storage.md @@ -190,10 +190,12 @@ async fn upload_file( By testing file storage in your controller you can follow this example: ```rust +use loco_rs::testing::prelude::*; + #[tokio::test] #[serial] async fn can_register() { - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let file_content = "loco file upload"; let file_part = Part::bytes(file_content.as_bytes()).file_name("loco.txt"); diff --git a/docs-site/content/docs/processing/mailers.md b/docs-site/content/docs/processing/mailers.md index 0f7e02b07..80be540a3 100644 --- a/docs-site/content/docs/processing/mailers.md +++ b/docs-site/content/docs/processing/mailers.md @@ -218,18 +218,19 @@ Test Description: - Retrieve the mailer instance from the context and call the deliveries() function, which contains information about the number of sent emails and their content. ```rust +use loco_rs::testing::prelude::*; #[tokio::test] #[serial] async fn can_register() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { // Create a request for user registration. // Now you can call the context mailer and use the deliveries function. with_settings!({ - filters => testing::cleanup_email() + filters => cleanup_email() }, { assert_debug_snapshot!(ctx.mailer.unwrap().deliveries()); }); diff --git a/docs-site/content/docs/processing/workers.md b/docs-site/content/docs/processing/workers.md index 3ced08f27..5809ed43a 100644 --- a/docs-site/content/docs/processing/workers.md +++ b/docs-site/content/docs/processing/workers.md @@ -199,11 +199,13 @@ Here's an example of how the test should be structured: ```rust +use loco_rs::testing::prelude::*; + #[tokio::test] #[serial] async fn test_run_report_worker_worker() { // Set up the test environment - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); // Execute the worker in 'ForegroundBlocking' mode, preventing it from running asynchronously assert!( diff --git a/docs-site/content/docs/the-app/controller.md b/docs-site/content/docs/the-app/controller.md index cbf35b9ea..09dd7ef06 100644 --- a/docs-site/content/docs/the-app/controller.md +++ b/docs-site/content/docs/the-app/controller.md @@ -858,18 +858,19 @@ impl PaginationResponse { # Testing When testing controllers, the goal is to call the router's controller endpoint and verify the HTTP response, including the status code, response content, headers, and more. -To initialize a test request, use `testing::request`, which prepares your app routers, providing the request instance and the application context. +To initialize a test request, use `use loco_rs::testing::prelude::*;`, which prepares your app routers, providing the request instance and the application context. In the following example, we have a POST endpoint that returns the data sent in the POST request. ```rust +use loco_rs::testing::prelude::*; #[tokio::test] #[serial] async fn can_print_echo() { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let response = request .post("/example") .json(&serde_json::json!({"site": "Loco"})) diff --git a/docs-site/content/docs/the-app/models.md b/docs-site/content/docs/the-app/models.md index 54a8bcc16..47637d280 100644 --- a/docs-site/content/docs/the-app/models.md +++ b/docs-site/content/docs/the-app/models.md @@ -572,12 +572,14 @@ impl Task for SeedData { 2. In your test section, follow the example below: ```rust +use loco_rs::testing::prelude::*; + #[tokio::test] #[serial] async fn handle_create_with_password_with_duplicate() { - let boot = testing::boot_test::().await; - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await; + seed::(&boot.app_context.db).await.unwrap(); assert!(get_user_by_id(1).ok()); } ``` @@ -695,11 +697,13 @@ If you used the generator to crate a model migration, you should also have an au A typical test contains everything you need to set up test data, boot the app, and reset the database automatically before the testing code runs. It looks like this: ```rust +use loco_rs::testing::prelude::*; + async fn can_find_by_pid() { configure_insta!(); - let boot = testing::boot_test::().await; - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await; + seed::(&boot.app_context.db).await.unwrap(); let existing_user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await; @@ -747,13 +751,15 @@ impl Hooks for App { ## Seeding ```rust +use loco_rs::testing::prelude::*; + #[tokio::test] #[serial] async fn is_user_exists() { configure_insta!(); - let boot = testing::boot_test::().await; - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await; + seed::(&boot.app_context.db).await.unwrap(); assert!(get_user_by_id(1).ok()); } @@ -770,14 +776,15 @@ Example using [insta](https://crates.io/crates/insta) for snapshots. in the following example you can use `cleanup_user_model` which clean all user model data. ```rust +use loco_rs::testing::prelude::*; #[tokio::test] #[serial] async fn can_create_user() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { // create user test with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(current_user_request.text()); }); diff --git a/examples/demo/src/models/roles.rs b/examples/demo/src/models/roles.rs index b591340cb..8c45bf9aa 100644 --- a/examples/demo/src/models/roles.rs +++ b/examples/demo/src/models/roles.rs @@ -1,5 +1,4 @@ use loco_rs::prelude::*; -use sea_orm::entity::prelude::*; pub use super::_entities::roles::{self, ActiveModel, Entity, Model}; use crate::models::{_entities::sea_orm_active_enums::RolesName, users, users_roles}; diff --git a/examples/demo/tests/models/roles.rs b/examples/demo/tests/models/roles.rs index 52f4d8af6..05b033100 100644 --- a/examples/demo/tests/models/roles.rs +++ b/examples/demo/tests/models/roles.rs @@ -1,9 +1,8 @@ use demo_app::{ app::App, - models::{roles, sea_orm_active_enums, users, users::RegisterParams, users_roles}, + models::{roles, sea_orm_active_enums, users, users::RegisterParams}, }; -use loco_rs::{prelude::*, testing}; -use sea_orm::DatabaseConnection; +use loco_rs::testing::prelude::*; use serial_test::serial; macro_rules! configure_insta { @@ -20,8 +19,8 @@ macro_rules! configure_insta { async fn can_add_user_to_admin() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - let new_user: Result = users::Model::create_with_password( + let boot = boot_test::().await.unwrap(); + let new_user = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { email: "user1@example.com".to_string(), @@ -29,8 +28,8 @@ async fn can_add_user_to_admin() { name: "framework".to_string(), }, ) - .await; - let new_user = new_user.unwrap(); + .await + .unwrap(); let role = roles::Model::add_user_to_admin_role(&boot.app_context.db, &new_user) .await .unwrap(); @@ -42,8 +41,8 @@ async fn can_add_user_to_admin() { async fn can_add_user_to_user() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - let new_user: Result = users::Model::create_with_password( + let boot = boot_test::().await.unwrap(); + let new_user = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { email: "user1@example.com".to_string(), @@ -51,8 +50,8 @@ async fn can_add_user_to_user() { name: "framework".to_string(), }, ) - .await; - let new_user = new_user.unwrap(); + .await + .unwrap(); let role = roles::Model::add_user_to_user_role(&boot.app_context.db, &new_user) .await .unwrap(); @@ -64,8 +63,8 @@ async fn can_add_user_to_user() { async fn can_convert_between_user_and_admin() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - let new_user: Result = users::Model::create_with_password( + let boot = boot_test::().await.unwrap(); + let new_user = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { email: "user1@example.com".to_string(), @@ -73,8 +72,8 @@ async fn can_convert_between_user_and_admin() { name: "framework".to_string(), }, ) - .await; - let new_user = new_user.unwrap(); + .await + .unwrap(); let role = roles::Model::add_user_to_user_role(&boot.app_context.db, &new_user) .await .unwrap(); @@ -94,8 +93,8 @@ async fn can_convert_between_user_and_admin() { async fn can_find_user_roles() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - let new_user: Result = users::Model::create_with_password( + let boot = boot_test::().await.unwrap(); + let new_user = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { email: "user1@example.com".to_string(), @@ -103,8 +102,8 @@ async fn can_find_user_roles() { name: "framework".to_string(), }, ) - .await; - let new_user = new_user.unwrap(); + .await + .unwrap(); let role = roles::Model::add_user_to_user_role(&boot.app_context.db, &new_user) .await .unwrap(); @@ -131,8 +130,8 @@ async fn can_find_user_roles() { async fn cannot_find_user_before_conversation() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - let new_user: Result = users::Model::create_with_password( + let boot = boot_test::().await.unwrap(); + let new_user = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { email: "user1@example.com".to_string(), @@ -140,8 +139,8 @@ async fn cannot_find_user_before_conversation() { name: "framework".to_string(), }, ) - .await; - let new_user = new_user.unwrap(); + .await + .unwrap(); let role = roles::Model::find_by_user(&boot.app_context.db, &new_user).await; assert!(role.is_err()); } diff --git a/examples/demo/tests/models/users.rs b/examples/demo/tests/models/users.rs index d4d935bd2..4200f64a7 100644 --- a/examples/demo/tests/models/users.rs +++ b/examples/demo/tests/models/users.rs @@ -3,7 +3,7 @@ use demo_app::{ models::users::{self, Model, RegisterParams}, }; use insta::assert_debug_snapshot; -use loco_rs::{model::ModelError, testing}; +use loco_rs::{model::ModelError, prelude::*}; use sea_orm::{ActiveModelTrait, ActiveValue, IntoActiveModel}; use serial_test::serial; @@ -21,7 +21,7 @@ macro_rules! configure_insta { async fn test_can_validate_model() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let res = users::ActiveModel { name: ActiveValue::set("1".to_string()), @@ -39,7 +39,7 @@ async fn test_can_validate_model() { async fn can_create_with_password() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let params = RegisterParams { email: "test@framework.com".to_string(), @@ -49,7 +49,7 @@ async fn can_create_with_password() { let res = Model::create_with_password(&boot.app_context.db, ¶ms).await; insta::with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(res); }); @@ -60,8 +60,8 @@ async fn can_create_with_password() { async fn handle_create_with_password_with_duplicate() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let new_user: Result = Model::create_with_password( &boot.app_context.db, @@ -80,8 +80,8 @@ async fn handle_create_with_password_with_duplicate() { async fn can_find_by_email() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let existing_user = Model::find_by_email(&boot.app_context.db, "user1@example.com").await; let non_existing_user_results = @@ -96,8 +96,8 @@ async fn can_find_by_email() { async fn can_find_by_pid() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let existing_user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await; @@ -113,8 +113,8 @@ async fn can_find_by_pid() { async fn can_verification_token() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -142,8 +142,8 @@ async fn can_verification_token() { async fn can_set_forgot_password_sent() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -171,8 +171,8 @@ async fn can_set_forgot_password_sent() { async fn can_verified() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -198,8 +198,8 @@ async fn can_verified() { async fn can_reset_password() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await diff --git a/examples/demo/tests/models/users_roles.rs b/examples/demo/tests/models/users_roles.rs index debacd2a8..619464fdb 100644 --- a/examples/demo/tests/models/users_roles.rs +++ b/examples/demo/tests/models/users_roles.rs @@ -2,8 +2,8 @@ use demo_app::{ app::App, models::{roles, sea_orm_active_enums, users, users::RegisterParams, users_roles}, }; -use loco_rs::{prelude::*, testing}; -use sea_orm::{ColumnTrait, DatabaseConnection}; +use loco_rs::prelude::*; +use sea_orm::ColumnTrait; use serial_test::serial; macro_rules! configure_insta { ($($expr:expr),*) => { @@ -19,7 +19,7 @@ macro_rules! configure_insta { async fn can_connect_user_to_user_role() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { @@ -61,7 +61,7 @@ async fn can_connect_user_to_user_role() { async fn can_connect_user_to_admin_role() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { diff --git a/examples/demo/tests/requests/auth.rs b/examples/demo/tests/requests/auth.rs index 957fb7632..93bb6bde4 100644 --- a/examples/demo/tests/requests/auth.rs +++ b/examples/demo/tests/requests/auth.rs @@ -1,6 +1,6 @@ use demo_app::{app::App, models::users}; use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::prelude::*; use rstest::rstest; use serial_test::serial; @@ -22,7 +22,7 @@ macro_rules! configure_insta { async fn can_register() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let email = "test@loco.com"; let payload = serde_json::json!({ "name": "loco", @@ -34,13 +34,13 @@ async fn can_register() { let saved_user = users::Model::find_by_email(&ctx.db, email).await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(saved_user); }); with_settings!({ - filters => testing::cleanup_email() + filters => cleanup_email() }, { assert_debug_snapshot!(ctx.mailer.unwrap().deliveries()); }); @@ -56,7 +56,7 @@ async fn can_register() { async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let email = "test@loco.com"; let register_payload = serde_json::json!({ "name": "loco", @@ -90,7 +90,7 @@ async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) .is_some()); with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(test_name, (response.status_code(), response.text())); }); @@ -103,7 +103,7 @@ async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) async fn can_login_without_verify() { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let email = "test@loco.com"; let password = "12341234"; let register_payload = serde_json::json!({ @@ -125,7 +125,7 @@ async fn can_login_without_verify() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); @@ -138,7 +138,7 @@ async fn can_login_without_verify() { async fn can_reset_password() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let login_data = prepare_data::init_user_login(&request, &ctx).await; let forgot_payload = serde_json::json!({ @@ -180,7 +180,7 @@ async fn can_reset_password() { assert_eq!(response.status_code(), 200); with_settings!({ - filters => testing::cleanup_email() + filters => cleanup_email() }, { assert_debug_snapshot!(ctx.mailer.unwrap().deliveries()); }); diff --git a/examples/demo/tests/requests/cache.rs b/examples/demo/tests/requests/cache.rs index 6dc67c94b..e431f91d7 100644 --- a/examples/demo/tests/requests/cache.rs +++ b/examples/demo/tests/requests/cache.rs @@ -1,6 +1,6 @@ use demo_app::{app::App, models::users}; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use sea_orm::ModelTrait; use serial_test::serial; @@ -20,7 +20,7 @@ macro_rules! configure_insta { async fn ping() { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let response = request.get("cache").await; assert_debug_snapshot!("key_not_exists", (response.text(), response.status_code())); let response = request.post("/cache/insert").await; @@ -36,8 +36,8 @@ async fn ping() { async fn can_get_or_insert() { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + request::(|request, ctx| async move { + seed::(&ctx.db).await.unwrap(); let response = request.get("/cache/get_or_insert").await; assert_eq!(response.text(), "user1"); diff --git a/examples/demo/tests/requests/mylayer.rs b/examples/demo/tests/requests/mylayer.rs index b78b45326..27b8a5e72 100644 --- a/examples/demo/tests/requests/mylayer.rs +++ b/examples/demo/tests/requests/mylayer.rs @@ -1,5 +1,5 @@ use demo_app::{app::App, views::user::UserResponse}; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; use crate::requests::prepare_data; @@ -15,7 +15,7 @@ macro_rules! configure_insta { #[serial] async fn cannot_get_echo_when_no_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -31,7 +31,7 @@ async fn cannot_get_echo_when_no_role_assigned() { #[serial] async fn can_get_echo_when_admin_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -54,7 +54,7 @@ async fn can_get_echo_when_admin_role_assigned() { #[serial] async fn can_get_echo_when_user_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -78,7 +78,7 @@ async fn can_get_echo_when_user_role_assigned() { #[serial] async fn cannot_get_admin_when_no_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -94,7 +94,7 @@ async fn cannot_get_admin_when_no_role() { #[serial] async fn cannot_get_admin_when_user_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -118,7 +118,7 @@ async fn cannot_get_admin_when_user_role_assigned() { #[serial] async fn can_get_admin_when_admin_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -142,7 +142,7 @@ async fn can_get_admin_when_admin_role_assigned() { #[serial] async fn cannot_get_user_when_no_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -158,7 +158,7 @@ async fn cannot_get_user_when_no_role() { #[serial] async fn can_get_user_when_user_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request diff --git a/examples/demo/tests/requests/notes.rs b/examples/demo/tests/requests/notes.rs index ec7ec2f83..5edd5d9a0 100644 --- a/examples/demo/tests/requests/notes.rs +++ b/examples/demo/tests/requests/notes.rs @@ -1,6 +1,6 @@ use demo_app::{app::App, models::_entities::notes::Entity}; use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use rstest::rstest; use sea_orm::entity::prelude::*; use serial_test::serial; @@ -26,14 +26,14 @@ macro_rules! configure_insta { async fn can_get_notes(#[case] test_name: &str, #[case] params: serde_json::Value) { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + request::(|request, ctx| async move { + seed::(&ctx.db).await.unwrap(); let notes = request.get("notes").add_query_params(params).await; with_settings!({ filters => { - let mut combined_filters = testing::get_cleanup_date().clone(); + let mut combined_filters = get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } @@ -51,7 +51,7 @@ async fn can_get_notes(#[case] test_name: &str, #[case] params: serde_json::Valu async fn can_add_note() { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let payload = serde_json::json!({ "title": "loco", "content": "loco note test", @@ -61,7 +61,7 @@ async fn can_add_note() { with_settings!({ filters => { - let mut combined_filters = testing::get_cleanup_date().clone(); + let mut combined_filters = get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } @@ -79,14 +79,14 @@ async fn can_add_note() { async fn can_get_note() { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + request::(|request, ctx| async move { + seed::(&ctx.db).await.unwrap(); let add_note_request = request.get("/notes/1").await; with_settings!({ filters => { - let mut combined_filters = testing::get_cleanup_date().clone(); + let mut combined_filters = get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } @@ -104,15 +104,15 @@ async fn can_get_note() { async fn can_delete_note() { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + request::(|request, ctx| async move { + seed::(&ctx.db).await.unwrap(); let count_before_delete = Entity::find().all(&ctx.db).await.unwrap().len(); let delete_note_request = request.delete("/notes/1").await; with_settings!({ filters => { - let mut combined_filters = testing::get_cleanup_date().clone(); + let mut combined_filters = get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } diff --git a/examples/demo/tests/requests/ping.rs b/examples/demo/tests/requests/ping.rs index 1303301de..84bfae8f6 100644 --- a/examples/demo/tests/requests/ping.rs +++ b/examples/demo/tests/requests/ping.rs @@ -1,6 +1,6 @@ use demo_app::app::App; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use rstest::rstest; // TODO: see how to dedup / extract this to app-local test utils @@ -22,7 +22,7 @@ macro_rules! configure_insta { async fn ping(#[case] test_name: &str, #[case] path: &str) { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let response = request.get(path).await; assert_debug_snapshot!(test_name, (response.text(), response.status_code())); diff --git a/examples/demo/tests/requests/responses.rs b/examples/demo/tests/requests/responses.rs index e68b84c60..05ae12f61 100644 --- a/examples/demo/tests/requests/responses.rs +++ b/examples/demo/tests/requests/responses.rs @@ -1,7 +1,7 @@ use axum::http::HeaderMap; use demo_app::app::App; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use rstest::rstest; use serial_test::serial; // TODO: see how to dedup / extract this to app-local test utils @@ -29,7 +29,7 @@ macro_rules! configure_insta { #[serial] async fn can_return_different_responses(#[case] uri: &str) { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let response = request.get(uri).await; let mut headers = HeaderMap::new(); diff --git a/examples/demo/tests/requests/upload.rs b/examples/demo/tests/requests/upload.rs index 0c2a46667..a5c1fa2e8 100644 --- a/examples/demo/tests/requests/upload.rs +++ b/examples/demo/tests/requests/upload.rs @@ -1,12 +1,12 @@ use axum_test::multipart::{MultipartForm, Part}; use demo_app::{app::App, views}; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; #[tokio::test] #[serial] async fn can_upload_file() { - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let file_content = "loco file upload"; let file_part = Part::bytes(file_content.as_bytes()).file_name("loco.txt"); diff --git a/examples/demo/tests/requests/user.rs b/examples/demo/tests/requests/user.rs index a6112a718..4ca15477c 100644 --- a/examples/demo/tests/requests/user.rs +++ b/examples/demo/tests/requests/user.rs @@ -1,6 +1,6 @@ use demo_app::app::App; use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; use super::prepare_data; @@ -21,7 +21,7 @@ macro_rules! configure_insta { async fn can_get_current_user() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); @@ -31,7 +31,7 @@ async fn can_get_current_user() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); @@ -44,7 +44,7 @@ async fn can_get_current_user() { async fn can_get_current_user_with_api_key() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user_data = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user_data.user.api_key); @@ -54,7 +54,7 @@ async fn can_get_current_user_with_api_key() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); @@ -67,7 +67,7 @@ async fn can_get_current_user_with_api_key() { async fn can_convert_user_to_user_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); @@ -77,7 +77,7 @@ async fn can_convert_user_to_user_role() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); @@ -90,7 +90,7 @@ async fn can_convert_user_to_user_role() { async fn can_convert_user_to_admin_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); @@ -100,7 +100,7 @@ async fn can_convert_user_to_admin_role() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); diff --git a/examples/demo/tests/requests/view_engine.rs b/examples/demo/tests/requests/view_engine.rs index 4f7422556..b8980e0e0 100644 --- a/examples/demo/tests/requests/view_engine.rs +++ b/examples/demo/tests/requests/view_engine.rs @@ -1,6 +1,6 @@ use demo_app::app::App; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use rstest::rstest; use serial_test::serial; // TODO: see how to dedup / extract this to app-local test utils @@ -22,7 +22,7 @@ macro_rules! configure_insta { #[serial] async fn can_get_view_engine(#[case] uri: &str) { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let response = request.get(&format!("/view-engine/{uri}")).await; assert_debug_snapshot!( diff --git a/examples/demo/tests/tasks/foo.rs b/examples/demo/tests/tasks/foo.rs index e45bbc085..4d19e4397 100644 --- a/examples/demo/tests/tasks/foo.rs +++ b/examples/demo/tests/tasks/foo.rs @@ -1,11 +1,11 @@ use demo_app::app::App; -use loco_rs::{boot::run_task, task, testing}; +use loco_rs::{boot::run_task, task, testing::prelude::*}; use serial_test::serial; #[tokio::test] #[serial] async fn test_can_run_foo_task() { - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); assert!(run_task::( &boot.app_context, diff --git a/examples/demo/tests/tasks/seed.rs b/examples/demo/tests/tasks/seed.rs index 2657bf2a4..f1666d08a 100644 --- a/examples/demo/tests/tasks/seed.rs +++ b/examples/demo/tests/tasks/seed.rs @@ -1,11 +1,11 @@ use demo_app::app::App; -use loco_rs::{boot::run_task, task, testing}; +use loco_rs::{boot::run_task, task, testing::prelude::*}; use serial_test::serial; #[tokio::test] #[serial] async fn test_can_seed_data() { - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); assert!(run_task::( &boot.app_context, diff --git a/loco-gen/src/templates/controller/api/test.t b/loco-gen/src/templates/controller/api/test.t index 006ccba28..9611bfb79 100644 --- a/loco-gen/src/templates/controller/api/test.t +++ b/loco-gen/src/templates/controller/api/test.t @@ -9,13 +9,13 @@ injections: content: "pub mod {{ file_name }};" --- use {{pkg_name}}::app::App; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; #[tokio::test] #[serial] async fn can_get_{{ name | plural | snake_case }}() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let res = request.get("/api/{{ name | plural | snake_case }}/").await; assert_eq!(res.status_code(), 200); @@ -29,7 +29,7 @@ async fn can_get_{{ name | plural | snake_case }}() { #[tokio::test] #[serial] async fn can_get_{{action}}() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let res = request.get("/{{ name | plural | snake_case }}/{{action}}").await; assert_eq!(res.status_code(), 200); }) diff --git a/loco-gen/src/templates/model_test.t b/loco-gen/src/templates/model_test.t index cde199460..cd7268999 100644 --- a/loco-gen/src/templates/model_test.t +++ b/loco-gen/src/templates/model_test.t @@ -9,7 +9,7 @@ injections: content: "mod {{plural_snake}};" --- use {{pkg_name}}::app::App; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; macro_rules! configure_insta { @@ -25,8 +25,8 @@ macro_rules! configure_insta { async fn test_model() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); // query your model, e.g.: // diff --git a/loco-gen/src/templates/request_test.t b/loco-gen/src/templates/request_test.t index 43302faf3..08d02f0a1 100644 --- a/loco-gen/src/templates/request_test.t +++ b/loco-gen/src/templates/request_test.t @@ -9,13 +9,13 @@ injections: content: "pub mod {{ file_name }};" --- use {{pkg_name}}::app::App; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; #[tokio::test] #[serial] async fn can_get_echo() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let payload = serde_json::json!({ "foo": "bar", }); @@ -30,7 +30,7 @@ async fn can_get_echo() { #[tokio::test] #[serial] async fn can_request_root() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let res = request.get("/{{ name | snake_case }}").await; assert_eq!(res.status_code(), 200); assert_eq!(res.text(), "hello"); diff --git a/loco-gen/src/templates/scaffold/api/test.t b/loco-gen/src/templates/scaffold/api/test.t index a0c672c3f..f20cfa0c6 100644 --- a/loco-gen/src/templates/scaffold/api/test.t +++ b/loco-gen/src/templates/scaffold/api/test.t @@ -9,13 +9,13 @@ injections: content: "pub mod {{ file_name }};" --- use {{pkg_name}}::app::App; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use serial_test::serial; #[tokio::test] #[serial] async fn can_get_{{ name | plural | snake_case }}() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let res = request.get("/api/{{ name | plural | snake_case }}/").await; assert_eq!(res.status_code(), 200); diff --git a/loco-gen/src/templates/task_test.t b/loco-gen/src/templates/task_test.t index 69eee2d05..096a435ee 100644 --- a/loco-gen/src/templates/task_test.t +++ b/loco-gen/src/templates/task_test.t @@ -9,7 +9,7 @@ injections: content: "pub mod {{ file_name }};" --- use {{pkg_name}}::app::App; -use loco_rs::{task, testing}; +use loco_rs::{task, testing::prelude::*}; use loco_rs::boot::run_task; use serial_test::serial; @@ -17,7 +17,7 @@ use serial_test::serial; #[tokio::test] #[serial] async fn test_can_run_{{name | snake_case}}() { - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); assert!( run_task::(&boot.app_context, Some(&"{{name}}".to_string()), &task::Vars::default()) diff --git a/loco-gen/src/templates/worker_test.t b/loco-gen/src/templates/worker_test.t index d302ae079..8fc46954c 100644 --- a/loco-gen/src/templates/worker_test.t +++ b/loco-gen/src/templates/worker_test.t @@ -9,8 +9,7 @@ injections: content: "pub mod {{ name | snake_case }};" --- use {{pkg_name}}::app::App; -use loco_rs::prelude::*; -use loco_rs::testing; +use loco_rs::{bgworker::BackgroundWorker, testing::prelude::*}; use {{pkg_name}}::workers::{{module_name}}::{{struct_name}}Worker; use {{pkg_name}}::workers::{{module_name}}::{{struct_name}}WorkerArgs; @@ -20,7 +19,7 @@ use serial_test::serial; #[tokio::test] #[serial] async fn test_run_{{module_name}}_worker() { - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); // Execute the worker ensuring that it operates in 'ForegroundBlocking' mode, which prevents the addition of your worker to the background assert!( diff --git a/loco-new/base_template/tests/models/users.rs.t b/loco-new/base_template/tests/models/users.rs.t index e569a1c4c..7ace87eca 100644 --- a/loco-new/base_template/tests/models/users.rs.t +++ b/loco-new/base_template/tests/models/users.rs.t @@ -1,5 +1,5 @@ use insta::assert_debug_snapshot; -use loco_rs::{model::ModelError, testing}; +use loco_rs::{model::ModelError, testing::prelude::*}; use {{settings.module_name}}::{ app::App, models::users::{self, Model, RegisterParams}, @@ -21,7 +21,7 @@ macro_rules! configure_insta { async fn test_can_validate_model() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let res = users::ActiveModel { name: ActiveValue::set("1".to_string()), @@ -39,7 +39,7 @@ async fn test_can_validate_model() { async fn can_create_with_password() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let params = RegisterParams { email: "test@framework.com".to_string(), @@ -49,7 +49,7 @@ async fn can_create_with_password() { let res = Model::create_with_password(&boot.app_context.db, ¶ms).await; insta::with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(res); }); @@ -60,8 +60,8 @@ async fn can_create_with_password() { async fn handle_create_with_password_with_duplicate() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let new_user: Result = Model::create_with_password( &boot.app_context.db, @@ -80,8 +80,8 @@ async fn handle_create_with_password_with_duplicate() { async fn can_find_by_email() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let existing_user = Model::find_by_email(&boot.app_context.db, "user1@example.com").await; let non_existing_user_results = @@ -96,8 +96,8 @@ async fn can_find_by_email() { async fn can_find_by_pid() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let existing_user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await; @@ -113,8 +113,8 @@ async fn can_find_by_pid() { async fn can_verification_token() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -142,8 +142,8 @@ async fn can_verification_token() { async fn can_set_forgot_password_sent() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -171,8 +171,8 @@ async fn can_set_forgot_password_sent() { async fn can_verified() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -198,8 +198,8 @@ async fn can_verified() { async fn can_reset_password() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context.db).await.unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await diff --git a/loco-new/base_template/tests/requests/auth.rs.t b/loco-new/base_template/tests/requests/auth.rs.t index fbc2d2875..c7e37da7d 100644 --- a/loco-new/base_template/tests/requests/auth.rs.t +++ b/loco-new/base_template/tests/requests/auth.rs.t @@ -1,5 +1,5 @@ use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::testing::prelude::*; use {{settings.module_name}}::{app::App, models::users}; use rstest::rstest; use serial_test::serial; @@ -22,7 +22,7 @@ macro_rules! configure_insta { async fn can_register() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let email = "test@loco.com"; let payload = serde_json::json!({ "name": "loco", @@ -34,13 +34,13 @@ async fn can_register() { let saved_user = users::Model::find_by_email(&ctx.db, email).await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(saved_user); }); with_settings!({ - filters => testing::cleanup_email() + filters => cleanup_email() }, { assert_debug_snapshot!(ctx.mailer.unwrap().deliveries()); }); @@ -56,7 +56,7 @@ async fn can_register() { async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let email = "test@loco.com"; let register_payload = serde_json::json!({ "name": "loco", @@ -93,7 +93,7 @@ async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) .is_some()); with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!(test_name, (response.status_code(), response.text())); }); @@ -106,7 +106,7 @@ async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) async fn can_login_without_verify() { configure_insta!(); - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let email = "test@loco.com"; let password = "12341234"; let register_payload = serde_json::json!({ @@ -131,7 +131,7 @@ async fn can_login_without_verify() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); @@ -144,7 +144,7 @@ async fn can_login_without_verify() { async fn can_reset_password() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let login_data = prepare_data::init_user_login(&request, &ctx).await; let forgot_payload = serde_json::json!({ @@ -186,7 +186,7 @@ async fn can_reset_password() { assert_eq!(response.status_code(), 200); with_settings!({ - filters => testing::cleanup_email() + filters => cleanup_email() }, { assert_debug_snapshot!(ctx.mailer.unwrap().deliveries()); }); @@ -199,7 +199,7 @@ async fn can_reset_password() { async fn can_get_current_user() { configure_insta!(); - testing::request::(|request, ctx| async move { + request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); @@ -209,7 +209,7 @@ async fn can_get_current_user() { .await; with_settings!({ - filters => testing::cleanup_user_model() + filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); diff --git a/loco-new/base_template/tests/requests/home.rs.t b/loco-new/base_template/tests/requests/home.rs.t index 492acc632..fc33ebe8f 100644 --- a/loco-new/base_template/tests/requests/home.rs.t +++ b/loco-new/base_template/tests/requests/home.rs.t @@ -1,4 +1,4 @@ -use loco_rs::testing; +use loco_rs::testing::prelude::*; use {{settings.module_name}}::app::App; use serial_test::serial; @@ -6,7 +6,7 @@ use serial_test::serial; #[serial] async fn can_get_home() { - testing::request::(|request, _ctx| async move { + request::(|request, _ctx| async move { let res = request.get("/api").await; assert_eq!(res.status_code(), 200); diff --git a/src/prelude.rs b/src/prelude.rs index 3f247cd0d..03ae9cf8c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -48,3 +48,5 @@ pub use crate::{ pub mod model { pub use crate::model::query; } +#[cfg(feature = "testing")] +pub use crate::testing::prelude::*; diff --git a/src/testing.rs b/src/testing.rs deleted file mode 100644 index 6ca5fda10..000000000 --- a/src/testing.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! # Test Utilities Module -//! -//! This module provides utility functions and constants for easy testing -//! purposes, including cleaning up data patterns and bootstrapping the -//! application for testing. - -use std::{net::SocketAddr, sync::OnceLock}; - -use axum_test::{TestServer, TestServerConfig}; -#[cfg(feature = "with-db")] -use sea_orm::DatabaseConnection; - -use crate::{ - app::{AppContext, Hooks}, - boot::{self, BootResult}, - environment::Environment, - Result, -}; - -static CLEANUP_USER_MODEL: OnceLock> = OnceLock::new(); -static CLEANUP_DATE: OnceLock> = OnceLock::new(); -static CLEANUP_MODEL: OnceLock> = OnceLock::new(); -static CLEANUP_MAIL: OnceLock> = OnceLock::new(); - -pub fn get_cleanup_user_model() -> &'static Vec<(&'static str, &'static str)> { - CLEANUP_USER_MODEL.get_or_init(|| { - vec![ - ( - r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", - "PID", - ), - (r"password: (.*{60}),", "password: \"PASSWORD\","), - (r"([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)", "TOKEN"), - ] - }) -} - -pub fn get_cleanup_date() -> &'static Vec<(&'static str, &'static str)> { - CLEANUP_DATE.get_or_init(|| { - vec![ - ( - r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+\d{2}:\d{2}", - "DATE", - ), // with tz - (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+", "DATE"), - (r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})", "DATE"), - ] - }) -} - -pub fn get_cleanup_model() -> &'static Vec<(&'static str, &'static str)> { - CLEANUP_MODEL.get_or_init(|| vec![(r"id: \d+,", "id: ID")]) -} - -pub fn get_cleanup_mail() -> &'static Vec<(&'static str, &'static str)> { - CLEANUP_MAIL.get_or_init(|| { - vec![ - (r"[0-9A-Za-z]+{40}", "IDENTIFIER"), - ( - r"\w+, \d{1,2} \w+ \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}", - "DATE", - ), - ( - r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", - "RANDOM_ID", - ), - ( - r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4})-[0-9a-fA-F]{4}-.*[0-9a-fA-F]{2}", - "RANDOM_ID", - ), - ] - }) -} - -/// Combines cleanup filters from various categories (user model, date, and -/// model) into one list. This is used for data cleaning and pattern -/// replacement. -/// -/// # Example -/// -/// The provided example demonstrates how to efficiently clean up a user model. -/// This process is particularly valuable when you need to capture a snapshot of -/// user model data that includes dynamic elements such as incrementing IDs, -/// automatically generated PIDs, creation/update timestamps, and similar -/// attributes. -/// -/// ```rust,ignore -/// use myapp::app::App; -/// use loco_rs::testing; -/// use migration::Migrator; -/// -/// #[tokio::test] -/// async fn test_create_user() { -/// let boot = testing::boot_test::().await; -/// -/// // Create a user and save into the database. -/// -/// // capture the snapshot and cleanup the data. -/// with_settings!({ -/// filters => testing::cleanup_user_model() -/// }, { -/// assert_debug_snapshot!(saved_user); -/// }); -/// } -/// ``` -#[must_use] -pub fn cleanup_user_model() -> Vec<(&'static str, &'static str)> { - let mut combined_filters = get_cleanup_user_model().clone(); - combined_filters.extend(get_cleanup_date().iter().copied()); - combined_filters.extend(get_cleanup_model().iter().copied()); - combined_filters -} - -/// Combines cleanup filters from emails that can be dynamic -#[must_use] -pub fn cleanup_email() -> Vec<(&'static str, &'static str)> { - let mut combined_filters = get_cleanup_mail().clone(); - combined_filters.extend(get_cleanup_date().iter().copied()); - combined_filters -} - -/// Bootstraps test application with test environment hard coded. -/// -/// # Errors -/// when could not bootstrap the test environment -/// -/// # Example -/// -/// The provided example demonstrates how to boot the test case with the -/// application context. -/// -/// ```rust,ignore -/// use myapp::app::App; -/// use loco_rs::testing; -/// use migration::Migrator; -/// -/// #[tokio::test] -/// async fn test_create_user() { -/// let boot = testing::boot_test::().await; -/// -/// /// ..... -/// assert!(false) -/// } -/// ``` -pub async fn boot_test() -> Result { - H::boot(boot::StartMode::ServerOnly, &Environment::Test).await -} - -#[cfg(feature = "with-db")] -/// Seeds data into the database. -/// -/// -/// # Errors -/// When seed fails -/// -/// # Example -/// -/// The provided example demonstrates how to boot the test case and run seed -/// data. -/// -/// ```rust,ignore -/// use myapp::app::App; -/// use loco_rs::testing; -/// use migration::Migrator; -/// -/// #[tokio::test] -/// async fn test_create_user() { -/// let boot = testing::boot_test::().await; -/// testing::seed::(&boot.app_context.db).await.unwrap(); -/// -/// /// ..... -/// assert!(false) -/// } -/// ``` -pub async fn seed(db: &DatabaseConnection) -> Result<()> { - let path = std::path::Path::new("src/fixtures"); - H::seed(db, path).await -} - -#[allow(clippy::future_not_send)] -/// Initiates a test request with a provided callback. -/// -/// -/// # Panics -/// When could not initialize the test request.this errors can be when could not -/// initialize the test app -/// -/// # Example -/// -/// The provided example demonstrates how to create a test that check -/// application HTTP endpoints -/// -/// ```rust,ignore -/// use myapp::app::App; -/// use loco_rs::testing; -/// -/// #[tokio::test] -/// #[serial] -/// async fn can_register() { -/// testing::request::(|request, ctx| async move { -/// let response = request.post("/auth/register").json(&serde_json::json!({})).await; -/// -/// with_settings!({ -/// filters => testing::cleanup_user_model() -/// }, { -/// assert_debug_snapshot!(response); -/// }); -/// }) -/// .await; -/// } -/// ``` -#[allow(clippy::future_not_send)] -pub async fn request(callback: F) -where - F: FnOnce(TestServer, AppContext) -> Fut, - Fut: std::future::Future, -{ - let boot = boot_test::().await.unwrap(); - - let config = TestServerConfig { - default_content_type: Some("application/json".to_string()), - ..Default::default() - }; - let server = TestServer::new_with_config( - boot.router - .unwrap() - .into_make_service_with_connect_info::(), - config, - ) - .unwrap(); - - callback(server, boot.app_context.clone()).await; -} diff --git a/src/testing/db.rs b/src/testing/db.rs new file mode 100644 index 000000000..6ea2889fc --- /dev/null +++ b/src/testing/db.rs @@ -0,0 +1,33 @@ +use sea_orm::DatabaseConnection; + +use crate::{app::Hooks, Result}; + +/// Seeds data into the database. +/// +/// +/// # Errors +/// When seed fails +/// +/// # Example +/// +/// The provided example demonstrates how to boot the test case and run seed +/// data. +/// +/// ```rust,ignore +/// use myapp::app::App; +/// use loco_rs::testing::prelude::*; +/// use migration::Migrator; +/// +/// #[tokio::test] +/// async fn test_create_user() { +/// let boot = boot_test::().await; +/// seed::(&boot.app_context.db).await.unwrap(); +/// +/// /// ..... +/// assert!(false) +/// } +/// ``` +pub async fn seed(db: &DatabaseConnection) -> Result<()> { + let path = std::path::Path::new("src/fixtures"); + H::seed(db, path).await +} diff --git a/src/testing/mod.rs b/src/testing/mod.rs new file mode 100644 index 000000000..2eefa8bd2 --- /dev/null +++ b/src/testing/mod.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "with-db")] +pub mod db; +pub mod prelude; +pub mod redaction; +pub mod request; +pub mod selector; diff --git a/src/testing/prelude.rs b/src/testing/prelude.rs new file mode 100644 index 000000000..fc5e22aac --- /dev/null +++ b/src/testing/prelude.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "with-db")] +pub use crate::testing::db::*; +pub use crate::testing::{redaction::*, request::*, selector::*}; diff --git a/src/testing/redaction.rs b/src/testing/redaction.rs new file mode 100644 index 000000000..0555f2f76 --- /dev/null +++ b/src/testing/redaction.rs @@ -0,0 +1,103 @@ +use std::sync::OnceLock; + +static CLEANUP_USER_MODEL: OnceLock> = OnceLock::new(); +static CLEANUP_DATE: OnceLock> = OnceLock::new(); +static CLEANUP_MODEL: OnceLock> = OnceLock::new(); +static CLEANUP_MAIL: OnceLock> = OnceLock::new(); + +pub fn get_cleanup_user_model() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_USER_MODEL.get_or_init(|| { + vec![ + ( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", + "PID", + ), + (r"password: (.*{60}),", "password: \"PASSWORD\","), + (r"([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)", "TOKEN"), + ] + }) +} + +pub fn get_cleanup_date() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_DATE.get_or_init(|| { + vec![ + ( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+\d{2}:\d{2}", + "DATE", + ), // with tz + (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+", "DATE"), + (r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})", "DATE"), + ] + }) +} + +pub fn get_cleanup_model() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_MODEL.get_or_init(|| vec![(r"id: \d+,", "id: ID")]) +} + +pub fn get_cleanup_mail() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_MAIL.get_or_init(|| { + vec![ + (r"[0-9A-Za-z]+{40}", "IDENTIFIER"), + ( + r"\w+, \d{1,2} \w+ \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}", + "DATE", + ), + ( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", + "RANDOM_ID", + ), + ( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4})-[0-9a-fA-F]{4}-.*[0-9a-fA-F]{2}", + "RANDOM_ID", + ), + ] + }) +} + +/// Combines cleanup filters from various categories (user model, date, and +/// model) into one list. This is used for data cleaning and pattern +/// replacement. +/// +/// # Example +/// +/// The provided example demonstrates how to efficiently clean up a user model. +/// This process is particularly valuable when you need to capture a snapshot of +/// user model data that includes dynamic elements such as incrementing IDs, +/// automatically generated PIDs, creation/update timestamps, and similar +/// attributes. +/// +/// ```rust,ignore +/// use myapp::app::App; +/// use loco_rs::testing::prelude::*; +/// use migration::Migrator; +/// +/// #[tokio::test] +/// async fn test_create_user() { +/// let boot = boot_test::().await; +/// +/// // Create a user and save into the database. +/// +/// // capture the snapshot and cleanup the data. +/// with_settings!({ +/// filters => cleanup_user_model() +/// }, { +/// assert_debug_snapshot!(saved_user); +/// }); +/// } +/// ``` +#[must_use] +pub fn cleanup_user_model() -> Vec<(&'static str, &'static str)> { + let mut combined_filters = get_cleanup_user_model().clone(); + combined_filters.extend(get_cleanup_date().iter().copied()); + combined_filters.extend(get_cleanup_model().iter().copied()); + combined_filters +} + +/// Combines cleanup filters from emails that can be dynamic +#[must_use] +pub fn cleanup_email() -> Vec<(&'static str, &'static str)> { + let mut combined_filters = get_cleanup_mail().clone(); + combined_filters.extend(get_cleanup_date().iter().copied()); + combined_filters +} diff --git a/src/testing/request.rs b/src/testing/request.rs new file mode 100644 index 000000000..1aec50ff0 --- /dev/null +++ b/src/testing/request.rs @@ -0,0 +1,92 @@ +use std::net::SocketAddr; + +use axum_test::{TestServer, TestServerConfig}; + +use crate::{ + app::{AppContext, Hooks}, + boot::{self, BootResult}, + environment::Environment, + Result, +}; + +/// Bootstraps test application with test environment hard coded. +/// +/// # Errors +/// when could not bootstrap the test environment +/// +/// # Example +/// +/// The provided example demonstrates how to boot the test case with the +/// application context. +/// +/// ```rust,ignore +/// use myapp::app::App; +/// use loco_rs::testing::prelude::*; +/// use migration::Migrator; +/// +/// #[tokio::test] +/// async fn test_create_user() { +/// let boot = boot_test::().await; +/// +/// /// ..... +/// assert!(false) +/// } +/// ``` +pub async fn boot_test() -> Result { + H::boot(boot::StartMode::ServerOnly, &Environment::Test).await +} + +#[allow(clippy::future_not_send)] +/// Initiates a test request with a provided callback. +/// +/// +/// # Panics +/// When could not initialize the test request.this errors can be when could not +/// initialize the test app +/// +/// # Example +/// +/// The provided example demonstrates how to create a test that check +/// application HTTP endpoints +/// +/// ```rust,ignore +/// use myapp::app::App; +/// use loco_rs::testing::prelude::*; +/// +/// #[tokio::test] +/// #[serial] +/// async fn can_register() { +/// request::(|request, ctx| async move { +/// let response = request.post("/auth/register").json(&serde_json::json!({})).await; +/// +/// with_settings!({ +/// filters => cleanup_user_model() +/// }, { +/// assert_debug_snapshot!(response); +/// }); +/// }) +/// .await; +/// } +/// ``` +#[allow(clippy::future_not_send)] +pub async fn request(callback: F) +where + F: FnOnce(TestServer, AppContext) -> Fut, + Fut: std::future::Future, +{ + let boot = boot_test::().await.unwrap(); + + let config = TestServerConfig { + default_content_type: Some("application/json".to_string()), + ..Default::default() + }; + let server = TestServer::new_with_config( + boot.router + .unwrap() + .into_make_service_with_connect_info::(), + config, + ) + .unwrap(); + + callback(server, boot.app_context.clone()).await; +} diff --git a/src/testing/selector.rs b/src/testing/selector.rs new file mode 100644 index 000000000..b5d4178ab --- /dev/null +++ b/src/testing/selector.rs @@ -0,0 +1,525 @@ +use scraper::{Html, Selector}; + +/// Asserts that an element matching the given CSS selector exists in the +/// provided HTML. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +///
Some content here
+/// +/// "#; +/// assert_css_exists(html, ".some-class"); +/// ``` +/// +/// # Panics +/// +/// This function will panic if no element matching the selector is found in the +/// HTML. +pub fn assert_css_exists(html: &str, selector: &str) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + assert!( + document.select(&parsed_selector).count() > 0, + "Element matching selector '{selector:?}' not found" + ); +} + +/// Asserts that an element matching the given CSS selector does **not** exist +/// in the provided HTML. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +///
Some content here
+/// +/// "#; +/// assert_css_not_exists(html, ".nonexistent-class"); +/// ``` +/// +/// # Panics +/// +/// This function will panic if an element matching the selector is found in the +/// HTML. +pub fn assert_css_not_exists(html: &str, selector: &str) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + assert!( + document.select(&parsed_selector).count() == 0, + "Element matching selector '{selector:?}' should not exist" + ); +} + +/// Asserts that the text content of an element matching the given CSS selector +/// exactly matches the expected text. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +///

Welcome to Loco

+/// +/// "#; +/// assert_css_eq(html, "h1.title", "Welcome to Loco"); +/// ``` +/// +/// # Panics +/// +/// This function will panic if the text of the found element does not match the +/// expected text. +pub fn assert_css_eq(html: &str, selector: &str, expected_text: &str) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + let mut found = false; + + for element in document.select(&parsed_selector) { + let text = element.text().collect::>().join(""); + if text == expected_text { + found = true; + break; + } + } + + assert!( + found, + "Text does not match: Expected '{expected_text:?}' but found a different value or no \ + match for selector '{selector:?}'" + ); +} + +/// Asserts that an `` element matching the given CSS selector has the `href` +/// attribute with the specified value. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +/// Link +/// +/// "#; +/// assert_link(html, "a", "https://loco.rs"); +/// ``` +/// +/// # Panics +/// +/// This function will panic if no `` element matching the selector is found, +/// if the element does not have the `href` attribute, or if the `href` +/// attribute's value does not match the expected value. +pub fn assert_link(html: &str, selector: &str, expected_href: &str) { + // Use `assert_attribute_eq` to check that the `href` attribute exists and + // matches the expected value + assert_attribute_eq(html, selector, "href", expected_href); +} + +/// Asserts that an element matching the given CSS selector has the specified +/// attribute. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +/// +/// Link +/// +/// "#; +/// assert_attribute_exists(html, "button", "onclick"); +/// assert_attribute_exists(html, "a", "href"); +/// ``` +/// +/// # Panics +/// +/// This function will panic if no element matching the selector is found, or if +/// the element does not have the specified attribute. +pub fn assert_attribute_exists(html: &str, selector: &str, attribute: &str) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + + let mut found = false; + + for element in document.select(&parsed_selector) { + if element.value().attr(attribute).is_some() { + found = true; + break; + } + } + + assert!( + found, + "Element matching selector '{selector:?}' does not have the attribute '{attribute}'" + ); +} + +/// Asserts that the specified attribute of an element matching the given CSS +/// selector matches the expected value. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +/// +/// Link +/// +/// "#; +/// assert_attribute_exists(html, "button", "onclick"); +/// assert_attribute_exists(html, "a", "href"); +/// ``` +/// +/// # Panics +/// +/// This function will panic if no element matching the selector is found, if +/// the element does not have the specified attribute, or if the attribute's +/// value does not match the expected value. +pub fn assert_attribute_eq(html: &str, selector: &str, attribute: &str, expected_value: &str) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + + let mut found = false; + + for element in document.select(&parsed_selector) { + if let Some(attr_value) = element.value().attr(attribute) { + if attr_value == expected_value { + found = true; + break; + } + } + } + + assert!( + found, + "Expected attribute '{attribute}' with value '{expected_value}' for selector \ + '{selector:?}', but found a different value or no value." + ); +} + +/// Asserts that the number of elements matching the given CSS selector in the +/// provided HTML is exactly the expected count. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +///
    +///
  • Post 1
  • +///
  • Post 2
  • +///
  • Post 3
  • +///
+/// +/// "#; +/// assert_count(html, "ul#posts li", 3); +/// ``` +/// +/// # Panics +/// +/// This function will panic if the number of elements matching the selector is +/// not equal to the expected count. +pub fn assert_count(html: &str, selector: &str, expected_count: usize) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + + let count = document.select(&parsed_selector).count(); + + assert!( + count == expected_count, + "Expected {expected_count} elements matching selector '{selector:?}', but found {count} \ + elements." + ); +} + +/// Collects the text content of all elements matching the given CSS selector +/// and asserts that they match the expected text. +/// +/// # Example +/// +/// ```rust +/// use loco_rs::testing::prelude::*; +/// +/// let html = r#" +/// +/// +///
    +///
  • Post 1
  • +///
  • Post 2
  • +///
  • Post 3
  • +///
+/// +/// "#; +/// assert_collect_text(html, "ul#posts li", &["Post 1", "Post 2", "Post 3"]); +/// ``` +/// +/// # Panics +/// +/// This function will panic if the text content of the elements does not match +/// the expected values. +pub fn assert_collect_text(html: &str, selector: &str, expected_texts: &[&str]) { + let document = Html::parse_document(html); + let parsed_selector = Selector::parse(selector).unwrap(); + + let collected_texts: Vec = document + .select(&parsed_selector) + .map(|element| element.text().collect::>().concat()) + .collect(); + + assert_eq!( + collected_texts, expected_texts, + "Expected texts {expected_texts:?}, but found {collected_texts:?}." + ); +} + +// Test cases +#[cfg(test)] +mod tests { + use super::*; + + fn setup_test_html() -> &'static str { + r#" + + +
Some content here
+
Another content here
+

Welcome to Loco

+ + Link +
    +
  • Post 1
  • +
  • Post 2
  • +
  • Post 3
  • +
+ + + + + + + + + + + + + + + +
Post 1Author 1
Post 2Author 2
Post 3Author 3
+ + + + "# + } + + #[test] + fn test_assert_css_exists() { + let html = setup_test_html(); + + assert_css_exists(html, ".some-class"); + + let result = std::panic::catch_unwind(|| { + assert_css_exists(html, ".nonexistent-class"); + }); + assert!(result.is_err(), "Expected panic for non-existent selector"); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Element matching selector '\".nonexistent-class\"' not found" + ); + } + } + + #[test] + fn test_assert_css_not_exists() { + let html = setup_test_html(); + + assert_css_not_exists(html, ".nonexistent-class"); + + let result = std::panic::catch_unwind(|| { + assert_css_not_exists(html, ".some-class"); + }); + assert!(result.is_err(), "Expected panic for non-existent selector"); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Element matching selector '\".some-class\"' should not exist" + ); + } + } + + #[test] + fn test_assert_css_eq() { + let html = setup_test_html(); + + assert_css_eq(html, "h1.title", "Welcome to Loco"); + + let result = std::panic::catch_unwind(|| { + assert_css_eq(html, "h1.title", "Wrong text"); + }); + assert!(result.is_err(), "Expected panic for mismatched text"); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Text does not match: Expected '\"Wrong text\"' but found a different value or \ + no match for selector '\"h1.title\"'" + ); + } + } + + #[test] + fn test_assert_link() { + let html = setup_test_html(); + + assert_link(html, "a", "https://loco.rs"); + + let result = std::panic::catch_unwind(|| { + assert_link(html, "a", "https://nonexistent.com"); + }); + + assert!(result.is_err()); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Expected attribute 'href' with value 'https://nonexistent.com' for selector \ + '\"a\"', but found a different value or no value." + ); + } + } + + #[test] + fn test_assert_attribute_exists() { + let html = setup_test_html(); + + assert_attribute_exists(html, "button", "onclick"); + assert_attribute_exists(html, "a", "href"); + + let result = std::panic::catch_unwind(|| { + assert_attribute_exists(html, "button", "href"); + }); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Element matching selector '\"button\"' does not have the attribute 'href'" + ); + } + } + + #[test] + fn test_assert_attribute_eq() { + let html = setup_test_html(); + assert_attribute_eq(html, "button", "onclick", "alert('clicked')"); + assert_attribute_eq(html, "a", "href", "https://loco.rs"); + + let result = std::panic::catch_unwind(|| { + assert_attribute_eq(html, "button", "onclick", "alert('wrong')"); + }); + + assert!(result.is_err()); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Expected attribute 'onclick' with value 'alert('wrong')' for selector \ + '\"button\"', but found a different value or no value." + ); + } + } + + #[test] + fn test_assert_count() { + let html = setup_test_html(); + assert_count(html, "ul#posts li", 3); + + let result = std::panic::catch_unwind(|| { + assert_count(html, "ul#posts li", 1); + }); + + assert!(result.is_err()); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"Expected 1 elements matching selector '\"ul#posts li\"', but found 3 elements." + ); + } + } + + #[test] + fn test_assert_collect_text() { + let html = setup_test_html(); + assert_collect_text(html, "ul#posts li", &["Post 1", "Post 2", "Post 3"]); + + let result = std::panic::catch_unwind(|| { + assert_collect_text(html, "ul#posts li", &["Post 1", "Post 2", "Wrong Post"]); + }); + + assert!(result.is_err()); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"assertion `left == right` failed: Expected texts [\"Post 1\", \"Post 2\", \ + \"Wrong Post\"], but found [\"Post 1\", \"Post 2\", \"Post 3\"].\n left: \ + [\"Post 1\", \"Post 2\", \"Post 3\"]\n right: [\"Post 1\", \"Post 2\", \"Wrong \ + Post\"]" + ); + } + } + + #[test] + fn test_assert_collect_text_table() { + let html = setup_test_html(); + assert_collect_text( + html, + "table tr td", + &[ + "Post 1", "Author 1", "Post 2", "Author 2", "Post 3", "Author 3", + ], + ); + + let result = std::panic::catch_unwind(|| { + assert_collect_text(html, "table#posts_t tr td", &["Post 1", "Post 2", "Post 3"]); + }); + + assert!(result.is_err()); + if let Err(panic_message) = result { + let panic_message = panic_message.downcast_ref::().unwrap(); + assert_eq!( + panic_message, + &"assertion `left == right` failed: Expected texts [\"Post 1\", \"Post 2\", \ + \"Post 3\"], but found [].\n left: []\n right: [\"Post 1\", \"Post 2\", \"Post \ + 3\"]" + ); + } + } +}