Coverage for app/backend/src/tests/test_public_trips.py: 100%
389 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 15:46 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 15:46 +0000
1from datetime import date, timedelta
2from unittest.mock import patch
4import grpc
5import pytest
6from sqlalchemy import select
8from couchers.db import session_scope
9from couchers.models import Cluster, ClusterRole, ClusterSubscription, Node, NodeType, User
10from couchers.models.public_trips import PublicTrip, PublicTripStatus
11from couchers.proto import public_trips_pb2
12from couchers.utils import create_polygon_lat_lng, now, to_multi, today
13from tests.fixtures.db import generate_user
14from tests.fixtures.sessions import public_trips_session
17@pytest.fixture(autouse=True)
18def _(testconfig):
19 pass
22# 150+ utf-16 code units to satisfy PUBLIC_TRIP_DESCRIPTION_MIN_LENGTH_UTF16.
23VALID_DESCRIPTION = (
24 "Visiting the area for a week for a music festival. I love meeting new people "
25 "and would really appreciate local tips. Happy to help with tasks in exchange."
26)
29def _make_node(node_type: NodeType = NodeType.locality, small_community_features_enabled: bool = True) -> int:
30 # Polygon inside the fake Europe/Helsinki timezone area so node.timezone resolves.
31 with session_scope() as session:
32 node = Node(
33 geom=to_multi(create_polygon_lat_lng([[60, 24], [60, 26], [62, 26], [62, 24], [60, 24]])),
34 node_type=node_type,
35 )
36 session.add(node)
37 session.flush()
38 cluster = Cluster(
39 name="Test community",
40 description="Test",
41 parent_node_id=node.id,
42 is_official_cluster=True,
43 small_community_features_enabled=small_community_features_enabled,
44 )
45 session.add(cluster)
46 session.flush()
47 return node.id
50def _make_node_admin(user_id: int, node_id: int):
51 with session_scope() as session:
52 cluster_id = session.execute(
53 select(Cluster.id).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
54 ).scalar_one()
55 session.add(ClusterSubscription(cluster_id=cluster_id, user_id=user_id, role=ClusterRole.admin))
58def _create_trip_directly(
59 user_id: int,
60 node_id: int,
61 from_date,
62 to_date,
63 *,
64 description: str = "Looking for a host!",
65 status=None,
66 same_gender_only: bool = False,
67) -> int:
68 with session_scope() as session:
69 trip = PublicTrip(
70 user_id=user_id,
71 node_id=node_id,
72 from_date=from_date,
73 to_date=to_date,
74 description=description,
75 status=status or PublicTripStatus.searching_for_host,
76 same_gender_only=same_gender_only,
77 )
78 session.add(trip)
79 session.flush()
80 return trip.id
83def test_create_public_trip(db):
84 user, token = generate_user()
85 node_id = _make_node()
87 from_date = today() + timedelta(days=5)
88 to_date = today() + timedelta(days=10)
90 with public_trips_session(token) as api:
91 res = api.CreatePublicTrip(
92 public_trips_pb2.CreatePublicTripReq(
93 node_id=node_id,
94 from_date=from_date.isoformat(),
95 to_date=to_date.isoformat(),
96 description=VALID_DESCRIPTION,
97 )
98 )
100 assert res.trip_id > 0
101 assert res.user.user_id == user.id
102 assert res.node_id == node_id
103 assert res.node_slug == "test-community"
104 assert res.from_date == from_date.isoformat()
105 assert res.to_date == to_date.isoformat()
106 assert res.description == VALID_DESCRIPTION
107 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST
109 with session_scope() as session:
110 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
111 assert trip.user_id == user.id
112 assert trip.node_id == node_id
113 assert trip.status == PublicTripStatus.searching_for_host
116def test_create_public_trip_incomplete_profile(db):
117 _, token = generate_user(complete_profile=False)
118 node_id = _make_node()
120 with public_trips_session(token) as api:
121 with pytest.raises(grpc.RpcError) as e:
122 api.CreatePublicTrip(
123 public_trips_pb2.CreatePublicTripReq(
124 node_id=node_id,
125 from_date=(today() + timedelta(days=5)).isoformat(),
126 to_date=(today() + timedelta(days=10)).isoformat(),
127 description="Visiting town!",
128 )
129 )
130 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
131 assert e.value.details() == "You have to complete your profile before you can create a public trip."
134def test_create_public_trip_community_not_found(db):
135 _, token = generate_user()
137 with public_trips_session(token) as api:
138 with pytest.raises(grpc.RpcError) as e:
139 api.CreatePublicTrip(
140 public_trips_pb2.CreatePublicTripReq(
141 node_id=999999,
142 from_date=(today() + timedelta(days=5)).isoformat(),
143 to_date=(today() + timedelta(days=10)).isoformat(),
144 description="Visiting town!",
145 )
146 )
147 assert e.value.code() == grpc.StatusCode.NOT_FOUND
148 assert e.value.details() == "Community not found."
151def test_create_public_trip_not_enabled(db):
152 _, token = generate_user()
153 node_id = _make_node(small_community_features_enabled=False)
155 with public_trips_session(token) as api:
156 with pytest.raises(grpc.RpcError) as e:
157 api.CreatePublicTrip(
158 public_trips_pb2.CreatePublicTripReq(
159 node_id=node_id,
160 from_date=(today() + timedelta(days=5)).isoformat(),
161 to_date=(today() + timedelta(days=10)).isoformat(),
162 description="Visiting town!",
163 )
164 )
165 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
166 assert e.value.details() == "Public trips are not enabled in this community."
169@pytest.mark.parametrize(
170 "node_type",
171 [NodeType.region, NodeType.subregion, NodeType.locality, NodeType.sublocality],
172)
173def test_create_public_trip_allows_region_and_narrower(db, node_type):
174 _, token = generate_user()
175 node_id = _make_node(node_type=node_type)
177 with public_trips_session(token) as api:
178 res = api.CreatePublicTrip(
179 public_trips_pb2.CreatePublicTripReq(
180 node_id=node_id,
181 from_date=(today() + timedelta(days=5)).isoformat(),
182 to_date=(today() + timedelta(days=10)).isoformat(),
183 description=VALID_DESCRIPTION,
184 )
185 )
186 assert res.trip_id > 0
189def test_create_public_trip_in_past_uses_node_timezone(db):
190 # Default user geom resolves to America/New_York; the node's geom is in Europe/Helsinki.
191 # Simulate a moment where Helsinki has already rolled into the next day (2026-01-16)
192 # while NYC is still on 2026-01-15. A from_date of 2026-01-15 is "today" in NYC but
193 # "yesterday" in Helsinki, and must be rejected because the check uses the node's tz.
194 _, token = generate_user()
195 node_id = _make_node()
197 fake_today_by_tz = {"America/New_York": date(2026, 1, 15), "Europe/Helsinki": date(2026, 1, 16)}
199 with patch(
200 "couchers.servicers.public_trips.today_in_timezone",
201 side_effect=lambda tz: fake_today_by_tz[tz],
202 ):
203 with public_trips_session(token) as api:
204 with pytest.raises(grpc.RpcError) as e:
205 api.CreatePublicTrip(
206 public_trips_pb2.CreatePublicTripReq(
207 node_id=node_id,
208 from_date="2026-01-15",
209 to_date="2026-01-20",
210 description=VALID_DESCRIPTION,
211 )
212 )
213 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
216def test_create_public_trip_date_errors(db):
217 _, token = generate_user()
218 node_id = _make_node()
220 with public_trips_session(token) as api:
221 # from_date in the past
222 with pytest.raises(grpc.RpcError) as e:
223 api.CreatePublicTrip(
224 public_trips_pb2.CreatePublicTripReq(
225 node_id=node_id,
226 from_date=(today() - timedelta(days=2)).isoformat(),
227 to_date=(today() + timedelta(days=1)).isoformat(),
228 description="Visiting town!",
229 )
230 )
231 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
233 # from_date after to_date
234 with pytest.raises(grpc.RpcError) as e:
235 api.CreatePublicTrip(
236 public_trips_pb2.CreatePublicTripReq(
237 node_id=node_id,
238 from_date=(today() + timedelta(days=10)).isoformat(),
239 to_date=(today() + timedelta(days=5)).isoformat(),
240 description="Visiting town!",
241 )
242 )
243 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
245 # empty description
246 with pytest.raises(grpc.RpcError) as e:
247 api.CreatePublicTrip(
248 public_trips_pb2.CreatePublicTripReq(
249 node_id=node_id,
250 from_date=(today() + timedelta(days=5)).isoformat(),
251 to_date=(today() + timedelta(days=10)).isoformat(),
252 description=" ",
253 )
254 )
255 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
257 # invalid date format
258 with pytest.raises(grpc.RpcError) as e:
259 api.CreatePublicTrip(
260 public_trips_pb2.CreatePublicTripReq(
261 node_id=node_id,
262 from_date="not-a-date",
263 to_date=(today() + timedelta(days=10)).isoformat(),
264 description="Visiting town!",
265 )
266 )
267 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
270def test_create_public_trip_overlap(db):
271 user, token = generate_user()
272 node_id = _make_node()
274 _create_trip_directly(
275 user.id,
276 node_id,
277 today() + timedelta(days=5),
278 today() + timedelta(days=10),
279 )
281 with public_trips_session(token) as api:
282 # overlapping dates should fail
283 with pytest.raises(grpc.RpcError) as e:
284 api.CreatePublicTrip(
285 public_trips_pb2.CreatePublicTripReq(
286 node_id=node_id,
287 from_date=(today() + timedelta(days=8)).isoformat(),
288 to_date=(today() + timedelta(days=12)).isoformat(),
289 description=VALID_DESCRIPTION,
290 )
291 )
292 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
294 # non-overlapping dates should succeed
295 res = api.CreatePublicTrip(
296 public_trips_pb2.CreatePublicTripReq(
297 node_id=node_id,
298 from_date=(today() + timedelta(days=20)).isoformat(),
299 to_date=(today() + timedelta(days=25)).isoformat(),
300 description=VALID_DESCRIPTION,
301 )
302 )
303 assert res.trip_id > 0
306def test_create_public_trip_closed_trip_allows_new_overlap(db):
307 user, token = generate_user()
308 node_id = _make_node()
310 _create_trip_directly(
311 user.id,
312 node_id,
313 today() + timedelta(days=5),
314 today() + timedelta(days=10),
315 status=PublicTripStatus.closed,
316 )
318 with public_trips_session(token) as api:
319 # closed trips shouldn't block new overlapping ones
320 res = api.CreatePublicTrip(
321 public_trips_pb2.CreatePublicTripReq(
322 node_id=node_id,
323 from_date=(today() + timedelta(days=7)).isoformat(),
324 to_date=(today() + timedelta(days=12)).isoformat(),
325 description=VALID_DESCRIPTION,
326 )
327 )
328 assert res.trip_id > 0
331def test_get_public_trip(db):
332 user, token = generate_user()
333 node_id = _make_node()
334 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
336 with public_trips_session(token) as api:
337 res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
338 assert res.trip_id == trip_id
339 assert res.user.user_id == user.id
340 assert res.node_slug == "test-community"
343def test_get_public_trip_not_found(db):
344 _, token = generate_user()
345 with public_trips_session(token) as api:
346 with pytest.raises(grpc.RpcError) as e:
347 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=999999))
348 assert e.value.code() == grpc.StatusCode.NOT_FOUND
351def test_list_public_trips(db):
352 traveler, _ = generate_user()
353 _, host_token = generate_user()
354 node_id = _make_node()
356 trip1 = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
357 trip2 = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=20), today() + timedelta(days=25))
359 with public_trips_session(host_token) as api:
360 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
361 returned_ids = {t.trip_id for t in res.public_trips}
362 assert returned_ids == {trip1, trip2}
363 assert all(t.node_slug == "test-community" for t in res.public_trips)
366def test_list_public_trips_filters_closed_and_past(db):
367 traveler, _ = generate_user()
368 _, host_token = generate_user()
369 node_id = _make_node()
371 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
372 # closed trip - should be hidden
373 _create_trip_directly(
374 traveler.id,
375 node_id,
376 today() + timedelta(days=15),
377 today() + timedelta(days=20),
378 status=PublicTripStatus.closed,
379 )
380 # past trip (to_date < today) - should be hidden
381 _create_trip_directly(traveler.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
383 with public_trips_session(host_token) as api:
384 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
385 assert [t.trip_id for t in res.public_trips] == [active]
388def test_list_public_trips_hides_invisible_user(db):
389 traveler, _ = generate_user()
390 _, host_token = generate_user()
391 node_id = _make_node()
393 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
395 # soft-delete the traveler
396 with session_scope() as session:
397 t = session.execute(select(User).where(User.id == traveler.id)).scalar_one()
398 t.deleted_at = now()
400 with public_trips_session(host_token) as api:
401 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
402 assert len(res.public_trips) == 0
405def test_list_public_trips_pagination(db):
406 traveler, _ = generate_user()
407 _, host_token = generate_user()
408 node_id = _make_node()
410 trip_ids = [
411 _create_trip_directly(
412 traveler.id, node_id, today() + timedelta(days=5 + i * 10), today() + timedelta(days=10 + i * 10)
413 )
414 for i in range(5)
415 ]
417 with public_trips_session(host_token) as api:
418 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2))
419 assert [t.trip_id for t in res.public_trips] == [trip_ids[4], trip_ids[3]]
420 assert res.next_page_token
422 res2 = api.ListPublicTrips(
423 public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2, page_token=res.next_page_token)
424 )
425 assert [t.trip_id for t in res2.public_trips] == [trip_ids[2], trip_ids[1]]
426 assert res2.next_page_token
428 res3 = api.ListPublicTrips(
429 public_trips_pb2.ListPublicTripsReq(community_id=node_id, page_size=2, page_token=res2.next_page_token)
430 )
431 assert [t.trip_id for t in res3.public_trips] == [trip_ids[0]]
432 assert not res3.next_page_token
435def test_list_public_trips_by_user_self_sees_all(db):
436 user, token = generate_user()
437 other, _ = generate_user()
438 node_id = _make_node()
440 mine_active = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
441 mine_closed = _create_trip_directly(
442 user.id,
443 node_id,
444 today() + timedelta(days=15),
445 today() + timedelta(days=20),
446 status=PublicTripStatus.closed,
447 )
448 mine_past = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
449 # other user's trip should not be returned
450 _create_trip_directly(other.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
452 with public_trips_session(token) as api:
453 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=user.id))
454 assert {t.trip_id for t in res.public_trips} == {mine_active, mine_closed, mine_past}
457def test_list_public_trips_by_user_other_filters_inactive_and_past(db):
458 traveler, _ = generate_user()
459 _, viewer_token = generate_user()
460 node_id = _make_node()
462 active = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
463 # closed trip - hidden from others
464 _create_trip_directly(
465 traveler.id,
466 node_id,
467 today() + timedelta(days=15),
468 today() + timedelta(days=20),
469 status=PublicTripStatus.closed,
470 )
471 # past trip - hidden from others
472 _create_trip_directly(traveler.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
474 with public_trips_session(viewer_token) as api:
475 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
476 assert [t.trip_id for t in res.public_trips] == [active]
479def test_list_public_trips_by_user_invisible_user(db):
480 traveler, _ = generate_user()
481 _, viewer_token = generate_user()
482 node_id = _make_node()
484 _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
486 # soft-delete the traveler
487 with session_scope() as session:
488 t = session.execute(select(User).where(User.id == traveler.id)).scalar_one()
489 t.deleted_at = now()
491 with public_trips_session(viewer_token) as api:
492 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
493 assert len(res.public_trips) == 0
496def test_update_public_trip_close(db):
497 user, token = generate_user()
498 node_id = _make_node()
499 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
501 with public_trips_session(token) as api:
502 res = api.UpdatePublicTrip(
503 public_trips_pb2.UpdatePublicTripReq(
504 trip_id=trip_id,
505 status=public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED,
506 )
507 )
508 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED
510 with session_scope() as session:
511 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
512 assert trip.status == PublicTripStatus.closed
515def test_update_public_trip_reopen(db):
516 user, token = generate_user()
517 node_id = _make_node()
518 trip_id = _create_trip_directly(
519 user.id,
520 node_id,
521 today() + timedelta(days=5),
522 today() + timedelta(days=10),
523 status=PublicTripStatus.closed,
524 )
526 with public_trips_session(token) as api:
527 res = api.UpdatePublicTrip(
528 public_trips_pb2.UpdatePublicTripReq(
529 trip_id=trip_id,
530 status=public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST,
531 )
532 )
533 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST
535 with session_scope() as session:
536 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
537 assert trip.status == PublicTripStatus.searching_for_host
540def test_update_public_trip_cant_reopen_past_trip(db):
541 user, token = generate_user()
542 node_id = _make_node()
543 trip_id = _create_trip_directly(
544 user.id,
545 node_id,
546 today() - timedelta(days=10),
547 today() - timedelta(days=1),
548 status=PublicTripStatus.closed,
549 )
551 with public_trips_session(token) as api:
552 with pytest.raises(grpc.RpcError) as e:
553 api.UpdatePublicTrip(
554 public_trips_pb2.UpdatePublicTripReq(
555 trip_id=trip_id,
556 status=public_trips_pb2.PUBLIC_TRIP_STATUS_SEARCHING_FOR_HOST,
557 )
558 )
559 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
562def test_update_public_trip_close_past_trip_allowed(db):
563 # Closing a trip whose dates are in the past is allowed, even though content edits are not.
564 user, token = generate_user()
565 node_id = _make_node()
566 trip_id = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=1))
568 with public_trips_session(token) as api:
569 res = api.UpdatePublicTrip(
570 public_trips_pb2.UpdatePublicTripReq(
571 trip_id=trip_id,
572 status=public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED,
573 )
574 )
575 assert res.status == public_trips_pb2.PUBLIC_TRIP_STATUS_CLOSED
578def test_update_public_trip_description_only(db):
579 user, token = generate_user()
580 node_id = _make_node()
581 trip_id = _create_trip_directly(
582 user.id,
583 node_id,
584 today() + timedelta(days=5),
585 today() + timedelta(days=10),
586 description="Original description",
587 )
589 updated = VALID_DESCRIPTION + " Updated plans."
591 with public_trips_session(token) as api:
592 res = api.UpdatePublicTrip(
593 public_trips_pb2.UpdatePublicTripReq(
594 trip_id=trip_id,
595 description=updated,
596 )
597 )
598 assert res.trip_id == trip_id
599 assert res.description == updated
600 # dates should be unchanged
601 assert res.from_date == (today() + timedelta(days=5)).isoformat()
602 assert res.to_date == (today() + timedelta(days=10)).isoformat()
604 with session_scope() as session:
605 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
606 assert trip.description == updated
609def test_update_public_trip_dates(db):
610 user, token = generate_user()
611 node_id = _make_node()
612 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
614 new_from = today() + timedelta(days=7)
615 new_to = today() + timedelta(days=14)
617 with public_trips_session(token) as api:
618 res = api.UpdatePublicTrip(
619 public_trips_pb2.UpdatePublicTripReq(
620 trip_id=trip_id,
621 from_date=new_from.isoformat(),
622 to_date=new_to.isoformat(),
623 )
624 )
625 assert res.from_date == new_from.isoformat()
626 assert res.to_date == new_to.isoformat()
629def test_update_public_trip_not_owner(db):
630 user, _ = generate_user()
631 _, other_token = generate_user()
632 node_id = _make_node()
633 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
635 with public_trips_session(other_token) as api:
636 with pytest.raises(grpc.RpcError) as e:
637 api.UpdatePublicTrip(
638 public_trips_pb2.UpdatePublicTripReq(
639 trip_id=trip_id,
640 description="I don't own this!",
641 )
642 )
643 assert e.value.code() == grpc.StatusCode.NOT_FOUND
646def test_update_public_trip_in_past(db):
647 user, token = generate_user()
648 node_id = _make_node()
649 trip_id = _create_trip_directly(user.id, node_id, today() - timedelta(days=10), today() - timedelta(days=2))
651 with public_trips_session(token) as api:
652 with pytest.raises(grpc.RpcError) as e:
653 api.UpdatePublicTrip(
654 public_trips_pb2.UpdatePublicTripReq(
655 trip_id=trip_id,
656 description="Too late!",
657 )
658 )
659 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
662def test_update_public_trip_date_validation(db):
663 user, token = generate_user()
664 node_id = _make_node()
665 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
667 with public_trips_session(token) as api:
668 # from_date after to_date (using the stored to_date of today+10)
669 with pytest.raises(grpc.RpcError) as e:
670 api.UpdatePublicTrip(
671 public_trips_pb2.UpdatePublicTripReq(
672 trip_id=trip_id,
673 from_date=(today() + timedelta(days=20)).isoformat(),
674 )
675 )
676 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
678 # Empty description
679 with pytest.raises(grpc.RpcError) as e:
680 api.UpdatePublicTrip(
681 public_trips_pb2.UpdatePublicTripReq(
682 trip_id=trip_id,
683 description=" ",
684 )
685 )
686 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
689def test_create_public_trip_description_too_short(db):
690 _, token = generate_user()
691 node_id = _make_node()
693 with public_trips_session(token) as api:
694 with pytest.raises(grpc.RpcError) as e:
695 api.CreatePublicTrip(
696 public_trips_pb2.CreatePublicTripReq(
697 node_id=node_id,
698 from_date=(today() + timedelta(days=5)).isoformat(),
699 to_date=(today() + timedelta(days=10)).isoformat(),
700 description="Too short.",
701 )
702 )
703 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
704 assert "150" in (e.value.details() or "")
707def test_update_public_trip_description_too_short(db):
708 user, token = generate_user()
709 node_id = _make_node()
710 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
712 with public_trips_session(token) as api:
713 with pytest.raises(grpc.RpcError) as e:
714 api.UpdatePublicTrip(
715 public_trips_pb2.UpdatePublicTripReq(
716 trip_id=trip_id,
717 description="Too short.",
718 )
719 )
720 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
721 assert "150" in (e.value.details() or "")
724def test_same_gender_only_create_and_retrieve(db):
725 user, token = generate_user(gender="Woman")
726 node_id = _make_node()
728 from_date = today() + timedelta(days=5)
729 to_date = today() + timedelta(days=10)
731 with public_trips_session(token) as api:
732 res = api.CreatePublicTrip(
733 public_trips_pb2.CreatePublicTripReq(
734 node_id=node_id,
735 from_date=from_date.isoformat(),
736 to_date=to_date.isoformat(),
737 description=VALID_DESCRIPTION,
738 same_gender_only=True,
739 )
740 )
741 assert res.same_gender_only is True
743 with session_scope() as session:
744 trip = session.execute(select(PublicTrip).where(PublicTrip.id == res.trip_id)).scalar_one()
745 assert trip.same_gender_only is True
748def test_same_gender_only_visibility_list_and_get(db):
749 traveler, _ = generate_user(gender="Woman")
750 _, same_gender_token = generate_user(gender="Woman")
751 _, diff_gender_token = generate_user(gender="Man")
752 node_id = _make_node()
754 filtered_trip_id = _create_trip_directly(
755 traveler.id,
756 node_id,
757 today() + timedelta(days=5),
758 today() + timedelta(days=10),
759 same_gender_only=True,
760 )
761 open_trip_id = _create_trip_directly(
762 traveler.id,
763 node_id,
764 today() + timedelta(days=20),
765 today() + timedelta(days=25),
766 )
768 with public_trips_session(same_gender_token) as api:
769 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
770 assert {t.trip_id for t in res.public_trips} == {filtered_trip_id, open_trip_id}
772 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=filtered_trip_id))
773 assert get_res.trip_id == filtered_trip_id
774 assert get_res.same_gender_only is True
776 with public_trips_session(diff_gender_token) as api:
777 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
778 assert [t.trip_id for t in res.public_trips] == [open_trip_id]
780 with pytest.raises(grpc.RpcError) as e:
781 api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=filtered_trip_id))
782 assert e.value.code() == grpc.StatusCode.NOT_FOUND
785def test_same_gender_only_moderator_bypass(db):
786 traveler, _ = generate_user(gender="Woman")
787 mod, mod_token = generate_user(gender="Man")
788 node_id = _make_node()
789 _make_node_admin(mod.id, node_id)
791 trip_id = _create_trip_directly(
792 traveler.id,
793 node_id,
794 today() + timedelta(days=5),
795 today() + timedelta(days=10),
796 same_gender_only=True,
797 )
799 with public_trips_session(mod_token) as api:
800 res = api.ListPublicTrips(public_trips_pb2.ListPublicTripsReq(community_id=node_id))
801 assert any(t.trip_id == trip_id for t in res.public_trips)
803 get_res = api.GetPublicTrip(public_trips_pb2.GetPublicTripReq(trip_id=trip_id))
804 assert get_res.trip_id == trip_id
807def test_same_gender_only_owner_always_sees_own_trips(db):
808 traveler, traveler_token = generate_user(gender="Woman")
809 _, diff_gender_token = generate_user(gender="Man")
810 node_id = _make_node()
812 trip_id = _create_trip_directly(
813 traveler.id,
814 node_id,
815 today() + timedelta(days=5),
816 today() + timedelta(days=10),
817 same_gender_only=True,
818 )
820 # Owner always sees their own trips (is_self path skips the gender filter)
821 with public_trips_session(traveler_token) as api:
822 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
823 assert any(t.trip_id == trip_id for t in res.public_trips)
825 # Different-gender viewer doesn't see it on the traveler's profile
826 with public_trips_session(diff_gender_token) as api:
827 res = api.ListPublicTripsByUser(public_trips_pb2.ListPublicTripsByUserReq(user_id=traveler.id))
828 assert not any(t.trip_id == trip_id for t in res.public_trips)
831def test_same_gender_only_update(db):
832 user, token = generate_user(gender="Woman")
833 node_id = _make_node()
834 trip_id = _create_trip_directly(user.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
836 with public_trips_session(token) as api:
837 res = api.UpdatePublicTrip(
838 public_trips_pb2.UpdatePublicTripReq(
839 trip_id=trip_id,
840 same_gender_only=True,
841 )
842 )
843 assert res.same_gender_only is True
845 res = api.UpdatePublicTrip(
846 public_trips_pb2.UpdatePublicTripReq(
847 trip_id=trip_id,
848 same_gender_only=False,
849 )
850 )
851 assert res.same_gender_only is False
853 with session_scope() as session:
854 trip = session.execute(select(PublicTrip).where(PublicTrip.id == trip_id)).scalar_one()
855 assert trip.same_gender_only is False