WIP - ActixWeb multi-tenant blog and newsletter API server. Originally forked from LukeMathWalker/zero-to-production.
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}