Coverage for src/tests/test_search.py: 100%
263 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-03-11 15:27 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-03-11 15:27 +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 testconfig,
18)
19from tests.test_references import create_friend_reference
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
59 assert res.total_items == len(res.results)
62def test_regression_search_in_area(db):
63 """
64 Makes sure search_in_area works.
66 At the equator/prime meridian intersection (0,0), one degree is roughly 111 km.
67 """
69 # outside
70 user1, token1 = generate_user(geom=create_coordinate(1, 0), geom_radius=100)
71 # outside
72 user2, token2 = generate_user(geom=create_coordinate(0, 1), geom_radius=100)
73 # inside
74 user3, token3 = generate_user(geom=create_coordinate(0.1, 0), geom_radius=100)
75 # inside
76 user4, token4 = generate_user(geom=create_coordinate(0, 0.1), geom_radius=100)
77 # outside
78 user5, token5 = generate_user(geom=create_coordinate(10, 10), geom_radius=100)
80 with search_session(token5) as api:
81 res = api.UserSearch(
82 search_pb2.UserSearchReq(
83 search_in_area=search_pb2.Area(
84 lat=0,
85 lng=0,
86 radius=100000,
87 )
88 )
89 )
90 assert [result.user.user_id for result in res.results] == [user3.id, user4.id]
93def test_user_search_in_rectangle(db):
94 """
95 Makes sure search_in_rectangle works as expected.
96 """
98 # outside
99 user1, token1 = generate_user(geom=create_coordinate(-1, 0), geom_radius=100)
100 # outside
101 user2, token2 = generate_user(geom=create_coordinate(0, -1), geom_radius=100)
102 # inside
103 user3, token3 = generate_user(geom=create_coordinate(0.1, 0.1), geom_radius=100)
104 # inside
105 user4, token4 = generate_user(geom=create_coordinate(1.2, 0.1), geom_radius=100)
106 # outside (not fully inside)
107 user5, token5 = generate_user(geom=create_coordinate(0, 0), geom_radius=100)
108 # outside
109 user6, token6 = generate_user(geom=create_coordinate(0.1, 1.2), geom_radius=100)
110 # outside
111 user7, token7 = generate_user(geom=create_coordinate(10, 10), geom_radius=100)
113 with search_session(token5) as api:
114 res = api.UserSearch(
115 search_pb2.UserSearchReq(
116 search_in_rectangle=search_pb2.RectArea(
117 lat_min=0,
118 lat_max=2,
119 lng_min=0,
120 lng_max=1,
121 )
122 )
123 )
124 assert [result.user.user_id for result in res.results] == [user3.id, user4.id]
127def test_user_filter_complete_profile(db):
128 """
129 Make sure the completed profile flag returns only completed user profile
130 """
131 user_complete_profile, token6 = generate_user(complete_profile=True)
133 user_incomplete_profile, token7 = generate_user(complete_profile=False)
135 with search_session(token7) as api:
136 res = api.UserSearch(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=False)))
137 assert user_incomplete_profile.id in [result.user.user_id for result in res.results]
139 with search_session(token6) as api:
140 res = api.UserSearch(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=True)))
141 assert [result.user.user_id for result in res.results] == [user_complete_profile.id]
144def test_user_filter_meetup_status(db):
145 """
146 Make sure the completed profile flag returns only completed user profile
147 """
148 user_wants_to_meetup, token8 = generate_user(meetup_status=MeetupStatus.wants_to_meetup)
150 user_does_not_want_to_meet, token9 = generate_user(meetup_status=MeetupStatus.does_not_want_to_meetup)
152 with search_session(token8) as api:
153 res = api.UserSearch(search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP]))
154 assert user_wants_to_meetup.id in [result.user.user_id for result in res.results]
156 with search_session(token9) as api:
157 res = api.UserSearch(
158 search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP])
159 )
160 assert [result.user.user_id for result in res.results] == [user_does_not_want_to_meet.id]
163def test_user_filter_language(db):
164 """
165 Test filtering users by language ability.
166 """
167 user_with_german_beginner, token11 = generate_user(hosting_status=HostingStatus.can_host)
168 user_with_japanese_conversational, token12 = generate_user(hosting_status=HostingStatus.can_host)
169 user_with_german_fluent, token13 = generate_user(hosting_status=HostingStatus.can_host)
171 with session_scope() as session:
172 session.add(
173 LanguageAbility(
174 user_id=user_with_german_beginner.id, language_code="deu", fluency=LanguageFluency.beginner
175 ),
176 )
177 session.add(
178 LanguageAbility(
179 user_id=user_with_japanese_conversational.id,
180 language_code="jpn",
181 fluency=LanguageFluency.fluent,
182 )
183 )
184 session.add(
185 LanguageAbility(user_id=user_with_german_fluent.id, language_code="deu", fluency=LanguageFluency.fluent)
186 )
188 with search_session(token11) as api:
189 res = api.UserSearch(
190 search_pb2.UserSearchReq(
191 language_ability_filter=[
192 api_pb2.LanguageAbility(
193 code="deu",
194 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
195 )
196 ]
197 )
198 )
199 assert [result.user.user_id for result in res.results] == [user_with_german_fluent.id]
201 res = api.UserSearch(
202 search_pb2.UserSearchReq(
203 language_ability_filter=[
204 api_pb2.LanguageAbility(
205 code="jpn",
206 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
207 )
208 ]
209 )
210 )
211 assert [result.user.user_id for result in res.results] == [user_with_japanese_conversational.id]
214def test_user_filter_strong_verification(db):
215 user1, token1 = generate_user()
216 user2, _ = generate_user(strong_verification=True)
217 user3, _ = generate_user()
218 user4, _ = generate_user(strong_verification=True)
219 user5, _ = generate_user(strong_verification=True)
221 with search_session(token1) as api:
222 res = api.UserSearch(search_pb2.UserSearchReq(only_with_strong_verification=False))
223 assert [result.user.user_id for result in res.results] == [user1.id, user2.id, user3.id, user4.id, user5.id]
225 res = api.UserSearch(search_pb2.UserSearchReq(only_with_strong_verification=True))
226 assert [result.user.user_id for result in res.results] == [user2.id, user4.id, user5.id]
229def test_regression_search_only_with_references(db):
230 user1, token1 = generate_user()
231 user2, _ = generate_user()
232 user3, _ = generate_user()
233 user4, _ = generate_user(delete_user=True)
235 with session_scope() as session:
236 # user 2 has references
237 create_friend_reference(session, user1.id, user2.id, timedelta(days=1))
238 create_friend_reference(session, user3.id, user2.id, timedelta(days=1))
239 create_friend_reference(session, user4.id, user2.id, timedelta(days=1))
241 # user 3 only has reference from a deleted user
242 create_friend_reference(session, user4.id, user3.id, timedelta(days=1))
244 with search_session(token1) as api:
245 res = api.UserSearch(search_pb2.UserSearchReq(only_with_references=False))
246 assert [result.user.user_id for result in res.results] == [user1.id, user2.id, user3.id]
248 res = api.UserSearch(search_pb2.UserSearchReq(only_with_references=True))
249 assert [result.user.user_id for result in res.results] == [user2.id]
252@pytest.fixture
253def sample_event_data() -> dict:
254 """Dummy data for creating events."""
255 start_time = now() + timedelta(hours=2)
256 end_time = start_time + timedelta(hours=3)
257 return {
258 "title": "Dummy Title",
259 "content": "Dummy content.",
260 "photo_key": None,
261 "offline_information": events_pb2.OfflineEventInformation(address="Near Null Island", lat=0.1, lng=0.2),
262 "start_time": Timestamp_from_datetime(start_time),
263 "end_time": Timestamp_from_datetime(end_time),
264 "timezone": "UTC",
265 }
268@pytest.fixture
269def create_event(sample_event_data):
270 """Factory for creating events."""
272 def _create_event(event_api, **kwargs) -> EventOccurrence:
273 """Create an event with default values, unless overridden by kwargs."""
274 return event_api.CreateEvent(events_pb2.CreateEventReq(**{**sample_event_data, **kwargs}))
276 return _create_event
279@pytest.fixture
280def sample_community(db) -> int:
281 """Create large community spanning from (-50, 0) to (50, 2) as events can only be created within communities."""
282 user, _ = generate_user()
283 with session_scope() as session:
284 return create_community(session, -50, 50, "Community", [user], [], None).id
287def test_EventSearch_no_filters(testing_communities):
288 """Test that EventSearch returns all events if no filter is set."""
289 user, token = generate_user()
290 with search_session(token) as api:
291 res = api.EventSearch(search_pb2.EventSearchReq())
292 assert len(res.events) > 0
295def test_event_search_by_query(sample_community, create_event):
296 """Test that EventSearch finds events by title (and content if query_title_only=False)."""
297 user, token = generate_user()
299 with events_session(token) as api:
300 event1 = create_event(api, title="Lorem Ipsum")
301 event2 = create_event(api, content="Lorem Ipsum")
302 create_event(api)
304 with search_session(token) as api:
305 res = api.EventSearch(search_pb2.EventSearchReq(query=wrappers_pb2.StringValue(value="Ipsum")))
306 assert len(res.events) == 2
307 assert {result.event_id for result in res.events} == {event1.event_id, event2.event_id}
309 res = api.EventSearch(
310 search_pb2.EventSearchReq(query=wrappers_pb2.StringValue(value="Ipsum"), query_title_only=True)
311 )
312 assert len(res.events) == 1
313 assert res.events[0].event_id == event1.event_id
316def test_event_search_by_time(sample_community, create_event):
317 """Test that EventSearch filters with the given time range."""
318 user, token = generate_user()
320 with events_session(token) as api:
321 event1 = create_event(
322 api,
323 start_time=Timestamp_from_datetime(now() + timedelta(hours=1)),
324 end_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
325 )
326 event2 = create_event(
327 api,
328 start_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
329 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
330 )
331 event3 = create_event(
332 api,
333 start_time=Timestamp_from_datetime(now() + timedelta(hours=7)),
334 end_time=Timestamp_from_datetime(now() + timedelta(hours=8)),
335 )
337 with search_session(token) as api:
338 res = api.EventSearch(search_pb2.EventSearchReq(before=Timestamp_from_datetime(now() + timedelta(hours=6))))
339 assert len(res.events) == 2
340 assert {result.event_id for result in res.events} == {event1.event_id, event2.event_id}
342 res = api.EventSearch(search_pb2.EventSearchReq(after=Timestamp_from_datetime(now() + timedelta(hours=3))))
343 assert len(res.events) == 2
344 assert {result.event_id for result in res.events} == {event2.event_id, event3.event_id}
346 res = api.EventSearch(
347 search_pb2.EventSearchReq(
348 before=Timestamp_from_datetime(now() + timedelta(hours=6)),
349 after=Timestamp_from_datetime(now() + timedelta(hours=3)),
350 )
351 )
352 assert len(res.events) == 1
353 assert res.events[0].event_id == event2.event_id
356def test_event_search_by_circle(sample_community, create_event):
357 """Test that EventSearch only returns events within the given circle."""
358 user, token = generate_user()
360 with events_session(token) as api:
361 inside_pts = [(0.1, 0.01), (0.01, 0.1)]
362 for i, (lat, lng) in enumerate(inside_pts):
363 create_event(
364 api,
365 title=f"Inside area {i}",
366 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Inside area {i}"),
367 )
369 outside_pts = [(1, 0.1), (0.1, 1), (10, 1)]
370 for i, (lat, lng) in enumerate(outside_pts):
371 create_event(
372 api,
373 title=f"Outside area {i}",
374 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Outside area {i}"),
375 )
377 with search_session(token) as api:
378 res = api.EventSearch(search_pb2.EventSearchReq(search_in_area=search_pb2.Area(lat=0, lng=0, radius=100000)))
379 assert len(res.events) == len(inside_pts)
380 assert all(event.title.startswith("Inside area") for event in res.events)
383def test_event_search_by_rectangle(sample_community, create_event):
384 """Test that EventSearch only returns events within the given rectangular area."""
385 user, token = generate_user()
387 with events_session(token) as api:
388 inside_pts = [(0.1, 0.2), (1.2, 0.2)]
389 for i, (lat, lng) in enumerate(inside_pts):
390 create_event(
391 api,
392 title=f"Inside area {i}",
393 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Inside area {i}"),
394 )
396 outside_pts = [(-1, 0.1), (0.1, 0.01), (-0.01, 0.01), (0.1, 1.2), (10, 1)]
397 for i, (lat, lng) in enumerate(outside_pts):
398 create_event(
399 api,
400 title=f"Outside area {i}",
401 offline_information=events_pb2.OfflineEventInformation(lat=lat, lng=lng, address=f"Outside area {i}"),
402 )
404 with search_session(token) as api:
405 res = api.EventSearch(
406 search_pb2.EventSearchReq(
407 search_in_rectangle=search_pb2.RectArea(lat_min=0, lat_max=2, lng_min=0.1, lng_max=1)
408 )
409 )
410 assert len(res.events) == len(inside_pts)
411 assert all(event.title.startswith("Inside area") for event in res.events)
414def test_event_search_pagination(sample_community, create_event):
415 """Test that EventSearch paginates correctly.
417 Check that
418 - <page_size> events are returned, if available
419 - sort order is applied (default: past=False)
420 - the next page token is correct
421 """
422 user, token = generate_user()
424 anchor_time = now()
425 with events_session(token) as api:
426 for i in range(5):
427 create_event(
428 api,
429 title=f"Event {i + 1}",
430 start_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1)),
431 end_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1, minutes=30)),
432 )
434 with search_session(token) as api:
435 res = api.EventSearch(search_pb2.EventSearchReq(past=False, page_size=4))
436 assert len(res.events) == 4
437 assert [event.title for event in res.events] == ["Event 1", "Event 2", "Event 3", "Event 4"]
438 assert res.next_page_token == str(millis_from_dt(anchor_time + timedelta(hours=5, minutes=30)))
440 res = api.EventSearch(search_pb2.EventSearchReq(page_size=4, page_token=res.next_page_token))
441 assert len(res.events) == 1
442 assert res.events[0].title == "Event 5"
443 assert res.next_page_token == ""
445 res = api.EventSearch(
446 search_pb2.EventSearchReq(
447 past=True, page_size=2, page_token=str(millis_from_dt(anchor_time + timedelta(hours=4, minutes=30)))
448 )
449 )
450 assert len(res.events) == 2
451 assert [event.title for event in res.events] == ["Event 4", "Event 3"]
452 assert res.next_page_token == str(millis_from_dt(anchor_time + timedelta(hours=2, minutes=30)))
454 res = api.EventSearch(search_pb2.EventSearchReq(past=True, page_size=2, page_token=res.next_page_token))
455 assert len(res.events) == 2
456 assert [event.title for event in res.events] == ["Event 2", "Event 1"]
457 assert res.next_page_token == ""
460def test_event_search_pagination_with_page_number(sample_community, create_event):
461 """Test that EventSearch paginates correctly with page number.
463 Check that
464 - <page_size> events are returned, if available
465 - sort order is applied (default: past=False)
466 - <page_number> is respected
467 - <total_items> is correct
468 """
469 user, token = generate_user()
471 anchor_time = now()
472 with events_session(token) as api:
473 for i in range(5):
474 create_event(
475 api,
476 title=f"Event {i + 1}",
477 start_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1)),
478 end_time=Timestamp_from_datetime(anchor_time + timedelta(hours=i + 1, minutes=30)),
479 )
481 with search_session(token) as api:
482 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=1))
483 assert len(res.events) == 2
484 assert [event.title for event in res.events] == ["Event 1", "Event 2"]
485 assert res.total_items == 5
487 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=2))
488 assert len(res.events) == 2
489 assert [event.title for event in res.events] == ["Event 3", "Event 4"]
490 assert res.total_items == 5
492 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=3))
493 assert len(res.events) == 1
494 assert [event.title for event in res.events] == ["Event 5"]
495 assert res.total_items == 5
497 # Verify no more pages
498 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=4))
499 assert not res.events
500 assert res.total_items == 5
503def test_event_search_online_status(sample_community, create_event):
504 """Test that EventSearch respects only_online and only_offline filters and by default returns both."""
505 user, token = generate_user()
507 with events_session(token) as api:
508 create_event(api, title="Offline event")
510 create_event(
511 api,
512 title="Online event",
513 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
514 parent_community_id=sample_community,
515 offline_information=events_pb2.OfflineEventInformation(),
516 )
518 with search_session(token) as api:
519 res = api.EventSearch(search_pb2.EventSearchReq())
520 assert len(res.events) == 2
521 assert {event.title for event in res.events} == {"Offline event", "Online event"}
523 res = api.EventSearch(search_pb2.EventSearchReq(only_online=True))
524 assert {event.title for event in res.events} == {"Online event"}
526 res = api.EventSearch(search_pb2.EventSearchReq(only_offline=True))
527 assert {event.title for event in res.events} == {"Offline event"}
530def test_event_search_filter_subscription_attendance_organizing_my_communities(sample_community, create_event):
531 """Test that EventSearch respects subscribed, attending, organizing and my_communities filters and by default
532 returns all events.
533 """
534 _, token = generate_user()
535 other_user, other_token = generate_user()
537 with communities_session(token) as api:
538 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=sample_community))
540 with session_scope() as session:
541 create_community(session, 55, 60, "Other community", [other_user], [], None)
543 with events_session(other_token) as api:
544 e_subscribed = create_event(api, title="Subscribed event")
545 e_attending = create_event(api, title="Attending event")
546 create_event(api, title="Community event")
547 create_event(
548 api,
549 title="Other community event",
550 offline_information=events_pb2.OfflineEventInformation(lat=58, lng=1, address="Somewhere"),
551 )
553 with events_session(token) as api:
554 create_event(api, title="Organized event")
555 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e_subscribed.event_id, subscribe=True))
556 api.SetEventAttendance(
557 events_pb2.SetEventAttendanceReq(
558 event_id=e_attending.event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
559 )
560 )
562 with search_session(token) as api:
563 res = api.EventSearch(search_pb2.EventSearchReq())
564 assert {event.title for event in res.events} == {
565 "Subscribed event",
566 "Attending event",
567 "Community event",
568 "Other community event",
569 "Organized event",
570 }
572 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True))
573 assert {event.title for event in res.events} == {"Subscribed event", "Organized event"}
575 res = api.EventSearch(search_pb2.EventSearchReq(attending=True))
576 assert {event.title for event in res.events} == {"Attending event", "Organized event"}
578 res = api.EventSearch(search_pb2.EventSearchReq(organizing=True))
579 assert {event.title for event in res.events} == {"Organized event"}
581 res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True))
582 assert {event.title for event in res.events} == {
583 "Subscribed event",
584 "Attending event",
585 "Community event",
586 "Organized event",
587 }
589 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True, attending=True))
590 assert {event.title for event in res.events} == {"Subscribed event", "Attending event", "Organized event"}