Coverage for app/backend/src/tests/test_public_trips.py: 99%
610 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 17:13 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 17:13 +0000
1from datetime import UTC, date, datetime, timedelta
2from unittest.mock import patch
4import grpc
5import pytest
6from sqlalchemy import select
8from couchers.constants import PUBLIC_TRIP_OFFER_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_trip_offer_text(text: str = "Offer to host") -> str:
33 utf16_length = len(text.encode("utf-16-le")) // 2
34 if utf16_length >= PUBLIC_TRIP_OFFER_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 + "_" * (PUBLIC_TRIP_OFFER_MIN_LENGTH_UTF16 - utf16_length)
39# 150+ utf-16 code units to satisfy PUBLIC_TRIP_DESCRIPTION_MIN_LENGTH_UTF16.
40VALID_DESCRIPTION = (
41 "Visiting the area for a week for a music festival. I love meeting new people "
42 "and would really appreciate local tips. Happy to help with tasks in exchange."
43)
46def _make_node(node_type: NodeType = NodeType.locality, small_community_features_enabled: bool = True) -> int:
47 # Polygon inside the fake Europe/Helsinki timezone area so node.timezone resolves.
48 with session_scope() as session:
49 node = Node(
50 geom=to_multi(create_polygon_lat_lng([[60, 24], [60, 26], [62, 26], [62, 24], [60, 24]])),
51 node_type=node_type,
52 )
53 session.add(node)
54 session.flush()
55 cluster = Cluster(
56 name="Test community",
57 description="Test",
58 parent_node_id=node.id,
59 is_official_cluster=True,
60 small_community_features_enabled=small_community_features_enabled,
61 )
62 session.add(cluster)
63 session.flush()
64 return node.id
67def _make_node_admin(user_id: int, node_id: int):
68 with session_scope() as session:
69 cluster_id = session.execute(
70 select(Cluster.id).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
71 ).scalar_one()
72 session.add(ClusterSubscription(cluster_id=cluster_id, user_id=user_id, role=ClusterRole.admin))
75def _create_trip_directly(
76 user_id: int,
77 node_id: int,
78 from_date,
79 to_date,
80 *,
81 description: str = "Looking for a host!",
82 status=None,
83 same_gender_only: bool = False,
84 visibility: ModerationVisibility = ModerationVisibility.visible,
85) -> int:
86 with session_scope() as session:
87 moderation_state = ModerationState(
88 object_type=ModerationObjectType.public_trip,
89 object_id=0, # placeholder, set after PublicTrip flush
90 visibility=visibility,
91 )
92 session.add(moderation_state)
93 session.flush()
94 trip = PublicTrip(
95 user_id=user_id,
96 node_id=node_id,
97 from_date=from_date,
98 to_date=to_date,
99 description=description,
100 status=status or PublicTripStatus.searching_for_host,
101 same_gender_only=same_gender_only,
102 moderation_state_id=moderation_state.id,
103 )
104 session.add(trip)
105 session.flush()
106 moderation_state.object_id = trip.id
107 return trip.id
110def test_create_public_trip(db):
111 user, token = generate_user()
112 node_id = _make_node()
114 from_date = today() + timedelta(days=5)
115 to_date = today() + timedelta(days=10)
117 with public_trips_session(token) as api:
118 res = api.CreatePublicTrip(
119 public_trips_pb2.CreatePublicTripReq(
120 community_id=node_id,
121 from_date=from_date.isoformat(),
122 to_date=to_date.isoformat(),
123 description=VALID_DESCRIPTION,
124 )
125 )
127 assert res.trip_id > 0
128 assert res.user.user_id == user.id
129 assert res.community_id == node_id
130 assert res.community_slug == "test-community"
131 assert res.community_name == "Test community"
132 assert res.from_date == from_date.isoformat()
133 assert res.to_date == to_date.isoformat()
134 assert res.description == VALID_DESCRIPTION
135 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST
137 with session_scope() as session:
138 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
139 assert trip.user_id == user.id
140 assert trip.node_id == node_id
141 assert trip.status == PublicTripStatus.searching_for_host
144def test_create_public_trip_incomplete_profile(db):
145 _, token = generate_user(complete_profile=False)
146 node_id = _make_node()
148 with public_trips_session(token) as api:
149 with pytest.raises(grpc.RpcError) as e:
150 api.CreatePublicTrip(
151 public_trips_pb2.CreatePublicTripReq(
152 community_id=node_id,
153 from_date=(today() + timedelta(days=5)).isoformat(),
154 to_date=(today() + timedelta(days=10)).isoformat(),
155 description="Visiting town!",
156 )
157 )
158 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
159 assert e.value.details() == "You have to complete your profile before you can create a public trip."
162def test_create_public_trip_community_not_found(db):
163 _, token = generate_user()
165 with public_trips_session(token) as api:
166 with pytest.raises(grpc.RpcError) as e:
167 api.CreatePublicTrip(
168 public_trips_pb2.CreatePublicTripReq(
169 community_id=999999,
170 from_date=(today() + timedelta(days=5)).isoformat(),
171 to_date=(today() + timedelta(days=10)).isoformat(),
172 description="Visiting town!",
173 )
174 )
175 assert e.value.code() == grpc.StatusCode.NOT_FOUND
176 assert e.value.details() == "Community not found."
179def test_create_public_trip_not_enabled(db):
180 _, token = generate_user()
181 node_id = _make_node(small_community_features_enabled=False)
183 with public_trips_session(token) as api:
184 with pytest.raises(grpc.RpcError) as e:
185 api.CreatePublicTrip(
186 public_trips_pb2.CreatePublicTripReq(
187 community_id=node_id,
188 from_date=(today() + timedelta(days=5)).isoformat(),
189 to_date=(today() + timedelta(days=10)).isoformat(),
190 description="Visiting town!",
191 )
192 )
193 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
194 assert e.value.details() == "Public trips are not enabled in this community."
197@pytest.mark.parametrize(
198 "node_type",
199 [NodeType.region, NodeType.subregion, NodeType.locality, NodeType.sublocality],
200)
201def test_create_public_trip_allows_region_and_narrower(db, node_type):
202 _, token = generate_user()
203 node_id = _make_node(node_type=node_type)
205 with public_trips_session(token) as api:
206 res = api.CreatePublicTrip(
207 public_trips_pb2.CreatePublicTripReq(
208 community_id=node_id,
209 from_date=(today() + timedelta(days=5)).isoformat(),
210 to_date=(today() + timedelta(days=10)).isoformat(),
211 description=VALID_DESCRIPTION,
212 )
213 )
214 assert res.trip_id > 0
217def test_create_public_trip_in_past_uses_node_timezone(db):
218 # Default user geom resolves to America/New_York; the node's geom is in Europe/Helsinki.
219 # Simulate a moment where Helsinki has already rolled into the next day (2026-01-16)
220 # while NYC is still on 2026-01-15. A from_date of 2026-01-15 is "today" in NYC but
221 # "yesterday" in Helsinki, and must be rejected because the check uses the node's tz.
222 _, token = generate_user()
223 node_id = _make_node()
225 fake_today_by_tz = {"America/New_York": date(2026, 1, 15), "Europe/Helsinki": date(2026, 1, 16)}
227 with patch(
228 "couchers.servicers.public_trips.today_in_timezone",
229 side_effect=lambda tz: fake_today_by_tz[tz],
230 ):
231 with public_trips_session(token) as api:
232 with pytest.raises(grpc.RpcError) as e:
233 api.CreatePublicTrip(
234 public_trips_pb2.CreatePublicTripReq(
235 community_id=node_id,
236 from_date="2026-01-15",
237 to_date="2026-01-20",
238 description=VALID_DESCRIPTION,
239 )
240 )
241 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
244def test_create_public_trip_date_errors(db):
245 _, token = generate_user()
246 node_id = _make_node()
248 with public_trips_session(token) as api:
249 # from_date in the past
250 with pytest.raises(grpc.RpcError) as e:
251 api.CreatePublicTrip(
252 public_trips_pb2.CreatePublicTripReq(
253 community_id=node_id,
254 from_date=(today() - timedelta(days=2)).isoformat(),
255 to_date=(today() + timedelta(days=1)).isoformat(),
256 description="Visiting town!",
257 )
258 )
259 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
261 # from_date after to_date
262 with pytest.raises(grpc.RpcError) as e:
263 api.CreatePublicTrip(
264 public_trips_pb2.CreatePublicTripReq(
265 community_id=node_id,
266 from_date=(today() + timedelta(days=10)).isoformat(),
267 to_date=(today() + timedelta(days=5)).isoformat(),
268 description="Visiting town!",
269 )
270 )
271 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
273 # empty description
274 with pytest.raises(grpc.RpcError) as e:
275 api.CreatePublicTrip(
276 public_trips_pb2.CreatePublicTripReq(
277 community_id=node_id,
278 from_date=(today() + timedelta(days=5)).isoformat(),
279 to_date=(today() + timedelta(days=10)).isoformat(),
280 description=" ",
281 )
282 )
283 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
285 # invalid date format
286 with pytest.raises(grpc.RpcError) as e:
287 api.CreatePublicTrip(
288 public_trips_pb2.CreatePublicTripReq(
289 community_id=node_id,
290 from_date="not-a-date",
291 to_date=(today() + timedelta(days=10)).isoformat(),
292 description="Visiting town!",
293 )
294 )
295 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
298def test_create_public_trip_overlap(db):
299 user, token = generate_user()
300 node_id = _make_node()
302 _create_trip_directly(
303 user.id,
304 node_id,
305 today() + timedelta(days=5),
306 today() + timedelta(days=10),
307 )
309 with public_trips_session(token) as api:
310 # overlapping dates should fail
311 with pytest.raises(grpc.RpcError) as e:
312 api.CreatePublicTrip(
313 public_trips_pb2.CreatePublicTripReq(
314 community_id=node_id,
315 from_date=(today() + timedelta(days=8)).isoformat(),
316 to_date=(today() + timedelta(days=12)).isoformat(),
317 description=VALID_DESCRIPTION,
318 )
319 )
320 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
322 # non-overlapping dates should succeed
323 res = api.CreatePublicTrip(
324 public_trips_pb2.CreatePublicTripReq(
325 community_id=node_id,
326 from_date=(today() + timedelta(days=20)).isoformat(),
327 to_date=(today() + timedelta(days=25)).isoformat(),
328 description=VALID_DESCRIPTION,
329 )
330 )
331 assert res.trip_id > 0
334def test_create_public_trip_closed_trip_allows_new_overlap(db):
335 user, token = generate_user()
336 node_id = _make_node()
338 _create_trip_directly(
339 user.id,
340 node_id,
341 today() + timedelta(days=5),
342 today() + timedelta(days=10),
343 status=PublicTripStatus.closed,
344 )
346 with public_trips_session(token) as api:
347 # closed trips shouldn't block new overlapping ones
348 res = api.CreatePublicTrip(
349 public_trips_pb2.CreatePublicTripReq(
350 community_id=node_id,
351 from_date=(today() + timedelta(days=7)).isoformat(),
352 to_date=(today() + timedelta(days=12)).isoformat(),
353 description=VALID_DESCRIPTION,
354 )
355 )
356 assert res.trip_id > 0
359def test_get_public_trip(db):
360 user, token = generate_user()
361 node_id = _make_node()
362 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
364 with public_trips_session(token) as api:
365 res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
366 assert res.trip_id == trip_id
367 assert res.user.user_id == user.id
368 assert res.community_slug == "test-community"
369 assert res.community_name == "Test community"
370 assert res.timezone == "Europe/Helsinki"
373def test_get_public_trip_not_found(db):
374 _, token = generate_user()
375 with public_trips_session(token) as api:
376 with pytest.raises(grpc.RpcError) as e:
377 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=999999))
378 assert e.value.code() == grpc.StatusCode.NOT_FOUND
381def test_list_public_trips(db):
382 traveler, _ = generate_user()
383 _, host_token = generate_user()
384 node_id = _make_node()
386 trip1 = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
387 trip2 = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=20), today() + timedelta(days=25))
389 with public_trips_session(host_token) as api:
390 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
391 returned_ids = {t.trip_id for t in res.public_trips}
392 assert returned_ids == {trip1, trip2}
393 assert all(t.community_slug == "test-community" for t in res.public_trips)
394 assert all(t.community_name == "Test community" for t in res.public_trips)
397def test_list_public_trips_filters_closed_and_past(db):
398 traveler, _ = generate_user()
399 _, host_token = generate_user()
400 node_id = _make_node()
402 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
403 # closed trip - should be hidden
404 _create_trip_directly(
405 traveler.id,
406 node_id,
407 today() + timedelta(days=15),
408 today() + timedelta(days=20),
409 status=PublicTripStatus.closed,
410 )
411 # past trip (to_date < today) - should be hidden
412 _create_trip_directly(traveler.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
414 with public_trips_session(host_token) as api:
415 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
416 assert [t.trip_id for t in res.public_trips] == [active]
419def test_list_public_trips_uses_timezone(db, frozen_timewarp):
420 # 1999-12-31 23:30 UTC == 2000-01-01 01:30 Helsinki
421 frozen_timewarp.freeze_at(datetime(1999, 12, 31, 23, 30, tzinfo=UTC))
422 traveler, _ = generate_user()
423 _, host_token = generate_user()
424 node_id = _make_node() # Europe/Helsinki timezone
426 # First trip is over in Europe/Helsinki, second trip is ongoing
427 _create_trip_directly(traveler.id, node_id, date(1999, 12, 30), date(1999, 12, 31))
428 ongoing = _create_trip_directly(traveler.id, node_id, date(2000, 1, 1), date(2000, 1, 2))
430 with public_trips_session(host_token) as api:
431 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
432 assert [t.trip_id for t in res.public_trips] == [ongoing]
433 assert res.public_trips[0].timezone == "Europe/Helsinki"
436def test_list_public_trips_hides_invisible_user(db):
437 traveler, _ = generate_user()
438 _, host_token = generate_user()
439 node_id = _make_node()
441 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
443 # soft-delete the traveler
444 with session_scope() as session:
445 t = session.execute(select(User).where(User.id == traveler.id)).scalar_one()
446 t.deleted_at = now()
448 with public_trips_session(host_token) as api:
449 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
450 assert len(res.public_trips) == 0
453def test_list_public_trips_pagination(db):
454 traveler, _ = generate_user()
455 _, host_token = generate_user()
456 node_id = _make_node()
458 trip_ids = [
459 _create_trip_directly(
460 traveler.id, node_id, today() + timedelta(days=5 + i * 10), today() + timedelta(days=10 + i * 10)
461 )
462 for i in range(5)
463 ]
465 with public_trips_session(host_token) as api:
466 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2))
467 assert [t.trip_id for t in res.public_trips] == [trip_ids[4], trip_ids[3]]
468 assert res.next_page_token
470 res2 = api.ListPublicTrips(
471 public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2, page_token=res.next_page_token)
472 )
473 assert [t.trip_id for t in res2.public_trips] == [trip_ids[2], trip_ids[1]]
474 assert res2.next_page_token
476 res3 = api.ListPublicTrips(
477 public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2, page_token=res2.next_page_token)
478 )
479 assert [t.trip_id for t in res3.public_trips] == [trip_ids[0]]
480 assert not res3.next_page_token
483def test_list_public_trips_by_user_self_sees_all(db):
484 user, token = generate_user()
485 other, _ = generate_user()
486 node_id = _make_node()
488 mine_active = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
489 mine_closed = _create_trip_directly(
490 user.id,
491 node_id,
492 today() + timedelta(days=15),
493 today() + timedelta(days=20),
494 status=PublicTripStatus.closed,
495 )
496 mine_past = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
497 # other user's trip should not be returned
498 _create_trip_directly(other.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
500 with public_trips_session(token) as api:
501 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id))
502 assert {t.trip_id for t in res.public_trips} == {mine_active, mine_closed, mine_past}
505def test_list_public_trips_by_user_other_filters_inactive_and_past(db):
506 traveler, _ = generate_user()
507 _, viewer_token = generate_user()
508 node_id = _make_node()
510 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
511 # closed trip - hidden from others
512 _create_trip_directly(
513 traveler.id,
514 node_id,
515 today() + timedelta(days=15),
516 today() + timedelta(days=20),
517 status=PublicTripStatus.closed,
518 )
519 # past trip - hidden from others
520 _create_trip_directly(traveler.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
522 with public_trips_session(viewer_token) as api:
523 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
524 assert [t.trip_id for t in res.public_trips] == [active]
527def test_list_public_trips_by_user_invisible_user(db):
528 traveler, _ = generate_user()
529 _, viewer_token = generate_user()
530 node_id = _make_node()
532 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
534 # soft-delete the traveler
535 with session_scope() as session:
536 t = session.execute(select(User).where(User.id == traveler.id)).scalar_one()
537 t.deleted_at = now()
539 with public_trips_session(viewer_token) as api:
540 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
541 assert len(res.public_trips) == 0
544def test_update_public_trip_close(db):
545 user, token = generate_user()
546 node_id = _make_node()
547 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
549 with public_trips_session(token) as api:
550 res = api.UpdatePublicTrip(
551 public_trips_pb2.UpdatePublicTripReq(
552 trip_id=trip_id,
553 status=public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED,
554 )
555 )
556 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED
558 with session_scope() as session:
559 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
560 assert trip.status == PublicTripStatus.closed
563def test_update_public_trip_reopen(db):
564 user, token = generate_user()
565 node_id = _make_node()
566 trip_id = _create_trip_directly(
567 user.id,
568 node_id,
569 today() + timedelta(days=5),
570 today() + timedelta(days=10),
571 status=PublicTripStatus.closed,
572 )
574 with public_trips_session(token) as api:
575 res = api.UpdatePublicTrip(
576 public_trips_pb2.UpdatePublicTripReq(
577 trip_id=trip_id,
578 status=public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST,
579 )
580 )
581 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST
583 with session_scope() as session:
584 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
585 assert trip.status == PublicTripStatus.searching_for_host
588def test_update_public_trip_cant_reopen_past_trip(db):
589 user, token = generate_user()
590 node_id = _make_node()
591 trip_id = _create_trip_directly(
592 user.id,
593 node_id,
594 today() - timedelta(days=10),
595 today() - timedelta(days=1),
596 status=PublicTripStatus.closed,
597 )
599 with public_trips_session(token) as api:
600 with pytest.raises(grpc.RpcError) as e:
601 api.UpdatePublicTrip(
602 public_trips_pb2.UpdatePublicTripReq(
603 trip_id=trip_id,
604 status=public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST,
605 )
606 )
607 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
610def test_update_public_trip_close_past_trip_allowed(db):
611 # Closing a trip whose dates are in the past is allowed, even though content edits are not.
612 user, token = generate_user()
613 node_id = _make_node()
614 trip_id = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
616 with public_trips_session(token) as api:
617 res = api.UpdatePublicTrip(
618 public_trips_pb2.UpdatePublicTripReq(
619 trip_id=trip_id,
620 status=public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED,
621 )
622 )
623 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED
626def test_update_public_trip_description_only(db):
627 user, token = generate_user()
628 node_id = _make_node()
629 trip_id = _create_trip_directly(
630 user.id,
631 node_id,
632 today() + timedelta(days=5),
633 today() + timedelta(days=10),
634 description="Original description",
635 )
637 updated = VALID_DESCRIPTION + " Updated plans."
639 with public_trips_session(token) as api:
640 res = api.UpdatePublicTrip(
641 public_trips_pb2.UpdatePublicTripReq(
642 trip_id=trip_id,
643 description=updated,
644 )
645 )
646 assert res.trip_id == trip_id
647 assert res.description == updated
648 # dates should be unchanged
649 assert res.from_date == (today() + timedelta(days=5)).isoformat()
650 assert res.to_date == (today() + timedelta(days=10)).isoformat()
652 with session_scope() as session:
653 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
654 assert trip.description == updated
657def test_update_public_trip_dates(db):
658 user, token = generate_user()
659 node_id = _make_node()
660 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
662 new_from = today() + timedelta(days=7)
663 new_to = today() + timedelta(days=14)
665 with public_trips_session(token) as api:
666 res = api.UpdatePublicTrip(
667 public_trips_pb2.UpdatePublicTripReq(
668 trip_id=trip_id,
669 from_date=new_from.isoformat(),
670 to_date=new_to.isoformat(),
671 )
672 )
673 assert res.from_date == new_from.isoformat()
674 assert res.to_date == new_to.isoformat()
677def test_update_public_trip_not_owner(db):
678 user, _ = generate_user()
679 _, other_token = generate_user()
680 node_id = _make_node()
681 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
683 with public_trips_session(other_token) as api:
684 with pytest.raises(grpc.RpcError) as e:
685 api.UpdatePublicTrip(
686 public_trips_pb2.UpdatePublicTripReq(
687 trip_id=trip_id,
688 description="I don't own this!",
689 )
690 )
691 assert e.value.code() == grpc.StatusCode.NOT_FOUND
694def test_update_public_trip_in_past(db):
695 user, token = generate_user()
696 node_id = _make_node()
697 trip_id = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=2))
699 with public_trips_session(token) as api:
700 with pytest.raises(grpc.RpcError) as e:
701 api.UpdatePublicTrip(
702 public_trips_pb2.UpdatePublicTripReq(
703 trip_id=trip_id,
704 description="Too late!",
705 )
706 )
707 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
710def test_update_public_trip_date_validation(db):
711 user, token = generate_user()
712 node_id = _make_node()
713 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
715 with public_trips_session(token) as api:
716 # from_date after to_date (using the stored to_date of today+10)
717 with pytest.raises(grpc.RpcError) as e:
718 api.UpdatePublicTrip(
719 public_trips_pb2.UpdatePublicTripReq(
720 trip_id=trip_id,
721 from_date=(today() + timedelta(days=20)).isoformat(),
722 )
723 )
724 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
726 # Empty description
727 with pytest.raises(grpc.RpcError) as e:
728 api.UpdatePublicTrip(
729 public_trips_pb2.UpdatePublicTripReq(
730 trip_id=trip_id,
731 description=" ",
732 )
733 )
734 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
737def test_create_public_trip_description_too_short(db):
738 _, token = generate_user()
739 node_id = _make_node()
741 with public_trips_session(token) as api:
742 with pytest.raises(grpc.RpcError) as e:
743 api.CreatePublicTrip(
744 public_trips_pb2.CreatePublicTripReq(
745 community_id=node_id,
746 from_date=(today() + timedelta(days=5)).isoformat(),
747 to_date=(today() + timedelta(days=10)).isoformat(),
748 description="Too short.",
749 )
750 )
751 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
752 assert "150" in (e.value.details() or "")
755def test_update_public_trip_description_too_short(db):
756 user, token = generate_user()
757 node_id = _make_node()
758 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
760 with public_trips_session(token) as api:
761 with pytest.raises(grpc.RpcError) as e:
762 api.UpdatePublicTrip(
763 public_trips_pb2.UpdatePublicTripReq(
764 trip_id=trip_id,
765 description="Too short.",
766 )
767 )
768 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
769 assert "150" in (e.value.details() or "")
772def test_same_gender_only_create_and_retrieve(db):
773 user, token = generate_user(gender="Woman")
774 node_id = _make_node()
776 from_date = today() + timedelta(days=5)
777 to_date = today() + timedelta(days=10)
779 with public_trips_session(token) as api:
780 res = api.CreatePublicTrip(
781 public_trips_pb2.CreatePublicTripReq(
782 community_id=node_id,
783 from_date=from_date.isoformat(),
784 to_date=to_date.isoformat(),
785 description=VALID_DESCRIPTION,
786 same_gender_only=True,
787 )
788 )
789 assert res.same_gender_only is True
791 with session_scope() as session:
792 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
793 assert trip.same_gender_only is True
796def test_same_gender_only_visibility_list_and_get(db):
797 traveler, _ = generate_user(gender="Woman")
798 _, same_gender_token = generate_user(gender="Woman")
799 _, diff_gender_token = generate_user(gender="Man")
800 node_id = _make_node()
802 filtered_trip_id = _create_trip_directly(
803 traveler.id,
804 node_id,
805 today() + timedelta(days=5),
806 today() + timedelta(days=10),
807 same_gender_only=True,
808 )
809 open_trip_id = _create_trip_directly(
810 traveler.id,
811 node_id,
812 today() + timedelta(days=20),
813 today() + timedelta(days=25),
814 )
816 with public_trips_session(same_gender_token) as api:
817 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
818 assert {t.trip_id for t in res.public_trips} == {filtered_trip_id, open_trip_id}
820 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=filtered_trip_id))
821 assert get_res.trip_id == filtered_trip_id
822 assert get_res.same_gender_only is True
824 with public_trips_session(diff_gender_token) as api:
825 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
826 assert [t.trip_id for t in res.public_trips] == [open_trip_id]
828 with pytest.raises(grpc.RpcError) as e:
829 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=filtered_trip_id))
830 assert e.value.code() == grpc.StatusCode.NOT_FOUND
833def test_same_gender_only_moderator_bypass(db):
834 traveler, _ = generate_user(gender="Woman")
835 mod, mod_token = generate_user(gender="Man")
836 node_id = _make_node()
837 _make_node_admin(mod.id, node_id)
839 trip_id = _create_trip_directly(
840 traveler.id,
841 node_id,
842 today() + timedelta(days=5),
843 today() + timedelta(days=10),
844 same_gender_only=True,
845 )
847 with public_trips_session(mod_token) as api:
848 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
849 assert any(t.trip_id == trip_id for t in res.public_trips)
851 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
852 assert get_res.trip_id == trip_id
855def test_same_gender_only_owner_always_sees_own_trips(db):
856 traveler, traveler_token = generate_user(gender="Woman")
857 _, diff_gender_token = generate_user(gender="Man")
858 node_id = _make_node()
860 trip_id = _create_trip_directly(
861 traveler.id,
862 node_id,
863 today() + timedelta(days=5),
864 today() + timedelta(days=10),
865 same_gender_only=True,
866 )
868 # Owner always sees their own trips (is_self path skips the gender filter)
869 with public_trips_session(traveler_token) as api:
870 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
871 assert any(t.trip_id == trip_id for t in res.public_trips)
873 # Different-gender viewer doesn't see it on the traveler's profile
874 with public_trips_session(diff_gender_token) as api:
875 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
876 assert not any(t.trip_id == trip_id for t in res.public_trips)
879def test_same_gender_only_update(db):
880 user, token = generate_user(gender="Woman")
881 node_id = _make_node()
882 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
884 with public_trips_session(token) as api:
885 res = api.UpdatePublicTrip(
886 public_trips_pb2.UpdatePublicTripReq(
887 trip_id=trip_id,
888 same_gender_only=True,
889 )
890 )
891 assert res.same_gender_only is True
893 res = api.UpdatePublicTrip(
894 public_trips_pb2.UpdatePublicTripReq(
895 trip_id=trip_id,
896 same_gender_only=False,
897 )
898 )
899 assert res.same_gender_only is False
901 with session_scope() as session:
902 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
903 assert trip.same_gender_only is False
906def test_list_public_trips_by_user_ascending_order(db):
907 user, token = generate_user()
908 node_id = _make_node()
910 trip_near = _create_trip_directly(user.id, node_id, today() + timedelta(days=3), today() + timedelta(days=5))
911 trip_far = _create_trip_directly(user.id, node_id, today() + timedelta(days=20), today() + timedelta(days=25))
912 trip_mid = _create_trip_directly(user.id, node_id, today() + timedelta(days=10), today() + timedelta(days=12))
914 with public_trips_session(token) as api:
915 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id, ascending=True))
916 trip_ids = [t.trip_id for t in res.public_trips]
917 assert trip_ids.index(trip_near) < trip_ids.index(trip_mid) < trip_ids.index(trip_far)
919 res_desc = api.ListPublicTripsByUser(
920 public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id, ascending=False)
921 )
922 trip_ids_desc = [t.trip_id for t in res_desc.public_trips]
923 assert trip_ids_desc.index(trip_far) < trip_ids_desc.index(trip_mid) < trip_ids_desc.index(trip_near)
926def test_list_public_trips_by_user_status_filter(db):
927 user, token = generate_user()
928 node_id = _make_node()
930 active = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
931 closed = _create_trip_directly(
932 user.id, node_id, today() + timedelta(days=15), today() + timedelta(days=20), status=PublicTripStatus.closed
933 )
935 with public_trips_session(token) as api:
936 # Filter to active only
937 res = api.ListPublicTripsByUser(
938 public_trips_pb2.ListPublicTripsByUserReq(
939 user_id=user.id,
940 statuses_in=[public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST],
941 )
942 )
943 assert {t.trip_id for t in res.public_trips} == {active}
945 # Filter to closed only
946 res = api.ListPublicTripsByUser(
947 public_trips_pb2.ListPublicTripsByUserReq(
948 user_id=user.id,
949 statuses_in=[public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED],
950 )
951 )
952 assert {t.trip_id for t in res.public_trips} == {closed}
954 # No filter returns all
955 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id))
956 assert {t.trip_id for t in res.public_trips} == {active, closed}
959def test_list_public_trips_by_user_status_filter_ignored_for_others(db):
960 traveler, _ = generate_user()
961 _, viewer_token = generate_user()
962 node_id = _make_node()
964 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
965 _create_trip_directly(
966 traveler.id, node_id, today() + timedelta(days=15), today() + timedelta(days=20), status=PublicTripStatus.closed
967 )
969 with public_trips_session(viewer_token) as api:
970 # status_filter is ignored for other users — always returns active+upcoming only
971 res = api.ListPublicTripsByUser(
972 public_trips_pb2.ListPublicTripsByUserReq(
973 user_id=traveler.id,
974 statuses_in=[public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED],
975 )
976 )
977 assert [t.trip_id for t in res.public_trips] == [active]
980def test_list_public_trips_by_user_offers_count_owner(db, moderator: Moderator):
981 traveler, traveler_token = generate_user()
982 host, host_token = generate_user()
983 node_id = _make_node()
985 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
987 with public_trips_session(traveler_token) as api:
988 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
989 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
990 assert trip.HasField("offers_count")
991 assert trip.offers_count == 0
993 # Host creates an offer via a host request linked to the trip
994 with requests_session(host_token) as api:
995 host_request_id = api.CreateHostRequest(
996 requests_pb2.CreateHostRequestReq(
997 host_user_id=traveler.id,
998 from_date=(today() + timedelta(days=5)).isoformat(),
999 to_date=(today() + timedelta(days=10)).isoformat(),
1000 text=_valid_trip_offer_text(),
1001 public_trip_id=trip_id,
1002 )
1003 ).host_request_id
1005 with public_trips_session(traveler_token) as api:
1006 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1007 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1008 assert trip.HasField("offers_count")
1009 assert trip.offers_count == 0
1011 moderator.approve_host_request(host_request_id)
1013 with public_trips_session(traveler_token) as api:
1014 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1015 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1016 assert trip.HasField("offers_count")
1017 assert trip.offers_count == 1
1020def test_list_public_trips_by_user_offers_count_excludes_invisible_offers(db, moderator: Moderator):
1021 """The count only covers offers the owner can open."""
1022 traveler, traveler_token = generate_user()
1023 _hidden_host, hidden_host_token = generate_user()
1024 banned_host, banned_host_token = generate_user()
1025 node_id = _make_node()
1027 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1029 offer_ids = []
1030 for token in (hidden_host_token, banned_host_token):
1031 with requests_session(token) as api:
1032 offer_ids.append(
1033 api.CreateHostRequest(
1034 requests_pb2.CreateHostRequestReq(
1035 host_user_id=traveler.id,
1036 from_date=(today() + timedelta(days=5)).isoformat(),
1037 to_date=(today() + timedelta(days=10)).isoformat(),
1038 text=_valid_trip_offer_text(),
1039 public_trip_id=trip_id,
1040 )
1041 ).host_request_id
1042 )
1043 hidden_offer_id = offer_ids[0]
1045 for offer_id in offer_ids:
1046 moderator.approve_host_request(offer_id)
1048 with public_trips_session(traveler_token) as api:
1049 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1050 assert next(t for t in res.public_trips if t.trip_id == trip_id).offers_count == 2
1052 moderator.hide_host_request(hidden_offer_id)
1053 with session_scope() as session:
1054 session.execute(select(User).where(User.id == banned_host.id)).scalar_one().banned_at = now()
1056 with public_trips_session(traveler_token) as api:
1057 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1058 assert next(t for t in res.public_trips if t.trip_id == trip_id).offers_count == 0
1061def test_list_public_trips_by_user_offers_count_not_set_for_others(db):
1062 traveler, _ = generate_user()
1063 _, viewer_token = generate_user()
1064 node_id = _make_node()
1066 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1068 with public_trips_session(viewer_token) as api:
1069 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1070 assert len(res.public_trips) == 1
1071 assert not res.public_trips[0].HasField("offers_count")
1074def test_list_public_trips_by_user_offers_count_excludes_cancelled(db, moderator: Moderator):
1075 traveler, traveler_token = generate_user()
1076 hosts = [generate_user() for _ in range(5)]
1077 node_id = _make_node()
1079 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1081 for _host, host_token in hosts:
1082 with requests_session(host_token) as api:
1083 offer_id = api.CreateHostRequest(
1084 requests_pb2.CreateHostRequestReq(
1085 host_user_id=traveler.id,
1086 from_date=(today() + timedelta(days=5)).isoformat(),
1087 to_date=(today() + timedelta(days=10)).isoformat(),
1088 text=_valid_trip_offer_text(),
1089 public_trip_id=trip_id,
1090 )
1091 ).host_request_id
1092 moderator.approve_host_request(offer_id)
1094 # Set one offer per status, so we cover every status the count has to consider.
1095 with session_scope() as session:
1096 offers = (
1097 session.execute(
1098 select(HostRequest).where(HostRequest.public_trip_id == trip_id).order_by(HostRequest.conversation_id)
1099 )
1100 .scalars()
1101 .all()
1102 )
1103 offers[0].status = HostRequestStatus.pending
1104 offers[1].status = HostRequestStatus.accepted
1105 offers[2].status = HostRequestStatus.confirmed
1106 offers[3].status = HostRequestStatus.rejected
1107 offers[4].status = HostRequestStatus.cancelled
1109 with public_trips_session(traveler_token) as api:
1110 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1111 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1112 # pending, accepted, confirmed and rejected all count; cancelled does not
1113 assert trip.offers_count == 4
1116def test_viewer_host_request_id_reflects_viewers_own_offer(db):
1117 traveler, _ = generate_user()
1118 host, host_token = generate_user()
1119 _, other_token = generate_user()
1120 node_id = _make_node()
1122 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1124 # Before offering, the host sees 0.
1125 with public_trips_session(host_token) as api:
1126 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1127 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1128 assert trip.viewer_host_request_id == 0
1130 # The host makes an offer on the trip.
1131 with requests_session(host_token) as api:
1132 create_res = api.CreateHostRequest(
1133 requests_pb2.CreateHostRequestReq(
1134 host_user_id=traveler.id,
1135 from_date=(today() + timedelta(days=5)).isoformat(),
1136 to_date=(today() + timedelta(days=10)).isoformat(),
1137 text=_valid_trip_offer_text(),
1138 public_trip_id=trip_id,
1139 )
1140 )
1141 host_request_id = create_res.host_request_id
1143 with public_trips_session(host_token) as api:
1144 # The offering host now sees their own host request id (List and Get).
1145 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1146 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1147 assert trip.viewer_host_request_id == host_request_id
1149 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1150 assert get_res.viewer_host_request_id == host_request_id
1152 # A different viewer who hasn't offered still sees 0.
1153 with public_trips_session(other_token) as api:
1154 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1155 trip = next(t for t in res.public_trips if t.trip_id == trip_id)
1156 assert trip.viewer_host_request_id == 0
1159def test_create_public_trip_creates_shadowed_moderation_state(db):
1160 """Creating a trip creates a ModerationState (shadowed) and an initial-review queue item."""
1161 user, token = generate_user()
1162 node_id = _make_node()
1164 with public_trips_session(token) as api:
1165 res = api.CreatePublicTrip(
1166 public_trips_pb2.CreatePublicTripReq(
1167 community_id=node_id,
1168 from_date=(today() + timedelta(days=5)).isoformat(),
1169 to_date=(today() + timedelta(days=10)).isoformat(),
1170 description=VALID_DESCRIPTION,
1171 )
1172 )
1174 with session_scope() as session:
1175 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
1176 state = session.execute(
1177 select(ModerationState).where(ModerationState.id == trip.moderation_state_id)
1178 ).scalar_one()
1179 assert state.object_type == ModerationObjectType.public_trip
1180 assert state.object_id == trip.id
1181 assert state.visibility == ModerationVisibility.shadowed
1183 queue_item = session.execute(
1184 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id)
1185 ).scalar_one()
1186 assert queue_item.trigger == ModerationTrigger.initial_review
1187 assert queue_item.resolved_by_log_id is None
1190def test_shadowed_public_trip_visible_to_author_hidden_from_others(db):
1191 traveler, traveler_token = generate_user()
1192 _, viewer_token = generate_user()
1193 node_id = _make_node()
1195 trip_id = _create_trip_directly(
1196 traveler.id,
1197 node_id,
1198 today() + timedelta(days=5),
1199 today() + timedelta(days=10),
1200 visibility=ModerationVisibility.shadowed,
1201 )
1203 # Author can fetch their own shadowed trip
1204 with public_trips_session(traveler_token) as api:
1205 res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1206 assert res.trip_id == trip_id
1208 # Author sees their shadowed trip in their own listing
1209 with public_trips_session(traveler_token) as api:
1210 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1211 assert any(t.trip_id == trip_id for t in res.public_trips)
1213 # Other viewers can't see it
1214 with public_trips_session(viewer_token) as api:
1215 with pytest.raises(grpc.RpcError) as e:
1216 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1217 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1219 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1220 assert not any(t.trip_id == trip_id for t in list_res.public_trips)
1222 by_user_res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1223 assert not any(t.trip_id == trip_id for t in by_user_res.public_trips)
1226def test_hidden_public_trip_invisible_to_everyone(db):
1227 traveler, traveler_token = generate_user()
1228 _, viewer_token = generate_user()
1229 node_id = _make_node()
1231 trip_id = _create_trip_directly(
1232 traveler.id,
1233 node_id,
1234 today() + timedelta(days=5),
1235 today() + timedelta(days=10),
1236 visibility=ModerationVisibility.hidden,
1237 )
1239 # Even author can't read a hidden trip via the public API
1240 with public_trips_session(traveler_token) as api:
1241 with pytest.raises(grpc.RpcError) as e:
1242 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
1243 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1245 by_user_res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
1246 assert not any(t.trip_id == trip_id for t in by_user_res.public_trips)
1248 with public_trips_session(viewer_token) as api:
1249 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1250 assert not any(t.trip_id == trip_id for t in list_res.public_trips)
1253def test_moderator_approval_makes_public_trip_visible(db, moderator):
1254 user, token = generate_user()
1255 _, viewer_token = generate_user()
1256 node_id = _make_node()
1258 with public_trips_session(token) as api:
1259 res = api.CreatePublicTrip(
1260 public_trips_pb2.CreatePublicTripReq(
1261 community_id=node_id,
1262 from_date=(today() + timedelta(days=5)).isoformat(),
1263 to_date=(today() + timedelta(days=10)).isoformat(),
1264 description=VALID_DESCRIPTION,
1265 )
1266 )
1268 # Before approval (shadowed): hidden from other users
1269 with public_trips_session(viewer_token) as api:
1270 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1271 assert not any(t.trip_id == res.trip_id for t in list_res.public_trips)
1273 moderator.approve_public_trip(res.trip_id)
1275 # After approval: visible to everyone
1276 with public_trips_session(viewer_token) as api:
1277 list_res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
1278 assert any(t.trip_id == res.trip_id for t in list_res.public_trips)
1280 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=res.trip_id))
1281 assert get_res.trip_id == res.trip_id
1283 # ... and the initial-review queue item is resolved, so the trip leaves the queue
1284 with session_scope() as session:
1285 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
1286 queue_item = session.execute(
1287 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == trip.moderation_state_id)
1288 ).scalar_one()
1289 assert queue_item.resolved_by_log_id is not None