Coverage for src/tests/test_email.py: 100%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

375 statements  

1from datetime import timedelta 

2from unittest.mock import patch 

3 

4import pytest 

5 

6import couchers.email 

7from couchers.config import config 

8from couchers.crypto import random_hex, urlsafe_secure_token 

9from couchers.db import session_scope 

10from couchers.models import ( 

11 ContentReport, 

12 Conversation, 

13 FriendRelationship, 

14 FriendStatus, 

15 HostRequest, 

16 HostRequestStatus, 

17 Message, 

18 MessageType, 

19 Reference, 

20 ReferenceType, 

21 SignupFlow, 

22 Upload, 

23 User, 

24) 

25from couchers.sql import couchers_select as select 

26from couchers.tasks import ( 

27 maybe_send_reference_report_email, 

28 send_account_deletion_confirmation_email, 

29 send_account_deletion_successful_email, 

30 send_account_recovered_email, 

31 send_api_key_email, 

32 send_content_report_email, 

33 send_email_changed_confirmation_to_new_email, 

34 send_email_changed_confirmation_to_old_email, 

35 send_email_changed_notification_email, 

36 send_friend_request_accepted_email, 

37 send_friend_request_email, 

38 send_login_email, 

39 send_new_host_request_email, 

40 send_password_reset_email, 

41 send_signup_email, 

42) 

43from couchers.utils import now 

44from tests.test_fixtures import db, generate_user, testconfig # noqa 

45 

46 

47@pytest.fixture(autouse=True) 

48def _(testconfig): 

49 pass 

50 

51 

52def test_login_email(db): 

53 user, api_token = generate_user() 

54 

55 with session_scope() as session: 

56 with patch("couchers.email.queue_email") as mock: 

57 login_token = send_login_email(session, user) 

58 

59 assert mock.call_count == 1 

60 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

61 assert recipient == user.email 

62 assert "login" in subject.lower() 

63 assert login_token.token in plain 

64 assert login_token.token in html 

65 

66 

67def test_signup_verification_email(db): 

68 request_email = f"{random_hex(12)}@couchers.org.invalid" 

69 

70 with session_scope() as session: 

71 flow = SignupFlow(name="Frodo", email=request_email) 

72 

73 with patch("couchers.email.queue_email") as mock: 

74 send_signup_email(flow) 

75 

76 assert mock.call_count == 1 

77 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

78 assert recipient == request_email 

79 assert flow.email_token in plain 

80 assert flow.email_token in html 

81 

82 

83def test_report_email(db): 

84 with session_scope(): 

85 user_reporter, api_token_author = generate_user() 

86 user_author, api_token_reported = generate_user() 

87 

88 report = ContentReport( 

89 reporting_user=user_reporter, 

90 reason="spam", 

91 description="I think this is spam and does not belong on couchers", 

92 content_ref="comment/123", 

93 author_user=user_author, 

94 user_agent="n/a", 

95 page="https://couchers.org/comment/123", 

96 ) 

97 

98 with patch("couchers.email.queue_email") as mock: 

99 send_content_report_email(report) 

100 

101 assert mock.call_count == 1 

102 

103 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

104 assert recipient == "reports@couchers.org.invalid" 

105 assert report.author_user.username in plain 

106 assert str(report.author_user.id) in plain 

107 assert report.author_user.email in plain 

108 assert report.author_user.username in html 

109 assert str(report.author_user.id) in html 

110 assert report.author_user.email in html 

111 assert report.reporting_user.username in plain 

112 assert str(report.reporting_user.id) in plain 

113 assert report.reporting_user.email in plain 

114 assert report.reporting_user.username in html 

115 assert str(report.reporting_user.id) in html 

116 assert report.reporting_user.email in html 

117 assert report.reason in plain 

118 assert report.reason in html 

119 assert report.description in plain 

120 assert report.description in html 

121 assert "report" in subject.lower() 

122 

123 

124def test_reference_report_email_not_sent(db): 

125 with session_scope() as session: 

126 from_user, api_token_author = generate_user() 

127 to_user, api_token_reported = generate_user() 

128 

129 friend_relationship = FriendRelationship(from_user=from_user, to_user=to_user, status=FriendStatus.accepted) 

130 session.add(friend_relationship) 

131 session.flush() 

132 

133 reference = Reference( 

134 from_user=from_user, 

135 to_user=to_user, 

136 reference_type=ReferenceType.friend, 

137 text="This person was very nice to me.", 

138 rating=0.9, 

139 was_appropriate=True, 

140 ) 

141 

142 # no email sent for a positive ref 

143 

144 with patch("couchers.email.queue_email") as mock: 

145 maybe_send_reference_report_email(reference) 

146 

147 assert mock.call_count == 0 

148 

149 

150def test_reference_report_email(db): 

151 with session_scope() as session: 

152 from_user, api_token_author = generate_user() 

153 to_user, api_token_reported = generate_user() 

154 

155 friend_relationship = FriendRelationship(from_user=from_user, to_user=to_user, status=FriendStatus.accepted) 

156 session.add(friend_relationship) 

157 session.flush() 

158 

159 reference = Reference( 

160 from_user=from_user, 

161 to_user=to_user, 

162 reference_type=ReferenceType.friend, 

163 text="This person was not nice to me.", 

164 rating=0.3, 

165 was_appropriate=False, 

166 ) 

167 

168 with patch("couchers.email.queue_email") as mock: 

169 maybe_send_reference_report_email(reference) 

170 

171 assert mock.call_count == 1 

172 

173 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

174 assert recipient == "reports@couchers.org.invalid" 

175 assert "report" in subject.lower() 

176 assert "reference" in subject.lower() 

177 assert reference.from_user.username in plain 

178 assert str(reference.from_user.id) in plain 

179 assert reference.from_user.email in plain 

180 assert reference.from_user.username in html 

181 assert str(reference.from_user.id) in html 

182 assert reference.from_user.email in html 

183 assert reference.to_user.username in plain 

184 assert str(reference.to_user.id) in plain 

185 assert reference.to_user.email in plain 

186 assert reference.to_user.username in html 

187 assert str(reference.to_user.id) in html 

188 assert reference.to_user.email in html 

189 assert reference.text in plain 

190 assert reference.text in html 

191 assert "friend" in plain.lower() 

192 assert "friend" in html.lower() 

193 

194 

195def test_host_request_email(db): 

196 with session_scope() as session: 

197 host, api_token_to = generate_user() 

198 # little trick here to get the upload correctly without invalidating users 

199 key = random_hex(32) 

200 filename = random_hex(32) + ".jpg" 

201 session.add( 

202 Upload( 

203 key=key, 

204 filename=filename, 

205 creator_user_id=host.id, 

206 ) 

207 ) 

208 session.commit() 

209 surfer, api_token_from = generate_user(avatar_key=key) 

210 from_date = "2020-01-01" 

211 to_date = "2020-01-05" 

212 

213 conversation = Conversation() 

214 message = Message( 

215 conversation=conversation, 

216 author_id=surfer.id, 

217 text=random_hex(64), 

218 message_type=MessageType.text, 

219 ) 

220 

221 host_request = HostRequest( 

222 conversation=conversation, 

223 surfer=surfer, 

224 host=host, 

225 from_date=from_date, 

226 to_date=to_date, 

227 status=HostRequestStatus.pending, 

228 surfer_last_seen_message_id=message.id, 

229 ) 

230 

231 session.add(host_request) 

232 

233 with patch("couchers.email.queue_email") as mock: 

234 send_new_host_request_email(host_request) 

235 

236 assert mock.call_count == 1 

237 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

238 assert recipient == host.email 

239 assert "host request" in subject.lower() 

240 assert host.name in plain 

241 assert host.name in html 

242 assert surfer.name in plain 

243 assert surfer.name in html 

244 assert from_date in plain 

245 assert from_date in html 

246 assert to_date in plain 

247 assert to_date in html 

248 assert surfer.avatar.thumbnail_url not in plain 

249 assert surfer.avatar.thumbnail_url in html 

250 assert f"{config['BASE_URL']}/messages/hosting/" in plain 

251 assert f"{config['BASE_URL']}/messages/hosting/" in html 

252 

253 

254def test_friend_request_email(db): 

255 with session_scope() as session: 

256 to_user, api_token_to = generate_user() 

257 # little trick here to get the upload correctly without invalidating users 

258 key = random_hex(32) 

259 filename = random_hex(32) + ".jpg" 

260 session.add( 

261 Upload( 

262 key=key, 

263 filename=filename, 

264 creator_user_id=to_user.id, 

265 ) 

266 ) 

267 session.commit() 

268 from_user, api_token_from = generate_user(avatar_key=key) 

269 friend_relationship = FriendRelationship(from_user=from_user, to_user=to_user, status=FriendStatus.pending) 

270 session.add(friend_relationship) 

271 

272 with patch("couchers.email.queue_email") as mock: 

273 send_friend_request_email(friend_relationship) 

274 

275 assert mock.call_count == 1 

276 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

277 assert recipient == to_user.email 

278 assert "friend" in subject.lower() 

279 assert to_user.name in plain 

280 assert to_user.name in html 

281 assert from_user.name in subject 

282 assert from_user.name in plain 

283 assert from_user.name in html 

284 assert from_user.avatar.thumbnail_url not in plain 

285 assert from_user.avatar.thumbnail_url in html 

286 assert f"{config['BASE_URL']}/connections/friends/" in plain 

287 assert f"{config['BASE_URL']}/connections/friends/" in html 

288 

289 

290def test_friend_request_accepted_email(db): 

291 with session_scope() as session: 

292 from_user, api_token_from = generate_user() 

293 to_user, api_token_to = generate_user() 

294 key = random_hex(32) 

295 filename = random_hex(32) + ".jpg" 

296 session.add( 

297 Upload( 

298 key=key, 

299 filename=filename, 

300 creator_user_id=from_user.id, 

301 ) 

302 ) 

303 session.commit() 

304 to_user, api_token_to = generate_user(avatar_key=key) 

305 friend_relationship = FriendRelationship(from_user=from_user, to_user=to_user, status=FriendStatus.accepted) 

306 session.add(friend_relationship) 

307 

308 with patch("couchers.email.queue_email") as mock: 

309 send_friend_request_accepted_email(friend_relationship) 

310 

311 assert mock.call_count == 1 

312 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

313 assert recipient == from_user.email 

314 assert "friend" in subject.lower() 

315 assert from_user.name in plain 

316 assert from_user.name in html 

317 assert to_user.name in subject 

318 assert to_user.name in plain 

319 assert to_user.name in html 

320 assert to_user.avatar.thumbnail_url not in plain 

321 assert to_user.avatar.thumbnail_url in html 

322 assert f"{config['BASE_URL']}/user/{to_user.username}" in plain 

323 assert f"{config['BASE_URL']}/user/{to_user.username}" in html 

324 

325 

326def test_email_patching_fails(db): 

327 """ 

328 There was a problem where the mocking wasn't happening and the email dev 

329 printing function was called instead, this makes sure the patching is 

330 actually done 

331 """ 

332 with session_scope() as session: 

333 from_user, api_token_from = generate_user() 

334 to_user, api_token_to = generate_user() 

335 friend_relationship = FriendRelationship(from_user=from_user, to_user=to_user, status=FriendStatus.pending) 

336 session.add(friend_relationship) 

337 

338 patched_msg = random_hex(64) 

339 

340 def mock_queue_email(sender_name, sender_email, recipient, subject, plain, html): 

341 raise Exception(patched_msg) 

342 

343 with pytest.raises(Exception) as e: 

344 with patch("couchers.email.queue_email", mock_queue_email): 

345 send_friend_request_email(friend_relationship) 

346 assert str(e.value) == patched_msg 

347 

348 

349def test_email_changed_notification_email(db): 

350 user, token = generate_user() 

351 user.new_email = f"{random_hex(12)}@couchers.org.invalid" 

352 with patch("couchers.email.queue_email") as mock: 

353 send_email_changed_notification_email(user) 

354 

355 assert mock.call_count == 1 

356 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

357 assert "change requested" in subject 

358 assert recipient == user.email 

359 assert user.name in plain 

360 assert user.name in html 

361 assert user.new_email in plain 

362 assert user.new_email in html 

363 assert "A confirmation was sent to that email address" in plain 

364 assert "A confirmation was sent to that email address" in html 

365 assert "support@couchers.org" in plain 

366 assert "support@couchers.org" in html 

367 

368 

369def test_email_changed_confirmation_sent_to_old_email(db): 

370 confirmation_token = urlsafe_secure_token() 

371 user, user_token = generate_user() 

372 user.new_email = f"{random_hex(12)}@couchers.org.invalid" 

373 user.old_email_token = confirmation_token 

374 with patch("couchers.email.queue_email") as mock: 

375 send_email_changed_confirmation_to_old_email(user) 

376 

377 assert mock.call_count == 1 

378 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

379 assert "new email" in subject 

380 assert recipient == user.email 

381 assert user.name in plain 

382 assert user.name in html 

383 assert user.new_email in plain 

384 assert user.new_email in html 

385 assert "via a similar email sent to your new email address" in plain 

386 assert "via a similar email sent to your new email address" in html 

387 assert f"{config['BASE_URL']}/confirm-email?token={confirmation_token}" in plain 

388 assert f"{config['BASE_URL']}/confirm-email?token={confirmation_token}" in html 

389 assert "support@couchers.org" in plain 

390 assert "support@couchers.org" in html 

391 

392 

393def test_email_changed_confirmation_sent_to_new_email(db): 

394 confirmation_token = urlsafe_secure_token() 

395 user, user_token = generate_user() 

396 user.new_email = f"{random_hex(12)}@couchers.org.invalid" 

397 user.new_email_token = confirmation_token 

398 with patch("couchers.email.queue_email") as mock: 

399 send_email_changed_confirmation_to_new_email(user) 

400 

401 assert mock.call_count == 1 

402 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

403 assert "new email" in subject 

404 assert recipient == user.new_email 

405 assert user.name in plain 

406 assert user.name in html 

407 assert user.email in plain 

408 assert user.email in html 

409 assert "via a similar email sent to your old email address" in plain 

410 assert "via a similar email sent to your old email address" in html 

411 assert f"{config['BASE_URL']}/confirm-email?token={confirmation_token}" in plain 

412 assert f"{config['BASE_URL']}/confirm-email?token={confirmation_token}" in html 

413 assert "support@couchers.org" in plain 

414 assert "support@couchers.org" in html 

415 

416 

417def test_password_reset_email(db): 

418 user, api_token = generate_user() 

419 

420 with session_scope() as session: 

421 with patch("couchers.email.queue_email") as mock: 

422 password_reset_token = send_password_reset_email(session, user) 

423 

424 assert mock.call_count == 1 

425 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

426 assert recipient == user.email 

427 assert "reset" in subject.lower() 

428 assert password_reset_token.token in plain 

429 assert password_reset_token.token in html 

430 unique_string = "You asked for your password to be reset on Couchers.org." 

431 assert unique_string in plain 

432 assert unique_string in html 

433 assert f"{config['BASE_URL']}/complete-password-reset?token={password_reset_token.token}" in plain 

434 assert f"{config['BASE_URL']}/complete-password-reset?token={password_reset_token.token}" in html 

435 assert "support@couchers.org" in plain 

436 assert "support@couchers.org" in html 

437 

438 

439def test_account_deletion_confirmation_email(db): 

440 user, api_token = generate_user() 

441 

442 with patch("couchers.email.queue_email") as mock: 

443 account_deletion_token = send_account_deletion_confirmation_email(user) 

444 assert mock.call_count == 1 

445 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

446 assert recipient == user.email 

447 assert "account deletion" in subject.lower() 

448 assert account_deletion_token.token in plain 

449 assert account_deletion_token.token in html 

450 unique_string = "You requested that we delete your account from Couchers.org." 

451 assert unique_string in plain 

452 assert unique_string in html 

453 url = f"{config['BASE_URL']}/delete-account?token={account_deletion_token.token}" 

454 assert url in plain 

455 assert url in html 

456 assert "support@couchers.org" in plain 

457 assert "support@couchers.org" in html 

458 

459 

460def test_api_key_email(db): 

461 user, api_token = generate_user() 

462 

463 token = random_hex(64) 

464 expiry = now() + timedelta(days=90) 

465 

466 with session_scope() as session: 

467 with patch("couchers.email.queue_email") as mock: 

468 send_api_key_email(session, user, token, expiry) 

469 

470 assert mock.call_count == 1 

471 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

472 assert recipient == user.email 

473 assert "api key" in subject.lower() 

474 assert token in plain 

475 assert token in html 

476 assert str(expiry) in plain 

477 assert str(expiry) in html 

478 unique_string = "We've issued you with the following API key:" 

479 assert unique_string in plain 

480 assert unique_string in html 

481 assert "support@couchers.org" in plain 

482 assert "support@couchers.org" in html 

483 

484 

485def test_account_deletion_successful_email(db): 

486 user, api_token = generate_user() 

487 

488 with session_scope() as session: 

489 user_ = session.execute(select(User)).scalar_one() 

490 user.undelete_token = "token" 

491 user.undelete_until = now() 

492 user.is_deleted = True 

493 

494 with patch("couchers.email.queue_email") as mock: 

495 send_account_deletion_successful_email(user, 7) 

496 

497 assert mock.call_count == 1 

498 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

499 assert recipient == user.email 

500 assert "account has been deleted" in subject.lower() 

501 unique_string = "You have successfully deleted your account from Couchers.org." 

502 assert unique_string in plain 

503 assert unique_string in html 

504 assert "7 days" in plain 

505 assert "7 days" in html 

506 url = f"{config['BASE_URL']}/recover-account?token={user.undelete_token}" 

507 assert url in plain 

508 assert url in html 

509 assert "support@couchers.org" in plain 

510 assert "support@couchers.org" in html 

511 

512 

513def test_account_recovery_successful_email(db): 

514 user, api_token = generate_user() 

515 

516 with patch("couchers.email.queue_email") as mock: 

517 send_account_recovered_email(user) 

518 

519 assert mock.call_count == 1 

520 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args 

521 assert recipient == user.email 

522 assert "account has been recovered" in subject.lower() 

523 unique_string = "You have successfully recovered your account on Couchers.org!" 

524 assert unique_string in plain 

525 assert unique_string in html 

526 assert "support@couchers.org" in plain 

527 assert "support@couchers.org" in html 

528 

529 

530def test_email_prefix_config(db, monkeypatch): 

531 user, token = generate_user() 

532 user.new_email = f"{random_hex(12)}@couchers.org.invalid" 

533 

534 with patch("couchers.email.queue_email") as mock: 

535 send_email_changed_notification_email(user) 

536 

537 assert mock.call_count == 1 

538 (sender_name, sender_email, _, subject, _, _), _ = mock.call_args 

539 

540 assert sender_name == "Couchers.org" 

541 assert sender_email == "notify@couchers.org.invalid" 

542 assert subject == "[TEST] Couchers.org email change requested" 

543 

544 new_config = config.copy() 

545 new_config["NOTIFICATION_EMAIL_SENDER"] = "TestCo" 

546 new_config["NOTIFICATION_EMAIL_ADDRESS"] = "testco@testing.co.invalid" 

547 new_config["NOTIFICATION_EMAIL_PREFIX"] = "" 

548 

549 monkeypatch.setattr(couchers.email, "config", new_config) 

550 

551 with patch("couchers.email.queue_email") as mock: 

552 send_email_changed_notification_email(user) 

553 

554 assert mock.call_count == 1 

555 (sender_name, sender_email, _, subject, _, _), _ = mock.call_args 

556 

557 assert sender_name == "TestCo" 

558 assert sender_email == "testco@testing.co.invalid" 

559 assert subject == "Couchers.org email change requested"