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

1from datetime import timedelta 

2 

3import pytest 

4from google.protobuf import wrappers_pb2 

5 

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 

20 

21 

22@pytest.fixture(autouse=True) 

23def _(testconfig): 

24 pass 

25 

26 

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 ) 

51 

52 

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) 

60 

61 

62def test_regression_search_in_area(db): 

63 """ 

64 Makes sure search_in_area works. 

65 

66 At the equator/prime meridian intersection (0,0), one degree is roughly 111 km. 

67 """ 

68 

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) 

79 

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] 

91 

92 

93def test_user_search_in_rectangle(db): 

94 """ 

95 Makes sure search_in_rectangle works as expected. 

96 """ 

97 

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) 

112 

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] 

125 

126 

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) 

132 

133 user_incomplete_profile, token7 = generate_user(complete_profile=False) 

134 

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] 

138 

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] 

142 

143 

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) 

149 

150 user_does_not_want_to_meet, token9 = generate_user(meetup_status=MeetupStatus.does_not_want_to_meetup) 

151 

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] 

155 

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] 

161 

162 

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) 

170 

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 ) 

187 

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] 

200 

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] 

212 

213 

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) 

220 

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] 

224 

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] 

227 

228 

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) 

234 

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

240 

241 # user 3 only has reference from a deleted user 

242 create_friend_reference(session, user4.id, user3.id, timedelta(days=1)) 

243 

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] 

247 

248 res = api.UserSearch(search_pb2.UserSearchReq(only_with_references=True)) 

249 assert [result.user.user_id for result in res.results] == [user2.id] 

250 

251 

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 } 

266 

267 

268@pytest.fixture 

269def create_event(sample_event_data): 

270 """Factory for creating events.""" 

271 

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

275 

276 return _create_event 

277 

278 

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 

285 

286 

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 

293 

294 

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

298 

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) 

303 

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} 

308 

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 

314 

315 

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

319 

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 ) 

336 

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} 

341 

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} 

345 

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 

354 

355 

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

359 

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 ) 

368 

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 ) 

376 

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) 

381 

382 

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

386 

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 ) 

395 

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 ) 

403 

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) 

412 

413 

414def test_event_search_pagination(sample_community, create_event): 

415 """Test that EventSearch paginates correctly. 

416 

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

423 

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 ) 

433 

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

439 

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 == "" 

444 

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

453 

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 == "" 

458 

459 

460def test_event_search_pagination_with_page_number(sample_community, create_event): 

461 """Test that EventSearch paginates correctly with page number. 

462 

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

470 

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 ) 

480 

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 

486 

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 

491 

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 

496 

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 

501 

502 

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

506 

507 with events_session(token) as api: 

508 create_event(api, title="Offline event") 

509 

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 ) 

517 

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"} 

522 

523 res = api.EventSearch(search_pb2.EventSearchReq(only_online=True)) 

524 assert {event.title for event in res.events} == {"Online event"} 

525 

526 res = api.EventSearch(search_pb2.EventSearchReq(only_offline=True)) 

527 assert {event.title for event in res.events} == {"Offline event"} 

528 

529 

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

536 

537 with communities_session(token) as api: 

538 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=sample_community)) 

539 

540 with session_scope() as session: 

541 create_community(session, 55, 60, "Other community", [other_user], [], None) 

542 

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 ) 

552 

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 ) 

561 

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 } 

571 

572 res = api.EventSearch(search_pb2.EventSearchReq(subscribed=True)) 

573 assert {event.title for event in res.events} == {"Subscribed event", "Organized event"} 

574 

575 res = api.EventSearch(search_pb2.EventSearchReq(attending=True)) 

576 assert {event.title for event in res.events} == {"Attending event", "Organized event"} 

577 

578 res = api.EventSearch(search_pb2.EventSearchReq(organizing=True)) 

579 assert {event.title for event in res.events} == {"Organized event"} 

580 

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 } 

588 

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"}