Coverage for app/backend/src/tests/test_search.py: 100%
452 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 22:54 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 22:54 +0000
1from datetime import timedelta
2from typing import Any
4import grpc
5import pytest
6from google.protobuf import empty_pb2, wrappers_pb2
7from sqlalchemy import select
9from couchers.db import session_scope
10from couchers.materialized_views import refresh_materialized_views, refresh_materialized_views_rapid
11from couchers.models import EventOccurrence, HostingStatus, LanguageAbility, LanguageFluency, MeetupStatus
12from couchers.proto import api_pb2, communities_pb2, events_pb2, search_pb2
13from couchers.utils import (
14 Timestamp_from_datetime,
15 create_coordinate,
16 datetime_to_iso8601_local,
17 millis_from_dt,
18 now,
19)
20from tests.fixtures.db import generate_user
21from tests.fixtures.misc import Moderator
22from tests.fixtures.sessions import communities_session, events_session, search_session
23from tests.test_communities import create_community, testing_communities # noqa
24from tests.test_references import create_friend_reference
27@pytest.fixture(autouse=True)
28def _(testconfig):
29 pass
32def test_Search(testing_communities):
33 user, token = generate_user()
34 with search_session(token) as api:
35 res = api.Search(
36 search_pb2.SearchReq(
37 query="Country 1, Region 1",
38 include_users=True,
39 include_communities=True,
40 include_groups=True,
41 include_places=True,
42 include_guides=True,
43 )
44 )
45 res = api.Search(
46 search_pb2.SearchReq(
47 query="Country 1, Region 1, Attraction",
48 title_only=True,
49 include_users=True,
50 include_communities=True,
51 include_groups=True,
52 include_places=True,
53 include_guides=True,
54 )
55 )
58def test_UserSearch(testing_communities):
59 """Test that UserSearch returns all users if no filter is set."""
60 user, token = generate_user()
62 refresh_materialized_views_rapid(empty_pb2.Empty())
63 refresh_materialized_views(empty_pb2.Empty())
65 with search_session(token) as api:
66 res = api.UserSearch(search_pb2.UserSearchReq())
67 assert len(res.results) > 0
68 assert res.total_items == len(res.results)
69 res = api.UserSearchV2(search_pb2.UserSearchReq())
70 assert len(res.results) > 0
71 assert res.total_items == len(res.results)
74def test_regression_search_in_area(db):
75 """
76 Makes sure search_in_area works.
78 At the equator/prime meridian intersection (0,0), one degree is roughly 111 km.
79 """
81 # outside
82 user1, token1 = generate_user(geom=create_coordinate(1, 0), geom_radius=100)
83 # outside
84 user2, token2 = generate_user(geom=create_coordinate(0, 1), geom_radius=100)
85 # inside
86 user3, token3 = generate_user(geom=create_coordinate(0.1, 0), geom_radius=100)
87 # inside
88 user4, token4 = generate_user(geom=create_coordinate(0, 0.1), geom_radius=100)
89 # outside
90 user5, token5 = generate_user(geom=create_coordinate(10, 10), geom_radius=100)
92 refresh_materialized_views_rapid(empty_pb2.Empty())
93 refresh_materialized_views(empty_pb2.Empty())
95 with search_session(token5) as api:
96 res = api.UserSearch(
97 search_pb2.UserSearchReq(
98 search_in_area=search_pb2.Area(
99 lat=0,
100 lng=0,
101 radius=100000,
102 )
103 )
104 )
105 assert [result.user.user_id for result in res.results] == [user3.id, user4.id]
107 res = api.UserSearchV2(
108 search_pb2.UserSearchReq(
109 search_in_area=search_pb2.Area(
110 lat=0,
111 lng=0,
112 radius=100000,
113 )
114 )
115 )
116 assert [result.user_id for result in res.results] == [user3.id, user4.id]
119def test_user_search_in_rectangle(db):
120 """
121 Makes sure search_in_rectangle works as expected.
122 """
124 # outside
125 user1, token1 = generate_user(geom=create_coordinate(-1, 0), geom_radius=100)
126 # outside
127 user2, token2 = generate_user(geom=create_coordinate(0, -1), geom_radius=100)
128 # inside
129 user3, token3 = generate_user(geom=create_coordinate(0.1, 0.1), geom_radius=100)
130 # inside
131 user4, token4 = generate_user(geom=create_coordinate(1.2, 0.1), geom_radius=100)
132 # outside (not fully inside)
133 user5, token5 = generate_user(geom=create_coordinate(0, 0), geom_radius=100)
134 # outside
135 user6, token6 = generate_user(geom=create_coordinate(0.1, 1.2), geom_radius=100)
136 # outside
137 user7, token7 = generate_user(geom=create_coordinate(10, 10), geom_radius=100)
139 refresh_materialized_views_rapid(empty_pb2.Empty())
140 refresh_materialized_views(empty_pb2.Empty())
142 with search_session(token5) as api:
143 res = api.UserSearch(
144 search_pb2.UserSearchReq(
145 search_in_rectangle=search_pb2.RectArea(
146 lat_min=0,
147 lat_max=2,
148 lng_min=0,
149 lng_max=1,
150 )
151 )
152 )
153 assert [result.user.user_id for result in res.results] == [user3.id, user4.id]
155 res = api.UserSearchV2(
156 search_pb2.UserSearchReq(
157 search_in_rectangle=search_pb2.RectArea(
158 lat_min=0,
159 lat_max=2,
160 lng_min=0,
161 lng_max=1,
162 )
163 )
164 )
165 assert [result.user_id for result in res.results] == [user3.id, user4.id]
168def test_user_filter_complete_profile(db):
169 """
170 Make sure the completed profile flag returns only completed user profile
171 """
172 user_complete_profile, token6 = generate_user(complete_profile=True)
174 user_incomplete_profile, token7 = generate_user(complete_profile=False)
176 refresh_materialized_views_rapid(empty_pb2.Empty())
177 refresh_materialized_views(empty_pb2.Empty())
179 with search_session(token7) as api:
180 res = api.UserSearch(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=False)))
181 assert user_incomplete_profile.id in [result.user.user_id for result in res.results]
183 res = api.UserSearchV2(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=False)))
184 assert user_incomplete_profile.id in [result.user_id for result in res.results]
186 with search_session(token6) as api:
187 res = api.UserSearch(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=True)))
188 assert [result.user.user_id for result in res.results] == [user_complete_profile.id]
190 res = api.UserSearchV2(search_pb2.UserSearchReq(profile_completed=wrappers_pb2.BoolValue(value=True)))
191 assert [result.user_id for result in res.results] == [user_complete_profile.id]
194def test_user_filter_meetup_status(db):
195 """
196 Make sure the completed profile flag returns only completed user profile
197 """
198 user_wants_to_meetup, token8 = generate_user(meetup_status=MeetupStatus.wants_to_meetup)
200 user_does_not_want_to_meet, token9 = generate_user(meetup_status=MeetupStatus.does_not_want_to_meetup)
202 refresh_materialized_views_rapid(empty_pb2.Empty())
203 refresh_materialized_views(empty_pb2.Empty())
205 with search_session(token8) as api:
206 res = api.UserSearch(search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP]))
207 assert user_wants_to_meetup.id in [result.user.user_id for result in res.results]
209 res = api.UserSearchV2(search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP]))
210 assert user_wants_to_meetup.id in [result.user_id for result in res.results]
212 with search_session(token9) as api:
213 res = api.UserSearch(
214 search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP])
215 )
216 assert [result.user.user_id for result in res.results] == [user_does_not_want_to_meet.id]
218 res = api.UserSearchV2(
219 search_pb2.UserSearchReq(meetup_status_filter=[api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP])
220 )
221 assert [result.user_id for result in res.results] == [user_does_not_want_to_meet.id]
224def test_user_filter_language(db):
225 """
226 Test filtering users by language ability.
227 """
228 user_with_german_beginner, token11 = generate_user(hosting_status=HostingStatus.can_host)
229 user_with_japanese_conversational, token12 = generate_user(hosting_status=HostingStatus.can_host)
230 user_with_german_fluent, token13 = generate_user(hosting_status=HostingStatus.can_host)
232 with session_scope() as session:
233 session.add(
234 LanguageAbility(
235 user_id=user_with_german_beginner.id, language_code="deu", fluency=LanguageFluency.beginner
236 ),
237 )
238 session.add(
239 LanguageAbility(
240 user_id=user_with_japanese_conversational.id,
241 language_code="jpn",
242 fluency=LanguageFluency.fluent,
243 )
244 )
245 session.add(
246 LanguageAbility(user_id=user_with_german_fluent.id, language_code="deu", fluency=LanguageFluency.fluent)
247 )
249 refresh_materialized_views_rapid(empty_pb2.Empty())
250 refresh_materialized_views(empty_pb2.Empty())
252 with search_session(token11) as api:
253 res = api.UserSearch(
254 search_pb2.UserSearchReq(
255 language_ability_filter=[
256 api_pb2.LanguageAbility(
257 code="deu",
258 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
259 )
260 ]
261 )
262 )
263 assert [result.user.user_id for result in res.results] == [user_with_german_fluent.id]
265 res = api.UserSearchV2(
266 search_pb2.UserSearchReq(
267 language_ability_filter=[
268 api_pb2.LanguageAbility(
269 code="deu",
270 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
271 )
272 ]
273 )
274 )
275 assert [result.user_id for result in res.results] == [user_with_german_fluent.id]
277 res = api.UserSearch(
278 search_pb2.UserSearchReq(
279 language_ability_filter=[
280 api_pb2.LanguageAbility(
281 code="jpn",
282 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
283 )
284 ]
285 )
286 )
287 assert [result.user.user_id for result in res.results] == [user_with_japanese_conversational.id]
289 res = api.UserSearchV2(
290 search_pb2.UserSearchReq(
291 language_ability_filter=[
292 api_pb2.LanguageAbility(
293 code="jpn",
294 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
295 )
296 ]
297 )
298 )
299 assert [result.user_id for result in res.results] == [user_with_japanese_conversational.id]
302def test_user_filter_strong_verification(db):
303 user1, token1 = generate_user()
304 user2, _ = generate_user(strong_verification=True)
305 user3, _ = generate_user()
306 user4, _ = generate_user(strong_verification=True)
307 user5, _ = generate_user(strong_verification=True)
309 refresh_materialized_views_rapid(empty_pb2.Empty())
310 refresh_materialized_views(empty_pb2.Empty())
312 with search_session(token1) as api:
313 res = api.UserSearch(search_pb2.UserSearchReq(only_with_strong_verification=False))
314 assert [result.user.user_id for result in res.results] == [user1.id, user2.id, user3.id, user4.id, user5.id]
316 res = api.UserSearchV2(search_pb2.UserSearchReq(only_with_strong_verification=False))
317 assert [result.user_id for result in res.results] == [user1.id, user2.id, user3.id, user4.id, user5.id]
319 res = api.UserSearch(search_pb2.UserSearchReq(only_with_strong_verification=True))
320 assert [result.user.user_id for result in res.results] == [user2.id, user4.id, user5.id]
322 res = api.UserSearchV2(search_pb2.UserSearchReq(only_with_strong_verification=True))
323 assert [result.user_id for result in res.results] == [user2.id, user4.id, user5.id]
326def test_regression_search_only_with_references(db):
327 user1, token1 = generate_user()
328 user2, _ = generate_user()
329 user3, _ = generate_user()
330 user4, _ = generate_user(delete_user=True)
332 refresh_materialized_views_rapid(empty_pb2.Empty())
333 refresh_materialized_views(empty_pb2.Empty())
335 with session_scope() as session:
336 # user 2 has references
337 create_friend_reference(session, user1.id, user2.id, timedelta(days=1))
338 create_friend_reference(session, user3.id, user2.id, timedelta(days=1))
339 create_friend_reference(session, user4.id, user2.id, timedelta(days=1))
341 # user 3 only has reference from a deleted user
342 create_friend_reference(session, user4.id, user3.id, timedelta(days=1))
344 with search_session(token1) as api:
345 res = api.UserSearch(search_pb2.UserSearchReq(only_with_references=False))
346 assert [result.user.user_id for result in res.results] == [user1.id, user2.id, user3.id]
348 res = api.UserSearchV2(search_pb2.UserSearchReq(only_with_references=False))
349 assert [result.user_id for result in res.results] == [user1.id, user2.id, user3.id]
351 res = api.UserSearch(search_pb2.UserSearchReq(only_with_references=True))
352 assert [result.user.user_id for result in res.results] == [user2.id]
354 res = api.UserSearchV2(search_pb2.UserSearchReq(only_with_references=True))
355 assert [result.user_id for result in res.results] == [user2.id]
358def test_user_search_exactly_user_ids(db):
359 """
360 Test that UserSearch with exactly_user_ids returns only those users and ignores other filters.
361 """
362 # Create users with different properties
363 user1, token1 = generate_user()
364 user2, _ = generate_user(strong_verification=True)
365 user3, _ = generate_user(complete_profile=True)
366 user4, _ = generate_user(meetup_status=MeetupStatus.wants_to_meetup)
367 user5, _ = generate_user(delete_user=True) # Deleted user
369 refresh_materialized_views_rapid(empty_pb2.Empty())
370 refresh_materialized_views(empty_pb2.Empty())
372 with search_session(token1) as api:
373 # Test that exactly_user_ids returns only the specified users
374 res = api.UserSearch(search_pb2.UserSearchReq(exactly_user_ids=[user2.id, user3.id, user4.id]))
375 assert sorted([result.user.user_id for result in res.results]) == sorted([user2.id, user3.id, user4.id])
377 res = api.UserSearchV2(search_pb2.UserSearchReq(exactly_user_ids=[user2.id, user3.id, user4.id]))
378 assert sorted([result.user_id for result in res.results]) == sorted([user2.id, user3.id, user4.id])
380 # Test that exactly_user_ids ignores other filters
381 res = api.UserSearch(
382 search_pb2.UserSearchReq(
383 exactly_user_ids=[user2.id, user3.id, user4.id],
384 only_with_strong_verification=True, # This would normally filter out user3 and user4
385 )
386 )
387 assert sorted([result.user.user_id for result in res.results]) == sorted([user2.id, user3.id, user4.id])
389 res = api.UserSearchV2(
390 search_pb2.UserSearchReq(
391 exactly_user_ids=[user2.id, user3.id, user4.id],
392 only_with_strong_verification=True, # This would normally filter out user3 and user4
393 )
394 )
395 assert sorted([result.user_id for result in res.results]) == sorted([user2.id, user3.id, user4.id])
397 # Test with non-existent user IDs (should be ignored)
398 res = api.UserSearch(search_pb2.UserSearchReq(exactly_user_ids=[user1.id, 99999]))
399 assert [result.user.user_id for result in res.results] == [user1.id]
401 res = api.UserSearchV2(search_pb2.UserSearchReq(exactly_user_ids=[user1.id, 99999]))
402 assert [result.user_id for result in res.results] == [user1.id]
404 # Test with deleted user ID (should be ignored due to visibility filter)
405 res = api.UserSearch(search_pb2.UserSearchReq(exactly_user_ids=[user1.id, user5.id]))
406 assert [result.user.user_id for result in res.results] == [user1.id]
408 res = api.UserSearchV2(search_pb2.UserSearchReq(exactly_user_ids=[user1.id, user5.id]))
409 assert [result.user_id for result in res.results] == [user1.id]
412@pytest.fixture
413def sample_event_data() -> dict[str, Any]:
414 """Dummy data for creating events."""
415 start_time = now() + timedelta(hours=2)
416 end_time = start_time + timedelta(hours=3)
417 return {
418 "title": "Dummy Title",
419 "content": "Dummy content.",
420 "photo_key": None,
421 "location": events_pb2.EventLocation(address="Near Null Island", lat=0.1, lng=0.2),
422 "start_datetime_iso8601_local": datetime_to_iso8601_local(start_time),
423 "end_datetime_iso8601_local": datetime_to_iso8601_local(end_time),
424 }
427@pytest.fixture
428def create_event(sample_event_data):
429 """Factory for creating events."""
431 def _create_event(event_api, **kwargs) -> EventOccurrence:
432 """Create an event with default values, unless overridden by kwargs."""
433 return event_api.CreateEvent(events_pb2.CreateEventReq(**{**sample_event_data, **kwargs})) # type: ignore
435 return _create_event
438@pytest.fixture
439def sample_community(db) -> int:
440 """Create large community spanning from (-50, 0) to (50, 2) as events can only be created within communities."""
441 user, _ = generate_user()
442 with session_scope() as session:
443 return create_community(session, -50, 50, "Community", [user], [], None).id
446def test_EventSearch_no_filters(testing_communities):
447 """Test that EventSearch returns all events if no filter is set."""
448 user, token = generate_user()
449 with search_session(token) as api:
450 res = api.EventSearch(search_pb2.EventSearchReq())
451 assert len(res.events) > 0
454def test_event_search_by_query(sample_community, create_event):
455 """Test that EventSearch finds events by title (and content if query_title_only=False)."""
456 user, token = generate_user()
458 with events_session(token) as api:
459 event1 = create_event(api, title="Lorem Ipsum")
460 event2 = create_event(api, content="Lorem Ipsum")
461 create_event(api)
463 with search_session(token) as api:
464 res = api.EventSearch(search_pb2.EventSearchReq(query=wrappers_pb2.StringValue(value="Ipsum")))
465 assert len(res.events) == 2
466 assert {result.event_id for result in res.events} == {event1.event_id, event2.event_id}
468 res = api.EventSearch(
469 search_pb2.EventSearchReq(query=wrappers_pb2.StringValue(value="Ipsum"), query_title_only=True)
470 )
471 assert len(res.events) == 1
472 assert res.events[0].event_id == event1.event_id
475def test_event_search_by_time(sample_community, create_event):
476 """Test that EventSearch filters with the given time range."""
477 user, token = generate_user()
479 with events_session(token) as api:
480 event1 = create_event(
481 api,
482 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=1)),
483 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
484 )
485 event2 = create_event(
486 api,
487 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
488 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
489 )
490 event3 = create_event(
491 api,
492 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=7)),
493 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=8)),
494 )
496 with search_session(token) as api:
497 res = api.EventSearch(search_pb2.EventSearchReq(before=Timestamp_from_datetime(now() + timedelta(hours=6))))
498 assert len(res.events) == 2
499 assert {result.event_id for result in res.events} == {event1.event_id, event2.event_id}
501 res = api.EventSearch(search_pb2.EventSearchReq(after=Timestamp_from_datetime(now() + timedelta(hours=3))))
502 assert len(res.events) == 2
503 assert {result.event_id for result in res.events} == {event2.event_id, event3.event_id}
505 res = api.EventSearch(
506 search_pb2.EventSearchReq(
507 before=Timestamp_from_datetime(now() + timedelta(hours=6)),
508 after=Timestamp_from_datetime(now() + timedelta(hours=3)),
509 )
510 )
511 assert len(res.events) == 1
512 assert res.events[0].event_id == event2.event_id
515def test_event_search_by_circle(sample_community, create_event):
516 """Test that EventSearch only returns events within the given circle."""
517 user, token = generate_user()
519 with events_session(token) as api:
520 inside_pts = [(0.1, 0.01), (0.01, 0.1)]
521 for i, (lat, lng) in enumerate(inside_pts):
522 create_event(
523 api,
524 title=f"Inside area {i}",
525 location=events_pb2.EventLocation(lat=lat, lng=lng, address=f"Inside area {i}"),
526 )
528 outside_pts = [(1, 0.1), (0.1, 1), (10, 1)]
529 for i, (lat, lng) in enumerate(outside_pts):
530 create_event(
531 api,
532 title=f"Outside area {i}",
533 location=events_pb2.EventLocation(lat=lat, lng=lng, address=f"Outside area {i}"),
534 )
536 with search_session(token) as api:
537 res = api.EventSearch(search_pb2.EventSearchReq(search_in_area=search_pb2.Area(lat=0, lng=0, radius=100000)))
538 assert len(res.events) == len(inside_pts)
539 assert all(event.title.startswith("Inside area") for event in res.events)
542def test_event_search_by_rectangle(sample_community, create_event):
543 """Test that EventSearch only returns events within the given rectangular area."""
544 user, token = generate_user()
546 with events_session(token) as api:
547 inside_pts = [(0.1, 0.2), (1.2, 0.2)]
548 for i, (lat, lng) in enumerate(inside_pts):
549 create_event(
550 api,
551 title=f"Inside area {i}",
552 location=events_pb2.EventLocation(lat=lat, lng=lng, address=f"Inside area {i}"),
553 )
555 outside_pts = [(-1, 0.1), (0.1, 0.01), (-0.01, 0.01), (0.1, 1.2), (10, 1)]
556 for i, (lat, lng) in enumerate(outside_pts):
557 create_event(
558 api,
559 title=f"Outside area {i}",
560 location=events_pb2.EventLocation(lat=lat, lng=lng, address=f"Outside area {i}"),
561 )
563 with search_session(token) as api:
564 res = api.EventSearch(
565 search_pb2.EventSearchReq(
566 search_in_rectangle=search_pb2.RectArea(lat_min=0, lat_max=2, lng_min=0.1, lng_max=1)
567 )
568 )
569 assert len(res.events) == len(inside_pts)
570 assert all(event.title.startswith("Inside area") for event in res.events)
573def test_event_search_pagination(sample_community, create_event):
574 """Test that EventSearch paginates correctly.
576 Check that
577 - <page_size> events are returned, if available
578 - sort order is applied (default: past=False)
579 - the next page token is correct
580 """
581 user, token = generate_user()
583 anchor_time = now().replace(second=0, microsecond=0) # Events are created at minute granularity
584 with events_session(token) as api:
585 for i in range(5):
586 create_event(
587 api,
588 title=f"Event {i + 1}",
589 start_datetime_iso8601_local=datetime_to_iso8601_local(anchor_time + timedelta(hours=i + 1)),
590 end_datetime_iso8601_local=datetime_to_iso8601_local(anchor_time + timedelta(hours=i + 1, minutes=30)),
591 )
593 with search_session(token) as api:
594 res = api.EventSearch(search_pb2.EventSearchReq(past=False, page_size=4))
595 assert len(res.events) == 4
596 assert [event.title for event in res.events] == ["Event 1", "Event 2", "Event 3", "Event 4"]
597 assert res.next_page_token == str(millis_from_dt(anchor_time + timedelta(hours=5, minutes=30)))
599 res = api.EventSearch(search_pb2.EventSearchReq(page_size=4, page_token=res.next_page_token))
600 assert len(res.events) == 1
601 assert res.events[0].title == "Event 5"
602 assert res.next_page_token == ""
604 res = api.EventSearch(
605 search_pb2.EventSearchReq(
606 past=True, page_size=2, page_token=str(millis_from_dt(anchor_time + timedelta(hours=4, minutes=30)))
607 )
608 )
609 assert len(res.events) == 2
610 assert [event.title for event in res.events] == ["Event 4", "Event 3"]
611 assert res.next_page_token == str(millis_from_dt(anchor_time + timedelta(hours=2, minutes=30)))
613 res = api.EventSearch(search_pb2.EventSearchReq(past=True, page_size=2, page_token=res.next_page_token))
614 assert len(res.events) == 2
615 assert [event.title for event in res.events] == ["Event 2", "Event 1"]
616 assert res.next_page_token == ""
619def test_event_search_pagination_with_page_number(sample_community, create_event):
620 """Test that EventSearch paginates correctly with page number.
622 Check that
623 - <page_size> events are returned, if available
624 - sort order is applied (default: past=False)
625 - <page_number> is respected
626 - <total_items> is correct
627 """
628 user, token = generate_user()
630 anchor_time = now()
631 with events_session(token) as api:
632 for i in range(5):
633 create_event(
634 api,
635 title=f"Event {i + 1}",
636 start_datetime_iso8601_local=datetime_to_iso8601_local(anchor_time + timedelta(hours=i + 1)),
637 end_datetime_iso8601_local=datetime_to_iso8601_local(anchor_time + timedelta(hours=i + 1, minutes=30)),
638 )
640 with search_session(token) as api:
641 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=1))
642 assert len(res.events) == 2
643 assert [event.title for event in res.events] == ["Event 1", "Event 2"]
644 assert res.total_items == 5
646 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=2))
647 assert len(res.events) == 2
648 assert [event.title for event in res.events] == ["Event 3", "Event 4"]
649 assert res.total_items == 5
651 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=3))
652 assert len(res.events) == 1
653 assert [event.title for event in res.events] == ["Event 5"]
654 assert res.total_items == 5
656 # Verify no more pages
657 res = api.EventSearch(search_pb2.EventSearchReq(page_size=2, page_number=4))
658 assert not res.events
659 assert res.total_items == 5
662def test_event_search_filter_subscription_attendance_organizing_my_communities(
663 sample_community, create_event, moderator: Moderator
664):
665 """Test that EventSearch respects subscribed, attending, organizing and my_communities filters and by default
666 returns all events.
667 """
668 _, token = generate_user()
669 other_user, other_token = generate_user()
671 with communities_session(token) as api:
672 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=sample_community))
674 with session_scope() as session:
675 create_community(session, 55, 60, "Other community", [other_user], [], None)
677 with events_session(other_token) as api:
678 e_subscribed = create_event(api, title="Subscribed event")
679 e_attending = create_event(api, title="Attending event")
680 create_event(api, title="Community event")
681 create_event(
682 api,
683 title="Other community event",
684 location=events_pb2.EventLocation(lat=58, lng=1, address="Somewhere"),
685 )
687 # Approve all events so they're visible to other users
688 with session_scope() as session:
689 occurrence_ids = session.execute(select(EventOccurrence.id)).scalars().all()
690 for oid in occurrence_ids:
691 moderator.approve_event_occurrence(oid)
693 with events_session(token) as api:
694 create_event(api, title="Organized event")
695 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e_subscribed.event_id, subscribe=True))
696 api.SetEventAttendance(
697 events_pb2.SetEventAttendanceReq(
698 event_id=e_attending.event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
699 )
700 )
702 with search_session(token) as api:
703 res = api.EventSearch(search_pb2.EventSearchReq())
704 assert {event.title for event in res.events} == {
705 "Subscribed event",
706 "Attending event",
707 "Community event",
708 "Other community event",
709 "Organized event",
710 }
712 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True))
713 assert {event.title for event in res.events} == {"Subscribed event", "Organized event"}
715 res = api.EventSearch(search_pb2.EventSearchReq(attending=True))
716 assert {event.title for event in res.events} == {"Attending event", "Organized event"}
718 res = api.EventSearch(search_pb2.EventSearchReq(organizing=True))
719 assert {event.title for event in res.events} == {"Organized event"}
721 res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True))
722 assert {event.title for event in res.events} == {
723 "Subscribed event",
724 "Attending event",
725 "Community event",
726 "Organized event",
727 }
729 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True, attending=True))
730 assert {event.title for event in res.events} == {"Subscribed event", "Attending event", "Organized event"}
733def test_event_search_exclude_attending(sample_community, create_event, moderator: Moderator):
734 """Test that exclude_attending removes events the user is attending or organizing."""
735 user, token = generate_user()
736 other_user, other_token = generate_user()
738 with communities_session(token) as api:
739 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=sample_community))
741 with session_scope() as session:
742 create_community(session, 55, 60, "Other community", [other_user], [], None)
744 with events_session(other_token) as api:
745 e_attending = create_event(api, title="Attending event")
746 e_community_only = create_event(api, title="Community only event")
747 create_event(
748 api,
749 title="Other community event",
750 location=events_pb2.EventLocation(lat=58, lng=1, address="Somewhere"),
751 )
753 with session_scope() as session:
754 occurrence_ids = session.execute(select(EventOccurrence.id)).scalars().all()
755 for oid in occurrence_ids:
756 moderator.approve_event_occurrence(oid)
758 with events_session(token) as api:
759 e_organized = create_event(api, title="Organized event")
760 api.SetEventAttendance(
761 events_pb2.SetEventAttendanceReq(
762 event_id=e_attending.event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
763 )
764 )
766 with search_session(token) as api:
767 # baseline: my_communities returns all community events including attended/organized
768 res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True))
769 assert {event.title for event in res.events} == {
770 "Attending event",
771 "Community only event",
772 "Organized event",
773 }
775 # my_communities + exclude_attending: drops attended and organized events
776 res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True, exclude_attending=True))
777 assert {event.title for event in res.events} == {"Community only event"}
779 # exclude_attending alone (no other filter = all events): drops attended and organized
780 res = api.EventSearch(search_pb2.EventSearchReq(exclude_attending=True))
781 assert {event.title for event in res.events} == {"Community only event", "Other community event"}
783 # attending + exclude_attending is invalid
784 with pytest.raises(grpc.RpcError) as e:
785 api.EventSearch(search_pb2.EventSearchReq(attending=True, exclude_attending=True))
786 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
789def test_regression_search_multiple_pages(db):
790 """
791 There was a bug when there are multiple pages of results
792 """
793 user, token = generate_user()
794 user_ids = [user.id]
795 for _ in range(10):
796 other_user, _ = generate_user()
797 user_ids.append(other_user.id)
799 refresh_materialized_views_rapid(empty_pb2.Empty())
800 refresh_materialized_views(empty_pb2.Empty())
802 with search_session(token) as api:
803 res = api.UserSearchV2(search_pb2.UserSearchReq(page_size=5))
804 assert [result.user_id for result in res.results] == user_ids[:5]
805 assert res.next_page_token
808def test_regression_search_no_results(db):
809 """
810 There was a bug when there were no results
811 """
812 # put us far away
813 user, token = generate_user()
815 refresh_materialized_views_rapid(empty_pb2.Empty())
816 refresh_materialized_views(empty_pb2.Empty())
818 with search_session(token) as api:
819 res = api.UserSearchV2(search_pb2.UserSearchReq(only_with_references=True))
820 assert len(res.results) == 0
823def test_user_filter_same_gender_only(db):
824 """Test that same_gender_only filter works correctly"""
825 # Create users with different genders and strong verification status
826 woman_with_sv, token_woman_with_sv = generate_user(strong_verification=True, gender="Woman")
827 woman_without_sv, token_woman_without_sv = generate_user(strong_verification=False, gender="Woman")
828 man_with_sv, token_man_with_sv = generate_user(strong_verification=True, gender="Man")
829 man_without_sv, _ = generate_user(strong_verification=False, gender="Man")
830 other_woman_with_sv, _ = generate_user(strong_verification=True, gender="Woman")
832 refresh_materialized_views_rapid(empty_pb2.Empty())
833 refresh_materialized_views(empty_pb2.Empty())
835 # Test 1: Woman with strong verification should see only women when same_gender_only=True
836 with search_session(token_woman_with_sv) as api:
837 res = api.UserSearch(search_pb2.UserSearchReq(same_gender_only=True))
838 result_ids = [result.user.user_id for result in res.results]
839 assert woman_with_sv.id in result_ids
840 assert woman_without_sv.id in result_ids
841 assert other_woman_with_sv.id in result_ids
842 assert man_with_sv.id not in result_ids
843 assert man_without_sv.id not in result_ids
845 res = api.UserSearchV2(search_pb2.UserSearchReq(same_gender_only=True))
846 result_ids = [result.user_id for result in res.results]
847 assert woman_with_sv.id in result_ids
848 assert woman_without_sv.id in result_ids
849 assert other_woman_with_sv.id in result_ids
850 assert man_with_sv.id not in result_ids
851 assert man_without_sv.id not in result_ids
853 # Test 2: Man with strong verification should see only men when same_gender_only=True
854 with search_session(token_man_with_sv) as api:
855 res = api.UserSearch(search_pb2.UserSearchReq(same_gender_only=True))
856 result_ids = [result.user.user_id for result in res.results]
857 assert man_with_sv.id in result_ids
858 assert man_without_sv.id in result_ids
859 assert woman_with_sv.id not in result_ids
860 assert woman_without_sv.id not in result_ids
861 assert other_woman_with_sv.id not in result_ids
863 res = api.UserSearchV2(search_pb2.UserSearchReq(same_gender_only=True))
864 result_ids = [result.user_id for result in res.results]
865 assert man_with_sv.id in result_ids
866 assert man_without_sv.id in result_ids
867 assert woman_with_sv.id not in result_ids
868 assert woman_without_sv.id not in result_ids
869 assert other_woman_with_sv.id not in result_ids
871 # Test 3: Woman without strong verification should get an error
872 with search_session(token_woman_without_sv) as api:
873 with pytest.raises(Exception) as e:
874 api.UserSearch(search_pb2.UserSearchReq(same_gender_only=True))
875 assert "NEED_STRONG_VERIFICATION" in str(e.value) or "FAILED_PRECONDITION" in str(e.value)
877 with pytest.raises(Exception) as e:
878 api.UserSearchV2(search_pb2.UserSearchReq(same_gender_only=True))
879 assert "NEED_STRONG_VERIFICATION" in str(e.value) or "FAILED_PRECONDITION" in str(e.value)
881 # Test 4: When same_gender_only=False, should see all users
882 with search_session(token_woman_with_sv) as api:
883 res = api.UserSearch(search_pb2.UserSearchReq(same_gender_only=False))
884 result_ids = [result.user.user_id for result in res.results]
885 assert woman_with_sv.id in result_ids
886 assert woman_without_sv.id in result_ids
887 assert other_woman_with_sv.id in result_ids
888 assert man_with_sv.id in result_ids
889 assert man_without_sv.id in result_ids
891 res = api.UserSearchV2(search_pb2.UserSearchReq(same_gender_only=False))
892 result_ids = [result.user_id for result in res.results]
893 assert woman_with_sv.id in result_ids
894 assert woman_without_sv.id in result_ids
895 assert other_woman_with_sv.id in result_ids
896 assert man_with_sv.id in result_ids
897 assert man_without_sv.id in result_ids
900def test_user_filter_same_gender_only_with_other_filters(db):
901 """Test that same_gender_only filter works correctly combined with other filters"""
902 # Create users with different properties
903 woman_host, token_woman = generate_user(
904 strong_verification=True, gender="Woman", hosting_status=HostingStatus.can_host
905 )
906 woman_cant_host, _ = generate_user(strong_verification=True, gender="Woman", hosting_status=HostingStatus.cant_host)
907 man_host, _ = generate_user(strong_verification=True, gender="Man", hosting_status=HostingStatus.can_host)
909 refresh_materialized_views_rapid(empty_pb2.Empty())
910 refresh_materialized_views(empty_pb2.Empty())
912 # Test: Combine same_gender_only with hosting_status filter
913 with search_session(token_woman) as api:
914 res = api.UserSearch(
915 search_pb2.UserSearchReq(same_gender_only=True, hosting_status_filter=[api_pb2.HOSTING_STATUS_CAN_HOST])
916 )
917 result_ids = [result.user.user_id for result in res.results]
918 # Should only see woman who can host
919 assert woman_host.id in result_ids
920 assert woman_cant_host.id not in result_ids
921 assert man_host.id not in result_ids
923 res = api.UserSearchV2(
924 search_pb2.UserSearchReq(same_gender_only=True, hosting_status_filter=[api_pb2.HOSTING_STATUS_CAN_HOST])
925 )
926 result_ids = [result.user_id for result in res.results]
927 assert woman_host.id in result_ids
928 assert woman_cant_host.id not in result_ids
929 assert man_host.id not in result_ids