Coverage for app/backend/src/tests/fixtures/api_helpers.py: 92%

42 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 20:52 +0000

1""" 

2Exposes typed functions mapping 1-to-1 to backend APIs, encapsulating protobuf encoding boilerplate. 

3Functions have neutral defaults for tests that don't need specific parameter values. 

4""" 

5 

6from datetime import date, datetime, time, timedelta 

7 

8from google.protobuf import wrappers_pb2 

9 

10from couchers.proto import events_pb2 

11from couchers.utils import today 

12from tests.fixtures.misc import Moderator 

13from tests.fixtures.sessions import events_session 

14 

15# Event location constants which timezone_areas.sql-fake has regions for. 

16# Neutral default location. Resolves to Etc/GMT with real dataset and Etc/UTC with fake dataset. 

17EVENT_LOCATION_DEFAULT_UTC = events_pb2.EventLocation(address="Near Null Island", lat=0.1, lng=0.2) 

18EVENT_LOCATION_HELSINKI = events_pb2.EventLocation(address="Helsinki, Finland", lat=60.192059, lng=24.945831) 

19EVENT_LOCATION_NEW_YORK = events_pb2.EventLocation(address="New York, New York", lat=40.712776, lng=-74.005974) 

20 

21 

22def _date_time_to_iso8601(value: tuple[date, time]) -> str: 

23 return f"{value[0].isoformat()}T{value[1].isoformat()}" 

24 

25 

26def _one_hour_after(value: tuple[date, time]) -> tuple[date, time]: 

27 end_datetime = datetime.combine(value[0], value[1]) + timedelta(hours=1) 

28 return end_datetime.date(), end_datetime.time() 

29 

30 

31def _string_value(value: str | None) -> wrappers_pb2.StringValue | None: 

32 return wrappers_pb2.StringValue(value=value) if value is not None else None 

33 

34 

35def create_event( 

36 *, 

37 token: str, 

38 title: str | None = "Dummy Title", 

39 content: str | None = "Dummy content.", 

40 photo_key: str | None = None, 

41 parent_community_id: int | None = None, 

42 location: events_pb2.EventLocation | None = EVENT_LOCATION_DEFAULT_UTC, 

43 # Force callers to provide local date/times, not datetimes with timezones, 

44 # since the API accepts local times and resolves the timezone from the location. 

45 start_date_time: tuple[date, time] | None = None, 

46 end_date_time: tuple[date, time] | None = None, 

47 approve_by: Moderator | None = None, 

48) -> events_pb2.Event: 

49 if start_date_time is None: 

50 start_date_time = (today() + timedelta(days=1), time(12, 0)) 

51 if end_date_time is None: 

52 end_date_time = _one_hour_after(start_date_time) 

53 

54 with events_session(token) as api: 

55 event: events_pb2.Event = api.CreateEvent( 

56 events_pb2.CreateEventReq( 

57 title=title, 

58 content=content, 

59 photo_key=photo_key, 

60 parent_community_id=parent_community_id, 

61 location=location, 

62 start_datetime_iso8601_local=_date_time_to_iso8601(start_date_time), 

63 end_datetime_iso8601_local=_date_time_to_iso8601(end_date_time), 

64 ) 

65 ) 

66 

67 if approve_by is not None: 

68 approve_by.approve_event_occurrence(event.event_id) 

69 

70 return event 

71 

72 

73def schedule_event( 

74 *, 

75 token: str, 

76 event_id: int, 

77 content: str | None = "Dummy content.", 

78 photo_key: str | None = None, 

79 location: events_pb2.EventLocation | None = EVENT_LOCATION_DEFAULT_UTC, 

80 # Force callers to provide local date/times, not datetimes with timezones, 

81 # since the API accepts local times and resolves the timezone from the location. 

82 start_date_time: tuple[date, time], 

83 end_date_time: tuple[date, time] | None = None, 

84 approve_by: Moderator | None = None, 

85) -> events_pb2.Event: 

86 if end_date_time is None: 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true

87 end_date_time = _one_hour_after(start_date_time) 

88 

89 with events_session(token) as api: 

90 event: events_pb2.Event = api.ScheduleEvent( 

91 events_pb2.ScheduleEventReq( 

92 event_id=event_id, 

93 content=content, 

94 photo_key=photo_key, 

95 location=location, 

96 start_datetime_iso8601_local=_date_time_to_iso8601(start_date_time), 

97 end_datetime_iso8601_local=_date_time_to_iso8601(end_date_time), 

98 ) 

99 ) 

100 

101 if approve_by is not None: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

102 approve_by.approve_event_occurrence(event.event_id) 

103 

104 return event 

105 

106 

107def update_event( 

108 *, 

109 token: str, 

110 event_id: int, 

111 # None leaves the field unchanged. 

112 title: str | None = None, 

113 content: str | None = None, 

114 photo_key: str | None = None, 

115 location: events_pb2.EventLocation | None = None, 

116 start_date_time: tuple[date, time] | None = None, 

117 end_date_time: tuple[date, time] | None = None, 

118 update_all_future: bool = False, 

119 should_notify: bool = False, 

120) -> events_pb2.Event: 

121 with events_session(token) as api: 

122 event: events_pb2.Event = api.UpdateEvent( 

123 events_pb2.UpdateEventReq( 

124 event_id=event_id, 

125 update_all_future=update_all_future, 

126 title=_string_value(title), 

127 content=_string_value(content), 

128 photo_key=_string_value(photo_key), 

129 location=location, 

130 start_datetime_iso8601_local=_string_value( 

131 _date_time_to_iso8601(start_date_time) if start_date_time is not None else None 

132 ), 

133 end_datetime_iso8601_local=_string_value( 

134 _date_time_to_iso8601(end_date_time) if end_date_time is not None else None 

135 ), 

136 should_notify=should_notify, 

137 ) 

138 ) 

139 return event 

140 

141 

142def get_event(token: str, event_id: int) -> events_pb2.Event: 

143 with events_session(token) as api: 

144 event: events_pb2.Event = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

145 return event