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

1from datetime import timedelta 

2 

3import pytest 

4from google.protobuf import empty_pb2, wrappers_pb2 

5from sqlalchemy import select 

6 

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 

31 

32 

33@pytest.fixture(autouse=True) 

34def _(testconfig): 

35 pass 

36 

37 

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 ] 

48 

49 

50def test_history_recorded_on_signup(db): 

51 user_id = _quick_signup() 

52 

53 assert get_history(user_id) == [ 

54 (HostingMeetupStatusSource.signup, HostingStatus.can_host, MeetupStatus.open_to_meetup) 

55 ] 

56 

57 

58def test_history_recorded_on_profile_edit(db): 

59 user, token = generate_user(hosting_status=HostingStatus.cant_host, meetup_status=MeetupStatus.open_to_meetup) 

60 

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)) 

64 

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 ] 

69 

70 

71def test_history_not_recorded_when_unchanged(db): 

72 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup) 

73 

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) == [] 

78 

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 ] 

90 

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 

99 

100 

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) 

103 

104 with notifications_session(token) as notifications: 

105 notifications.SetNotificationSettings(notifications_pb2.SetNotificationSettingsReq(enable_do_not_email=True)) 

106 

107 assert get_history(user.id) == [ 

108 (HostingMeetupStatusSource.do_not_email, HostingStatus.cant_host, MeetupStatus.does_not_want_to_meetup) 

109 ] 

110 

111 

112def test_history_recorded_on_unsubscribe_link(db): 

113 user, token = generate_user(hosting_status=HostingStatus.can_host, meetup_status=MeetupStatus.wants_to_meetup) 

114 

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)) 

119 

120 with auth_api_session() as (auth_api, metadata_interceptor): 

121 auth_api.Unsubscribe(auth_pb2.UnsubscribeReq(payload=payload, sig=sig)) 

122 

123 assert get_history(user.id) == [ 

124 (HostingMeetupStatusSource.unsubscribe_link, HostingStatus.cant_host, MeetupStatus.does_not_want_to_meetup) 

125 ] 

126 

127 

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 ) 

134 

135 with session_scope() as session: 

136 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty()) 

137 

138 process_jobs() 

139 

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 ) 

144 

145 assert get_history(user.id) == [ 

146 (HostingMeetupStatusSource.activeness_probe_response, HostingStatus.cant_host, MeetupStatus.wants_to_meetup) 

147 ] 

148 

149 

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 ) 

156 

157 with session_scope() as session: 

158 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty()) 

159 

160 process_jobs() 

161 

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 

166 

167 queue_job(session, job=send_activeness_probes, payload=empty_pb2.Empty()) 

168 

169 process_jobs() 

170 

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 

175 

176 assert get_history(user.id) == [ 

177 (HostingMeetupStatusSource.activeness_probe_expired, HostingStatus.maybe, MeetupStatus.open_to_meetup) 

178 ]