Coverage for app/backend/src/tests/test_hosting_meetup_status_history.py: 100%
76 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-17 00:57 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-17 00:57 +0000
1from datetime import timedelta
3import pytest
4from google.protobuf import empty_pb2, wrappers_pb2
5from sqlalchemy import select
7from couchers.crypto import UNSUBSCRIBE_KEY_NAME, generate_hash_signature, get_secret
8from couchers.db import session_scope
9from couchers.jobs.enqueue import queue_job
10from couchers.jobs.handlers import send_activeness_probes
11from couchers.models import (
12 ActivenessProbe,
13 HostingMeetupStatusHistory,
14 HostingMeetupStatusSource,
15 HostingStatus,
16 MeetupStatus,
17 User,
18)
19from couchers.proto import api_pb2, auth_pb2, jail_pb2, notifications_pb2
20from couchers.proto.internal import unsubscribe_pb2
21from couchers.utils import now
22from tests.fixtures.db import generate_user
23from tests.fixtures.misc import PushCollector, process_jobs
24from tests.fixtures.sessions import (
25 api_session,
26 auth_api_session,
27 notifications_session,
28 real_jail_session,
29)
30from tests.test_auth import _quick_signup
33@pytest.fixture(autouse=True)
34def _(testconfig):
35 pass
38def get_history(user_id: int) -> list[tuple[HostingMeetupStatusSource, HostingStatus, MeetupStatus]]:
39 with session_scope() as session:
40 return [
41 (row.source, row.hosting_status, row.meetup_status)
42 for row in session.execute(
43 select(HostingMeetupStatusHistory)
44 .where(HostingMeetupStatusHistory.user_id == user_id)
45 .order_by(HostingMeetupStatusHistory.id)
46 ).scalars()
47 ]
50def test_history_recorded_on_signup(db):
51 user_id = _quick_signup()
53 assert get_history(user_id) == [
54 (HostingMeetupStatusSource.signup, HostingStatus.can_host, MeetupStatus.open_to_meetup)
55 ]
58def test_history_recorded_on_profile_edit(db):
59 user, token = generate_user(hosting_status=HostingStatus.cant_host, meetup_status=MeetupStatus.open_to_meetup)
61 with api_session(token) as api:
62 api.UpdateProfile(api_pb2.UpdateProfileReq(hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST))
63 api.UpdateProfile(api_pb2.UpdateProfileReq(meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP))
65 assert get_history(user.id) == [
66 (HostingMeetupStatusSource.profile_edit, HostingStatus.can_host, MeetupStatus.open_to_meetup),
67 (HostingMeetupStatusSource.profile_edit, HostingStatus.can_host, MeetupStatus.wants_to_meetup),
68 ]
71def test_history_not_recorded_when_unchanged(db):
72 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup)
74 with api_session(token) as api:
75 # neither status is touched
76 api.UpdateProfile(api_pb2.UpdateProfileReq(name=wrappers_pb2.StringValue(value="Frodo Baggins")))
77 assert get_history(user.id) == []
79 # both statuses set to what they already are, but nothing has ever been recorded for this user, so this
80 # becomes their first row
81 api.UpdateProfile(
82 api_pb2.UpdateProfileReq(
83 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST,
84 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
85 )
86 )
87 assert get_history(user.id) == [
88 (HostingMeetupStatusSource.profile_edit, HostingStatus.can_host, MeetupStatus.wants_to_meetup)
89 ]
91 # now that it's been recorded, a no-op edit doesn't add another
92 api.UpdateProfile(
93 api_pb2.UpdateProfileReq(
94 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST,
95 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
96 )
97 )
98 assert len(get_history(user.id)) == 1
101def test_history_recorded_on_do_not_email(db):
102 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup)
104 with notifications_session(token) as notifications:
105 notifications.SetNotificationSettings(notifications_pb2.SetNotificationSettingsReq(enable_do_not_email=True))
107 assert get_history(user.id) == [
108 (HostingMeetupStatusSource.do_not_email, HostingStatus.cant_host, MeetupStatus.does_not_want_to_meetup)
109 ]
112def test_history_recorded_on_unsubscribe_link(db):
113 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup)
115 payload = unsubscribe_pb2.UnsubscribePayload(
116 user_id=user.id, do_not_email=unsubscribe_pb2.DoNotEmail()
117 ).SerializeToString()
118 sig = generate_hash_signature(message=payload, key=get_secret(UNSUBSCRIBE_KEY_NAME))
120 with auth_api_session() as (auth_api, metadata_interceptor):
121 auth_api.Unsubscribe(auth_pb2.UnsubscribeReq(payload=payload, sig=sig))
123 assert get_history(user.id) == [
124 (HostingMeetupStatusSource.unsubscribe_link, HostingStatus.cant_host, MeetupStatus.does_not_want_to_meetup)
125 ]
128def test_history_recorded_on_activeness_probe_response(db, push_collector: PushCollector):
129 user, token = generate_user(
130 hosting_status=HostingStatus.can_host,
131 meetup_status=MeetupStatus.wants_to_meetup,
132 last_active=now() - timedelta(days=335),
133 )
135 with session_scope() as session:
136 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty())
138 process_jobs()
140 with real_jail_session(token) as jail:
141 jail.RespondToActivenessProbe(
142 jail_pb2.RespondToActivenessProbeReq(response=jail_pb2.ACTIVENESS_PROBE_RESPONSE_NO_LONGER_ACTIVE)
143 )
145 assert get_history(user.id) == [
146 (HostingMeetupStatusSource.activeness_probe_response, HostingStatus.cant_host, MeetupStatus.wants_to_meetup)
147 ]
150def test_history_recorded_on_activeness_probe_expiry(db, push_collector: PushCollector):
151 user, token = generate_user(
152 hosting_status=HostingStatus.can_host,
153 meetup_status=MeetupStatus.wants_to_meetup,
154 last_active=now() - timedelta(days=335),
155 )
157 with session_scope() as session:
158 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty())
160 process_jobs()
162 with session_scope() as session:
163 probe = session.execute(select(ActivenessProbe)).scalar_one()
164 probe.probe_initiated = now() - timedelta(days=15)
165 probe.notifications_sent = 2
167 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty())
169 process_jobs()
171 with session_scope() as session:
172 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
173 assert db_user.hosting_status == HostingStatus.maybe
174 assert db_user.meetup_status == MeetupStatus.open_to_meetup
176 assert get_history(user.id) == [
177 (HostingMeetupStatusSource.activeness_probe_expired, HostingStatus.maybe, MeetupStatus.open_to_meetup)
178 ]