Coverage for app/backend/src/tests/test_public_trips.py: 99%
601 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 13:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 13:25 +0000
1from datetime import date, timedelta
2from unittest.mock import patch
4import grpc
5import pytest
6from sqlalchemy import select
8from couchers.constants import HOST_REQUEST_MIN_LENGTH_UTF16
9from couchers.db import session_scope
10from couchers.models import (
11 Cluster,
12 ClusterRole,
13 ClusterSubscription,
14 ModerationObjectType,
15 ModerationQueueItem,
16 ModerationState,
17 ModerationTrigger,
18 ModerationVisibility,
19 Node,
20 NodeType,
21 User,
22)
23from couchers.models.host_requests import HostRequest, HostRequestStatus
24from couchers.models.public_trips import PublicTrip, PublicTripStatus
25from couchers.proto import public_trips_pb2, requests_pb2
26from couchers.utils import create_polygon_lat_lng, now, to_multi, today
27from tests.fixtures.db import generate_user
28from tests.fixtures.misc import Moderator
29from tests.fixtures.sessions import public_trips_session, requests_session
32def _valid_request_text(text: str = "Offer to host") -> str:
33 utf16_length = len(text.encode("utf-16-le")) // 2
34 if utf16_length >= HOST_REQUEST_MIN_LENGTH_UTF16: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 return text
36 return text + "_" * (HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length)
39@pytest.fixture(autouse=True)
40def _(testconfig):
41 pass
44# 150+ utf-16 code units to satisfy PUBLIC_TRIP_DESCRIPTION_MIN_LENGTH_UTF16.
45VALID_DESCRIPTION = (
46 "Visiting the area for a week for a music festival. I love meeting new people "
47 "and would really appreciate local tips. Happy to help with tasks in exchange."
48)
51def _make_node(node_type: NodeType = NodeType.locality, small_community_features_enabled: bool = True) -> int:
52 # Polygon inside the fake Europe/Helsinki timezone area so node.timezone resolves.
53 with session_scope() as session:
54 node = Node(
55 geom=to_multi(create_polygon_lat_lng([[60, 24], [60, 26], [62, 26], [62, 24], [60, 24]])),
56 node_type=node_type,
57 )
58 session.add(node)
59 session.flush()
60 cluster = Cluster(
61 name="Test community",
62 description="Test",
63 parent_node_id=node.id,
64 is_official_cluster=True,
65 small_community_features_enabled=small_community_features_enabled,
66 )
67 session.add(cluster)
68 session.flush()
69 return node.id
72def _make_node_admin(user_id: int, node_id: int):
73 with session_scope() as session:
74 cluster_id = session.execute(
75 select(Cluster.id).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
76 ).scalar_one()
77 session.add(ClusterSubscription(cluster_id=cluster_id, user_id=user_id, role=ClusterRole.admin))
80def _create_trip_directly(
81 user_id: int,
82 node_id: int,
83 from_date,
84 to_date,
85 *,
86 description: str = "Looking for a host!",
87 status=None,
88 same_gender_only: bool = False,
89 visibility: ModerationVisibility = ModerationVisibility.visible,
90) -> int:
91 with session_scope() as session:
92 moderation_state = ModerationState(
93 object_type=ModerationObjectType.public_trip,
94 object_id=0, # placeholder, set after PublicTrip flush
95 visibility=visibility,
96 )
97 session.add(moderation_state)
98 session.flush()
99 trip = PublicTrip(
100 user_id=user_id,
101 node_id=node_id,
102 from_date=from_date,
103 to_date=to_date,
104 description=description,
105 status=status or PublicTripStatus.searching_for_host,
106 same_gender_only=same_gender_only,
107 moderation_state_id=moderation_state.id,
108 )
109 session.add(trip)
110 session.flush()
111 moderation_state.object_id = trip.id
112 return trip.id
115def test_create_public_trip(db):
116 user, token = generate_user()
117 node_id = _make_node()
119 from_date = today() + timedelta(days=5)
120 to_date = today() + timedelta(days=10)
122 with public_trips_session(token) as api:
123 res = api.CreatePublicTrip(
124 public_trips_pb2.CreatePublicTripReq(
125 community_id=node_id,
126 from_date=from_date.isoformat(),
127 to_date=to_date.isoformat(),
128 description=VALID_DESCRIPTION,
129 )
130 )
132 assert res.trip_id > 0
133 assert res.user.user_id == user.id
134 assert res.community_id == node_id
135 assert res.community_slug == "test-community"
136 assert res.community_name == "Test community"
137 assert res.from_date == from_date.isoformat()
138 assert res.to_date == to_date.isoformat()
139 assert res.description == VALID_DESCRIPTION
140 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST
142 with session_scope() as session:
143 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
144 assert trip.user_id == user.id
145 assert trip.node_id == node_id
146 assert trip.status == PublicTripStatus.searching_for_host
149def test_create_public_trip_incomplete_profile(db):
150 _, token = generate_user(complete_profile=False)
151 node_id = _make_node()
153 with public_trips_session(token) as api:
154 with pytest.raises(grpc.RpcError) as e:
155 api.CreatePublicTrip(
156 public_trips_pb2.CreatePublicTripReq(
157 community_id=node_id,
158 from_date=(today() + timedelta(days=5)).isoformat(),
159 to_date=(today() + timedelta(days=10)).isoformat(),
160 description="Visiting town!",
161 )
162 )
163 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
164 assert e.value.details() == "You have to complete your profile before you can create a public trip."
167def test_create_public_trip_community_not_found(db):
168 _, token = generate_user()
170 with public_trips_session(token) as api:
171 with pytest.raises(grpc.RpcError) as e:
172 api.CreatePublicTrip(
173 public_trips_pb2.CreatePublicTripReq(
174 community_id=999999,
175 from_date=(today() + timedelta(days=5)).isoformat(),
176 to_date=(today() + timedelta(days=10)).isoformat(),
177 description="Visiting town!",
178 )
179 )
180 assert e.value.code() == grpc.StatusCode.NOT_FOUND
181 assert e.value.details() == "Community not found."
184def test_create_public_trip_not_enabled(db):
185 _, token = generate_user()
186 node_id = _make_node(small_community_features_enabled=False)
188 with public_trips_session(token) as api:
189 with pytest.raises(grpc.RpcError) as e:
190 api.CreatePublicTrip(
191 public_trips_pb2.CreatePublicTripReq(
192 community_id=node_id,
193 from_date=(today() + timedelta(days=5)).isoformat(),
194 to_date=(today() + timedelta(days=10)).isoformat(),
195 description="Visiting town!",
196 )
197 )
198 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
199 assert e.value.details() == "Public trips are not enabled in this community."
202@pytest.mark.parametrize(
203 "node_type",
204 [NodeType.region, NodeType.subregion, NodeType.locality, NodeType.sublocality],
205)
206def test_create_public_trip_allows_region_and_narrower(db, node_type):
207 _, token = generate_user()
208 node_id = _make_node(node_type=node_type)
210 with public_trips_session(token) as api:
211 res = api.CreatePublicTrip(
212 public_trips_pb2.CreatePublicTripReq(
213 community_id=node_id,
214 from_date=(today() + timedelta(days=5)).isoformat(),
215 to_date=(today() + timedelta(days=10)).isoformat(),
216 description=VALID_DESCRIPTION,
217 )
218 )
219 assert res.trip_id > 0
222def test_create_public_trip_in_past_uses_node_timezone(db):
223 # Default user geom resolves to America/New_York; the node's geom is in Europe/Helsinki.
224 # Simulate a moment where Helsinki has already rolled into the next day (2026-01-16)
225 # while NYC is still on 2026-01-15. A from_date of 2026-01-15 is "today" in NYC but
226 # "yesterday" in Helsinki, and must be rejected because the check uses the node's tz.
227 _, token = generate_user()
228 node_id = _make_node()
230 fake_today_by_tz = {"America/New_York": date(2026, 1, 15), "Europe/Helsinki": date(2026, 1, 16)}
232 with patch(
233 "couchers.servicers.public_trips.today_in_timezone",
234 side_effect=lambda tz: fake_today_by_tz[tz],
235 ):
236 with public_trips_session(token) as api:
237 with pytest.raises(grpc.RpcError) as e:
238 api.CreatePublicTrip(
239 public_trips_pb2.CreatePublicTripReq(
240 community_id=node_id,
241 from_date="2026-01-15",
242 to_date="2026-01-20",
243 description=VALID_DESCRIPTION,
244 )
245 )
246 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
249def test_create_public_trip_date_errors(db):
250 _, token = generate_user()
251 node_id = _make_node()
253 with public_trips_session(token) as api:
254 # from_date in the past
255 with pytest.raises(grpc.RpcError) as e:
256 api.CreatePublicTrip(
257 public_trips_pb2.CreatePublicTripReq(
258 community_id=node_id,
259 from_date=(today() - timedelta(days=2)).isoformat(),
260 to_date=(today() + timedelta(days=1)).isoformat(),
261 description="Visiting town!",
262 )
263 )
264 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
266 # from_date after to_date
267 with pytest.raises(grpc.RpcError) as e:
268 api.CreatePublicTrip(
269 public_trips_pb2.CreatePublicTripReq(
270 community_id=node_id,
271 from_date=(today() + timedelta(days=10)).isoformat(),
272 to_date=(today() + timedelta(days=5)).isoformat(),
273 description="Visiting town!",
274 )
275 )
276 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
278 # empty description
279 with pytest.raises(grpc.RpcError) as e:
280 api.CreatePublicTrip(
281 public_trips_pb2.CreatePublicTripReq(
282 community_id=node_id,
283 from_date=(today() + timedelta(days=5)).isoformat(),
284 to_date=(today() + timedelta(days=10)).isoformat(),
285 description=" ",
286 )
287 )
288 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
290 # invalid date format
291 with pytest.raises(grpc.RpcError) as e:
292 api.CreatePublicTrip(
293 public_trips_pb2.CreatePublicTripReq(
294 community_id=node_id,
295 from_date="not-a-date",
296 to_date=(today() + timedelta(days=10)).isoformat(),
297 description="Visiting town!",
298 )
299 )
300 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
303def test_create_public_trip_overlap(db):
304 user, token = generate_user()
305 node_id = _make_node()
307 _create_trip_directly(
308 user.id,
309 node_id,
310 today() + timedelta(days=5),
311 today() + timedelta(days=10),
312 )
314 with public_trips_session(token) as api:
315 # overlapping dates should fail
316 with pytest.raises(grpc.RpcError) as e:
317 api.CreatePublicTrip(
318 public_trips_pb2.CreatePublicTripReq(
319 community_id=node_id,
320 from_date=(today() + timedelta(days=8)).isoformat(),
321 to_date=(today() + timedelta(days=12)).isoformat(),
322 description=VALID_DESCRIPTION,
323 )
324 )
325 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
327 # non-overlapping dates should succeed
328 res = api.CreatePublicTrip(
329 public_trips_pb2.CreatePublicTripReq(
330 community_id=node_id,
331 from_date=(today() + timedelta(days=20)).isoformat(),
332 to_date=(today() + timedelta(days=25)).isoformat(),
333 description=VALID_DESCRIPTION,
334 )
335 )
336 assert res.trip_id > 0
339def test_create_public_trip_closed_trip_allows_new_overlap(db):
340 user, token = generate_user()
341 node_id = _make_node()
343 _create_trip_directly(
344 user.id,
345 node_id,
346 today() + timedelta(days=5),
347 today() + timedelta(days=10),
348 status=PublicTripStatus.closed,
349 )
351 with public_trips_session(token) as api:
352 # closed trips shouldn't block new overlapping ones
353 res = api.CreatePublicTrip(
354 public_trips_pb2.CreatePublicTripReq(
355 community_id=node_id,
356 from_date=(today() + timedelta(days=7)).isoformat(),
357 to_date=(today() + timedelta(days=12)).isoformat(),
358 description=VALID_DESCRIPTION,
359 )
360 )
361 assert res.trip_id > 0
364def test_get_public_trip(db):
365 user, token = generate_user()
366 node_id = _make_node()
367 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
369 with public_trips_session(token) as api:
370 res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
371 assert res.trip_id == trip_id
372 assert res.user.user_id == user.id
373 assert res.community_slug == "test-community"
374 assert res.community_name == "Test community"
377def test_get_public_trip_not_found(db):
378 _, token = generate_user()
379 with public_trips_session(token) as api:
380 with pytest.raises(grpc.RpcError) as e:
381 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=999999))
382 assert e.value.code() == grpc.StatusCode.NOT_FOUND
385def test_list_public_trips(db):
386 traveler, _ = generate_user()
387 _, host_token = generate_user()
388 node_id = _make_node()
390 trip1 = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
391 trip2 = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=20), today() + timedelta(days=25))
393 with public_trips_session(host_token) as api:
394 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
395 returned_ids = {t.trip_id for t in res.public_trips}
396 assert returned_ids == {trip1, trip2}
397 assert all(t.community_slug == "test-community" for t in res.public_trips)
398 assert all(t.community_name == "Test community" for t in res.public_trips)
401def test_list_public_trips_filters_closed_and_past(db):
402 traveler, _ = generate_user()
403 _, host_token = generate_user()
404 node_id = _make_node()
406 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
407 # closed trip - should be hidden
408 _create_trip_directly(
409 traveler.id,
410 node_id,
411 today() + timedelta(days=15),
412 today() + timedelta(days=20),
413 status=PublicTripStatus.closed,
414 )
415 # past trip (to_date < today) - should be hidden
416 _create_trip_directly(traveler.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
418 with public_trips_session(host_token) as api:
419 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
420 assert [t.trip_id for t in res.public_trips] == [active]
423def test_list_public_trips_hides_invisible_user(db):
424 traveler, _ = generate_user()
425 _, host_token = generate_user()
426 node_id = _make_node()
428 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
430 # soft-delete the traveler
431 with session_scope() as session:
432 t = session.execute(select(User).where(User.id == traveler.id)).scalar_one()
433 t.deleted_at = now()
435 with public_trips_session(host_token) as api:
436 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
437 assert len(res.public_trips) == 0
440def test_list_public_trips_pagination(db):
441 traveler, _ = generate_user()
442 _, host_token = generate_user()
443 node_id = _make_node()
445 trip_ids = [
446 _create_trip_directly(
447 traveler.id, node_id, today() + timedelta(days=5 + i * 10), today() + timedelta(days=10 + i * 10)
448 )
449 for i in range(5)
450 ]
452 with public_trips_session(host_token) as api:
453 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2))
454 assert [t.trip_id for t in res.public_trips] == [trip_ids[4], trip_ids[3]]
455 assert res.next_page_token
457 res2 = api.ListPublicTrips(
458 public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2, page_token=res.next_page_token)
459 )
460 assert [t.trip_id for t in res2.public_trips] == [trip_ids[2], trip_ids[1]]
461 assert res2.next_page_token
463 res3 = api.ListPublicTrips(
464 public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2, page_token=res2.next_page_token)
465 )
466 assert [t.trip_id for t in res3.public_trips] == [trip_ids[0]]
467 assert not res3.next_page_token
470def test_list_public_trips_by_user_self_sees_all(db):
471 user, token = generate_user()
472 other, _ = generate_user()
473 node_id = _make_node()
475 mine_active = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
476 mine_closed = _create_trip_directly(
477 user.id,
478 node_id,
479 today() + timedelta(days=15),
480 today() + timedelta(days=20),
481 status=PublicTripStatus.closed,
482 )
483 mine_past = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
484 # other user's trip should not be returned
485 _create_trip_directly(other.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
487 with public_trips_session(token) as api:
488 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id))
489 assert {t.trip_id for t in res.public_trips} == {mine_active, mine_closed, mine_past}
492def test_list_public_trips_by_user_other_filters_inactive_and_past(db):
493 traveler, _ = generate_user()
494 _, viewer_token = generate_user()
495 node_id = _make_node()
497 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
498 # closed trip - hidden from others
499 _create_trip_directly(
500 traveler.id,
501 node_id,
502 today() + timedelta(days=15),
503 today() + timedelta(days=20),
504 status=PublicTripStatus.closed,
505 )
506 # past trip - hidden from others
507 _create_trip_directly(traveler.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
509 with public_trips_session(viewer_token) as api:
510 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
511 assert [t.trip_id for t in res.public_trips] == [active]
514def test_list_public_trips_by_user_invisible_user(db):
515 traveler, _ = generate_user()
516 _, viewer_token = generate_user()
517 node_id = _make_node()
519 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
521 # soft-delete the traveler
522 with session_scope() as session:
523 t = session.execute(select(User).where(User.id == traveler.id)).scalar_one()
524 t.deleted_at = now()
526 with public_trips_session(viewer_token) as api:
527 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
528 assert len(res.public_trips) == 0
531def test_update_public_trip_close(db):
532 user, token = generate_user()
533 node_id = _make_node()
534 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
536 with public_trips_session(token) as api:
537 res = api.UpdatePublicTrip(
538 public_trips_pb2.UpdatePublicTripReq(
539 trip_id=trip_id,
540 status=public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED,
541 )
542 )
543 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED
545 with session_scope() as session:
546 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
547 assert trip.status == PublicTripStatus.closed
550def test_update_public_trip_reopen(db):
551 user, token = generate_user()
552 node_id = _make_node()
553 trip_id = _create_trip_directly(
554 user.id,
555 node_id,
556 today() + timedelta(days=5),
557 today() + timedelta(days=10),
558 status=PublicTripStatus.closed,
559 )
561 with public_trips_session(token) as api:
562 res = api.UpdatePublicTrip(
563 public_trips_pb2.UpdatePublicTripReq(
564 trip_id=trip_id,
565 status=public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST,
566 )
567 )
568 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST
570 with session_scope() as session:
571 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
572 assert trip.status == PublicTripStatus.searching_for_host
575def test_update_public_trip_cant_reopen_past_trip(db):
576 user, token = generate_user()
577 node_id = _make_node()
578 trip_id = _create_trip_directly(
579 user.id,
580 node_id,
581 today() - timedelta(days=10),
582 today() - timedelta(days=1),
583 status=PublicTripStatus.closed,
584 )
586 with public_trips_session(token) as api:
587 with pytest.raises(grpc.RpcError) as e:
588 api.UpdatePublicTrip(
589 public_trips_pb2.UpdatePublicTripReq(
590 trip_id=trip_id,
591 status=public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST,
592 )
593 )
594 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
597def test_update_public_trip_close_past_trip_allowed(db):
598 # Closing a trip whose dates are in the past is allowed, even though content edits are not.
599 user, token = generate_user()
600 node_id = _make_node()
601 trip_id = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
603 with public_trips_session(token) as api:
604 res = api.UpdatePublicTrip(
605 public_trips_pb2.UpdatePublicTripReq(
606 trip_id=trip_id,
607 status=public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED,
608 )
609 )
610 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED
613def test_update_public_trip_description_only(db):
614 user, token = generate_user()
615 node_id = _make_node()
616 trip_id = _create_trip_directly(
617 user.id,
618 node_id,
619 today() + timedelta(days=5),
620 today() + timedelta(days=10),
621 description="Original description",
622 )
624 updated = VALID_DESCRIPTION + " Updated plans."
626 with public_trips_session(token) as api:
627 res = api.UpdatePublicTrip(
628 public_trips_pb2.UpdatePublicTripReq(
629 trip_id=trip_id,
630 description=updated,
631 )
632 )
633 assert res.trip_id == trip_id
634 assert res.description == updated
635 # dates should be unchanged
636 assert res.from_date == (today() + timedelta(days=5)).isoformat()
637 assert res.to_date == (today() + timedelta(days=10)).isoformat()
639 with session_scope() as session:
640 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
641 assert trip.description == updated
644def test_update_public_trip_dates(db):
645 user, token = generate_user()
646 node_id = _make_node()
647 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
649 new_from = today() + timedelta(days=7)
650 new_to = today() + timedelta(days=14)
652 with public_trips_session(token) as api:
653 res = api.UpdatePublicTrip(
654 public_trips_pb2.UpdatePublicTripReq(
655 trip_id=trip_id,
656 from_date=new_from.isoformat(),
657 to_date=new_to.isoformat(),
658 )
659 )
660 assert res.from_date == new_from.isoformat()
661 assert res.to_date == new_to.isoformat()
664def test_update_public_trip_not_owner(db):
665 user, _ = generate_user()
666 _, other_token = generate_user()
667 node_id = _make_node()
668 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
670 with public_trips_session(other_token) as api:
671 with pytest.raises(grpc.RpcError) as e:
672 api.UpdatePublicTrip(
673 public_trips_pb2.UpdatePublicTripReq(
674 trip_id=trip_id,
675 description="I don't own this!",
676 )
677 )
678 assert e.value.code() == grpc.StatusCode.NOT_FOUND
681def test_update_public_trip_in_past(db):
682 user, token = generate_user()
683 node_id = _make_node()
684 trip_id = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=2))
686 with public_trips_session(token) as api:
687 with pytest.raises(grpc.RpcError) as e:
688 api.UpdatePublicTrip(
689 public_trips_pb2.UpdatePublicTripReq(
690 trip_id=trip_id,
691 description="Too late!",
692 )
693 )
694 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
697def test_update_public_trip_date_validation(db):
698 user, token = generate_user()
699 node_id = _make_node()
700 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
702 with public_trips_session(token) as api:
703 # from_date after to_date (using the stored to_date of today+10)
704 with pytest.raises(grpc.RpcError) as e:
705 api.UpdatePublicTrip(
706 public_trips_pb2.UpdatePublicTripReq(
707 trip_id=trip_id,
708 from_date=(today() + timedelta(days=20)).isoformat(),
709 )
710 )
711 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
713 # Empty description
714 with pytest.raises(grpc.RpcError) as e:
715 api.UpdatePublicTrip(
716 public_trips_pb2.UpdatePublicTripReq(
717 trip_id=trip_id,
718 description=" ",
719 )
720 )
721 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
724def test_create_public_trip_description_too_short(db):
725 _, token = generate_user()
726 node_id = _make_node()
728 with public_trips_session(token) as api:
729 with pytest.raises(grpc.RpcError) as e:
730 api.CreatePublicTrip(
731 public_trips_pb2.CreatePublicTripReq(
732 community_id=node_id,
733 from_date=(today() + timedelta(days=5)).isoformat(),
734 to_date=(today() + timedelta(days=10)).isoformat(),
735 description="Too short.",
736 )
737 )
738 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
739 assert "150" in (e.value.details() or "")
742def test_update_public_trip_description_too_short(db):
743 user, token = generate_user()
744 node_id = _make_node()
745 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
747 with public_trips_session(token) as api:
748 with pytest.raises(grpc.RpcError) as e:
749 api.UpdatePublicTrip(
750 public_trips_pb2.UpdatePublicTripReq(
751 trip_id=trip_id,
752 description="Too short.",
753 )
754 )
755 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
756 assert "150" in (e.value.details() or "")
759def test_same_gender_only_create_and_retrieve(db):
760 user, token = generate_user(gender="Woman")
761 node_id = _make_node()
763 from_date = today() + timedelta(days=5)
764 to_date = today() + timedelta(days=10)
766 with public_trips_session(token) as api:
767 res = api.CreatePublicTrip(
768 public_trips_pb2.CreatePublicTripReq(
769 community_id=node_id,
770 from_date=from_date.isoformat(),
771 to_date=to_date.isoformat(),
772 description=VALID_DESCRIPTION,
773 same_gender_only=True,
774 )
775 )
776 assert res.same_gender_only is True
778 with session_scope() as session:
779 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
780 assert trip.same_gender_only is True
783def test_same_gender_only_visibility_list_and_get(db):
784 traveler, _ = generate_user(gender="Woman")
785 _, same_gender_token = generate_user(gender="Woman")
786 _, diff_gender_token = generate_user(gender="Man")
787 node_id = _make_node()
789 filtered_trip_id = _create_trip_directly(
790 traveler.id,
791 node_id,
792 today() + timedelta(days=5),
793 today() + timedelta(days=10),
794 same_gender_only=True,
795 )
796 open_trip_id = _create_trip_directly(
797 traveler.id,
798 node_id,
799 today() + timedelta(days=20),
800 today() + timedelta(days=25),
801 )
803 with public_trips_session(same_gender_token) as api:
804 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
805 assert {t.trip_id for t in res.public_trips} == {filtered_trip_id, open_trip_id}
807 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=filtered_trip_id))
808 assert get_res.trip_id == filtered_trip_id
809 assert get_res.same_gender_only is True
811 with public_trips_session(diff_gender_token) as api:
812 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
813 assert [t.trip_id for t in res.public_trips] == [open_trip_id]
815 with pytest.raises(grpc.RpcError) as e:
816 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=filtered_trip_id))
817 assert e.value.code() == grpc.StatusCode.NOT_FOUND
820def test_same_gender_only_moderator_bypass(db):
821 traveler, _ = generate_user(gender="Woman")
822 mod, mod_token = generate_user(gender="Man")
823 node_id = _make_node()
824 _make_node_admin(mod.id, node_id)
826 trip_id = _create_trip_directly(
827 traveler.id,
828 node_id,
829 today() + timedelta(days=5),
830 today() + timedelta(days=10),
831 same_gender_only=True,
832 )
834 with public_trips_session(mod_token) as api:
835 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
836 assert any(t.trip_id == trip_id for t in res.public_trips)
838 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
839 assert get_res.trip_id == trip_id
842def test_same_gender_only_owner_always_sees_own_trips(db):
843 traveler, traveler_token = generate_user(gender="Woman")
844 _, diff_gender_token = generate_user(gender="Man")
845 node_id = _make_node()
847 trip_id = _create_trip_directly(
848 traveler.id,
849 node_id,
850 today() + timedelta(days=5),
851 today() + timedelta(days=10),
852 same_gender_only=True,
853 )
855 # Owner always sees their own trips (is_self path skips the gender filter)
856 with public_trips_session(traveler_token) as api:
857 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
858 assert any(t.trip_id == trip_id for t in res.public_trips)
860 # Different-gender viewer doesn't see it on the traveler's profile
861 with public_trips_session(diff_gender_token) as api:
862 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
863 assert not any(t.trip_id == trip_id for t in res.public_trips)
866def test_same_gender_only_update(db):
867 user, token = generate_user(gender="Woman")
868 node_id = _make_node()
869 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
871 with public_trips_session(token) as api:
872 res = api.UpdatePublicTrip(
873 public_trips_pb2.UpdatePublicTripReq(
874 trip_id=trip_id,
875 same_gender_only=True,
876 )
877 )
878 assert res.same_gender_only is True
880 res = api.UpdatePublicTrip(
881 public_trips_pb2.UpdatePublicTripReq(
882 trip_id=trip_id,
883 same_gender_only=False,
884 )
885 )
886 assert res.same_gender_only is False
888 with session_scope() as session:
889 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
890 assert trip.same_gender_only is False
893def test_list_public_trips_by_user_ascending_order(db):
894 user, token = generate_user()
895 node_id = _make_node()
897 trip_near = _create_trip_directly(user.id, node_id, today() + timedelta(days=3), today() + timedelta(days=5))
898 trip_far = _create_trip_directly(user.id, node_id, today() + timedelta(days=20), today() + timedelta(days=25))
899 trip_mid = _create_trip_directly(user.id, node_id, today() + timedelta(days=10), today() + timedelta(days=12))
901 with public_trips_session(token) as api:
902 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id, ascending=True))
903 trip_ids = [t.trip_id for t in res.public_trips]
904 assert trip_ids.index(trip_near) < trip_ids.index(trip_mid) < trip_ids.index(trip_far)
906 res_desc = api.ListPublicTripsByUser(
907 public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id, ascending=False)
908 )
909 trip_ids_desc = [t.trip_id for t in res_desc.public_trips]
910 assert trip_ids_desc.index(trip_far) < trip_ids_desc.index(trip_mid) < trip_ids_desc.index(trip_near)
913def test_list_public_trips_by_user_status_filter(db):
914 user, token = generate_user()
915 node_id = _make_node()
917 active = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
918 closed = _create_trip_directly(
919 user.id, node_id, today() + timedelta(days=15), today() + timedelta(days=20), status=PublicTripStatus.closed
920 )
922 with public_trips_session(token) as api:
923 # Filter to active only
924 res = api.ListPublicTripsByUser(
925 public_trips_pb2.ListPublicTripsByUserReq(
926 user_id=user.id,
927 statuses_in=[public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST],
928 )
929 )
930 assert {t.trip_id for t in res.public_trips} == {active}
932 # Filter to closed only
933 res = api.ListPublicTripsByUser(
934 public_trips_pb2.ListPublicTripsByUserReq(
935 user_id=user.id,
936 statuses_in=[public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED],
937 )
938 )
939 assert {t.trip_id for t in res.public_trips} == {closed}
941 # No filter returns all
942 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id))
943 assert {t.trip_id for t in res.public_trips} == {active, closed}
946def test_list_public_trips_by_user_status_filter_ignored_for_others(db):
947 traveler, _ = generate_user()
948 _, viewer_token = generate_user()
949 node_id = _make_node()
951 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
952 _create_trip_directly(
953 traveler.id, node_id, today() + timedelta(days=15), today() + timedelta(days=20), status=PublicTripStatus.closed
954 )
956 with public_trips_session(viewer_token) as api:
957 # status_filter is ignored for other users — always returns active+upcoming only
958 res = api.ListPublicTripsByUser(
959 public_trips_pb2.ListPublicTripsByUserReq(
960 user_id=traveler.id,
961 statuses_in=[public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED],
962 )
963 )
964 assert [t.trip_id for t in res.public_trips] == [active]
967def test_list_public_trips_by_user_offers_count_owner(db, moderator: Moderator):
968 traveler, traveler_token = generate_user()
969 host, host_token = generate_user()
970 node_id = _make_node()
972 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
974 with public_trips_session(traveler_token) as api:
975 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
976 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
977 assert trip.HasField("offers_count")
978 assert trip.offers_count == 0
980 # Host creates an offer via a host request linked to the trip
981 with requests_session(host_token) as api:
982 host_request_id = api.CreateHostRequest(
983 requests_pb2.CreateHostRequestReq(
984 host_user_id=traveler.id,
985 from_date=(today() + timedelta(days=5)).isoformat(),
986 to_date=(today() + timedelta(days=10)).isoformat(),
987 text=_valid_request_text(),
988 public_trip_id=trip_id,
989 )
990 ).host_request_id
992 with public_trips_session(traveler_token) as api:
993 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
994 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
995 assert trip.HasField("offers_count")
996 assert trip.offers_count == 0
998 moderator.approve_host_request(host_request_id)
1000 with public_trips_session(traveler_token) as api:
1001 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1002 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1003 assert trip.HasField("offers_count")
1004 assert trip.offers_count == 1
1007def test_list_public_trips_by_user_offers_count_excludes_invisible_offers(db, moderator: Moderator):
1008 """The count only covers offers the owner can open."""
1009 traveler, traveler_token = generate_user()
1010 _hidden_host, hidden_host_token = generate_user()
1011 banned_host, banned_host_token = generate_user()
1012 node_id = _make_node()
1014 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1016 offer_ids = []
1017 for token in (hidden_host_token, banned_host_token):
1018 with requests_session(token) as api:
1019 offer_ids.append(
1020 api.CreateHostRequest(
1021 requests_pb2.CreateHostRequestReq(
1022 host_user_id=traveler.id,
1023 from_date=(today() + timedelta(days=5)).isoformat(),
1024 to_date=(today() + timedelta(days=10)).isoformat(),
1025 text=_valid_request_text(),
1026 public_trip_id=trip_id,
1027 )
1028 ).host_request_id
1029 )
1030 hidden_offer_id = offer_ids[0]
1032 for offer_id in offer_ids:
1033 moderator.approve_host_request(offer_id)
1035 with public_trips_session(traveler_token) as api:
1036 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1037 assert next(t for t in res.public_trips if t.trip_id == trip_id).offers_count == 2
1039 moderator.hide_host_request(hidden_offer_id)
1040 with session_scope() as session:
1041 session.execute(select(User).where(User.id == banned_host.id)).scalar_one().banned_at = now()
1043 with public_trips_session(traveler_token) as api:
1044 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1045 assert next(t for t in res.public_trips if t.trip_id == trip_id).offers_count == 0
1048def test_list_public_trips_by_user_offers_count_not_set_for_others(db):
1049 traveler, _ = generate_user()
1050 _, viewer_token = generate_user()
1051 node_id = _make_node()
1053 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1055 with public_trips_session(viewer_token) as api:
1056 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1057 assert len(res.public_trips) == 1
1058 assert not res.public_trips[0].HasField("offers_count")
1061def test_list_public_trips_by_user_offers_count_excludes_cancelled(db, moderator: Moderator):
1062 traveler, traveler_token = generate_user()
1063 hosts = [generate_user() for _ in range(5)]
1064 node_id = _make_node()
1066 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1068 for _host, host_token in hosts:
1069 with requests_session(host_token) as api:
1070 offer_id = api.CreateHostRequest(
1071 requests_pb2.CreateHostRequestReq(
1072 host_user_id=traveler.id,
1073 from_date=(today() + timedelta(days=5)).isoformat(),
1074 to_date=(today() + timedelta(days=10)).isoformat(),
1075 text=_valid_request_text(),
1076 public_trip_id=trip_id,
1077 )
1078 ).host_request_id
1079 moderator.approve_host_request(offer_id)
1081 # Set one offer per status, so we cover every status the count has to consider.
1082 with session_scope() as session:
1083 offers = (
1084 session.execute(
1085 select(HostRequest).where(HostRequest.public_trip_id == trip_id).order_by(HostRequest.conversation_id)
1086 )
1087 .scalars()
1088 .all()
1089 )
1090 offers[0].status = HostRequestStatus.pending
1091 offers[1].status = HostRequestStatus.accepted
1092 offers[2].status = HostRequestStatus.confirmed
1093 offers[3].status = HostRequestStatus.rejected
1094 offers[4].status = HostRequestStatus.cancelled
1096 with public_trips_session(traveler_token) as api:
1097 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1098 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1099 # pending, accepted, confirmed and rejected all count; cancelled does not
1100 assert trip.offers_count == 4
1103def test_viewer_host_request_id_reflects_viewers_own_offer(db):
1104 traveler, _ = generate_user()
1105 host, host_token = generate_user()
1106 _, other_token = generate_user()
1107 node_id = _make_node()
1109 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1111 # Before offering, the host sees 0.
1112 with public_trips_session(host_token) as api:
1113 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1114 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1115 assert trip.viewer_host_request_id == 0
1117 # The host makes an offer on the trip.
1118 with requests_session(host_token) as api:
1119 create_res = api.CreateHostRequest(
1120 requests_pb2.CreateHostRequestReq(
1121 host_user_id=traveler.id,
1122 from_date=(today() + timedelta(days=5)).isoformat(),
1123 to_date=(today() + timedelta(days=10)).isoformat(),
1124 text=_valid_request_text(),
1125 public_trip_id=trip_id,
1126 )
1127 )
1128 host_request_id = create_res.host_request_id
1130 with public_trips_session(host_token) as api:
1131 # The offering host now sees their own host request id (List and Get).
1132 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1133 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1134 assert trip.viewer_host_request_id == host_request_id
1136 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1137 assert get_res.viewer_host_request_id == host_request_id
1139 # A different viewer who hasn't offered still sees 0.
1140 with public_trips_session(other_token) as api:
1141 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1142 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1143 assert trip.viewer_host_request_id == 0
1146def test_create_public_trip_creates_shadowed_moderation_state(db):
1147 """Creating a trip creates a ModerationState (shadowed) and an initial-review queue item."""
1148 user, token = generate_user()
1149 node_id = _make_node()
1151 with public_trips_session(token) as api:
1152 res = api.CreatePublicTrip(
1153 public_trips_pb2.CreatePublicTripReq(
1154 community_id=node_id,
1155 from_date=(today() + timedelta(days=5)).isoformat(),
1156 to_date=(today() + timedelta(days=10)).isoformat(),
1157 description=VALID_DESCRIPTION,
1158 )
1159 )
1161 with session_scope() as session:
1162 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
1163 state = session.execute(
1164 select(ModerationState).where(ModerationState.id == trip.moderation_state_id)
1165 ).scalar_one()
1166 assert state.object_type == ModerationObjectType.public_trip
1167 assert state.object_id == trip.id
1168 assert state.visibility == ModerationVisibility.shadowed
1170 queue_item = session.execute(
1171 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id)
1172 ).scalar_one()
1173 assert queue_item.trigger == ModerationTrigger.initial_review
1174 assert queue_item.resolved_by_log_id is None
1177def test_shadowed_public_trip_visible_to_author_hidden_from_others(db):
1178 traveler, traveler_token = generate_user()
1179 _, viewer_token = generate_user()
1180 node_id = _make_node()
1182 trip_id = _create_trip_directly(
1183 traveler.id,
1184 node_id,
1185 today() + timedelta(days=5),
1186 today() + timedelta(days=10),
1187 visibility=ModerationVisibility.shadowed,
1188 )
1190 # Author can fetch their own shadowed trip
1191 with public_trips_session(traveler_token) as api:
1192 res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1193 assert res.trip_id == trip_id
1195 # Author sees their shadowed trip in their own listing
1196 with public_trips_session(traveler_token) as api:
1197 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1198 assert any(t.trip_id == trip_id for t in res.public_trips)
1200 # Other viewers can't see it
1201 with public_trips_session(viewer_token) as api:
1202 with pytest.raises(grpc.RpcError) as e:
1203 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1204 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1206 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1207 assert not any(t.trip_id == trip_id for t in list_res.public_trips)
1209 by_user_res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1210 assert not any(t.trip_id == trip_id for t in by_user_res.public_trips)
1213def test_hidden_public_trip_invisible_to_everyone(db):
1214 traveler, traveler_token = generate_user()
1215 _, viewer_token = generate_user()
1216 node_id = _make_node()
1218 trip_id = _create_trip_directly(
1219 traveler.id,
1220 node_id,
1221 today() + timedelta(days=5),
1222 today() + timedelta(days=10),
1223 visibility=ModerationVisibility.hidden,
1224 )
1226 # Even author can't read a hidden trip via the public API
1227 with public_trips_session(traveler_token) as api:
1228 with pytest.raises(grpc.RpcError) as e:
1229 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1230 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1232 by_user_res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1233 assert not any(t.trip_id == trip_id for t in by_user_res.public_trips)
1235 with public_trips_session(viewer_token) as api:
1236 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1237 assert not any(t.trip_id == trip_id for t in list_res.public_trips)
1240def test_moderator_approval_makes_public_trip_visible(db, moderator):
1241 user, token = generate_user()
1242 _, viewer_token = generate_user()
1243 node_id = _make_node()
1245 with public_trips_session(token) as api:
1246 res = api.CreatePublicTrip(
1247 public_trips_pb2.CreatePublicTripReq(
1248 community_id=node_id,
1249 from_date=(today() + timedelta(days=5)).isoformat(),
1250 to_date=(today() + timedelta(days=10)).isoformat(),
1251 description=VALID_DESCRIPTION,
1252 )
1253 )
1255 # Before approval (shadowed): hidden from other users
1256 with public_trips_session(viewer_token) as api:
1257 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1258 assert not any(t.trip_id == res.trip_id for t in list_res.public_trips)
1260 moderator.approve_public_trip(res.trip_id)
1262 # After approval: visible to everyone
1263 with public_trips_session(viewer_token) as api:
1264 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1265 assert any(t.trip_id == res.trip_id for t in list_res.public_trips)
1267 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=res.trip_id))
1268 assert get_res.trip_id == res.trip_id
1270 # ... and the initial-review queue item is resolved, so the trip leaves the queue
1271 with session_scope() as session:
1272 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
1273 queue_item = session.execute(
1274 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == trip.moderation_state_id)
1275 ).scalar_one()
1276 assert queue_item.resolved_by_log_id is not None