Coverage for app/backend/src/tests/test_message_threads.py: 100%
327 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 12:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 12:25 +0000
1from datetime import timedelta
3import grpc
4import pytest
5from google.protobuf import empty_pb2
6from sqlalchemy import select
8from couchers.constants import MISSED_MESSAGES_DELAY
9from couchers.db import session_scope
10from couchers.helpers.host_requests import has_unseen_host_request_messages
11from couchers.jobs.handlers import send_message_notifications
12from couchers.models import HostRequest, NotificationTopicAction, User
13from couchers.proto import api_pb2, conversations_pb2, messages_pb2, notifications_pb2, requests_pb2
14from couchers.utils import today
15from tests.fixtures.db import generate_user, make_user_block
16from tests.fixtures.misc import process_jobs
17from tests.fixtures.sessions import (
18 conversations_session,
19 notifications_session,
20 real_api_session,
21 requests_session,
22)
23from tests.fixtures.timewarp import Timewarp
24from tests.test_communities import create_community
25from tests.test_public_trips import _create_trip_directly
26from tests.test_requests import valid_request_text
29@pytest.fixture(autouse=True)
30def _(testconfig):
31 pass
34def _make_trip(user: User) -> tuple[int, int]:
35 """Create a community + an active public trip for the given traveller."""
36 with session_scope() as session:
37 node_id = create_community(session, 0, 2, "Test community", [user], [], None).id
38 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
39 return node_id, trip_id
42def _create_group_chat(token: str, recipient_ids: list[int], moderator, text: str = "hi") -> int:
43 with conversations_session(token) as c:
44 res = c.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=recipient_ids))
45 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=res.group_chat_id, text=text))
46 moderator.approve_group_chat(res.group_chat_id)
47 return int(res.group_chat_id)
50def _create_host_request(surfer_token: str, host_id: int, moderator, public_trip_id: int | None = None) -> int:
51 with requests_session(surfer_token) as api:
52 res = api.CreateHostRequest(
53 requests_pb2.CreateHostRequestReq(
54 host_user_id=host_id,
55 from_date=(today() + timedelta(days=5)).isoformat(),
56 to_date=(today() + timedelta(days=10)).isoformat(),
57 text=valid_request_text(),
58 public_trip_id=public_trip_id,
59 )
60 )
61 moderator.approve_host_request(res.host_request_id)
62 return int(res.host_request_id)
65def test_has_unseen_host_request_messages_is_false_for_a_non_party(db, moderator):
66 user1, _token1 = generate_user()
67 _user2, token2 = generate_user()
68 outsider, _token3 = generate_user()
70 conversation_id = _create_host_request(token2, user1.id, moderator)
72 with session_scope() as session:
73 unseen_for = {
74 user_id: session.execute(
75 select(HostRequest.conversation_id)
76 .where(HostRequest.conversation_id == conversation_id)
77 .where(has_unseen_host_request_messages(user_id))
78 ).scalar_one_or_none()
79 for user_id in (user1.id, outsider.id)
80 }
82 assert unseen_for[user1.id] == conversation_id
83 assert unseen_for[outsider.id] is None
86def test_list_message_threads_latest_status_change_message(db, moderator):
87 # Regression: a thread whose latest message is a host-request status change
88 # must serialize with its content set (not an empty control message).
89 user1, token1 = generate_user()
90 user2, token2 = generate_user()
92 request_id = _create_host_request(token2, user1.id, moderator)
94 # user1 (the host) accepts, so the latest message becomes a status change
95 with requests_session(token1) as api:
96 api.RespondHostRequest(
97 requests_pb2.RespondHostRequestReq(
98 host_request_id=request_id,
99 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
100 text="",
101 )
102 )
104 with conversations_session(token1) as c:
105 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
106 thread = next(t for t in res.threads if t.WhichOneof("thread") == "host_request")
107 assert thread.host_request.latest_message.WhichOneof("content") == "host_request_status_changed"
108 assert (
109 thread.host_request.latest_message.host_request_status_changed.status
110 == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
111 )
114def test_list_message_threads_need_host_request_feedback(db, moderator):
115 user1, token1 = generate_user()
116 user2, token2 = generate_user()
118 request_id = _create_host_request(token2, user1.id, moderator)
120 def host_request_thread(token: str) -> requests_pb2.HostRequest:
121 with conversations_session(token) as c:
122 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
123 thread: requests_pb2.HostRequest = next(
124 t.host_request for t in res.threads if t.host_request.host_request_id == request_id
125 )
126 return thread
128 assert not host_request_thread(token1).need_host_request_feedback
130 with requests_session(token1) as api:
131 api.RespondHostRequest(
132 requests_pb2.RespondHostRequestReq(
133 host_request_id=request_id,
134 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
135 )
136 )
138 assert host_request_thread(token1).need_host_request_feedback
139 # only the host is asked for feedback
140 assert not host_request_thread(token2).need_host_request_feedback
142 with requests_session(token1) as api:
143 api.SendHostRequestFeedback(
144 requests_pb2.SendHostRequestFeedbackReq(
145 host_request_id=request_id,
146 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW,
147 )
148 )
150 assert not host_request_thread(token1).need_host_request_feedback
153def test_list_message_threads_can_message(db, moderator):
154 user1, token1 = generate_user()
155 user2, token2 = generate_user()
156 user3, token3 = generate_user()
158 blocked_dm_id = _create_group_chat(token1, [user2.id], moderator)
159 departed_dm_id = _create_group_chat(token1, [user3.id], moderator)
160 chat_id = _create_group_chat(token1, [user2.id, user3.id], moderator)
162 def can_message_by_chat(token: str) -> dict[int, bool]:
163 with conversations_session(token) as c:
164 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
165 return {t.group_chat.group_chat_id: t.group_chat.can_message for t in res.threads}
167 assert can_message_by_chat(token1) == {blocked_dm_id: True, departed_dm_id: True, chat_id: True}
169 make_user_block(user2, user1)
170 with conversations_session(token3) as c:
171 c.LeaveGroupChat(conversations_pb2.LeaveGroupChatReq(group_chat_id=departed_dm_id))
173 # a DM whose other party is blocked or gone can't be messaged; a true group chat always can
174 assert can_message_by_chat(token1) == {blocked_dm_id: False, departed_dm_id: False, chat_id: True}
177def test_list_message_threads_interleaves_chats_and_requests(db, moderator):
178 user1, token1 = generate_user()
179 user2, token2 = generate_user()
181 chat_id = _create_group_chat(token1, [user2.id], moderator)
182 # user2 sends a host request to user1 (user1 is the host)
183 request_id = _create_host_request(token2, user1.id, moderator)
185 with conversations_session(token1) as c:
186 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
188 kinds = [t.WhichOneof("thread") for t in res.threads]
189 assert "group_chat" in kinds
190 assert "host_request" in kinds
191 ids = {
192 (t.group_chat.group_chat_id if t.WhichOneof("thread") == "group_chat" else t.host_request.host_request_id)
193 for t in res.threads
194 }
195 assert ids == {chat_id, request_id}
196 # The host request was created after the group chat, so it sorts first (latest message).
197 assert res.threads[0].WhichOneof("thread") == "host_request"
200def test_list_message_threads_single_cursor_pagination_across_kinds(db, moderator):
201 user1, token1 = generate_user()
202 others = [generate_user() for _ in range(6)]
204 # track the two kinds separately so we verify each id comes back as the right kind
205 expected_chat_ids = set()
206 expected_request_ids = set()
207 # interleave creating group chats and host requests so both kinds straddle page boundaries
208 for other, other_token in others:
209 expected_chat_ids.add(_create_group_chat(token1, [other.id], moderator))
210 expected_request_ids.add(_create_host_request(other_token, user1.id, moderator))
212 collected_chat_ids: list[int] = []
213 collected_request_ids: list[int] = []
214 latest_ids: list[int] = []
215 page_token = ""
216 while True:
217 with conversations_session(token1) as c:
218 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq(page_size=3, page_token=page_token))
219 for t in res.threads:
220 if t.WhichOneof("thread") == "group_chat":
221 collected_chat_ids.append(t.group_chat.group_chat_id)
222 latest_ids.append(t.group_chat.latest_message.message_id)
223 else:
224 collected_request_ids.append(t.host_request.host_request_id)
225 latest_ids.append(t.host_request.latest_message.message_id)
226 if not res.next_page_token:
227 break
228 page_token = res.next_page_token
230 # every thread appears exactly once as its correct kind, none missing or duplicated
231 assert sorted(collected_chat_ids) == sorted(expected_chat_ids)
232 assert sorted(collected_request_ids) == sorted(expected_request_ids)
233 assert len(collected_chat_ids) == len(set(collected_chat_ids))
234 assert len(collected_request_ids) == len(set(collected_request_ids))
235 # globally ordered by latest message id, descending, with no straddling across pages
236 assert latest_ids == sorted(latest_ids, reverse=True)
239def test_list_message_threads_chats_filter_excludes_host_requests(db, moderator):
240 user1, token1 = generate_user()
241 user2, token2 = generate_user()
243 chat_id = _create_group_chat(token1, [user2.id], moderator)
244 _create_host_request(token2, user1.id, moderator)
246 with conversations_session(token1) as c:
247 res = c.ListMessageThreads(
248 conversations_pb2.ListMessageThreadsReq(categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_CHATS])
249 )
251 assert [t.WhichOneof("thread") for t in res.threads] == ["group_chat"]
252 assert res.threads[0].group_chat.group_chat_id == chat_id
255def test_list_message_threads_roster_is_frozen_for_a_departed_viewer(db, moderator):
256 """
257 A viewer who was removed from a chat sees its roster as it was when they left, so anyone added
258 afterwards is invisible to them.
259 """
260 admin, admin_token = generate_user()
261 viewer, viewer_token = generate_user()
262 member, _member_token = generate_user()
263 latecomer, _latecomer_token = generate_user()
265 chat_id = _create_group_chat(admin_token, [viewer.id, member.id], moderator)
267 with conversations_session(admin_token) as c:
268 c.RemoveGroupChatUser(conversations_pb2.RemoveGroupChatUserReq(group_chat_id=chat_id, user_id=viewer.id))
269 c.InviteToGroupChat(conversations_pb2.InviteToGroupChatReq(group_chat_id=chat_id, user_id=latecomer.id))
271 with conversations_session(viewer_token) as c:
272 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
273 departed_view = res.threads[0].group_chat
274 assert set(departed_view.member_user_ids) == {admin.id, viewer.id, member.id}
275 assert list(departed_view.admin_user_ids) == [admin.id]
276 # chat created, "hi" and the removal notice; the invite notice is out of reach
277 assert departed_view.unseen_message_count == 3
279 with conversations_session(admin_token) as c:
280 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
281 admin_view = res.threads[0].group_chat
282 assert set(admin_view.member_user_ids) == {admin.id, member.id, latecomer.id}
285def test_list_message_threads_rejects_unspecified_category(db):
286 _user1, token1 = generate_user()
288 with conversations_session(token1) as c:
289 with pytest.raises(grpc.RpcError) as e:
290 c.ListMessageThreads(
291 conversations_pb2.ListMessageThreadsReq(
292 categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_UNSPECIFIED]
293 )
294 )
295 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
298def test_list_message_threads_unread_filter(db, moderator):
299 user1, token1 = generate_user()
300 user2, token2 = generate_user()
302 # user2 sends a request to user1 -> user1 has unseen messages
303 request_id = _create_host_request(token2, user1.id, moderator)
305 with conversations_session(token1) as c:
306 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq(only_unread=True))
307 assert [t.host_request.host_request_id for t in res.threads] == [request_id]
309 # after marking everything seen, the unread filter is empty
310 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq())
311 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq(only_unread=True))
312 assert len(res.threads) == 0
315def test_list_message_threads_archived_is_orthogonal(db, moderator):
316 user1, token1 = generate_user()
317 user2, token2 = generate_user()
319 chat_id = _create_group_chat(token1, [user2.id], moderator)
321 with conversations_session(token1) as c:
322 # archive the chat
323 c.SetGroupChatArchiveStatus(
324 conversations_pb2.SetGroupChatArchiveStatusReq(group_chat_id=chat_id, is_archived=True)
325 )
327 # default (non-archived) excludes it
328 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq(only_archived=False))
329 assert len(res.threads) == 0
331 # only_archived=True includes it
332 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq(only_archived=True))
333 assert [t.group_chat.group_chat_id for t in res.threads] == [chat_id]
336def test_list_message_threads_public_trip_offer_role_based(db, moderator):
337 traveler, traveler_token = generate_user()
338 host, host_token = generate_user()
339 _, trip_id = _make_trip(traveler)
341 # host offers to host the traveller's public trip (role reversal)
342 request_id = _create_host_request(host_token, traveler.id, moderator, public_trip_id=trip_id)
344 # From the offering host's view: appears under HOSTING (role-based filter)
345 with conversations_session(host_token) as c:
346 res = c.ListMessageThreads(
347 conversations_pb2.ListMessageThreadsReq(categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_HOSTING])
348 )
349 assert len(res.threads) == 1
350 hr = res.threads[0].host_request
351 assert hr.host_request_id == request_id
352 assert hr.HasField("public_trip_id")
353 assert hr.public_trip_id == trip_id
354 # payload keeps the Requests API semantics: surfer/host are the stay roles, which an
355 # offer reverses — the offering host is the host, the trip's traveller is the surfer
356 assert hr.surfer_user_id == traveler.id
357 assert hr.host_user_id == host.id
359 # not under SURFING for the host
360 res = c.ListMessageThreads(
361 conversations_pb2.ListMessageThreadsReq(categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_SURFING])
362 )
363 assert len(res.threads) == 0
365 # the same request served by the Requests API has to agree on who's who
366 with requests_session(host_token) as api:
367 from_requests_api = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=request_id))
368 assert (from_requests_api.surfer_user_id, from_requests_api.host_user_id) == (hr.surfer_user_id, hr.host_user_id)
370 # From the traveller's view: appears under SURFING and MY_PUBLIC_TRIPS (role-based filters)
371 with conversations_session(traveler_token) as c:
372 res = c.ListMessageThreads(
373 conversations_pb2.ListMessageThreadsReq(categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_SURFING])
374 )
375 assert [t.host_request.host_request_id for t in res.threads] == [request_id]
376 # same payload regardless of viewer
377 assert res.threads[0].host_request.surfer_user_id == traveler.id
378 assert res.threads[0].host_request.host_user_id == host.id
380 res = c.ListMessageThreads(
381 conversations_pb2.ListMessageThreadsReq(
382 categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_MY_PUBLIC_TRIPS]
383 )
384 )
385 assert [t.host_request.host_request_id for t in res.threads] == [request_id]
387 # also present in ALL
388 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
389 assert request_id in {t.host_request.host_request_id for t in res.threads}
392def test_list_message_threads_public_trips_filter_gated_by_flag(db, moderator, feature_flags):
393 feature_flags.set("public_trips_enabled", False)
395 traveler, traveler_token = generate_user()
397 with conversations_session(traveler_token) as c:
398 res = c.ListMessageThreads(
399 conversations_pb2.ListMessageThreadsReq(
400 categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_MY_PUBLIC_TRIPS]
401 )
402 )
403 assert len(res.threads) == 0
404 assert not res.next_page_token
407def test_mark_all_threads_seen_rejects_unspecified_category(db):
408 _user1, token1 = generate_user()
410 with conversations_session(token1) as c:
411 with pytest.raises(grpc.RpcError) as e:
412 c.MarkAllThreadsSeen(
413 conversations_pb2.MarkAllThreadsSeenReq(
414 categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_UNSPECIFIED]
415 )
416 )
417 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
420def test_mark_all_threads_seen_respects_categories(db, moderator):
421 user1, token1 = generate_user()
422 _user2, token2 = generate_user()
424 # an unread group chat and an unread host request, both from user2
425 _create_group_chat(token2, [user1.id], moderator, text="hello there")
426 _create_host_request(token2, user1.id, moderator)
428 def unseen() -> tuple[int, int]:
429 with real_api_session(token1) as api:
430 res = api.Ping(api_pb2.PingReq())
431 return res.unseen_message_count, res.unseen_received_host_request_count
433 assert unseen() == (2, 1)
435 with conversations_session(token1) as c:
436 c.MarkAllThreadsSeen(
437 conversations_pb2.MarkAllThreadsSeenReq(categories=[conversations_pb2.MESSAGE_THREAD_CATEGORY_CHATS])
438 )
439 # the chat is now read; the host request is untouched
440 assert unseen() == (0, 1)
442 with conversations_session(token1) as c:
443 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq())
444 assert unseen() == (0, 0)
447def test_mark_all_threads_seen_respects_unread_and_archived_filters(db, moderator):
448 user1, token1 = generate_user()
449 _user2, token2 = generate_user()
450 _user3, token3 = generate_user()
452 archived_chat_id = _create_group_chat(token2, [user1.id], moderator, text="archived")
453 _create_group_chat(token3, [user1.id], moderator, text="not archived")
455 with conversations_session(token1) as c:
456 c.SetGroupChatArchiveStatus(
457 conversations_pb2.SetGroupChatArchiveStatusReq(group_chat_id=archived_chat_id, is_archived=True)
458 )
460 def unseen_chat_messages() -> int:
461 with real_api_session(token1) as api:
462 return int(api.Ping(api_pb2.PingReq()).unseen_message_count)
464 # two messages in each chat: the creation notice and the text
465 assert unseen_chat_messages() == 4
467 with conversations_session(token1) as c:
468 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq(only_archived=True))
469 assert unseen_chat_messages() == 2
471 with conversations_session(token1) as c:
472 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq(only_unread=True))
473 assert unseen_chat_messages() == 0
476def test_departed_group_chat_unseen_count_agrees_with_ping_and_clears(db, moderator):
477 """
478 Regression test: the per-thread unseen count used to include messages sent after the viewer left
479 the chat, which the Ping badge never counted, and MarkAllThreadsSeen skipped departed chats
480 entirely — so the list showed unread messages the viewer couldn't read and couldn't clear.
482 The viewer is removed rather than leaving, because leaving posts a message of your own, which
483 marks everything up to it seen.
484 """
485 user1, token1 = generate_user()
486 _user2, token2 = generate_user()
487 user3, _token3 = generate_user()
489 chat_id = _create_group_chat(token2, [user1.id, user3.id], moderator)
491 with conversations_session(token2) as c:
492 c.RemoveGroupChatUser(conversations_pb2.RemoveGroupChatUserReq(group_chat_id=chat_id, user_id=user1.id))
493 for _ in range(3):
494 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=chat_id, text="after you left"))
496 def unseen_counts() -> tuple[int, int]:
497 with real_api_session(token1) as api:
498 ping_count = api.Ping(api_pb2.PingReq()).unseen_message_count
499 with conversations_session(token1) as c:
500 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
501 thread = next(t for t in res.threads if t.group_chat.group_chat_id == chat_id)
502 return thread.group_chat.unseen_message_count, ping_count
504 # chat created, "hi", and the removal notice; the three messages sent afterwards are out of reach
505 assert unseen_counts() == (3, 3)
507 with conversations_session(token1) as c:
508 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq())
509 assert unseen_counts() == (0, 0)
512def test_rejoined_group_chat_reads_the_current_subscription(db, moderator):
513 """
514 Regression test: rejoining a chat leaves the earlier subscription behind, with its own last-seen
515 state. The unread filter and the Ping badge used to match on any of the viewer's subscriptions,
516 so the stale one held the chat unread and kept the badge up, while the count the list showed and
517 the row MarkAllThreadsSeen advanced came from the current subscription and could never clear it.
518 """
519 user1, token1 = generate_user()
520 _user2, token2 = generate_user()
521 user3, _token3 = generate_user()
523 chat_id = _create_group_chat(token2, [user1.id, user3.id], moderator)
525 # removed rather than leaving, so user1's first subscription is left behind with unread messages
526 with conversations_session(token2) as c:
527 c.RemoveGroupChatUser(conversations_pb2.RemoveGroupChatUserReq(group_chat_id=chat_id, user_id=user1.id))
528 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=chat_id, text="while you were away"))
529 c.InviteToGroupChat(conversations_pb2.InviteToGroupChatReq(group_chat_id=chat_id, user_id=user1.id))
530 c.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=chat_id, text="welcome back"))
532 def unread_chat_ids() -> list[int]:
533 with conversations_session(token1) as c:
534 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq(only_unread=True))
535 return [t.group_chat.group_chat_id for t in res.threads]
537 def unseen_counts() -> tuple[int, int]:
538 with real_api_session(token1) as api:
539 ping_count = api.Ping(api_pb2.PingReq()).unseen_message_count
540 with conversations_session(token1) as c:
541 res = c.ListMessageThreads(conversations_pb2.ListMessageThreadsReq())
542 threads = [t for t in res.threads if t.group_chat.group_chat_id == chat_id]
543 # the two subscriptions must not surface the chat twice
544 assert len(threads) == 1
545 return threads[0].group_chat.unseen_message_count, ping_count
547 # only the invite notice and "welcome back" are in reach; the first stint's messages are not
548 assert unseen_counts() == (2, 2)
549 assert unread_chat_ids() == [chat_id]
551 with conversations_session(token1) as c:
552 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq())
553 assert unseen_counts() == (0, 0)
554 assert unread_chat_ids() == []
557def test_mark_all_threads_seen_clears_missed_messages_notification(db, moderator, timewarp: Timewarp):
558 """
559 Regression test: chat__missed_messages is a summary keyed with "" rather than a chat id, so it
560 needs its own (topic actions, keys) group instead of being pooled with the per-chat keys.
561 """
562 user1, token1 = generate_user()
563 user2, token2 = generate_user()
565 # this notification is email-only by default, and the in-app feed only shows push-enabled ones
566 with notifications_session(token2) as n:
567 n.SetNotificationSettings(
568 notifications_pb2.SetNotificationSettingsReq(
569 preferences=[
570 notifications_pb2.SingleNotificationPreference(
571 topic=NotificationTopicAction.chat__missed_messages.topic,
572 action=NotificationTopicAction.chat__missed_messages.action,
573 delivery_method="push",
574 enabled=True,
575 )
576 ]
577 )
578 )
580 _create_group_chat(token1, [user2.id], moderator, text="hello there")
582 # the job only picks up messages that have been unseen for five minutes
583 timewarp.advance(MISSED_MESSAGES_DELAY)
584 send_message_notifications(empty_pb2.Empty())
585 process_jobs()
587 def unseen_missed_messages():
588 with notifications_session(token2) as n:
589 res = n.ListNotifications(notifications_pb2.ListNotificationsReq(only_unread=True))
590 return [
591 notification
592 for notification in res.notifications
593 if notification.topic == NotificationTopicAction.chat__missed_messages.topic
594 and notification.action == NotificationTopicAction.chat__missed_messages.action
595 ]
597 assert len(unseen_missed_messages()) == 1
599 with conversations_session(token2) as c:
600 c.MarkAllThreadsSeen(conversations_pb2.MarkAllThreadsSeenReq())
602 assert unseen_missed_messages() == []