Coverage for src/tests/test_search.py: 100%
235 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-22 06:42 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-22 06:42 +0000
1from datetime import timedelta
3import pytest
4from google.protobuf import wrappers_pb2
6from couchers.db import session_scope
7from couchers.models import EventOccurrence, HostingStatus, LanguageAbility, LanguageFluency, MeetupStatus
8from couchers.utils import Timestamp_from_datetime, create_coordinate, millis_from_dt, now
9from proto import api_pb2, communities_pb2, events_pb2, search_pb2
10from tests.test_communities import create_community, testing_communities # noqa
11from tests.test_fixtures import ( # noqa
12 communities_session,
13 db,
14 events_session,
15 generate_user,
16 search_session,
17 session_scope,
18 testconfig,
19)
22@pytest.fixture(autouse=True)
23def _(testconfig):
24 pass
27def test_Search(testing_communities):
28 user, token = generate_user()
29 with search_session(token) as api:
30 res = api.Search(
31 search_pb2.SearchReq(
32 query="Country 1, Region 1",
33 include_users=True,
34 include_communities=True,
35 include_groups=True,
36 include_places=True,
37 include_guides=True,
38 )
39 )
40 res = api.Search(
41 search_pb2.SearchReq(
42 query="Country 1, Region 1, Attraction",
43 title_only=True,
44 include_users=True,
45 include_communities=True,
46 include_groups=True,
47 include_places=True,
48 include_guides=True,
49 )
50 )
53def test_UserSearch(testing_communities):
54 """Test that UserSearch returns all users if no filter is set."""
55 user, token = generate_user()
56 with search_session(token) as api:
57 res = api.UserSearch(search_pb2.UserSearchReq())
58 assert len(res.results) > 0
61def test_regression_search_in_area(db):
62 """
63 Makes sure search_in_area works.
65 At the equator/prime meridian intersection (0,0), one degree is roughly 111 km.
66 """
68 # outside
69 user1, token1 = generate_user(geom=create_coordinate(1, 0), geom_radius=100)
70 # outside
71 user2, token2 = generate_user(geom=create_coordinate(0, 1), geom_radius=100)
72 # inside
73 user3, token3 = generate_user(geom=create_coordinate(0.1, 0), geom_radius=100)
74 # inside
75 user4, token4 = generate_user(geom=create_coordinate(0, 0.1), geom_radius=100)
76 # outside
77 user5, token5 = generate_user(geom=create_coordinate(10, 10), geom_radius=100)
79 with search_session(token5) as api:
80 res = api.UserSearch(
81 search_pb2.UserSearchReq(
82 search_in_area=search_pb2.Area(
83 lat=0,
84 lng=0,
85 radius=100000,
86 )
87 )
88 )
89 assert [result.user.user_id for result in res.results] == [user3.id, user4.id]
92def test_user_search_in_rectangle(db):
93 """
94 Makes sure search_in_rectangle works as expected.
95 """
97 # outside
98 user1, token1 = generate_user(geom=create_coordinate(-1, 0), geom_radius=100)
99 # outside
100 user2, token2 = generate_user(geom=create_coordinate(0, -1), geom_radius=100)
101 # inside
102 user3, token3 = generate_user(geom=create_coordinate(0.1, 0.1), geom_radius=100)
103 # inside
104 user4, token4 = generate_user(geom=create_coordinate(1.2, 0.1), geom_radius=100)
105 # outside (not fully inside)
106 user5, token5 = generate_user(geom=create_coordinate(0, 0), geom_radius=100)
107 # outside
108 user6, token6 = generate_user(geom=create_coordinate(0.1, 1.2), geom_radius=100)
109 # outside
110 user7, token7 = generate_user(geom=create_coordinate(10, 10), geom_radius=100)
112 with search_session(token5) as api:
113 res = api.UserSearch(
114 search_pb2.UserSearchReq(
115 search_in_rectangle=search_pb2.RectArea(
116 lat_min=0,
117 lat_max=2,
118 lng_min=0,
119 lng_max=1,
120 )
121 )
122 )
123 assert [result.user.user_id for result in res.results] == [user3.id, user4.id]
126def test_user_filter_complete_profile(db):
127 """
128 Make sure the completed profile flag returns only completed user profile
129 """
130 user_complete_profile, token6 = generate_user(complete_profile=True)
132 user_incomplete_profile, token7 = generate_user(complete_profile=False)
134 with search_session(token7) as api:
135 res = api.UserSearch(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=False)))
136 assert user_incomplete_profile.id in [result.user.user_id for result in res.results]
138 with search_session(token6) as api:
139 res = api.UserSearch(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=True)))
140 assert [result.user.user_id for result in res.results] == [user_complete_profile.id]
143def test_user_filter_meetup_status(db):
144 """
145 Make sure the completed profile flag returns only completed user profile
146 """
147 user_wants_to_meetup, token8 = generate_user(meetup_status=MeetupStatus.wants_to_meetup)
149 user_does_not_want_to_meet, token9 = generate_user(meetup_status=MeetupStatus.does_not_want_to_meetup)
151 with search_session(token8) as api:
152 res = api.UserSearch(search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP]))
153 assert user_wants_to_meetup.id in [result.user.user_id for result in res.results]
155 with search_session(token9) as api:
156 res = api.UserSearch(
157 search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP])
158 )
159 assert [result.user.user_id for result in res.results] == [user_does_not_want_to_meet.id]
162def test_user_filter_language(db):
163 """
164 Test filtering users by language ability.
165 """
166 user_with_german_beginner, token11 = generate_user(hosting_status=HostingStatus.can_host)
167 user_with_japanese_conversational, token12 = generate_user(hosting_status=HostingStatus.can_host)
168 user_with_german_fluent, token13 = generate_user(hosting_status=HostingStatus.can_host)
170 with session_scope() as session:
171 session.add(
172 LanguageAbility(
173 user_id=user_with_german_beginner.id, language_code="deu", fluency=LanguageFluency.beginner
174 ),
175 )
176 session.add(
177 LanguageAbility(
178 user_id=user_with_japanese_conversational.id,
179 language_code="jpn",
180 fluency=LanguageFluency.fluent,
181 )
182 )
183 session.add(
184 LanguageAbility(user_id=user_with_german_fluent.id, language_code="deu", fluency=LanguageFluency.fluent)
185 )
187 with search_session(token11) as api:
188 res = api.UserSearch(
189 search_pb2.UserSearchReq(
190 language_ability_filter=[
191 api_pb2.LanguageAbility(
192 code="deu",
193 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
194 )
195 ]
196 )
197 )
198 assert [result.user.user_id for result in res.results] == [user_with_german_fluent.id]
200 res = api.UserSearch(
201 search_pb2.UserSearchReq(
202 language_ability_filter=[
203 api_pb2.LanguageAbility(
204 code="jpn",
205 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
206 )
207 ]
208 )
209 )
210 assert [result.user.user_id for result in res.results] == [user_with_japanese_conversational.id]
213@pytest.fixture
214def sample_event_data() -> dict:
215 """Dummy data for creating events."""
216 start_time = now() + timedelta(hours=2)
217 end_time = start_time + timedelta(hours=3)
218 return {
219 "title": "Dummy Title",
220 "content": "Dummy content.",
221 "photo_key": None,
222 "offline_information": events_pb2.OfflineEventInformation(address="Near Null Island", lat=0.1, lng=0.2),
223 "start_time": Timestamp_from_datetime(start_time),
224 "end_time": Timestamp_from_datetime(end_time),
225 "timezone": "UTC",
226 }
229@pytest.fixture
230def create_event(sample_event_data):
231 """Factory for creating events."""
233 def _create_event(event_api, **kwargs) -> EventOccurrence:
234 """Create an event with default values, unless overridden by kwargs."""
235 return event_api.CreateEvent(events_pb2.CreateEventReq(**{**sample_event_data, **kwargs}))
237 return _create_event
240@pytest.fixture
241def sample_community(db) -> int:
242 """Create large community spanning from (-50, 0) to (50, 2) as events can only be created within communities."""
243 user, _ = generate_user()
244 with session_scope() as session:
245 return create_community(session, -50, 50, "Community", [user], [], None).id
248def test_EventSearch_no_filters(testing_communities):
249 """Test that EventSearch returns all events if no filter is set."""
250 user, token = generate_user()
251 with search_session(token) as api:
252 res = api.EventSearch(search_pb2.EventSearchReq())
253 assert len(res.events) > 0
256def test_event_search_by_query(sample_community, create_event):
257 """Test that EventSearch finds events by title (and content if query_title_only=False)."""
258 user, token = generate_user()
260 with events_session(token) as api:
261 event1 = create_event(api, title="Lorem Ipsum")
262 event2 = create_event(api, content="Lorem Ipsum")
263 create_event(api)
265 with search_session(token) as api:
266 res = api.EventSearch(search_pb2.EventSearchReq(query=wrappers_pb2.StringValue(value="Ipsum")))
267 assert len(res.events) == 2
268 assert {result.event_id for result in res.events} == {event1.event_id, event2.event_id}
270 res = api.EventSearch(
271 search_pb2.EventSearchReq(query=wrappers_pb2.StringValue(value="Ipsum"), query_title_only=True)
272 )
273 assert len(res.events) == 1
274 assert res.events[0].event_id == event1.event_id
277def test_event_search_by_time(sample_community, create_event):
278 """Test that EventSearch filters with the given time range."""
279 user, token = generate_user()
281 with events_session(token) as api:
282 event1 = create_event(
283 api,
284 start_time=Timestamp_from_datetime(now() + timedelta(hours=1)),
285 end_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
286 )
287 event2 = create_event(
288 api,
289 start_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
290 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
291 )
292 event3 = create_event(
293 api,
294 start_time=Timestamp_from_datetime(now() + timedelta(hours=7)),
295 end_time=Timestamp_from_datetime(now() + timedelta(hours=8)),
296 )
298 with search_session(token) as api:
299 res = api.EventSearch(search_pb2.EventSearchReq(before=Timestamp_from_datetime(now() + timedelta(hours=6))))
300 assert len(res.events) == 2
301 assert {result.event_id for result in res.events} == {event1.event_id, event2.event_id}
303 res = api.EventSearch(search_pb2.EventSearchReq(after=Timestamp_from_datetime(now() + timedelta(hours=3))))
304 assert len(res.events) == 2
305 assert {result.event_id for result in res.events} == {event2.event_id, event3.event_id}
307 res = api.EventSearch(
308 search_pb2.EventSearchReq(
309 before=Timestamp_from_datetime(now() + timedelta(hours=6)),
310 after=Timestamp_from_datetime(now() + timedelta(hours=3)),
311 )
312 )
313 assert len(res.events) == 1
314 assert res.events[0].event_id == event2.event_id
317def test_event_search_by_circle(sample_community, create_event):
318 """Test that EventSearch only returns events within the given circle."""
319 user, token = generate_user()
321 with events_session(token) as api:
322 inside_pts = [(0.1, 0.01), (0.01, 0.1)]
323 for i, (lat, lng) in enumerate(inside_pts):
324 create_event(
325 api,
326 title=f"Inside area {i}",
327 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Inside area {i}"),
328 )
330 outside_pts = [(1, 0.1), (0.1, 1), (10, 1)]
331 for i, (lat, lng) in enumerate(outside_pts):
332 create_event(
333 api,
334 title=f"Outside area {i}",
335 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Outside area {i}"),
336 )
338 with search_session(token) as api:
339 res = api.EventSearch(search_pb2.EventSearchReq(search_in_area=search_pb2.Area(lat=0, lng=0, radius=100000)))
340 assert len(res.events) == len(inside_pts)
341 assert all(event.title.startswith("Inside area") for event in res.events)
344def test_event_search_by_rectangle(sample_community, create_event):
345 """Test that EventSearch only returns events within the given rectangular area."""
346 user, token = generate_user()
348 with events_session(token) as api:
349 inside_pts = [(0.1, 0.2), (1.2, 0.2)]
350 for i, (lat, lng) in enumerate(inside_pts):
351 create_event(
352 api,
353 title=f"Inside area {i}",
354 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Inside area {i}"),
355 )
357 outside_pts = [(-1, 0.1), (0.1, 0.01), (-0.01, 0.01), (0.1, 1.2), (10, 1)]
358 for i, (lat, lng) in enumerate(outside_pts):
359 create_event(
360 api,
361 title=f"Outside area {i}",
362 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Outside area {i}"),
363 )
365 with search_session(token) as api:
366 res = api.EventSearch(
367 search_pb2.EventSearchReq(
368 search_in_rectangle=search_pb2.RectArea(lat_min=0, lat_max=2, lng_min=0.1, lng_max=1)
369 )
370 )
371 assert len(res.events) == len(inside_pts)
372 assert all(event.title.startswith("Inside area") for event in res.events)
375def test_event_search_pagination(sample_community, create_event):
376 """Test that EventSearch paginates correctly.
378 Check that
379 - <page_size> events are returned, if available
380 - sort order is applied (default: past=False)
381 - the next page token is correct
382 """
383 user, token = generate_user()
385 anchor_time = now()
386 with events_session(token) as api:
387 for i in range(5):
388 create_event(
389 api,
390 title=f"Event {i + 1}",
391 start_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1)),
392 end_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1, minutes=30)),
393 )
395 with search_session(token) as api:
396 res = api.EventSearch(search_pb2.EventSearchReq(past=False, page_size=4))
397 assert len(res.events) == 4
398 assert [event.title for event in res.events] == ["Event 1", "Event 2", "Event 3", "Event 4"]
399 assert res.next_page_token == str(millis_from_dt(anchor_time + timedelta(hours=5, minutes=30)))
401 res = api.EventSearch(search_pb2.EventSearchReq(page_size=4, page_token=res.next_page_token))
402 assert len(res.events) == 1
403 assert res.events[0].title == "Event 5"
404 assert res.next_page_token == ""
406 res = api.EventSearch(
407 search_pb2.EventSearchReq(
408 past=True, page_size=2, page_token=str(millis_from_dt(anchor_time + timedelta(hours=4, minutes=30)))
409 )
410 )
411 assert len(res.events) == 2
412 assert [event.title for event in res.events] == ["Event 4", "Event 3"]
413 assert res.next_page_token == str(millis_from_dt(anchor_time + timedelta(hours=2, minutes=30)))
415 res = api.EventSearch(search_pb2.EventSearchReq(past=True, page_size=2, page_token=res.next_page_token))
416 assert len(res.events) == 2
417 assert [event.title for event in res.events] == ["Event 2", "Event 1"]
418 assert res.next_page_token == ""
421def test_event_search_pagination_with_page_number(sample_community, create_event):
422 """Test that EventSearch paginates correctly with page number.
424 Check that
425 - <page_size> events are returned, if available
426 - sort order is applied (default: past=False)
427 - <page_number> is respected
428 - <total_items> is correct
429 """
430 user, token = generate_user()
432 anchor_time = now()
433 with events_session(token) as api:
434 for i in range(5):
435 create_event(
436 api,
437 title=f"Event {i + 1}",
438 start_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1)),
439 end_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1, minutes=30)),
440 )
442 with search_session(token) as api:
443 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=1))
444 assert len(res.events) == 2
445 assert [event.title for event in res.events] == ["Event 1", "Event 2"]
446 assert res.total_items == 5
448 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=2))
449 assert len(res.events) == 2
450 assert [event.title for event in res.events] == ["Event 3", "Event 4"]
451 assert res.total_items == 5
453 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=3))
454 assert len(res.events) == 1
455 assert [event.title for event in res.events] == ["Event 5"]
456 assert res.total_items == 5
458 # Verify no more pages
459 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=4))
460 assert not res.events
461 assert res.total_items == 5
464def test_event_search_online_status(sample_community, create_event):
465 """Test that EventSearch respects only_online and only_offline filters and by default returns both."""
466 user, token = generate_user()
468 with events_session(token) as api:
469 create_event(api, title="Offline event")
471 create_event(
472 api,
473 title="Online event",
474 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
475 parent_community_id=sample_community,
476 offline_information=events_pb2.OfflineEventInformation(),
477 )
479 with search_session(token) as api:
480 res = api.EventSearch(search_pb2.EventSearchReq())
481 assert len(res.events) == 2
482 assert {event.title for event in res.events} == {"Offline event", "Online event"}
484 res = api.EventSearch(search_pb2.EventSearchReq(only_online=True))
485 assert {event.title for event in res.events} == {"Online event"}
487 res = api.EventSearch(search_pb2.EventSearchReq(only_offline=True))
488 assert {event.title for event in res.events} == {"Offline event"}
491def test_event_search_filter_subscription_attendance_organizing_my_communities(sample_community, create_event):
492 """Test that EventSearch respects subscribed, attending, organizing and my_communities filters and by default
493 returns all events.
494 """
495 _, token = generate_user()
496 other_user, other_token = generate_user()
498 with communities_session(token) as api:
499 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=sample_community))
501 with session_scope() as session:
502 create_community(session, 55, 60, "Other community", [other_user], [], None)
504 with events_session(other_token) as api:
505 e_subscribed = create_event(api, title="Subscribed event")
506 e_attending = create_event(api, title="Attending event")
507 create_event(api, title="Community event")
508 create_event(
509 api,
510 title="Other community event",
511 offline_information=events_pb2.OfflineEventInformation(lat=58, lng=1, address="Somewhere"),
512 )
514 with events_session(token) as api:
515 create_event(api, title="Organized event")
516 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e_subscribed.event_id, subscribe=True))
517 api.SetEventAttendance(
518 events_pb2.SetEventAttendanceReq(
519 event_id=e_attending.event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
520 )
521 )
523 with search_session(token) as api:
524 res = api.EventSearch(search_pb2.EventSearchReq())
525 assert {event.title for event in res.events} == {
526 "Subscribed event",
527 "Attending event",
528 "Community event",
529 "Other community event",
530 "Organized event",
531 }
533 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True))
534 assert {event.title for event in res.events} == {"Subscribed event", "Organized event"}
536 res = api.EventSearch(search_pb2.EventSearchReq(attending=True))
537 assert {event.title for event in res.events} == {"Attending event", "Organized event"}
539 res = api.EventSearch(search_pb2.EventSearchReq(organizing=True))
540 assert {event.title for event in res.events} == {"Organized event"}
542 res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True))
543 assert {event.title for event in res.events} == {
544 "Subscribed event",
545 "Attending event",
546 "Community event",
547 "Organized event",
548 }
550 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True, attending=True))
551 assert {event.title for event in res.events} == {"Subscribed event", "Attending event", "Organized event"}