WIP - ActixWeb multi-tenant blog and newsletter API server. Originally forked from LukeMathWalker/zero-to-production.
at main 298 lines 8.7 kB view raw
1use crate::helpers::spawn_app; 2use claims::assert_ok; 3use newsletter_api::utils::ResponseErrorMessage; 4use wiremock::matchers::{method, path}; 5use wiremock::{Mock, ResponseTemplate}; 6 7#[tokio::test] 8async fn subscribe_returns_a_200_for_valid_params() { 9 // Arrange 10 let app = spawn_app().await; 11 let (answer, challenge) = app.get_solved_captcha_challenge(); 12 13 Mock::given(path("/api/v1/send")) 14 .and(method("POST")) 15 .respond_with(ResponseTemplate::new(200)) 16 .mount(&app.email_server) 17 .await; 18 19 // Act 20 let response = app 21 .post_subscriptions(&serde_json::json!({ 22 "name": "le guin", 23 "email": "ursula_le_guin@gmail.com", 24 "username": &app.test_user.username, 25 "signed_answer": challenge, 26 "answer_attempt": answer 27 })) 28 .await; 29 30 // Assert 31 assert_eq!(200, response.status().as_u16()); 32} 33 34#[tokio::test] 35async fn subscribe_persists_the_new_subscriber() { 36 // Arrange 37 let app = spawn_app().await; 38 let (answer, challenge) = app.get_solved_captcha_challenge(); 39 40 // Act 41 app.post_subscriptions(&serde_json::json!({ 42 "name": "le guin", 43 "email": "ursula_le_guin@gmail.com", 44 "username": &app.test_user.username, 45 "signed_answer": challenge, 46 "answer_attempt": answer 47 })) 48 .await; 49 50 // Assert 51 let saved = sqlx::query!("SELECT email, name, status FROM subscriptions",) 52 .fetch_one(&app.db_pool) 53 .await 54 .expect("Failed to fetch saved subscription."); 55 56 assert_eq!(saved.email, "ursula_le_guin@gmail.com"); 57 assert_eq!(saved.name, "le guin"); 58 assert_eq!(saved.status, "pending_confirmation"); 59} 60 61#[tokio::test] 62async fn subscribe_fails_if_there_is_a_fatal_database_error() { 63 // Arrange 64 let app = spawn_app().await; 65 let (answer, challenge) = app.get_solved_captcha_challenge(); 66 67 // Sabotage the database 68 sqlx::query!("ALTER TABLE subscriptions DROP COLUMN email;",) 69 .execute(&app.db_pool) 70 .await 71 .unwrap(); 72 73 // Act 74 let response = app 75 .post_subscriptions(&serde_json::json!({ 76 "name": "le guin", "email": 77 "ursula_le_guin@gmail.com", 78 "username": &app.test_user.username, 79 "signed_answer": challenge, 80 "answer_attempt": answer 81 })) 82 .await; 83 84 // Assert 85 assert_eq!(response.status().as_u16(), 500); 86} 87 88#[tokio::test] 89async fn subscribe_sends_a_confirmation_email_for_valid_data() { 90 // Arrange 91 let app = spawn_app().await; 92 let (answer, challenge) = app.get_solved_captcha_challenge(); 93 94 Mock::given(path("/api/v1/send")) 95 .and(method("POST")) 96 .respond_with(ResponseTemplate::new(200)) 97 .expect(1) 98 .mount(&app.email_server) 99 .await; 100 101 // Act 102 app.post_subscriptions(&serde_json::json!({ 103 "name": "le guin", 104 "email": "ursula_le_guin@gmail.com", 105 "username": &app.test_user.username, 106 "signed_answer": challenge, 107 "answer_attempt": answer 108 })) 109 .await; 110 111 // Assert 112 // Mock asserts on drop 113} 114 115#[tokio::test] 116async fn subscribe_sends_a_confirmation_email_with_a_link() { 117 // Arrange 118 let app = spawn_app().await; 119 let (answer, challenge) = app.get_solved_captcha_challenge(); 120 121 Mock::given(path("/api/v1/send")) 122 .and(method("POST")) 123 .respond_with(ResponseTemplate::new(200)) 124 .mount(&app.email_server) 125 .await; 126 127 // Act 128 app.post_subscriptions(&serde_json::json!({ 129 "name": "le guin", 130 "email": "ursula_le_guin@gmail.com", 131 "username": &app.test_user.username, 132 "signed_answer": challenge, 133 "answer_attempt": answer 134 })) 135 .await; 136 137 // Assert 138 let email_request = &app.email_server.received_requests().await.unwrap()[0]; 139 let confirmation_links = app.get_confirmation_links(email_request); 140 141 // The two links should be identical 142 assert_eq!(confirmation_links.html, confirmation_links.plain_text); 143} 144 145#[tokio::test] 146async fn subscribe_returns_a_400_when_data_is_missing() { 147 // Arrange 148 let app = spawn_app().await; 149 let (answer, challenge) = app.get_solved_captcha_challenge(); 150 let test_cases = vec![ 151 ( 152 serde_json::json!({ 153 "name": "le guin", 154 "username": &app.test_user.username, 155 "signed_answer": challenge, 156 "answer_attempt": answer 157 }), 158 "missing the email", 159 ), 160 ( 161 serde_json::json!({ 162 "email": "ursula_le_guin@gmail.com", 163 "username": &app.test_user.username, 164 "signed_answer": challenge, 165 "answer_attempt": answer 166 }), 167 "missing the name", 168 ), 169 ( 170 serde_json::json!({ 171 "username": &app.test_user.username, 172 "signed_answer": challenge, 173 "answer_attempt": answer 174 }), 175 "missing both name and email", 176 ), 177 ( 178 serde_json::json!({ 179 "name": "le guin", 180 "email": "ursula_le_guin@gmail.com", 181 "answer_attempt": answer, 182 "signed_answer": challenge, 183 }), 184 "missing the username", 185 ), 186 ( 187 serde_json::json!({ 188 "name": "le guin", 189 "email": "ursula_le_guin@gmail.com", 190 "username": &app.test_user.username, 191 "answer_attempt": answer 192 }), 193 "missing the signed answer", 194 ), 195 ( 196 serde_json::json!({ 197 "name": "le guin", 198 "email": "ursula_le_guin@gmail.com", 199 "username": &app.test_user.username, 200 "signed_answer": challenge, 201 }), 202 "missing the answer attempt", 203 ), 204 ]; 205 206 for (invalid_body, error_message) in test_cases { 207 // Act 208 let response = app.post_subscriptions(&invalid_body).await; 209 210 // Assert 211 assert_eq!( 212 400, 213 response.status().as_u16(), 214 // Additional customised error message on test failure 215 "The API did not fail with 400 Bad Request when the payload was {}.", 216 error_message 217 ); 218 } 219} 220 221#[tokio::test] 222async fn subscribe_returns_a_400_with_json_message_when_fields_are_present_but_invalid() { 223 // Arrange 224 let app = spawn_app().await; 225 let (answer, challenge) = app.get_solved_captcha_challenge(); 226 let test_cases = vec![ 227 ( 228 serde_json::json!({ 229 "name": "", 230 "email": "ursula_le_guin@gmail.com", 231 "username": &app.test_user.username, 232 "signed_answer": challenge, 233 "answer_attempt": answer 234 }), 235 "empty name", 236 ), 237 ( 238 serde_json::json!({ 239 "name": "Ursula", 240 "email": "", 241 "username": &app.test_user.username, 242 "signed_answer": challenge, 243 "answer_attempt": answer 244 }), 245 "empty email", 246 ), 247 ( 248 serde_json::json!({ 249 "name": "Ursula", 250 "email": 251 "definitely-not-an-email", 252 "username": &app.test_user.username, 253 "signed_answer": challenge, 254 "answer_attempt": answer 255 }), 256 "invalid email", 257 ), 258 ( 259 serde_json::json!({ 260 "name": "Ursula", 261 "email": 262 "definitely-not-an-email", 263 "username": &app.test_user.username, 264 "signed_answer": challenge, 265 "answer_attempt": "badanswer" 266 }), 267 "invalid answer", 268 ), 269 ( 270 serde_json::json!({ 271 "name": "Ursula", 272 "email": 273 "definitely-not-an-email", 274 "username": &app.test_user.username, 275 "signed_answer": "invalidsig", 276 "answer_attempt": answer 277 }), 278 "invalid challenge", 279 ), 280 ]; 281 282 for (body, description) in test_cases { 283 // Act 284 let response = app.post_subscriptions(&body).await; 285 286 // Assert 287 assert_eq!( 288 400, 289 response.status().as_u16(), 290 "The API did not return a 400 Bad Request when the payload was {}.", 291 description 292 ); 293 294 let response_body: Result<ResponseErrorMessage, reqwest::Error> = response.json().await; 295 296 assert_ok!(response_body); 297 } 298}