Coverage for src/tests/test_bg_jobs.py: 99%
628 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-06 08:02 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-06 08:02 +0000
1from datetime import date, timedelta
2from unittest.mock import call, patch
4import pytest
5import requests
6from google.protobuf import empty_pb2
7from google.protobuf.empty_pb2 import Empty
8from sqlalchemy.sql import delete, func
10import couchers.jobs.worker
11from couchers.config import config
12from couchers.constants import HOST_REQUEST_MAX_REMINDERS, HOST_REQUEST_REMINDER_INTERVAL
13from couchers.crypto import urlsafe_secure_token
14from couchers.db import session_scope
15from couchers.email import queue_email
16from couchers.email.dev import print_dev_email
17from couchers.jobs import handlers
18from couchers.jobs.enqueue import queue_job
19from couchers.jobs.handlers import (
20 add_users_to_email_list,
21 send_host_request_reminders,
22 send_message_notifications,
23 send_onboarding_emails,
24 send_reference_reminders,
25 send_request_notifications,
26 update_badges,
27 update_recommendation_scores,
28)
29from couchers.jobs.worker import _run_job_and_schedule, process_job, run_scheduler, service_jobs
30from couchers.metrics import create_prometheus_server
31from couchers.models import (
32 AccountDeletionToken,
33 BackgroundJob,
34 BackgroundJobState,
35 Email,
36 HostRequestStatus,
37 LoginToken,
38 Message,
39 MessageType,
40 PasswordResetToken,
41 UserBadge,
42 UserBlock,
43 Volunteer,
44)
45from couchers.proto import conversations_pb2, requests_pb2
46from couchers.sql import couchers_select as select
47from couchers.utils import now, today
48from tests.test_fixtures import ( # noqa
49 auth_api_session,
50 conversations_session,
51 db,
52 generate_user,
53 make_friends,
54 make_user_block,
55 process_jobs,
56 push_collector,
57 requests_session,
58 testconfig,
59)
60from tests.test_references import create_host_reference, create_host_request, create_host_request_by_date
61from tests.test_requests import valid_request_text
64def now_5_min_in_future():
65 return now() + timedelta(minutes=5)
68@pytest.fixture(autouse=True)
69def _(testconfig):
70 pass
73def _check_job_counter(job, status, attempt, exception):
74 metrics_string = requests.get("http://localhost:8000").text
75 string_to_check = f'attempt="{attempt}",exception="{exception}",job="{job}",status="{status}"'
76 assert string_to_check in metrics_string
79def test_email_job(db):
80 with session_scope() as session:
81 queue_email(session, "sender_name", "sender_email", "recipient", "subject", "plain", "html")
83 def mock_print_dev_email(
84 sender_name, sender_email, recipient, subject, plain, html, list_unsubscribe_header, source_data
85 ):
86 assert sender_name == "sender_name"
87 assert sender_email == "sender_email"
88 assert recipient == "recipient"
89 assert subject == "subject"
90 assert plain == "plain"
91 assert html == "html"
92 return print_dev_email(
93 sender_name, sender_email, recipient, subject, plain, html, list_unsubscribe_header, source_data
94 )
96 with patch("couchers.jobs.handlers.print_dev_email", mock_print_dev_email):
97 process_job()
99 with session_scope() as session:
100 assert (
101 session.execute(
102 select(func.count())
103 .select_from(BackgroundJob)
104 .where(BackgroundJob.state == BackgroundJobState.completed)
105 ).scalar_one()
106 == 1
107 )
108 assert (
109 session.execute(
110 select(func.count())
111 .select_from(BackgroundJob)
112 .where(BackgroundJob.state != BackgroundJobState.completed)
113 ).scalar_one()
114 == 0
115 )
118def test_purge_login_tokens(db):
119 user, api_token = generate_user()
121 with session_scope() as session:
122 login_token = LoginToken(token=urlsafe_secure_token(), user=user, expiry=now())
123 session.add(login_token)
124 assert session.execute(select(func.count()).select_from(LoginToken)).scalar_one() == 1
126 queue_job(session, "purge_login_tokens", empty_pb2.Empty())
127 process_job()
129 with session_scope() as session:
130 assert session.execute(select(func.count()).select_from(LoginToken)).scalar_one() == 0
132 with session_scope() as session:
133 assert (
134 session.execute(
135 select(func.count())
136 .select_from(BackgroundJob)
137 .where(BackgroundJob.state == BackgroundJobState.completed)
138 ).scalar_one()
139 == 1
140 )
141 assert (
142 session.execute(
143 select(func.count())
144 .select_from(BackgroundJob)
145 .where(BackgroundJob.state != BackgroundJobState.completed)
146 ).scalar_one()
147 == 0
148 )
151def test_purge_password_reset_tokens(db):
152 user, api_token = generate_user()
154 with session_scope() as session:
155 password_reset_token = PasswordResetToken(token=urlsafe_secure_token(), user=user, expiry=now())
156 session.add(password_reset_token)
157 assert session.execute(select(func.count()).select_from(PasswordResetToken)).scalar_one() == 1
159 queue_job(session, "purge_password_reset_tokens", empty_pb2.Empty())
160 process_job()
162 with session_scope() as session:
163 assert session.execute(select(func.count()).select_from(PasswordResetToken)).scalar_one() == 0
165 with session_scope() as session:
166 assert (
167 session.execute(
168 select(func.count())
169 .select_from(BackgroundJob)
170 .where(BackgroundJob.state == BackgroundJobState.completed)
171 ).scalar_one()
172 == 1
173 )
174 assert (
175 session.execute(
176 select(func.count())
177 .select_from(BackgroundJob)
178 .where(BackgroundJob.state != BackgroundJobState.completed)
179 ).scalar_one()
180 == 0
181 )
184def test_purge_account_deletion_tokens(db):
185 user, api_token = generate_user()
186 user2, api_token2 = generate_user()
187 user3, api_token3 = generate_user()
189 with session_scope() as session:
190 """
191 3 cases:
192 1) Token is valid
193 2) Token expired but account retrievable
194 3) Account is irretrievable (and expired)
195 """
196 account_deletion_tokens = [
197 AccountDeletionToken(token=urlsafe_secure_token(), user=user, expiry=now() - timedelta(hours=2)),
198 AccountDeletionToken(token=urlsafe_secure_token(), user=user2, expiry=now()),
199 AccountDeletionToken(token=urlsafe_secure_token(), user=user3, expiry=now() + timedelta(hours=5)),
200 ]
201 for token in account_deletion_tokens:
202 session.add(token)
203 assert session.execute(select(func.count()).select_from(AccountDeletionToken)).scalar_one() == 3
205 queue_job(session, "purge_account_deletion_tokens", empty_pb2.Empty())
206 process_job()
208 with session_scope() as session:
209 assert session.execute(select(func.count()).select_from(AccountDeletionToken)).scalar_one() == 1
211 with session_scope() as session:
212 assert (
213 session.execute(
214 select(func.count())
215 .select_from(BackgroundJob)
216 .where(BackgroundJob.state == BackgroundJobState.completed)
217 ).scalar_one()
218 == 1
219 )
220 assert (
221 session.execute(
222 select(func.count())
223 .select_from(BackgroundJob)
224 .where(BackgroundJob.state != BackgroundJobState.completed)
225 ).scalar_one()
226 == 0
227 )
230def test_enforce_community_memberships(db):
231 with session_scope() as session:
232 queue_job(session, "enforce_community_membership", empty_pb2.Empty())
233 process_job()
235 with session_scope() as session:
236 assert (
237 session.execute(
238 select(func.count())
239 .select_from(BackgroundJob)
240 .where(BackgroundJob.state == BackgroundJobState.completed)
241 ).scalar_one()
242 == 1
243 )
244 assert (
245 session.execute(
246 select(func.count())
247 .select_from(BackgroundJob)
248 .where(BackgroundJob.state != BackgroundJobState.completed)
249 ).scalar_one()
250 == 0
251 )
254def test_refresh_materialized_views(db):
255 with session_scope() as session:
256 queue_job(session, "refresh_materialized_views", empty_pb2.Empty())
258 process_job()
260 with session_scope() as session:
261 assert (
262 session.execute(
263 select(func.count())
264 .select_from(BackgroundJob)
265 .where(BackgroundJob.state == BackgroundJobState.completed)
266 ).scalar_one()
267 == 1
268 )
269 assert (
270 session.execute(
271 select(func.count())
272 .select_from(BackgroundJob)
273 .where(BackgroundJob.state != BackgroundJobState.completed)
274 ).scalar_one()
275 == 0
276 )
279def test_service_jobs(db):
280 with session_scope() as session:
281 queue_email(session, "sender_name", "sender_email", "recipient", "subject", "plain", "html")
283 # we create this HitSleep exception here, and mock out the normal sleep(1) in the infinite loop to instead raise
284 # this. that allows us to conveniently get out of the infinite loop and know we had no more jobs left
285 class HitSleep(Exception):
286 pass
288 # the mock `sleep` function that instead raises the aforementioned exception
289 def raising_sleep(seconds):
290 raise HitSleep()
292 with pytest.raises(HitSleep):
293 with patch("couchers.jobs.worker.sleep", raising_sleep):
294 service_jobs()
296 with session_scope() as session:
297 assert (
298 session.execute(
299 select(func.count())
300 .select_from(BackgroundJob)
301 .where(BackgroundJob.state == BackgroundJobState.completed)
302 ).scalar_one()
303 == 1
304 )
305 assert (
306 session.execute(
307 select(func.count())
308 .select_from(BackgroundJob)
309 .where(BackgroundJob.state != BackgroundJobState.completed)
310 ).scalar_one()
311 == 0
312 )
315def test_scheduler(db, monkeypatch):
316 MOCK_SCHEDULE = [
317 ("purge_login_tokens", timedelta(seconds=7)),
318 ("send_message_notifications", timedelta(seconds=11)),
319 ]
321 current_time = 0
322 end_time = 70
324 class EndOfTime(Exception):
325 pass
327 def mock_monotonic():
328 nonlocal current_time
329 return current_time
331 def mock_sleep(seconds):
332 nonlocal current_time
333 current_time += seconds
334 if current_time > end_time:
335 raise EndOfTime()
337 realized_schedule = []
339 def mock_run_job_and_schedule(sched, schedule_id):
340 nonlocal current_time
341 realized_schedule.append((current_time, schedule_id))
342 _run_job_and_schedule(sched, schedule_id)
344 monkeypatch.setattr(couchers.jobs.worker, "_run_job_and_schedule", mock_run_job_and_schedule)
345 monkeypatch.setattr(couchers.jobs.worker, "SCHEDULE", MOCK_SCHEDULE)
346 monkeypatch.setattr(couchers.jobs.worker, "monotonic", mock_monotonic)
347 monkeypatch.setattr(couchers.jobs.worker, "sleep", mock_sleep)
349 with pytest.raises(EndOfTime):
350 run_scheduler()
352 assert realized_schedule == [
353 (0.0, 0),
354 (0.0, 1),
355 (7.0, 0),
356 (11.0, 1),
357 (14.0, 0),
358 (21.0, 0),
359 (22.0, 1),
360 (28.0, 0),
361 (33.0, 1),
362 (35.0, 0),
363 (42.0, 0),
364 (44.0, 1),
365 (49.0, 0),
366 (55.0, 1),
367 (56.0, 0),
368 (63.0, 0),
369 (66.0, 1),
370 (70.0, 0),
371 ]
373 with session_scope() as session:
374 assert (
375 session.execute(
376 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending)
377 ).scalar_one()
378 == 18
379 )
380 assert (
381 session.execute(
382 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.state != BackgroundJobState.pending)
383 ).scalar_one()
384 == 0
385 )
388def test_job_retry(db):
389 with session_scope() as session:
390 queue_job(session, "mock_job", empty_pb2.Empty())
392 called_count = 0
394 def mock_job(payload):
395 nonlocal called_count
396 called_count += 1
397 raise Exception()
399 MOCK_JOBS = {
400 "mock_job": (empty_pb2.Empty, mock_job),
401 }
402 create_prometheus_server(port=8000)
404 # if IN_TEST is true, then the bg worker will raise on exceptions
405 new_config = config.copy()
406 new_config["IN_TEST"] = False
408 with patch("couchers.jobs.worker.config", new_config), patch("couchers.jobs.worker.JOBS", MOCK_JOBS):
409 process_job()
410 with session_scope() as session:
411 assert (
412 session.execute(
413 select(func.count())
414 .select_from(BackgroundJob)
415 .where(BackgroundJob.state == BackgroundJobState.error)
416 ).scalar_one()
417 == 1
418 )
419 assert (
420 session.execute(
421 select(func.count())
422 .select_from(BackgroundJob)
423 .where(BackgroundJob.state != BackgroundJobState.error)
424 ).scalar_one()
425 == 0
426 )
428 session.execute(select(BackgroundJob)).scalar_one().next_attempt_after = func.now()
429 process_job()
430 with session_scope() as session:
431 session.execute(select(BackgroundJob)).scalar_one().next_attempt_after = func.now()
432 process_job()
433 with session_scope() as session:
434 session.execute(select(BackgroundJob)).scalar_one().next_attempt_after = func.now()
435 process_job()
436 with session_scope() as session:
437 session.execute(select(BackgroundJob)).scalar_one().next_attempt_after = func.now()
438 process_job()
440 with session_scope() as session:
441 assert (
442 session.execute(
443 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.failed)
444 ).scalar_one()
445 == 1
446 )
447 assert (
448 session.execute(
449 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.state != BackgroundJobState.failed)
450 ).scalar_one()
451 == 0
452 )
454 _check_job_counter("mock_job", "error", "4", "Exception")
455 _check_job_counter("mock_job", "failed", "5", "Exception")
458def test_no_jobs_no_problem(db):
459 with session_scope() as session:
460 assert session.execute(select(func.count()).select_from(BackgroundJob)).scalar_one() == 0
462 assert not process_job()
464 with session_scope() as session:
465 assert session.execute(select(func.count()).select_from(BackgroundJob)).scalar_one() == 0
468def test_send_message_notifications_basic(db):
469 user1, token1 = generate_user()
470 user2, token2 = generate_user()
471 user3, token3 = generate_user()
473 make_friends(user1, user2)
474 make_friends(user1, user3)
475 make_friends(user2, user3)
477 send_message_notifications(empty_pb2.Empty())
478 process_jobs()
480 # should find no jobs, since there's no messages
481 with session_scope() as session:
482 assert (
483 session.execute(
484 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
485 ).scalar_one()
486 == 0
487 )
489 with conversations_session(token1) as c:
490 group_chat_id = c.CreateGroupChat(
491 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id, user3.id])
492 ).group_chat_id
493 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 1"))
494 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 2"))
495 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 3"))
496 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 4"))
498 with conversations_session(token3) as c:
499 group_chat_id = c.CreateGroupChat(
500 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])
501 ).group_chat_id
502 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 5"))
503 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 6"))
505 send_message_notifications(empty_pb2.Empty())
506 process_jobs()
508 # no emails sent out
509 with session_scope() as session:
510 assert (
511 session.execute(
512 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
513 ).scalar_one()
514 == 0
515 )
517 # this should generate emails for both user2 and user3
518 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
519 send_message_notifications(empty_pb2.Empty())
520 process_jobs()
522 with session_scope() as session:
523 assert (
524 session.execute(
525 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
526 ).scalar_one()
527 == 2
528 )
529 # delete them all
530 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
532 # shouldn't generate any more emails
533 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
534 send_message_notifications(empty_pb2.Empty())
535 process_jobs()
537 with session_scope() as session:
538 assert (
539 session.execute(
540 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
541 ).scalar_one()
542 == 0
543 )
546def test_send_message_notifications_muted(db):
547 user1, token1 = generate_user()
548 user2, token2 = generate_user()
549 user3, token3 = generate_user()
551 make_friends(user1, user2)
552 make_friends(user1, user3)
553 make_friends(user2, user3)
555 send_message_notifications(empty_pb2.Empty())
556 process_jobs()
558 # should find no jobs, since there's no messages
559 with session_scope() as session:
560 assert (
561 session.execute(
562 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
563 ).scalar_one()
564 == 0
565 )
567 with conversations_session(token1) as c:
568 group_chat_id = c.CreateGroupChat(
569 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id, user3.id])
570 ).group_chat_id
572 with conversations_session(token3) as c:
573 # mute it for user 3
574 c.MuteGroupChat(conversations_pb2.MuteGroupChatReq(group_chat_id=group_chat_id, forever=True))
576 with conversations_session(token1) as c:
577 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 1"))
578 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 2"))
579 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 3"))
580 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 4"))
582 with conversations_session(token3) as c:
583 group_chat_id = c.CreateGroupChat(
584 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])
585 ).group_chat_id
586 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 5"))
587 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 6"))
589 send_message_notifications(empty_pb2.Empty())
590 process_jobs()
592 # no emails sent out
593 with session_scope() as session:
594 assert (
595 session.execute(
596 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
597 ).scalar_one()
598 == 0
599 )
601 # this should generate emails for both user2 and NOT user3
602 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
603 send_message_notifications(empty_pb2.Empty())
604 process_jobs()
606 with session_scope() as session:
607 assert (
608 session.execute(
609 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
610 ).scalar_one()
611 == 1
612 )
613 # delete them all
614 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
616 # shouldn't generate any more emails
617 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
618 send_message_notifications(empty_pb2.Empty())
619 process_jobs()
621 with session_scope() as session:
622 assert (
623 session.execute(
624 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
625 ).scalar_one()
626 == 0
627 )
630def test_send_request_notifications_host_request(db):
631 user1, token1 = generate_user()
632 user2, token2 = generate_user()
634 today_plus_2 = (today() + timedelta(days=2)).isoformat()
635 today_plus_3 = (today() + timedelta(days=3)).isoformat()
637 send_request_notifications(empty_pb2.Empty())
638 process_jobs()
640 # should find no jobs, since there's no messages
641 with session_scope() as session:
642 assert session.execute(select(func.count()).select_from(BackgroundJob)).scalar_one() == 0
644 # first test that sending host request creates email
645 with requests_session(token1) as requests:
646 host_request_id = requests.CreateHostRequest(
647 requests_pb2.CreateHostRequestReq(
648 host_user_id=user2.id, from_date=today_plus_2, to_date=today_plus_3, text=valid_request_text()
649 )
650 ).host_request_id
652 with session_scope() as session:
653 # delete send_email BackgroundJob created by CreateHostRequest
654 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
656 # check send_request_notifications successfully creates background job
657 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
658 send_request_notifications(empty_pb2.Empty())
659 process_jobs()
660 assert (
661 session.execute(
662 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
663 ).scalar_one()
664 == 1
665 )
667 # delete all BackgroundJobs
668 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
670 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
671 send_request_notifications(empty_pb2.Empty())
672 process_jobs()
673 # should find no messages since host has already been notified
674 assert (
675 session.execute(
676 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
677 ).scalar_one()
678 == 0
679 )
681 # then test that responding to host request creates email
682 with requests_session(token2) as requests:
683 requests.RespondHostRequest(
684 requests_pb2.RespondHostRequestReq(
685 host_request_id=host_request_id,
686 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
687 text="Test request",
688 )
689 )
691 with session_scope() as session:
692 # delete send_email BackgroundJob created by RespondHostRequest
693 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
695 # check send_request_notifications successfully creates background job
696 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
697 send_request_notifications(empty_pb2.Empty())
698 process_jobs()
699 assert (
700 session.execute(
701 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
702 ).scalar_one()
703 == 1
704 )
706 # delete all BackgroundJobs
707 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
709 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
710 send_request_notifications(empty_pb2.Empty())
711 process_jobs()
712 # should find no messages since guest has already been notified
713 assert (
714 session.execute(
715 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
716 ).scalar_one()
717 == 0
718 )
721def test_send_message_notifications_seen(db):
722 user1, token1 = generate_user()
723 user2, token2 = generate_user()
725 make_friends(user1, user2)
727 send_message_notifications(empty_pb2.Empty())
729 # should find no jobs, since there's no messages
730 with session_scope() as session:
731 assert (
732 session.execute(
733 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
734 ).scalar_one()
735 == 0
736 )
738 with conversations_session(token1) as c:
739 group_chat_id = c.CreateGroupChat(
740 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])
741 ).group_chat_id
742 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 1"))
743 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 2"))
744 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 3"))
745 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 4"))
747 # user 2 now marks those messages as seen
748 with conversations_session(token2) as c:
749 m_id = c.GetGroupChat(conversations_pb2.GetGroupChatReq(group_chat_id=group_chat_id)).latest_message.message_id
750 c.MarkLastSeenGroupChat(
751 conversations_pb2.MarkLastSeenGroupChatReq(group_chat_id=group_chat_id, last_seen_message_id=m_id)
752 )
754 send_message_notifications(empty_pb2.Empty())
756 # no emails sent out
757 with session_scope() as session:
758 assert (
759 session.execute(
760 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
761 ).scalar_one()
762 == 0
763 )
765 def now_30_min_in_future():
766 return now() + timedelta(minutes=30)
768 # still shouldn't generate emails as user2 has seen all messages
769 with patch("couchers.jobs.handlers.now", now_30_min_in_future):
770 send_message_notifications(empty_pb2.Empty())
772 with session_scope() as session:
773 assert (
774 session.execute(
775 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
776 ).scalar_one()
777 == 0
778 )
781def test_send_onboarding_emails(db):
782 # needs to get first onboarding email
783 user1, token1 = generate_user(onboarding_emails_sent=0, last_onboarding_email_sent=None, complete_profile=False)
785 send_onboarding_emails(empty_pb2.Empty())
786 process_jobs()
788 with session_scope() as session:
789 assert (
790 session.execute(
791 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
792 ).scalar_one()
793 == 1
794 )
796 # needs to get second onboarding email, but not yet
797 user2, token2 = generate_user(
798 onboarding_emails_sent=1, last_onboarding_email_sent=now() - timedelta(days=6), complete_profile=False
799 )
801 send_onboarding_emails(empty_pb2.Empty())
802 process_jobs()
804 with session_scope() as session:
805 assert (
806 session.execute(
807 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
808 ).scalar_one()
809 == 1
810 )
812 # needs to get second onboarding email
813 user3, token3 = generate_user(
814 onboarding_emails_sent=1, last_onboarding_email_sent=now() - timedelta(days=8), complete_profile=False
815 )
817 send_onboarding_emails(empty_pb2.Empty())
818 process_jobs()
820 with session_scope() as session:
821 assert (
822 session.execute(
823 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
824 ).scalar_one()
825 == 2
826 )
829def test_send_reference_reminders(db):
830 # need to test:
831 # case 1: bidirectional (no emails)
832 # case 2: host left ref (surfer needs an email)
833 # case 3: surfer left ref (host needs an email)
834 # case 4: neither left ref (host & surfer need an email)
835 # case 5: neither left ref, but host blocked surfer, so neither should get an email
836 # case 6: neither left ref, surfer indicated they didn't meet up, (host still needs an email)
838 send_reference_reminders(empty_pb2.Empty())
840 # case 1: bidirectional (no emails)
841 user1, token1 = generate_user(email="user1@couchers.org.invalid", name="User 1")
842 user2, token2 = generate_user(email="user2@couchers.org.invalid", name="User 2")
844 # case 2: host left ref (surfer needs an email)
845 # host
846 user3, token3 = generate_user(email="user3@couchers.org.invalid", name="User 3")
847 # surfer
848 user4, token4 = generate_user(email="user4@couchers.org.invalid", name="User 4")
850 # case 3: surfer left ref (host needs an email)
851 # host
852 user5, token5 = generate_user(email="user5@couchers.org.invalid", name="User 5")
853 # surfer
854 user6, token6 = generate_user(email="user6@couchers.org.invalid", name="User 6")
856 # case 4: neither left ref (host & surfer need an email)
857 # surfer
858 user7, token7 = generate_user(email="user7@couchers.org.invalid", name="User 7")
859 # host
860 user8, token8 = generate_user(email="user8@couchers.org.invalid", name="User 8")
862 # case 5: neither left ref, but host blocked surfer, so neither should get an email
863 # surfer
864 user9, token9 = generate_user(email="user9@couchers.org.invalid", name="User 9")
865 # host
866 user10, token10 = generate_user(email="user10@couchers.org.invalid", name="User 10")
868 make_user_block(user9, user10)
870 # case 6: neither left ref, surfer indicated they didn't meet up, (host still needs an email)
871 # host
872 user11, token11 = generate_user(email="user11@couchers.org.invalid", name="User 11")
873 # surfer
874 user12, token12 = generate_user(email="user12@couchers.org.invalid", name="User 12")
876 with session_scope() as session:
877 # note that create_host_reference creates a host request whose age is one day older than the timedelta here
879 # case 1: bidirectional (no emails)
880 ref1, hr1 = create_host_reference(session, user2.id, user1.id, timedelta(days=7), surfing=True)
881 create_host_reference(session, user1.id, user2.id, timedelta(days=7), host_request_id=hr1)
883 # case 2: host left ref (surfer needs an email)
884 ref2, hr2 = create_host_reference(session, user3.id, user4.id, timedelta(days=11), surfing=False)
886 # case 3: surfer left ref (host needs an email)
887 ref3, hr3 = create_host_reference(session, user6.id, user5.id, timedelta(days=9), surfing=True)
889 # case 4: neither left ref (host & surfer need an email)
890 hr4 = create_host_request(session, user7.id, user8.id, timedelta(days=4))
892 # case 5: neither left ref, but host blocked surfer, so neither should get an email
893 hr5 = create_host_request(session, user9.id, user10.id, timedelta(days=7))
895 # case 6: neither left ref, surfer indicated they didn't meet up, (host still needs an email)
896 hr6 = create_host_request(session, user12.id, user11.id, timedelta(days=6), surfer_reason_didnt_meetup="")
898 expected_emails = [
899 (
900 "user11@couchers.org.invalid",
901 "[TEST] You have 14 days to write a reference for User 12!",
902 ("from when you hosted them", "/leave-reference/hosted/"),
903 ),
904 (
905 "user4@couchers.org.invalid",
906 "[TEST] You have 3 days to write a reference for User 3!",
907 ("from when you surfed with them", "/leave-reference/surfed/"),
908 ),
909 (
910 "user5@couchers.org.invalid",
911 "[TEST] You have 7 days to write a reference for User 6!",
912 ("from when you hosted them", "/leave-reference/hosted/"),
913 ),
914 (
915 "user7@couchers.org.invalid",
916 "[TEST] You have 14 days to write a reference for User 8!",
917 ("from when you surfed with them", "/leave-reference/surfed/"),
918 ),
919 (
920 "user8@couchers.org.invalid",
921 "[TEST] You have 14 days to write a reference for User 7!",
922 ("from when you hosted them", "/leave-reference/hosted/"),
923 ),
924 ]
926 send_reference_reminders(empty_pb2.Empty())
928 while process_job():
929 pass
931 with session_scope() as session:
932 emails = [
933 (email.recipient, email.subject, email.plain, email.html)
934 for email in session.execute(select(Email).order_by(Email.recipient.asc())).scalars().all()
935 ]
937 actual_addresses_and_subjects = [email[:2] for email in emails]
938 expected_addresses_and_subjects = [email[:2] for email in expected_emails]
940 print(actual_addresses_and_subjects)
941 print(expected_addresses_and_subjects)
943 assert actual_addresses_and_subjects == expected_addresses_and_subjects
945 for (address, subject, plain, html), (_, _, search_strings) in zip(emails, expected_emails):
946 for find in search_strings:
947 assert find in plain, f"Expected to find string {find} in PLAIN email {subject} to {address}, didn't"
948 assert find in html, f"Expected to find string {find} in HTML email {subject} to {address}, didn't"
951def test_send_host_request_reminders(db):
952 user1, token1 = generate_user(email="user1@couchers.org.invalid", name="User 1")
953 user2, token2 = generate_user(email="user2@couchers.org.invalid", name="User 2")
954 user3, token3 = generate_user(email="user3@couchers.org.invalid", name="User 3")
955 user4, token4 = generate_user(email="user4@couchers.org.invalid", name="User 4")
956 user5, token5 = generate_user(email="user5@couchers.org.invalid", name="User 5")
957 user6, token6 = generate_user(email="user6@couchers.org.invalid", name="User 6")
958 user7, token7 = generate_user(email="user7@couchers.org.invalid", name="User 7")
959 user8, token8 = generate_user(email="user8@couchers.org.invalid", name="User 8")
960 user9, token9 = generate_user(email="user9@couchers.org.invalid", name="User 9")
961 user10, token10 = generate_user(email="user10@couchers.org.invalid", name="User 10")
962 user11, token11 = generate_user(email="user11@couchers.org.invalid", name="User 11")
963 user12, token12 = generate_user(email="user12@couchers.org.invalid", name="User 12")
964 user13, token13 = generate_user(email="user13@couchers.org.invalid", name="User 13")
965 user14, token14 = generate_user(email="user14@couchers.org.invalid", name="User 14")
967 with session_scope() as session:
968 # case 1: pending, future, interval elapsed => notify
969 hr1 = create_host_request_by_date(
970 session=session,
971 surfer_user_id=user1.id,
972 host_user_id=user2.id,
973 from_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=1),
974 to_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=2),
975 status=HostRequestStatus.pending,
976 host_sent_request_reminders=0,
977 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
978 )
980 # case 2: max reminders reached => do not notify
981 hr2 = create_host_request_by_date(
982 session=session,
983 surfer_user_id=user3.id,
984 host_user_id=user4.id,
985 from_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=1),
986 to_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=2),
987 status=HostRequestStatus.pending,
988 host_sent_request_reminders=HOST_REQUEST_MAX_REMINDERS,
989 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
990 )
992 # case 3: interval not yet elapsed => do not notify
993 hr3 = create_host_request_by_date(
994 session=session,
995 surfer_user_id=user5.id,
996 host_user_id=user6.id,
997 from_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=1),
998 to_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=2),
999 status=HostRequestStatus.pending,
1000 host_sent_request_reminders=0,
1001 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL + timedelta(hours=1),
1002 )
1004 # case 4: start date is today => do not notify
1005 hr4 = create_host_request_by_date(
1006 session=session,
1007 surfer_user_id=user7.id,
1008 host_user_id=user8.id,
1009 from_date=today(),
1010 to_date=today() + timedelta(days=2),
1011 status=HostRequestStatus.pending,
1012 host_sent_request_reminders=0,
1013 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
1014 )
1016 # case 5: from_date in the past => do not notify
1017 hr5 = create_host_request_by_date(
1018 session=session,
1019 surfer_user_id=user9.id,
1020 host_user_id=user10.id,
1021 from_date=today() - timedelta(days=1),
1022 to_date=today() + timedelta(days=1),
1023 status=HostRequestStatus.pending,
1024 host_sent_request_reminders=0,
1025 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
1026 )
1028 # case 6: non-pending status => do not notify
1029 hr6 = create_host_request_by_date(
1030 session=session,
1031 surfer_user_id=user11.id,
1032 host_user_id=user12.id,
1033 from_date=today() + timedelta(days=3),
1034 to_date=today() + timedelta(days=4),
1035 status=HostRequestStatus.accepted,
1036 host_sent_request_reminders=0,
1037 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
1038 )
1040 # case 7: host already sent a message => do not notify
1041 hr7 = create_host_request_by_date(
1042 session=session,
1043 surfer_user_id=user13.id,
1044 host_user_id=user14.id,
1045 from_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=1),
1046 to_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=2),
1047 status=HostRequestStatus.pending,
1048 host_sent_request_reminders=0,
1049 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
1050 )
1052 session.add(
1053 Message(
1054 time=now(),
1055 conversation_id=hr7,
1056 author_id=user14.id,
1057 text="Looking forward to hosting you!",
1058 message_type=MessageType.text,
1059 )
1060 )
1062 send_host_request_reminders(empty_pb2.Empty())
1064 while process_job():
1065 pass
1067 with session_scope() as session:
1068 emails = [
1069 (email.recipient, email.subject, email.plain, email.html)
1070 for email in session.execute(select(Email).order_by(Email.recipient.asc())).scalars().all()
1071 ]
1073 expected_emails = [
1074 (
1075 "user2@couchers.org.invalid",
1076 "[TEST] You have a pending host request from User 1!",
1077 ("Please respond to the request!", "User 1"),
1078 )
1079 ]
1081 actual_addresses_and_subjects = [email[:2] for email in emails]
1082 expected_addresses_and_subjects = [email[:2] for email in expected_emails]
1084 print(actual_addresses_and_subjects)
1085 print(expected_addresses_and_subjects)
1087 assert actual_addresses_and_subjects == expected_addresses_and_subjects
1089 for (address, subject, plain, html), (_, _, search_strings) in zip(emails, expected_emails):
1090 for find in search_strings:
1091 assert find in plain, f"Expected to find string {find} in PLAIN email {subject} to {address}, didn't"
1092 assert find in html, f"Expected to find string {find} in HTML email {subject} to {address}, didn't"
1095def test_add_users_to_email_list(db):
1096 new_config = config.copy()
1097 new_config["LISTMONK_ENABLED"] = True
1098 new_config["LISTMONK_BASE_URL"] = "https://example.com"
1099 new_config["LISTMONK_API_USERNAME"] = "test_user"
1100 new_config["LISTMONK_API_KEY"] = "dummy_api_key"
1101 new_config["LISTMONK_LIST_ID"] = 6
1103 with patch("couchers.jobs.handlers.config", new_config):
1104 with patch("couchers.jobs.handlers.requests.post") as mock:
1105 add_users_to_email_list(empty_pb2.Empty())
1106 mock.assert_not_called()
1108 generate_user(in_sync_with_newsletter=False, email="testing1@couchers.invalid", name="Tester1", id=15)
1109 generate_user(in_sync_with_newsletter=True, email="testing2@couchers.invalid", name="Tester2")
1110 generate_user(in_sync_with_newsletter=False, email="testing3@couchers.invalid", name="Tester3 von test", id=17)
1111 generate_user(
1112 in_sync_with_newsletter=False, email="testing4@couchers.invalid", name="Tester4", opt_out_of_newsletter=True
1113 )
1115 with patch("couchers.jobs.handlers.requests.post") as mock:
1116 ret = mock.return_value
1117 ret.status_code = 200
1118 add_users_to_email_list(empty_pb2.Empty())
1119 mock.assert_has_calls(
1120 [
1121 call(
1122 "https://example.com/api/subscribers",
1123 auth=("test_user", "dummy_api_key"),
1124 json={
1125 "email": "testing1@couchers.invalid",
1126 "name": "Tester1",
1127 "lists": [6],
1128 "preconfirm_subscriptions": True,
1129 "attribs": {"couchers_user_id": 15},
1130 "status": "enabled",
1131 },
1132 timeout=10,
1133 ),
1134 call(
1135 "https://example.com/api/subscribers",
1136 auth=("test_user", "dummy_api_key"),
1137 json={
1138 "email": "testing3@couchers.invalid",
1139 "name": "Tester3 von test",
1140 "lists": [6],
1141 "preconfirm_subscriptions": True,
1142 "attribs": {"couchers_user_id": 17},
1143 "status": "enabled",
1144 },
1145 timeout=10,
1146 ),
1147 ],
1148 any_order=True,
1149 )
1151 with patch("couchers.jobs.handlers.requests.post") as mock:
1152 add_users_to_email_list(empty_pb2.Empty())
1153 mock.assert_not_called()
1156def test_update_recommendation_scores(db):
1157 update_recommendation_scores(empty_pb2.Empty())
1160def test_update_badges(db, push_collector):
1161 user1, _ = generate_user()
1162 user2, _ = generate_user()
1163 user3, _ = generate_user()
1164 user4, _ = generate_user(phone="+15555555555", phone_verification_verified=func.now())
1165 user5, _ = generate_user(phone="+15555555556", phone_verification_verified=func.now())
1166 user6, _ = generate_user()
1168 with session_scope() as session:
1169 session.add(UserBadge(user_id=user5.id, badge_id="board_member"))
1171 update_badges(empty_pb2.Empty())
1172 process_jobs()
1174 with session_scope() as session:
1175 badge_tuples = session.execute(
1176 select(UserBadge.user_id, UserBadge.badge_id).order_by(UserBadge.user_id.asc(), UserBadge.id.asc())
1177 ).all()
1179 expected = [
1180 (user1.id, "founder"),
1181 (user1.id, "board_member"),
1182 (user2.id, "founder"),
1183 (user2.id, "board_member"),
1184 (user4.id, "phone_verified"),
1185 (user5.id, "phone_verified"),
1186 ]
1188 assert badge_tuples == expected
1190 print(push_collector.pushes)
1192 push_collector.assert_user_push_matches_fields(
1193 user1.id,
1194 ix=0,
1195 title="The Founder badge was added to your profile",
1196 body="Check out your profile to see the new badge!",
1197 )
1198 push_collector.assert_user_push_matches_fields(
1199 user1.id,
1200 ix=1,
1201 title="The Board Member badge was added to your profile",
1202 body="Check out your profile to see the new badge!",
1203 )
1204 push_collector.assert_user_push_matches_fields(
1205 user2.id,
1206 ix=0,
1207 title="The Founder badge was added to your profile",
1208 body="Check out your profile to see the new badge!",
1209 )
1210 push_collector.assert_user_push_matches_fields(
1211 user2.id,
1212 ix=1,
1213 title="The Board Member badge was added to your profile",
1214 body="Check out your profile to see the new badge!",
1215 )
1216 push_collector.assert_user_push_matches_fields(
1217 user4.id,
1218 ix=0,
1219 title="The Verified Phone badge was added to your profile",
1220 body="Check out your profile to see the new badge!",
1221 )
1222 push_collector.assert_user_push_matches_fields(
1223 user5.id,
1224 ix=0,
1225 title="The Board Member badge was removed from your profile",
1226 body="You can see all your badges on your profile.",
1227 )
1228 push_collector.assert_user_push_matches_fields(
1229 user5.id,
1230 ix=1,
1231 title="The Verified Phone badge was added to your profile",
1232 body="Check out your profile to see the new badge!",
1233 )
1236def test_send_request_notifications_blocked_users_no_notification(db):
1237 """
1238 Regression test: send_request_notifications should not send notifications
1239 when the host and surfer are not visible to each other (e.g., one blocked the other).
1240 """
1241 user1, token1 = generate_user()
1242 user2, token2 = generate_user()
1244 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1245 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1247 # Create a host request
1248 with requests_session(token1) as requests:
1249 host_request_id = requests.CreateHostRequest(
1250 requests_pb2.CreateHostRequestReq(
1251 host_user_id=user2.id, from_date=today_plus_2, to_date=today_plus_3, text=valid_request_text()
1252 )
1253 ).host_request_id
1255 with session_scope() as session:
1256 # delete send_email BackgroundJob created by CreateHostRequest
1257 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1259 # Now user2 (host) blocks user1 (surfer)
1260 make_user_block(user2, user1)
1262 with session_scope() as session:
1263 # check send_request_notifications does NOT create background job because users are blocked
1264 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
1265 send_request_notifications(empty_pb2.Empty())
1266 process_jobs()
1268 # Should be 0 emails because the host blocked the surfer
1269 assert (
1270 session.execute(
1271 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
1272 ).scalar_one()
1273 == 0
1274 ), "No notification email should be sent when host has blocked surfer"
1276 # Also test the reverse direction: surfer sends message to host, host should not get notification
1277 # First unblock
1278 with session_scope() as session:
1279 session.execute(delete(UserBlock).execution_options(synchronize_session=False))
1280 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1282 # Host responds
1283 with requests_session(token2) as requests:
1284 requests.RespondHostRequest(
1285 requests_pb2.RespondHostRequestReq(
1286 host_request_id=host_request_id,
1287 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1288 text="Accepting your request",
1289 )
1290 )
1292 with session_scope() as session:
1293 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1295 # Now user1 (surfer) blocks user2 (host)
1296 make_user_block(user1, user2)
1298 with session_scope() as session:
1299 # check send_request_notifications does NOT create background job
1300 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
1301 send_request_notifications(empty_pb2.Empty())
1302 process_jobs()
1304 # Should be 0 emails because the surfer blocked the host
1305 assert (
1306 session.execute(
1307 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
1308 ).scalar_one()
1309 == 0
1310 ), "No notification email should be sent when surfer has blocked host"
1313def test_send_host_request_reminders_blocked_users_no_notification(db):
1314 """
1315 send_host_request_reminders should not send notifications when the host and surfer are not visible to each other
1316 (e.g., one blocked the other).
1317 """
1318 user1, token1 = generate_user(email="user1@couchers.org.invalid", name="User 1")
1319 user2, token2 = generate_user(email="user2@couchers.org.invalid", name="User 2")
1321 with session_scope() as session:
1322 # Create a pending host request where the host has not replied
1323 hr = create_host_request_by_date(
1324 session=session,
1325 surfer_user_id=user1.id,
1326 host_user_id=user2.id,
1327 from_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=1),
1328 to_date=today() + HOST_REQUEST_REMINDER_INTERVAL + timedelta(days=2),
1329 status=HostRequestStatus.pending,
1330 host_sent_request_reminders=0,
1331 last_sent_request_reminder_time=now() - HOST_REQUEST_REMINDER_INTERVAL,
1332 )
1334 # Verify that without blocking, a reminder would be sent
1335 send_host_request_reminders(empty_pb2.Empty())
1337 while process_job():
1338 pass
1340 with session_scope() as session:
1341 emails = session.execute(select(Email)).scalars().all()
1342 assert len(emails) == 1, "Expected 1 reminder email before blocking"
1344 # Clean up emails and background jobs
1345 session.execute(delete(Email).execution_options(synchronize_session=False))
1346 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1348 # Reset the reminder counter so we can test again
1349 from couchers.models import HostRequest
1351 host_request = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr)).scalar_one()
1352 host_request.host_sent_request_reminders = 0
1353 host_request.last_sent_request_reminder_time = now() - HOST_REQUEST_REMINDER_INTERVAL
1355 # Now have the host block the surfer
1356 make_user_block(user2, user1)
1358 send_host_request_reminders(empty_pb2.Empty())
1360 while process_job():
1361 pass
1363 with session_scope() as session:
1364 emails = session.execute(select(Email)).scalars().all()
1365 assert len(emails) == 0, "No reminder email should be sent when host has blocked surfer"
1368def test_send_message_notifications_blocked_users_no_notification(db):
1369 """
1370 Regression test: send_message_notifications should not send notifications
1371 for messages from users who are blocked by the recipient.
1372 """
1373 user1, token1 = generate_user()
1374 user2, token2 = generate_user()
1376 make_friends(user1, user2)
1378 # Create a group chat and send messages
1379 with conversations_session(token1) as c:
1380 group_chat_id = c.CreateGroupChat(
1381 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])
1382 ).group_chat_id
1383 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 1"))
1384 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Test message 2"))
1386 # Verify that without blocking, a notification would be sent
1387 with session_scope() as session:
1388 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1390 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
1391 send_message_notifications(empty_pb2.Empty())
1392 process_jobs()
1394 with session_scope() as session:
1395 email_job_count = session.execute(
1396 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
1397 ).scalar_one()
1398 assert email_job_count == 1, "Expected 1 notification email before blocking"
1400 # Clean up
1401 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1403 # Reset the notification state so user2 will receive notifications for old messages again
1404 with session_scope() as session:
1405 from couchers.models import User
1407 u2 = session.execute(select(User).where(User.id == user2.id)).scalar_one()
1408 u2.last_notified_message_id = 0
1410 # Now have user2 block user1
1411 make_user_block(user2, user1)
1413 # The existing messages from user1 should now NOT trigger notifications
1414 # since user2 has blocked user1
1415 with session_scope() as session:
1416 session.execute(delete(BackgroundJob).execution_options(synchronize_session=False))
1418 with patch("couchers.jobs.handlers.now", now_5_min_in_future):
1419 send_message_notifications(empty_pb2.Empty())
1420 process_jobs()
1422 with session_scope() as session:
1423 email_job_count = session.execute(
1424 select(func.count()).select_from(BackgroundJob).where(BackgroundJob.job_type == "send_email")
1425 ).scalar_one()
1426 assert email_job_count == 0, "No notification email should be sent when recipient has blocked sender"
1429def test_update_badges_volunteers(db, push_collector):
1430 """Test that volunteer and past_volunteer badges are automatically granted based on Volunteer model."""
1431 # Create 6 users - users 1 and 2 get founder/board_member badges from static_badges
1432 user1, _ = generate_user()
1433 user2, _ = generate_user()
1434 user3, _ = generate_user()
1435 user4, _ = generate_user()
1436 user5, _ = generate_user()
1437 user6, _ = generate_user()
1439 with session_scope() as session:
1440 # user3: active volunteer (stopped_volunteering is null)
1441 session.add(
1442 Volunteer(
1443 user_id=user3.id,
1444 role="Developer",
1445 started_volunteering=date(2020, 1, 1),
1446 stopped_volunteering=None,
1447 )
1448 )
1450 # user4: past volunteer (stopped_volunteering is set)
1451 session.add(
1452 Volunteer(
1453 user_id=user4.id,
1454 role="Designer",
1455 started_volunteering=date(2020, 1, 1),
1456 stopped_volunteering=date(2023, 6, 1),
1457 )
1458 )
1460 # user5: has old volunteer badge that should be removed (not a volunteer anymore)
1461 session.add(UserBadge(user_id=user5.id, badge_id="volunteer"))
1463 # user6: has old past_volunteer badge that should be removed
1464 session.add(UserBadge(user_id=user6.id, badge_id="past_volunteer"))
1466 update_badges(empty_pb2.Empty())
1467 process_jobs()
1469 with session_scope() as session:
1470 # Check user3 has volunteer badge
1471 user3_badges = session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == user3.id)).scalars().all()
1472 assert "volunteer" in user3_badges
1473 assert "past_volunteer" not in user3_badges
1475 # Check user4 has past_volunteer badge
1476 user4_badges = session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == user4.id)).scalars().all()
1477 assert "past_volunteer" in user4_badges
1478 assert "volunteer" not in user4_badges
1480 # Check user5 lost the volunteer badge (not in Volunteer table)
1481 user5_badges = session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == user5.id)).scalars().all()
1482 assert "volunteer" not in user5_badges
1484 # Check user6 lost the past_volunteer badge (not in Volunteer table)
1485 user6_badges = session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == user6.id)).scalars().all()
1486 assert "past_volunteer" not in user6_badges
1488 # Check notifications for volunteer badge users
1489 push_collector.assert_user_has_single_matching(
1490 user3.id,
1491 title="The Active Volunteer badge was added to your profile",
1492 body="Check out your profile to see the new badge!",
1493 )
1494 push_collector.assert_user_has_single_matching(
1495 user4.id,
1496 title="The Past Volunteer badge was added to your profile",
1497 body="Check out your profile to see the new badge!",
1498 )
1499 push_collector.assert_user_has_single_matching(
1500 user5.id,
1501 title="The Active Volunteer badge was removed from your profile",
1502 body="You can see all your badges on your profile.",
1503 )
1504 push_collector.assert_user_has_single_matching(
1505 user6.id,
1506 title="The Past Volunteer badge was removed from your profile",
1507 body="You can see all your badges on your profile.",
1508 )
1511def test_update_badges_volunteer_status_change(db, push_collector):
1512 """Test that badge is updated when volunteer status changes from active to past."""
1513 # Create users - users 1 and 2 get founder/board_member badges from static_badges
1514 user1, _ = generate_user()
1515 user2, _ = generate_user()
1516 user3, _ = generate_user()
1518 with session_scope() as session:
1519 # user3: start as active volunteer
1520 session.add(
1521 Volunteer(
1522 user_id=user3.id,
1523 role="Developer",
1524 started_volunteering=date(2020, 1, 1),
1525 stopped_volunteering=None,
1526 )
1527 )
1529 update_badges(empty_pb2.Empty())
1530 process_jobs()
1532 with session_scope() as session:
1533 user3_badges = session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == user3.id)).scalars().all()
1534 assert "volunteer" in user3_badges
1535 assert "past_volunteer" not in user3_badges
1537 push_collector.assert_user_has_single_matching(
1538 user3.id,
1539 title="The Active Volunteer badge was added to your profile",
1540 body="Check out your profile to see the new badge!",
1541 )
1543 # Now change the volunteer to past volunteer
1544 with session_scope() as session:
1545 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == user3.id)).scalar_one()
1546 volunteer.stopped_volunteering = date(2023, 12, 1)
1548 update_badges(empty_pb2.Empty())
1549 process_jobs()
1551 with session_scope() as session:
1552 user3_badges = session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == user3.id)).scalars().all()
1553 assert "volunteer" not in user3_badges
1554 assert "past_volunteer" in user3_badges
1556 # Check both badges were updated
1557 push_collector.assert_user_push_matches_fields(
1558 user3.id,
1559 ix=1,
1560 title="The Active Volunteer badge was removed from your profile",
1561 body="You can see all your badges on your profile.",
1562 )
1563 push_collector.assert_user_push_matches_fields(
1564 user3.id,
1565 ix=2,
1566 title="The Past Volunteer badge was added to your profile",
1567 body="Check out your profile to see the new badge!",
1568 )
1571def test_send_message_notifications_empty_unseen_simple(monkeypatch):
1572 class DummyUser:
1573 id = 1
1574 is_visible = True
1575 last_notified_message_id = 0
1577 class FirstResult:
1578 def scalars(self):
1579 return self
1581 def unique(self):
1582 return [DummyUser()]
1584 class SecondResult:
1585 def all(self):
1586 return []
1588 class DummySession:
1589 def __init__(self):
1590 self.calls = 0
1592 def execute(self, *a, **k):
1593 self.calls += 1
1594 return FirstResult() if self.calls == 1 else SecondResult()
1596 def commit(self):
1597 pass
1599 def flush(self):
1600 pass
1602 def fake_session_scope():
1603 class Ctx:
1604 def __enter__(self):
1605 return DummySession()
1607 def __exit__(self, exc_type, exc, tb):
1608 pass
1610 return Ctx()
1612 monkeypatch.setattr(handlers, "session_scope", fake_session_scope)
1614 handlers.send_message_notifications(Empty())