WIP - ActixWeb multi-tenant blog and newsletter API server. Originally forked from LukeMathWalker/zero-to-production.
at main 541 lines 17 kB view raw
1use fake::Fake; 2use fake::faker::internet::en::SafeEmail; 3use fake::faker::name::en::Name; 4use newsletter_api::challenge::Base64Challenger; 5use newsletter_api::clients::cloudinary_client::CloudinaryClient; 6use newsletter_api::configuration::{DatabaseSettings, get_configuration}; 7use newsletter_api::email_client::{EmailClient, EmailServer}; 8use newsletter_api::issue_delivery_worker::{ExecutionOutcome, try_execute_task}; 9use newsletter_api::models::{NewUser, NewUserData, UserProfile}; 10use newsletter_api::startup::{Application, get_connection_pool}; 11use newsletter_api::telemetry::{get_subscriber, init_subscriber}; 12use secrecy::SecretString; 13use sqlx::{Connection, Executor, PgConnection, PgPool}; 14use std::sync::LazyLock; 15use uuid::Uuid; 16use wiremock::matchers::{method, path}; 17use wiremock::{Mock, MockServer, ResponseTemplate}; 18 19// Ensure that the `tracing` stack is only initialised once using `once_cell` 20static TRACING: LazyLock<()> = LazyLock::new(|| { 21 let default_filter_level = "info".to_string(); 22 let subscriber_name = "test".to_string(); 23 if std::env::var("TEST_LOG").is_ok() { 24 let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout); 25 init_subscriber(subscriber); 26 } else { 27 let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::sink); 28 init_subscriber(subscriber); 29 }; 30}); 31 32pub struct TestApp { 33 pub address: String, 34 pub port: u16, 35 pub db_pool: PgPool, 36 pub email_server: MockServer, 37 pub test_user: TestUser, 38 pub api_client: reqwest::Client, 39 pub cloudinary_client: CloudinaryClient, 40 pub cloudinary_server: MockServer, 41 pub email_client: EmailClient, 42 pub captcha_secret: SecretString, 43} 44 45/// Confirmation links embedded in the request to the email API. 46pub struct ConfirmationLinks { 47 pub html: reqwest::Url, 48 pub plain_text: reqwest::Url, 49} 50 51impl TestApp { 52 pub async fn dispatch_all_pending_emails(&self) { 53 loop { 54 if let ExecutionOutcome::EmptyQueue = 55 try_execute_task(&self.db_pool, &self.email_client) 56 .await 57 .unwrap() 58 { 59 break; 60 } 61 } 62 } 63 64 pub async fn post_subscriptions<Body>(&self, body: &Body) -> reqwest::Response 65 where 66 Body: serde::Serialize, 67 { 68 self.api_client 69 .post(&format!("{}/subscriptions", &self.address)) 70 .json(body) 71 .send() 72 .await 73 .expect("Failed to execute request.") 74 } 75 76 pub async fn post_login<Body>(&self, body: &Body) -> reqwest::Response 77 where 78 Body: serde::Serialize, 79 { 80 self.api_client 81 .post(&format!("{}/login", &self.address)) 82 .json(body) 83 .send() 84 .await 85 .expect("Failed to execute request.") 86 } 87 88 pub async fn post_logout(&self) -> reqwest::Response { 89 self.api_client 90 .post(&format!("{}/admin/logout", &self.address)) 91 .send() 92 .await 93 .expect("Failed to execute request.") 94 } 95 96 pub async fn put_change_password<Body>(&self, body: &Body) -> reqwest::Response 97 where 98 Body: serde::Serialize, 99 { 100 self.api_client 101 .put(&format!("{}/admin/password", &self.address)) 102 .json(body) 103 .send() 104 .await 105 .expect("Failed to execute request.") 106 } 107 108 pub async fn get_admin_newsletter_issues(&self) -> reqwest::Response { 109 self.api_client 110 .get(&format!("{}/admin/newsletters", &self.address)) 111 .send() 112 .await 113 .expect("Failed to execute request.") 114 } 115 116 pub async fn get_admin_unpublished_newsletter_issues(&self) -> reqwest::Response { 117 self.api_client 118 .get(&format!("{}/admin/newsletters/drafts", &self.address)) 119 .send() 120 .await 121 .expect("Failed to execute request.") 122 } 123 124 pub async fn get_admin_newsletter_issue( 125 &self, 126 newsletter_issue_id: &Uuid, 127 ) -> reqwest::Response { 128 self.api_client 129 .get(&format!( 130 "{}/admin/newsletters/{}", 131 &self.address, newsletter_issue_id 132 )) 133 .send() 134 .await 135 .expect("Failed to execute request.") 136 } 137 138 pub async fn post_admin_create_newsletter<Body>(&self, body: &Body) -> reqwest::Response 139 where 140 Body: serde::Serialize, 141 { 142 self.api_client 143 .post(&format!("{}/admin/newsletters", &self.address)) 144 .json(body) 145 .send() 146 .await 147 .expect("Failed to execute request.") 148 } 149 150 pub async fn put_admin_publish_newsletter<Body>( 151 &self, 152 newsletter_issue_id: &Uuid, 153 body: &Body, 154 ) -> reqwest::Response 155 where 156 Body: serde::Serialize, 157 { 158 self.api_client 159 .put(&format!( 160 "{}/admin/newsletters/{}/publish", 161 &self.address, newsletter_issue_id 162 )) 163 .json(body) 164 .send() 165 .await 166 .expect("Failed to execute request.") 167 } 168 169 pub async fn put_admin_update_newsletter<Body>( 170 &self, 171 newsletter_issue_id: &Uuid, 172 body: &Body, 173 ) -> reqwest::Response 174 where 175 Body: serde::Serialize, 176 { 177 self.api_client 178 .put(&format!( 179 "{}/admin/newsletters/{}", 180 &self.address, newsletter_issue_id 181 )) 182 .json(body) 183 .send() 184 .await 185 .expect("Failed to execute request.") 186 } 187 188 pub async fn put_admin_update_newsletter_issue_cover_image<Body>( 189 &self, 190 newsletter_issue_id: &Uuid, 191 body: &Body, 192 ) -> reqwest::Response 193 where 194 Body: serde::Serialize, 195 { 196 self.api_client 197 .put(&format!( 198 "{}/admin/newsletter/{}/cover_image", 199 &self.address, newsletter_issue_id 200 )) 201 .json(body) 202 .send() 203 .await 204 .expect("Failed to execute request.") 205 } 206 207 pub async fn get_admin_user(&self) -> reqwest::Response { 208 self.api_client 209 .get(&format!("{}/admin/user", &self.address)) 210 .send() 211 .await 212 .expect("Failed to execute request.") 213 } 214 215 pub async fn put_admin_update_user<Body>(&self, body: &Body) -> reqwest::Response 216 where 217 Body: serde::Serialize, 218 { 219 self.api_client 220 .put(&format!("{}/admin/user", &self.address)) 221 .json(body) 222 .send() 223 .await 224 .expect("Failed to execute request.") 225 } 226 227 pub async fn put_admin_update_user_profile_avatar<Body>(&self, body: &Body) -> reqwest::Response 228 where 229 Body: serde::Serialize, 230 { 231 self.api_client 232 .put(&format!("{}/admin/user/avatar", &self.address)) 233 .json(body) 234 .send() 235 .await 236 .expect("Failed to execute request.") 237 } 238 239 pub async fn put_admin_update_user_profile_banner<Body>(&self, body: &Body) -> reqwest::Response 240 where 241 Body: serde::Serialize, 242 { 243 self.api_client 244 .put(&format!("{}/admin/user/banner", &self.address)) 245 .json(body) 246 .send() 247 .await 248 .expect("Failed to execute request.") 249 } 250 251 pub async fn get_public_newsletters(&self) -> reqwest::Response { 252 self.api_client 253 .get(&format!("{}/newsletters", &self.address)) 254 .send() 255 .await 256 .expect("Failed to execute request.") 257 } 258 259 pub async fn get_public_newsletter( 260 &self, 261 username: &String, 262 slug: &String, 263 ) -> reqwest::Response { 264 self.api_client 265 .get(&format!( 266 "{}/newsletters/by_user/{}/issue/{}", 267 &self.address, username, slug 268 )) 269 .send() 270 .await 271 .expect("Failed to execute request.") 272 } 273 274 pub async fn get_public_newsletters_by_user(&self, username: &String) -> reqwest::Response { 275 self.api_client 276 .get(&format!( 277 "{}/newsletters/by_user/{}", 278 &self.address, username 279 )) 280 .send() 281 .await 282 .expect("Failed to execute request.") 283 } 284 285 pub async fn get_users(&self) -> reqwest::Response { 286 self.api_client 287 .get(&format!("{}/users", &self.address)) 288 .send() 289 .await 290 .expect("Failed to execute request.") 291 } 292 293 pub async fn get_user(&self, username: &String) -> reqwest::Response { 294 self.api_client 295 .get(&format!("{}/users/{}", &self.address, username)) 296 .send() 297 .await 298 .expect("Failed to execute request.") 299 } 300 301 pub async fn get_authenticate(&self) -> reqwest::Response { 302 self.api_client 303 .get(&format!("{}/admin/authenticate", &self.address)) 304 .send() 305 .await 306 .expect("Failed to execute request.") 307 } 308 309 pub async fn create_unconfirmed_subscriber( 310 &self, 311 username: Option<String>, 312 email: Option<String>, 313 ) -> ConfirmationLinks { 314 let (answer, challenge) = self.get_solved_captcha_challenge(); 315 // We are working with multiple subscribers now, 316 // their details must be randomised to avoid conflicts! 317 let name: String = Name().fake(); 318 let email: String = email.unwrap_or(SafeEmail().fake()); 319 let username: String = username.unwrap_or(self.test_user.username.clone()); 320 let body = &serde_json::json!({ 321 "name": name, 322 "email": email, 323 "username": username, 324 "signed_answer": challenge, 325 "answer_attempt": answer, 326 }); 327 328 let _mock_guard = Mock::given(path("/api/v1/send")) 329 .and(method("POST")) 330 .respond_with(ResponseTemplate::new(200)) 331 .named("Create unconfirmed subscriber") 332 .expect(1) 333 .mount_as_scoped(&self.email_server) 334 .await; 335 self.post_subscriptions(body) 336 .await 337 .error_for_status() 338 .unwrap(); 339 340 let email_request = self 341 .email_server 342 .received_requests() 343 .await 344 .unwrap() 345 .pop() 346 .unwrap(); 347 self.get_confirmation_links(&email_request) 348 } 349 350 pub async fn create_confirmed_subscriber( 351 &self, 352 username: Option<String>, 353 email: Option<String>, 354 ) { 355 let confirmation_link = self.create_unconfirmed_subscriber(username, email).await; 356 357 self.api_client 358 .put(confirmation_link.html) 359 .send() 360 .await 361 .expect("Failed to confirm subscriber."); 362 } 363 364 /// Extract the confirmation links embedded in the request to the email API. 365 pub fn get_confirmation_links(&self, email_request: &wiremock::Request) -> ConfirmationLinks { 366 let body: serde_json::Value = serde_json::from_slice(&email_request.body).unwrap(); 367 368 // Extract the link from one of the request fields. 369 let get_link = |s: &str| { 370 let links: Vec<_> = linkify::LinkFinder::new() 371 .links(s) 372 .filter(|l| *l.kind() == linkify::LinkKind::Url) 373 .collect(); 374 assert_eq!(links.len(), 1); 375 let raw_link = links[0].as_str().to_owned(); 376 let mut confirmation_link = reqwest::Url::parse(&raw_link).unwrap(); 377 // Let's make sure we don't call random APIs on the web 378 assert_eq!(confirmation_link.host_str().unwrap(), "127.0.0.1"); 379 confirmation_link.set_port(Some(self.port)).unwrap(); 380 confirmation_link 381 }; 382 383 let html = match self.email_client.server { 384 EmailServer::Mailpit => get_link(body["Html"].as_str().unwrap()), 385 EmailServer::Postmark => get_link(body["HtmlBody"].as_str().unwrap()), 386 }; 387 let plain_text = match self.email_client.server { 388 EmailServer::Mailpit => get_link(body["Text"].as_str().unwrap()), 389 EmailServer::Postmark => get_link(body["TextBody"].as_str().unwrap()), 390 }; 391 392 ConfirmationLinks { html, plain_text } 393 } 394 395 pub fn get_solved_captcha_challenge(&self) -> (String, String) { 396 let challenge = Base64Challenger::new(self.captcha_secret.clone()) 397 .expect("Creating new captcha challenge.") 398 .encrypt() 399 .expect("Encrypting message."); 400 let answer = Base64Challenger::decrypt(&challenge, self.captcha_secret.clone()) 401 .expect("decrypting answer."); 402 403 (answer, challenge) 404 } 405} 406 407pub async fn spawn_app() -> TestApp { 408 LazyLock::force(&TRACING); 409 410 // Launch a mock server to stand in for Postmark's API 411 let email_server = MockServer::start().await; 412 let cloudinary_server = MockServer::start().await; 413 414 unsafe { 415 std::env::set_var("APP_ENVIRONMENT", "test"); 416 } 417 418 // Randomise configuration to ensure test isolation 419 let configuration = { 420 let mut c = get_configuration().expect("Failed to read configuration."); 421 // Use a different database for each test case 422 c.database.database_name = Uuid::new_v4().to_string(); 423 // Use a random OS port 424 c.application.port = 0; 425 // Use the mock server as email API 426 c.email_client.base_url = email_server.uri(); 427 c.cloudinary_client.base_url = cloudinary_server.uri(); 428 c 429 }; 430 431 // Create and migrate the database 432 configure_database(&configuration.database).await; 433 434 // Launch the application as a background task 435 let application = Application::build(configuration.clone()) 436 .await 437 .expect("Failed to build application."); 438 let application_port = application.port(); 439 let _ = tokio::spawn(application.run_until_stopped()); 440 441 let client = reqwest::Client::builder() 442 .redirect(reqwest::redirect::Policy::none()) 443 .cookie_store(true) 444 .build() 445 .unwrap(); 446 447 let db_pool = get_connection_pool(&configuration.database); 448 let test_user = TestUser::create(&db_pool) 449 .await 450 .expect("Failed to create test user."); 451 let test_app = TestApp { 452 address: format!("http://localhost:{}", application_port), 453 port: application_port, 454 cloudinary_client: configuration.cloudinary_client.client(), 455 cloudinary_server, 456 db_pool, 457 email_server, 458 test_user, 459 api_client: client, 460 email_client: configuration.email_client.client(), 461 captcha_secret: configuration.application.captcha_secret, 462 }; 463 464 test_app 465} 466 467async fn configure_database(config: &DatabaseSettings) -> PgPool { 468 // Create database 469 let maintenance_settings = DatabaseSettings { 470 database_name: "postgres".to_string(), 471 username: "postgres".to_string(), 472 password: SecretString::from("password".to_string()), 473 ..config.clone() 474 }; 475 let mut connection = PgConnection::connect_with(&maintenance_settings.connect_options()) 476 .await 477 .expect("Failed to connect to Postgres"); 478 connection 479 .execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str()) 480 .await 481 .expect("Failed to create database."); 482 483 // Migrate database 484 let connection_pool = PgPool::connect_with(config.connect_options()) 485 .await 486 .expect("Failed to connect to Postgres."); 487 sqlx::migrate!("./migrations") 488 .run(&connection_pool) 489 .await 490 .expect("Failed to migrate the database"); 491 connection_pool 492} 493 494pub struct TestUser { 495 pub username: String, 496 pub password: String, 497 pub user_id: Uuid, 498} 499 500impl TestUser { 501 pub async fn create(pool: &PgPool) -> Result<Self, &str> { 502 let password: String = Uuid::new_v4().to_string(); 503 let new_user: NewUser = NewUserData { 504 username: Uuid::new_v4().to_string(), 505 password: SecretString::from(password.clone()), 506 email: SafeEmail().fake(), 507 } 508 .try_into() 509 .expect("Failed to initialize new user."); 510 let mut transaction = pool 511 .begin() 512 .await 513 .expect("Failed to begin database transaction."); 514 let new_user = new_user 515 .store(&mut transaction) 516 .await 517 .expect("Failed to store test user."); 518 UserProfile::initialize(&new_user.user_id) 519 .insert(&mut transaction) 520 .await 521 .unwrap(); 522 transaction 523 .commit() 524 .await 525 .expect("Failed to commit database transaction."); 526 527 Ok(Self { 528 username: new_user.username, 529 password, 530 user_id: new_user.user_id, 531 }) 532 } 533 534 pub async fn login(&self, app: &TestApp) { 535 app.post_login(&serde_json::json!({ 536 "username": &self.username, 537 "password": &self.password 538 })) 539 .await; 540 } 541}