Coverage for app/backend/src/tests/test_hosting_meetup_status_history.py: 100%
72 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-01 13:53 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-01 13:53 +0000
1from datetime import timedelta
3from google.protobuf import empty_pb2, wrappers_pb2
4from sqlalchemy import select
6from couchers.crypto import UNSUBSCRIBE_KEY_NAME, generate_hash_signature, get_secret
7from couchers.db import session_scope
8from couchers.jobs.enqueue import queue_job
9from couchers.jobs.handlers import send_activeness_probes
10from couchers.models import (
11 ActivenessProbe,
12 HostingMeetupStatusHistory,
13 HostingMeetupStatusSource,
14 HostingStatus,
15 MeetupStatus,
16 User,
17)
18from couchers.proto import api_pb2, auth_pb2, jail_pb2, notifications_pb2
19from couchers.proto.internal import unsubscribe_pb2
20from couchers.utils import now
21from tests.fixtures.db import generate_user
22from tests.fixtures.misc import PushCollector, process_jobs
23from tests.fixtures.sessions import (
24 api_session,
25 auth_api_session,
26 notifications_session,
27 real_jail_session,
28)
29from tests.test_auth import _quick_signup
32def get_history(user_id: int) -> list[tuple[HostingMeetupStatusSource, HostingStatus, MeetupStatus]]:
33 with session_scope() as session:
34 return [
35 (row.source, row.hosting_status, row.meetup_status)
36 for row in session.execute(
37 select(HostingMeetupStatusHistory)
38 .where(HostingMeetupStatusHistory.user_id == user_id)
39 .order_by(HostingMeetupStatusHistory.id)
40 ).scalars()
41 ]
44def test_history_recorded_on_signup(db, fast_passwords):
45 user_id = _quick_signup()
47 assert get_history(user_id) == [
48 (HostingMeetupStatusSource.signup, HostingStatus.can_host, MeetupStatus.open_to_meetup)
49 ]
52def test_history_recorded_on_profile_edit(db):
53 user, token = generate_user(hosting_status=HostingStatus.cant_host, meetup_status=MeetupStatus.open_to_meetup)
55 with api_session(token) as api:
56 api.UpdateProfile(api_pb2.UpdateProfileReq(hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST))
57 api.UpdateProfile(api_pb2.UpdateProfileReq(meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP))
59 assert get_history(user.id) == [
60 (HostingMeetupStatusSource.profile_edit, HostingStatus.can_host, MeetupStatus.open_to_meetup),
61 (HostingMeetupStatusSource.profile_edit, HostingStatus.can_host, MeetupStatus.wants_to_meetup),
62 ]
65def test_history_not_recorded_when_unchanged(db):
66 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup)
68 with api_session(token) as api:
69 # neither status is touched
70 api.UpdateProfile(api_pb2.UpdateProfileReq(name=wrappers_pb2.StringValue(value="Frodo Baggins")))
71 assert get_history(user.id) == []
73 # both statuses set to what they already are, but nothing has ever been recorded for this user, so this
74 # becomes their first row
75 api.UpdateProfile(
76 api_pb2.UpdateProfileReq(
77 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST,
78 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
79 )
80 )
81 assert get_history(user.id) == [
82 (HostingMeetupStatusSource.profile_edit, HostingStatus.can_host, MeetupStatus.wants_to_meetup)
83 ]
85 # now that it's been recorded, a no-op edit doesn't add another
86 api.UpdateProfile(
87 api_pb2.UpdateProfileReq(
88 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST,
89 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
90 )
91 )
92 assert len(get_history(user.id)) == 1
95def test_history_recorded_on_do_not_email(db):
96 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup)
98 with notifications_session(token) as notifications:
99 notifications.SetNotificationSettings(notifications_pb2.SetNotificationSettingsReq(enable_do_not_email=True))
101 assert get_history(user.id) == [
102 (HostingMeetupStatusSource.do_not_email, HostingStatus.cant_host, MeetupStatus.does_not_want_to_meetup)
103 ]
106def test_history_recorded_on_unsubscribe_link(db):
107 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup)
109 payload = unsubscribe_pb2.UnsubscribePayload(
110 user_id=user.id, do_not_email=unsubscribe_pb2.DoNotEmail()
111 ).SerializeToString()
112 sig = generate_hash_signature(message=payload, key=get_secret(UNSUBSCRIBE_KEY_NAME))
114 with auth_api_session() as (auth_api, metadata_interceptor):
115 auth_api.Unsubscribe(auth_pb2.UnsubscribeReq(payload=payload, sig=sig))
117 assert get_history(user.id) == [
118 (HostingMeetupStatusSource.unsubscribe_link, HostingStatus.cant_host, MeetupStatus.does_not_want_to_meetup)
119 ]
122def test_history_recorded_on_activeness_probe_response(db, push_collector: PushCollector):
123 user, token = generate_user(
124 hosting_status=HostingStatus.can_host,
125 meetup_status=MeetupStatus.wants_to_meetup,
126 last_active=now() - timedelta(days=335),
127 )
129 with session_scope() as session:
130 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty())
132 process_jobs()
134 with real_jail_session(token) as jail:
135 jail.RespondToActivenessProbe(
136 jail_pb2.RespondToActivenessProbeReq(response=jail_pb2.ACTIVENESS_PROBE_RESPONSE_NO_LONGER_ACTIVE)
137 )
139 assert get_history(user.id) == [
140 (HostingMeetupStatusSource.activeness_probe_response, HostingStatus.cant_host, MeetupStatus.wants_to_meetup)
141 ]
144def test_history_recorded_on_activeness_probe_expiry(db, push_collector: PushCollector):
145 user, token = generate_user(
146 hosting_status=HostingStatus.can_host,
147 meetup_status=MeetupStatus.wants_to_meetup,
148 last_active=now() - timedelta(days=335),
149 )
151 with session_scope() as session:
152 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty())
154 process_jobs()
156 with session_scope() as session:
157 probe = session.execute(select(ActivenessProbe)).scalar_one()
158 probe.probe_initiated = now() - timedelta(days=15)
159 probe.notifications_sent = 2
161 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty())
163 process_jobs()
165 with session_scope() as session:
166 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
167 assert db_user.hosting_status == HostingStatus.maybe
168 assert db_user.meetup_status == MeetupStatus.open_to_meetup
170 assert get_history(user.id) == [
171 (HostingMeetupStatusSource.activeness_probe_expired, HostingStatus.maybe, MeetupStatus.open_to_meetup)
172 ]