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

1from datetime import timedelta 

2 

3from google.protobuf import empty_pb2, wrappers_pb2 

4from sqlalchemy import select 

5 

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 

30 

31 

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 ] 

42 

43 

44def test_history_recorded_on_signup(db, fast_passwords): 

45 user_id = _quick_signup() 

46 

47 assert get_history(user_id) == [ 

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

49 ] 

50 

51 

52def test_history_recorded_on_profile_edit(db): 

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

54 

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

58 

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 ] 

63 

64 

65def test_history_not_recorded_when_unchanged(db): 

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

67 

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

72 

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 ] 

84 

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 

93 

94 

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) 

97 

98 with notifications_session(token) as notifications: 

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

100 

101 assert get_history(user.id) == [ 

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

103 ] 

104 

105 

106def test_history_recorded_on_unsubscribe_link(db): 

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

108 

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

113 

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

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

116 

117 assert get_history(user.id) == [ 

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

119 ] 

120 

121 

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 ) 

128 

129 with session_scope() as session: 

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

131 

132 process_jobs() 

133 

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 ) 

138 

139 assert get_history(user.id) == [ 

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

141 ] 

142 

143 

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 ) 

150 

151 with session_scope() as session: 

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

153 

154 process_jobs() 

155 

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 

160 

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

162 

163 process_jobs() 

164 

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 

169 

170 assert get_history(user.id) == [ 

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

172 ]