Based on the late-night notes, a commit was created, which triggered the dominoes. I need to rewrite quite a few things, which will later facilitate writing tests and make them more precise.

What’s changed? Link to heading

The test now directly calls the Axum handler, so I can write tests for the states that occur there. For example: If I call the endpoint with an invalid token, I expect a 401 (Unauthorized) HTTP response, or if I write a wrong URI in the request, I expect a 404 (Not Found) HTTP response. Previously, with tests that directly called the internal function, this was not possible.

In any case, I have a lot to adjust to this new concept, so I already have something to do for today.

Here is how the new test looks now:

  1#[cfg(test)]
  2mod tests {
  3    use super::*;
  4    use crate::app::app_state::AppStateBuilder;
  5    use crate::app::config::{AppConfig, BasicDatabaseConfig, DatabaseUrlProvider};
  6    use crate::app::database::{DatabaseMigrator, MockDatabaseMigrator, MockPgPoolManagerTrait};
  7    use crate::app::init::app;
  8    use crate::auth::AuthModule;
  9    use crate::auth::dto::claims::Claims;
 10    use crate::auth::service::Argon2Hasher;
 11    use crate::tenants::model::Tenant;
 12    use crate::tenants::repository::{MockTenantsRepository, TenantsRepository};
 13    use crate::users::UsersModule;
 14    use axum::body::Body;
 15    use axum::http::Request;
 16    use chrono::Local;
 17    use sqlx::postgres::PgPoolOptions;
 18    use std::ops::Add;
 19    use std::time::Duration;
 20    use tower::ServiceExt;
 21    use uuid::Uuid;
 22    use http_body_util::BodyExt;
 23    use crate::common::dto::{OkResponse, SimpleMessageResponse};
 24
 25    #[tokio::test]
 26    async fn test_create_managed_success() {
 27        let mut pool_manager_mock = MockPgPoolManagerTrait::new();
 28        pool_manager_mock
 29            .expect_add_tenant_pool()
 30            .times(1)
 31            .returning(|tenant_id, _| Ok(tenant_id));
 32        pool_manager_mock
 33            .expect_get_tenant_pool()
 34            .times(1)
 35            .returning(|_| {
 36                let database_config = BasicDatabaseConfig::default();
 37                Ok(Some(
 38                    PgPoolOptions::new()
 39                        .connect_lazy(&database_config.url())
 40                        .unwrap(),
 41                ))
 42            });
 43        let pool_manager_mock = Arc::new(pool_manager_mock);
 44
 45        let repo_factory = Box::new(|| {
 46            let mut repo = MockTenantsRepository::new();
 47            repo.expect_setup_managed()
 48                .times(1)
 49                .withf(|_, name, _, _, _| name == "test")
 50                .returning(|uuid: Uuid, _, _, _, _| {
 51                    Ok(Tenant {
 52                        id: uuid,
 53                        name: "test".to_string(),
 54                        db_host: "localhost".to_string(),
 55                        db_port: 5432,
 56                        db_name: "database".to_string(),
 57                        db_user: "user".to_string(),
 58                        db_password: "password".to_string(),
 59                        db_max_pool_size: 5,
 60                        db_ssl_mode: "disable".to_string(),
 61                        created_at: Local::now(),
 62                        updated_at: Local::now(),
 63                        deleted_at: None,
 64                    })
 65                });
 66            Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
 67        });
 68
 69        let migrator_factory = Box::new(|| {
 70            let mut migrator = MockDatabaseMigrator::new();
 71            migrator
 72                .expect_migrate_tenant_db()
 73                .times(1)
 74                .returning(|_| Ok(()));
 75            Box::new(migrator) as Box<dyn DatabaseMigrator + Send + Sync>
 76        });
 77
 78        let config = Arc::new(AppConfig::default());
 79
 80        let payload = serde_json::to_string(&TenantCreateRequestHelper {
 81            name: String::from("test"),
 82            is_self_hosted: false,
 83            db_host: None,
 84            db_port: None,
 85            db_name: None,
 86            db_user: None,
 87            db_password: None,
 88        })
 89            .unwrap();
 90
 91        let exp = Local::now().add(Duration::from_secs(100)).timestamp();
 92        let iat = Local::now().timestamp();
 93        let nbf = Local::now().timestamp();
 94
 95        let bearer = Claims::new(
 96            Uuid::new_v4().to_string(),
 97            usize::try_from(exp).unwrap(),
 98            usize::try_from(iat).unwrap(),
 99            usize::try_from(nbf).unwrap(),
100            config.auth().jwt_issuer().to_string(),
101            config.auth().jwt_audience().to_string(),
102            Uuid::new_v4().to_string(),
103        )
104            .to_token(config.auth().jwt_secret().as_bytes())
105            .unwrap();
106
107        let request = Request::builder()
108            .header("Authorization", format!("Bearer {}", bearer))
109            .header("Content-Type", "application/json")
110            .method("POST")
111            .uri("/tenants/create")
112            .body(Body::from(payload))
113            .unwrap();
114
115        let app_state = Arc::new(
116            AppStateBuilder::new()
117                .users_module(Arc::new(UsersModule {}))
118                .config_module(config.clone())
119                .tenants_module(Arc::new(TenantsModule {
120                    pool_manager: pool_manager_mock.clone(),
121                    config: config.clone(),
122                    repo_factory,
123                    migrator_factory,
124                }))
125                .auth_module(Arc::new(AuthModule {
126                    pool_manager: pool_manager_mock.clone(),
127                    password_hasher: Arc::new(Argon2Hasher),
128                    config: config.clone(),
129                }))
130                .build()
131                .unwrap(),
132        );
133        let app = app(app_state).await;
134
135        let response = app.oneshot(request).await.unwrap();
136
137        assert_eq!(response.status(), StatusCode::CREATED);
138
139        let body = response.into_body().collect().await.unwrap().to_bytes();
140
141        let expected_response = serde_json::to_string(&OkResponse::new(SimpleMessageResponse {
142            message: String::from(
143                "Szervezeti egység létrehozása sikeresen megtörtént!",
144            ),
145        })).unwrap();
146
147        assert_eq!(&body[..], expected_response.as_bytes());
148    }
149}