Coverage for app/backend/src/tests/test_moderation.py: 100%
1313 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-14 11:44 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-14 11:44 +0000
1"""
2Comprehensive tests for the Unified Moderation System (UMS)
3"""
5from datetime import datetime, timedelta
7import grpc
8import pytest
9from google.protobuf import empty_pb2
10from sqlalchemy.sql import select
12from couchers.config import config
13from couchers.db import session_scope
14from couchers.jobs.handlers import auto_approve_moderation_queue
15from couchers.jobs.worker import process_job
16from couchers.models import (
17 AdminAction,
18 EventOccurrence,
19 FriendRelationship,
20 GroupChat,
21 HostRequest,
22 ModerationAction,
23 ModerationLog,
24 ModerationObjectType,
25 ModerationQueueItem,
26 ModerationState,
27 ModerationTrigger,
28 ModerationVisibility,
29 User,
30)
31from couchers.moderation.utils import create_moderation
32from couchers.proto import api_pb2, conversations_pb2, events_pb2, moderation_pb2, notifications_pb2, requests_pb2
33from couchers.utils import Timestamp_from_datetime, now, today
34from tests.fixtures.db import generate_user, make_friends
35from tests.fixtures.misc import EmailCollector, PushCollector, process_jobs
36from tests.fixtures.sessions import (
37 api_session,
38 conversations_session,
39 events_session,
40 notifications_session,
41 real_moderation_session,
42 requests_session,
43)
44from tests.test_communities import create_community
45from tests.test_requests import valid_request_text
48@pytest.fixture(autouse=True)
49def _(testconfig):
50 pass
53def create_test_host_request_with_moderation(surfer_token, host_user_id):
54 """Helper to create a host request and return its moderation state ID"""
55 today_plus_2 = (today() + timedelta(days=2)).isoformat()
56 today_plus_3 = (today() + timedelta(days=3)).isoformat()
58 with requests_session(surfer_token) as api:
59 hr_id = api.CreateHostRequest(
60 requests_pb2.CreateHostRequestReq(
61 host_user_id=host_user_id,
62 from_date=today_plus_2,
63 to_date=today_plus_3,
64 text=valid_request_text(),
65 )
66 ).host_request_id
68 with session_scope() as session:
69 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
70 return hr.moderation_state_id
73# ============================================================================
74# Tests for moderation helper functions
75# ============================================================================
78def test_create_moderation(db):
79 """Test creating a moderation state with associated log entry"""
80 user, _ = generate_user()
82 with session_scope() as session:
83 # Create a moderation state
84 moderation_state = create_moderation(
85 session=session,
86 object_type=ModerationObjectType.host_request,
87 object_id=123,
88 creator_user_id=user.id,
89 )
91 assert moderation_state.object_type == ModerationObjectType.host_request
92 assert moderation_state.object_id == 123
93 assert moderation_state.visibility == ModerationVisibility.shadowed
95 # Check that log entry was created
96 log_entries = (
97 session.execute(select(ModerationLog).where(ModerationLog.moderation_state_id == moderation_state.id))
98 .scalars()
99 .all()
100 )
102 assert len(log_entries) == 1
103 assert log_entries[0].action == ModerationAction.create
104 assert log_entries[0].reason == "Object created."
105 assert log_entries[0].moderator_user_id == user.id
108def test_add_to_moderation_queue(db):
109 """Test adding content to moderation queue via API"""
110 super_user, super_token = generate_user(is_superuser=True)
111 user1, token1 = generate_user()
112 user2, _ = generate_user()
114 today_plus_2 = (today() + timedelta(days=2)).isoformat()
115 today_plus_3 = (today() + timedelta(days=3)).isoformat()
117 # Create a real host request (which automatically creates moderation state and adds to queue)
118 with requests_session(token1) as api:
119 host_request_id = api.CreateHostRequest(
120 requests_pb2.CreateHostRequestReq(
121 host_user_id=user2.id,
122 from_date=today_plus_2,
123 to_date=today_plus_3,
124 text=valid_request_text(),
125 )
126 ).host_request_id
128 # Get the moderation state ID
129 state_id = None
130 with session_scope() as session:
131 host_request = session.execute(
132 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
133 ).scalar_one()
134 state_id = host_request.moderation_state_id
136 # Add another item to moderation queue via API (the first one was created automatically)
137 with real_moderation_session(super_token) as api:
138 res = api.FlagContentForReview(
139 moderation_pb2.FlagContentForReviewReq(
140 moderation_state_id=state_id,
141 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG,
142 reason="Admin manually flagged for additional review",
143 )
144 )
146 assert res.queue_item.moderation_state_id == state_id
147 assert res.queue_item.trigger == moderation_pb2.MODERATION_TRIGGER_USER_FLAG
148 assert res.queue_item.reason == "Admin manually flagged for additional review"
149 assert res.queue_item.moderation_state.author.user_id == user1.id
150 assert res.queue_item.is_resolved == False
153def test_moderate_content(db):
154 """Test moderating content via API"""
155 super_user, super_token = generate_user(is_superuser=True)
156 user, token = generate_user()
157 host, _ = generate_user()
159 today_plus_2 = (today() + timedelta(days=2)).isoformat()
160 today_plus_3 = (today() + timedelta(days=3)).isoformat()
162 # Create a real host request
163 state_id = None
164 with requests_session(token) as api:
165 hr_id = api.CreateHostRequest(
166 requests_pb2.CreateHostRequestReq(
167 host_user_id=host.id,
168 from_date=today_plus_2,
169 to_date=today_plus_3,
170 text=valid_request_text(),
171 )
172 ).host_request_id
174 with session_scope() as session:
175 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
176 state_id = hr.moderation_state_id
178 # Moderate the content via API
179 with real_moderation_session(super_token) as api:
180 res = api.ModerateContent(
181 moderation_pb2.ModerateContentReq(
182 moderation_state_id=state_id,
183 action=moderation_pb2.MODERATION_ACTION_APPROVE,
184 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
185 reason="Content looks good",
186 )
187 )
189 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
191 # Check that state was updated in database
192 with session_scope() as session:
193 updated_state = session.get_one(ModerationState, state_id)
194 assert updated_state.visibility == ModerationVisibility.visible
196 # Check that log entry was created
197 log_entries = (
198 session.execute(
199 select(ModerationLog)
200 .where(ModerationLog.moderation_state_id == state_id)
201 .order_by(ModerationLog.time.desc(), ModerationLog.id.desc())
202 )
203 .scalars()
204 .all()
205 )
207 assert len(log_entries) == 2 # CREATE + APPROVE
208 assert log_entries[0].action == ModerationAction.approve
209 assert log_entries[0].moderator_user_id == super_user.id
210 assert log_entries[0].reason == "Content looks good"
213def test_resolve_queue_item(db):
214 """Test resolving a moderation queue item via ModerateContent API"""
215 user1, token1 = generate_user()
216 user2, _ = generate_user()
217 moderator, moderator_token = generate_user(is_superuser=True)
219 today_plus_2 = (today() + timedelta(days=2)).isoformat()
220 today_plus_3 = (today() + timedelta(days=3)).isoformat()
222 # Create a host request using the API (which automatically creates moderation state)
223 with requests_session(token1) as api:
224 host_request_id = api.CreateHostRequest(
225 requests_pb2.CreateHostRequestReq(
226 host_user_id=user2.id,
227 from_date=today_plus_2,
228 to_date=today_plus_3,
229 text=valid_request_text(),
230 )
231 ).host_request_id
233 state_id = None
234 with session_scope() as session:
235 # Get the host request and its moderation state
236 host_request = session.execute(
237 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
238 ).scalar_one()
239 state_id = host_request.moderation_state_id
241 # The moderation state should already exist and be in the queue
242 queue_item = session.execute(
243 select(ModerationQueueItem)
244 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
245 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
246 ).scalar_one()
248 assert queue_item.resolved_by_log_id is None
250 # Approve content via API (which should resolve the queue item)
251 with real_moderation_session(moderator_token) as api:
252 api.ModerateContent(
253 moderation_pb2.ModerateContentReq(
254 moderation_state_id=state_id,
255 action=moderation_pb2.MODERATION_ACTION_APPROVE,
256 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
257 reason="Approved after review",
258 )
259 )
261 # Check that queue item was resolved
262 with session_scope() as session:
263 queue_item = session.execute(
264 select(ModerationQueueItem)
265 .where(ModerationQueueItem.moderation_state_id == state_id)
266 .where(ModerationQueueItem.resolved_by_log_id.is_not(None))
267 ).scalar_one()
268 assert queue_item.resolved_by_log_id is not None
271def test_approve_content_via_api(db):
272 """Test approving content via ModerateContent API"""
273 user1, token1 = generate_user()
274 user2, _ = generate_user()
275 moderator, moderator_token = generate_user(is_superuser=True)
277 today_plus_2 = (today() + timedelta(days=2)).isoformat()
278 today_plus_3 = (today() + timedelta(days=3)).isoformat()
280 # Create a host request using the API (which automatically creates moderation state)
281 with requests_session(token1) as api:
282 host_request_id = api.CreateHostRequest(
283 requests_pb2.CreateHostRequestReq(
284 host_user_id=user2.id,
285 from_date=today_plus_2,
286 to_date=today_plus_3,
287 text=valid_request_text(),
288 )
289 ).host_request_id
291 state_id = None
292 with session_scope() as session:
293 # Get the host request and its moderation state
294 host_request = session.execute(
295 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
296 ).scalar_one()
297 state_id = host_request.moderation_state_id
299 # Approve via API
300 with real_moderation_session(moderator_token) as api:
301 api.ModerateContent(
302 moderation_pb2.ModerateContentReq(
303 moderation_state_id=state_id,
304 action=moderation_pb2.MODERATION_ACTION_APPROVE,
305 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
306 reason="Quick approval",
307 )
308 )
310 # Check that state was updated to VISIBLE
311 with session_scope() as session:
312 updated_state = session.get_one(ModerationState, state_id)
313 assert updated_state.visibility == ModerationVisibility.visible
315 # Check log entry
316 log_entry = session.execute(
317 select(ModerationLog)
318 .where(ModerationLog.moderation_state_id == state_id)
319 .where(ModerationLog.action == ModerationAction.approve)
320 ).scalar_one()
322 assert log_entry.moderator_user_id == moderator.id
323 assert log_entry.reason == "Quick approval"
326# ============================================================================
327# Tests for host request moderation integration
328# ============================================================================
331def test_create_host_request_creates_moderation_state(db):
332 """Test that creating a host request automatically creates a moderation state"""
333 user1, token1 = generate_user()
334 user2, token2 = generate_user()
336 today_plus_2 = (today() + timedelta(days=2)).isoformat()
337 today_plus_3 = (today() + timedelta(days=3)).isoformat()
339 with requests_session(token1) as api:
340 host_request_id = api.CreateHostRequest(
341 requests_pb2.CreateHostRequestReq(
342 host_user_id=user2.id,
343 from_date=today_plus_2,
344 to_date=today_plus_3,
345 text=valid_request_text(),
346 )
347 ).host_request_id
349 with session_scope() as session:
350 # Check that host request has a moderation state
351 host_request = session.execute(
352 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
353 ).scalar_one()
355 # Check moderation state properties
356 moderation_state = session.execute(
357 select(ModerationState).where(ModerationState.id == host_request.moderation_state_id)
358 ).scalar_one()
360 assert moderation_state.object_type == ModerationObjectType.host_request
361 assert moderation_state.object_id == host_request_id
362 assert moderation_state.visibility == ModerationVisibility.shadowed
364 # Check that it was added to moderation queue
365 queue_items = (
366 session.execute(
367 select(ModerationQueueItem)
368 .where(ModerationQueueItem.moderation_state_id == moderation_state.id)
369 .where(ModerationQueueItem.resolved_by_log_id == None)
370 )
371 .scalars()
372 .all()
373 )
375 assert len(queue_items) == 1
376 assert queue_items[0].trigger == ModerationTrigger.initial_review
377 # item_author_user_id is no longer stored in the model, it's dynamically retrieved
380def test_host_request_no_notification_before_approval(db, push_collector: PushCollector):
381 """Test that host requests don't send notifications until approved"""
382 user1, token1 = generate_user()
383 user2, token2 = generate_user()
385 today_plus_2 = (today() + timedelta(days=2)).isoformat()
386 today_plus_3 = (today() + timedelta(days=3)).isoformat()
388 with requests_session(token1) as api:
389 host_request_id = api.CreateHostRequest(
390 requests_pb2.CreateHostRequestReq(
391 host_user_id=user2.id,
392 from_date=today_plus_2,
393 to_date=today_plus_3,
394 text=valid_request_text(),
395 )
396 ).host_request_id
398 # Process all jobs (including the notification job)
399 process_jobs()
401 # No push notification should be sent yet (host requests are shadowed initially)
402 assert push_collector.count_for_user(user2.id) == 0
405def test_shadowed_notification_not_in_list_notifications(db):
406 """Test that notifications for shadowed content don't appear in ListNotifications API"""
407 user1, token1 = generate_user()
408 user2, token2 = generate_user()
410 today_plus_2 = (today() + timedelta(days=2)).isoformat()
411 today_plus_3 = (today() + timedelta(days=3)).isoformat()
413 # Create a host request (which creates a shadowed notification for the host)
414 with requests_session(token1) as api:
415 host_request_id = api.CreateHostRequest(
416 requests_pb2.CreateHostRequestReq(
417 host_user_id=user2.id,
418 from_date=today_plus_2,
419 to_date=today_plus_3,
420 text=valid_request_text(),
421 )
422 ).host_request_id
424 # Host (recipient) should NOT see the notification in ListNotifications - it's for shadowed content
425 with notifications_session(token2) as api:
426 res = api.ListNotifications(notifications_pb2.ListNotificationsReq())
427 # Should be empty - the host request is still shadowed
428 assert len(res.notifications) == 0
431def test_notification_visible_after_approval(db):
432 """Test that notifications appear in ListNotifications after content is approved"""
433 user1, token1 = generate_user()
434 user2, token2 = generate_user()
435 mod, mod_token = generate_user(is_superuser=True)
437 today_plus_2 = (today() + timedelta(days=2)).isoformat()
438 today_plus_3 = (today() + timedelta(days=3)).isoformat()
440 # Create a host request (which creates a shadowed notification for the host)
441 with requests_session(token1) as api:
442 host_request_id = api.CreateHostRequest(
443 requests_pb2.CreateHostRequestReq(
444 host_user_id=user2.id,
445 from_date=today_plus_2,
446 to_date=today_plus_3,
447 text=valid_request_text(),
448 )
449 ).host_request_id
451 # Host (recipient) should NOT see the notification initially
452 with notifications_session(token2) as api:
453 res = api.ListNotifications(notifications_pb2.ListNotificationsReq())
454 assert len(res.notifications) == 0
456 # Get the moderation state ID and approve
457 with session_scope() as session:
458 host_request = session.execute(
459 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
460 ).scalar_one()
461 state_id = host_request.moderation_state_id
463 with real_moderation_session(mod_token) as api:
464 api.ModerateContent(
465 moderation_pb2.ModerateContentReq(
466 moderation_state_id=state_id,
467 action=moderation_pb2.MODERATION_ACTION_APPROVE,
468 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
469 reason="Looks good",
470 )
471 )
473 # Now host SHOULD see the notification
474 with notifications_session(token2) as api:
475 res = api.ListNotifications(notifications_pb2.ListNotificationsReq())
476 assert len(res.notifications) == 1
477 assert res.notifications[0].topic == "host_request"
478 assert res.notifications[0].action == "create"
481def test_shadowed_host_request_visible_to_author_only(db):
482 """Test that SHADOWED host requests are visible only to the author (surfer), not the recipient (host)"""
483 user1, token1 = generate_user()
484 user2, token2 = generate_user()
486 today_plus_2 = (today() + timedelta(days=2)).isoformat()
487 today_plus_3 = (today() + timedelta(days=3)).isoformat()
489 with requests_session(token1) as api:
490 host_request_id = api.CreateHostRequest(
491 requests_pb2.CreateHostRequestReq(
492 host_user_id=user2.id,
493 from_date=today_plus_2,
494 to_date=today_plus_3,
495 text=valid_request_text(),
496 )
497 ).host_request_id
499 # Surfer (author) can see it with GetHostRequest
500 with requests_session(token1) as api:
501 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
502 assert res.host_request_id == host_request_id
503 assert res.latest_message.text.text == valid_request_text()
505 # Host (recipient) CANNOT see it with GetHostRequest - it's shadowed
506 with requests_session(token2) as api:
507 with pytest.raises(grpc.RpcError) as e:
508 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
509 assert e.value.code() == grpc.StatusCode.NOT_FOUND
512def test_unlisted_host_request_not_in_lists(db):
513 """Test that SHADOWED host requests are visible to author but not to recipient"""
514 user1, token1 = generate_user()
515 user2, token2 = generate_user()
517 today_plus_2 = (today() + timedelta(days=2)).isoformat()
518 today_plus_3 = (today() + timedelta(days=3)).isoformat()
520 with requests_session(token1) as api:
521 host_request_id = api.CreateHostRequest(
522 requests_pb2.CreateHostRequestReq(
523 host_user_id=user2.id,
524 from_date=today_plus_2,
525 to_date=today_plus_3,
526 text=valid_request_text(),
527 )
528 ).host_request_id
530 # Surfer (author) should see it in their sent list even though it's SHADOWED
531 with requests_session(token1) as api:
532 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
533 assert len(res.host_requests) == 1
535 # Host should NOT see it in their received list (still SHADOWED from them)
536 with requests_session(token2) as api:
537 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
538 assert len(res.host_requests) == 0
541def test_approved_host_request_in_lists_and_notifications(db, push_collector: PushCollector):
542 """Test that approved host requests appear in lists and send notifications"""
543 user1, token1 = generate_user()
544 user2, token2 = generate_user()
545 mod, mod_token = generate_user(is_superuser=True)
547 today_plus_2 = (today() + timedelta(days=2)).isoformat()
548 today_plus_3 = (today() + timedelta(days=3)).isoformat()
550 with requests_session(token1) as api:
551 host_request_id = api.CreateHostRequest(
552 requests_pb2.CreateHostRequestReq(
553 host_user_id=user2.id,
554 from_date=today_plus_2,
555 to_date=today_plus_3,
556 text=valid_request_text(),
557 )
558 ).host_request_id
560 # Process the initial notification job - should be deferred (no notification sent)
561 process_jobs()
562 assert push_collector.count_for_user(user2.id) == 0
564 # Get the moderation state ID
565 state_id = None
566 with session_scope() as session:
567 host_request = session.execute(
568 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
569 ).scalar_one()
570 state_id = host_request.moderation_state_id
572 # Approve the host request via API
573 with real_moderation_session(mod_token) as api:
574 api.ModerateContent(
575 moderation_pb2.ModerateContentReq(
576 moderation_state_id=state_id,
577 action=moderation_pb2.MODERATION_ACTION_APPROVE,
578 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
579 reason="Looks good",
580 )
581 )
583 # Process the re-queued notification job - should now send notification
584 process_jobs()
586 # Now surfer SHOULD see it in their sent list
587 with requests_session(token1) as api:
588 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
589 assert len(res.host_requests) == 1
590 assert res.host_requests[0].host_request_id == host_request_id
592 # Host SHOULD see it in their received list
593 with requests_session(token2) as api:
594 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
595 assert len(res.host_requests) == 1
596 assert res.host_requests[0].host_request_id == host_request_id
598 # After approval, the host should have received a push notification
599 assert push_collector.pop_for_user(user2.id, last=True).topic_action == "host_request:create"
602def test_hidden_host_request_invisible_to_all(db):
603 """Test that HIDDEN host requests are invisible to everyone except moderators"""
604 user1, token1 = generate_user()
605 user2, token2 = generate_user()
606 user3, token3 = generate_user() # Third party
607 moderator, moderator_token = generate_user(is_superuser=True)
609 today_plus_2 = (today() + timedelta(days=2)).isoformat()
610 today_plus_3 = (today() + timedelta(days=3)).isoformat()
612 with requests_session(token1) as api:
613 host_request_id = api.CreateHostRequest(
614 requests_pb2.CreateHostRequestReq(
615 host_user_id=user2.id,
616 from_date=today_plus_2,
617 to_date=today_plus_3,
618 text=valid_request_text(),
619 )
620 ).host_request_id
622 # Get the moderation state ID
623 state_id = None
624 with session_scope() as session:
625 host_request = session.execute(
626 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
627 ).scalar_one()
628 state_id = host_request.moderation_state_id
630 # Hide the host request via API (e.g., spam/abuse)
631 with real_moderation_session(moderator_token) as api:
632 api.ModerateContent(
633 moderation_pb2.ModerateContentReq(
634 moderation_state_id=state_id,
635 action=moderation_pb2.MODERATION_ACTION_HIDE,
636 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
637 reason="Spam content",
638 )
639 )
641 # Surfer can't see it with GetHostRequest
642 with requests_session(token1) as api:
643 with pytest.raises(grpc.RpcError) as e:
644 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
645 assert e.value.code() == grpc.StatusCode.NOT_FOUND
647 # Host can't see it with GetHostRequest
648 with requests_session(token2) as api:
649 with pytest.raises(grpc.RpcError) as e:
650 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
651 assert e.value.code() == grpc.StatusCode.NOT_FOUND
653 # Third party definitely can't see it
654 with requests_session(token3) as api:
655 with pytest.raises(grpc.RpcError) as e:
656 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
657 assert e.value.code() == grpc.StatusCode.NOT_FOUND
659 # Not in any lists
660 with requests_session(token1) as api:
661 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
662 assert len(res.host_requests) == 0
664 with requests_session(token2) as api:
665 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
666 assert len(res.host_requests) == 0
669def test_multiple_host_requests_listing_visibility(db):
670 """Test that ListHostRequests correctly filters based on moderation state"""
671 user1, token1 = generate_user()
672 user2, token2 = generate_user()
673 moderator, moderator_token = generate_user(is_superuser=True)
675 today_plus_2 = (today() + timedelta(days=2)).isoformat()
676 today_plus_3 = (today() + timedelta(days=3)).isoformat()
678 # Create 3 host requests
679 host_request_ids = []
680 state_ids = []
681 with requests_session(token1) as api:
682 for i in range(3):
683 hr_id = api.CreateHostRequest(
684 requests_pb2.CreateHostRequestReq(
685 host_user_id=user2.id,
686 from_date=today_plus_2,
687 to_date=today_plus_3,
688 text=valid_request_text(f"Test request {i + 1}"),
689 )
690 ).host_request_id
691 host_request_ids.append(hr_id)
693 # Get state IDs
694 with session_scope() as session:
695 for hr_id in host_request_ids:
696 host_request = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
697 state_ids.append(host_request.moderation_state_id)
699 # Approve the first one via API
700 with real_moderation_session(moderator_token) as api:
701 api.ModerateContent(
702 moderation_pb2.ModerateContentReq(
703 moderation_state_id=state_ids[0],
704 action=moderation_pb2.MODERATION_ACTION_APPROVE,
705 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
706 reason="Approved",
707 )
708 )
710 # Hide the third one via API
711 with real_moderation_session(moderator_token) as api:
712 api.ModerateContent(
713 moderation_pb2.ModerateContentReq(
714 moderation_state_id=state_ids[2],
715 action=moderation_pb2.MODERATION_ACTION_HIDE,
716 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
717 reason="Spam",
718 )
719 )
721 # Surfer should see the approved one and the shadowed one (author can see their SHADOWED content)
722 with requests_session(token1) as api:
723 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
724 assert len(res.host_requests) == 2
725 visible_ids = {hr.host_request_id for hr in res.host_requests}
726 assert visible_ids == {host_request_ids[0], host_request_ids[1]}
728 # Host should see only the approved one in received list
729 with requests_session(token2) as api:
730 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
731 assert len(res.host_requests) == 1
732 assert res.host_requests[0].host_request_id == host_request_ids[0]
735def test_moderation_log_tracking(db):
736 """Test that moderation actions are properly logged via API"""
737 user, user_token = generate_user()
738 host, _ = generate_user()
739 moderator1, moderator1_token = generate_user(is_superuser=True)
740 moderator2, moderator2_token = generate_user(is_superuser=True)
742 # Create a real host request
743 state_id = create_test_host_request_with_moderation(user_token, host.id)
745 # Perform several moderation actions via API
746 with real_moderation_session(moderator1_token) as api:
747 api.ModerateContent(
748 moderation_pb2.ModerateContentReq(
749 moderation_state_id=state_id,
750 action=moderation_pb2.MODERATION_ACTION_APPROVE,
751 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
752 reason="Looks good initially",
753 )
754 )
756 with real_moderation_session(moderator2_token) as api:
757 api.FlagContentForReview(
758 moderation_pb2.FlagContentForReviewReq(
759 moderation_state_id=state_id,
760 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW,
761 reason="Wait, this needs another look",
762 )
763 )
764 # Shadow it back
765 api.ModerateContent(
766 moderation_pb2.ModerateContentReq(
767 moderation_state_id=state_id,
768 action=moderation_pb2.MODERATION_ACTION_HIDE,
769 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
770 reason="Wait, this needs another look",
771 )
772 )
774 with real_moderation_session(moderator1_token) as api:
775 api.ModerateContent(
776 moderation_pb2.ModerateContentReq(
777 moderation_state_id=state_id,
778 action=moderation_pb2.MODERATION_ACTION_HIDE,
779 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
780 reason="Actually it's spam",
781 )
782 )
784 # Check all log entries
785 with session_scope() as session:
786 log_entries = (
787 session.execute(
788 select(ModerationLog)
789 .where(ModerationLog.moderation_state_id == state_id)
790 .order_by(ModerationLog.time.asc())
791 )
792 .scalars()
793 .all()
794 )
796 # CREATE + APPROVE + HIDE + HIDE (shadowing back counts as HIDE action)
797 assert len(log_entries) >= 3
799 assert log_entries[0].action == ModerationAction.create
800 assert log_entries[0].moderator_user_id == user.id
801 assert log_entries[0].reason == "Object created."
803 assert log_entries[1].action == ModerationAction.approve
804 assert log_entries[1].moderator_user_id == moderator1.id
805 assert log_entries[1].reason == "Looks good initially"
807 # The last action should be hiding
808 assert log_entries[-1].action == ModerationAction.hide
809 assert log_entries[-1].moderator_user_id == moderator1.id
810 assert log_entries[-1].reason == "Actually it's spam"
813def test_moderation_queue_workflow(db):
814 """Test the full moderation queue workflow via API"""
815 user1, token1 = generate_user()
816 user2, _ = generate_user()
817 moderator, moderator_token = generate_user(is_superuser=True)
819 today_plus_2 = (today() + timedelta(days=2)).isoformat()
820 today_plus_3 = (today() + timedelta(days=3)).isoformat()
822 # Create a host request using the API (which automatically creates moderation state and adds to queue)
823 with requests_session(token1) as api:
824 host_request_id = api.CreateHostRequest(
825 requests_pb2.CreateHostRequestReq(
826 host_user_id=user2.id,
827 from_date=today_plus_2,
828 to_date=today_plus_3,
829 text=valid_request_text(),
830 )
831 ).host_request_id
833 state_id = None
834 queue_item_id = None
835 with session_scope() as session:
836 # Get the host request and its moderation state
837 host_request = session.execute(
838 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
839 ).scalar_one()
840 state_id = host_request.moderation_state_id
842 # The queue item should already exist (created automatically)
843 queue_item = session.execute(
844 select(ModerationQueueItem)
845 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
846 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
847 ).scalar_one()
848 queue_item_id = queue_item.id
850 # Verify it's in the queue
851 unresolved_items = (
852 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.resolved_by_log_id == None))
853 .scalars()
854 .all()
855 )
857 assert len(unresolved_items) >= 1
858 assert queue_item.id in [item.id for item in unresolved_items]
860 # Moderator reviews and approves via API (which also resolves the queue item)
861 with real_moderation_session(moderator_token) as api:
862 api.ModerateContent(
863 moderation_pb2.ModerateContentReq(
864 moderation_state_id=state_id,
865 action=moderation_pb2.MODERATION_ACTION_APPROVE,
866 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
867 reason="Content approved",
868 )
869 )
871 # Verify queue item was resolved
872 with session_scope() as session:
873 # Verify it's no longer in unresolved queue
874 unresolved_items = (
875 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.resolved_by_log_id == None))
876 .scalars()
877 .all()
878 )
880 assert queue_item_id not in [item.id for item in unresolved_items]
882 # Verify the queue item was linked to a log entry
883 queue_item = session.get_one(ModerationQueueItem, queue_item_id)
884 assert queue_item.resolved_by_log_id is not None
887# ============================================================================
888# Moderation API Tests (testing the gRPC servicer)
889# ============================================================================
892def test_GetModerationQueue_empty(db):
893 """Test getting an empty moderation queue"""
894 super_user, super_token = generate_user(is_superuser=True)
896 with real_moderation_session(super_token) as api:
897 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
898 assert len(res.queue_items) == 0
899 assert res.next_page_token == ""
902def test_GetModerationQueue_with_items(db):
903 """Test getting moderation queue with items via API"""
904 super_user, super_token = generate_user(is_superuser=True)
905 normal_user, user_token = generate_user()
906 host, _ = generate_user()
908 # Create some host requests (which automatically adds them to moderation queue)
909 state1_id = create_test_host_request_with_moderation(user_token, host.id)
910 state2_id = create_test_host_request_with_moderation(user_token, host.id)
912 with real_moderation_session(super_token) as api:
913 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
914 assert len(res.queue_items) == 2
915 assert res.queue_items[0].is_resolved == False
916 assert res.queue_items[1].is_resolved == False
919def test_GetModerationQueue_filter_by_trigger(db):
920 """Test filtering moderation queue by trigger type via API"""
921 super_user, super_token = generate_user(is_superuser=True)
922 normal_user, user_token = generate_user()
923 host, _ = generate_user()
925 # Create host requests (which automatically adds them to moderation queue with INITIAL_REVIEW)
926 state1_id = create_test_host_request_with_moderation(user_token, host.id)
927 state2_id = create_test_host_request_with_moderation(user_token, host.id)
929 # Add USER_FLAG trigger to second item via API
930 with real_moderation_session(super_token) as api:
931 api.FlagContentForReview(
932 moderation_pb2.FlagContentForReviewReq(
933 moderation_state_id=state2_id,
934 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG,
935 reason="Reported by user",
936 )
937 )
939 # Filter by INITIAL_REVIEW (should get first item and maybe both depending on how queue works)
940 with real_moderation_session(super_token) as api:
941 res = api.GetModerationQueue(
942 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW])
943 )
944 assert len(res.queue_items) == 2 # Both have INITIAL_REVIEW triggers
945 assert all(item.trigger == moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW for item in res.queue_items)
947 # Filter by USER_FLAG (should get second item only)
948 with real_moderation_session(super_token) as api:
949 res = api.GetModerationQueue(
950 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_USER_FLAG])
951 )
952 assert len(res.queue_items) == 1
953 assert res.queue_items[0].trigger == moderation_pb2.MODERATION_TRIGGER_USER_FLAG
956def test_GetModerationQueue_filter_created_before(db):
957 """Test filtering moderation queue by created_before timestamp"""
958 super_user, super_token = generate_user(is_superuser=True)
959 normal_user, user_token = generate_user()
960 host, _ = generate_user()
962 # Create host requests
963 state1_id = create_test_host_request_with_moderation(user_token, host.id)
964 state2_id = create_test_host_request_with_moderation(user_token, host.id)
966 # Backdate the first queue item
967 with session_scope() as session:
968 queue_item1 = session.execute(
969 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
970 ).scalar_one()
971 # Set it to 2 hours ago
972 queue_item1.time_created = now() - timedelta(hours=2)
974 # The second item remains at current time
976 # Filter to items created before 1 hour ago (should only get the first item)
977 cutoff_time = now() - timedelta(hours=1)
978 with real_moderation_session(super_token) as api:
979 res = api.GetModerationQueue(
980 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(cutoff_time))
981 )
982 assert len(res.queue_items) == 1
983 assert res.queue_items[0].moderation_state_id == state1_id
985 # Filter to items created before now (should get both)
986 with real_moderation_session(super_token) as api:
987 res = api.GetModerationQueue(
988 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(now() + timedelta(seconds=10)))
989 )
990 assert len(res.queue_items) == 2
992 # Filter to items created before 3 hours ago (should get none)
993 old_cutoff = now() - timedelta(hours=3)
994 with real_moderation_session(super_token) as api:
995 res = api.GetModerationQueue(
996 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(old_cutoff))
997 )
998 assert len(res.queue_items) == 0
1001def test_GetModerationQueue_filter_created_after(db):
1002 """Test filtering moderation queue by created_after timestamp"""
1003 super_user, super_token = generate_user(is_superuser=True)
1004 normal_user, user_token = generate_user()
1005 host, _ = generate_user()
1007 # Create host requests
1008 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1009 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1011 # Backdate the first queue item to 2 hours ago
1012 with session_scope() as session:
1013 queue_item1 = session.execute(
1014 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1015 ).scalar_one()
1016 queue_item1.time_created = now() - timedelta(hours=2)
1018 # The second item remains at current time
1020 # Filter to items created after 1 hour ago (should only get the second item)
1021 cutoff_time = now() - timedelta(hours=1)
1022 with real_moderation_session(super_token) as api:
1023 res = api.GetModerationQueue(
1024 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(cutoff_time))
1025 )
1026 assert len(res.queue_items) == 1
1027 assert res.queue_items[0].moderation_state_id == state2_id
1029 # Filter to items created after 3 hours ago (should get both)
1030 old_cutoff = now() - timedelta(hours=3)
1031 with real_moderation_session(super_token) as api:
1032 res = api.GetModerationQueue(
1033 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(old_cutoff))
1034 )
1035 assert len(res.queue_items) == 2
1037 # Filter to items created after now (should get none)
1038 with real_moderation_session(super_token) as api:
1039 res = api.GetModerationQueue(
1040 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(now() + timedelta(seconds=10)))
1041 )
1042 assert len(res.queue_items) == 0
1045def test_GetModerationQueue_filter_created_before_and_after(db):
1046 """Test filtering moderation queue by both created_before and created_after timestamps"""
1047 super_user, super_token = generate_user(is_superuser=True)
1048 normal_user, user_token = generate_user()
1049 host, _ = generate_user()
1051 # Create 3 host requests
1052 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1053 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1054 state3_id = create_test_host_request_with_moderation(user_token, host.id)
1056 # Set different times: state1 = 3 hours ago, state2 = 1.5 hours ago, state3 = now
1057 with session_scope() as session:
1058 queue_item1 = session.execute(
1059 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1060 ).scalar_one()
1061 queue_item1.time_created = now() - timedelta(hours=3)
1063 queue_item2 = session.execute(
1064 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state2_id)
1065 ).scalar_one()
1066 queue_item2.time_created = now() - timedelta(hours=1, minutes=30)
1068 # Filter to items between 2 hours ago and 1 hour ago (should only get state2)
1069 after_cutoff = now() - timedelta(hours=2)
1070 before_cutoff = now() - timedelta(hours=1)
1071 with real_moderation_session(super_token) as api:
1072 res = api.GetModerationQueue(
1073 moderation_pb2.GetModerationQueueReq(
1074 created_after=Timestamp_from_datetime(after_cutoff),
1075 created_before=Timestamp_from_datetime(before_cutoff),
1076 )
1077 )
1078 assert len(res.queue_items) == 1
1079 assert res.queue_items[0].moderation_state_id == state2_id
1081 # Filter to items between 4 hours ago and 2.5 hours ago (should only get state1)
1082 after_cutoff = now() - timedelta(hours=4)
1083 before_cutoff = now() - timedelta(hours=2, minutes=30)
1084 with real_moderation_session(super_token) as api:
1085 res = api.GetModerationQueue(
1086 moderation_pb2.GetModerationQueueReq(
1087 created_after=Timestamp_from_datetime(after_cutoff),
1088 created_before=Timestamp_from_datetime(before_cutoff),
1089 )
1090 )
1091 assert len(res.queue_items) == 1
1092 assert res.queue_items[0].moderation_state_id == state1_id
1095def test_GetModerationQueue_filter_unresolved(db):
1096 """Test filtering moderation queue for unresolved items only via API"""
1097 super_user, super_token = generate_user(is_superuser=True)
1098 normal_user, user_token = generate_user()
1099 host, _ = generate_user()
1101 # Create 2 host requests
1102 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1103 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1105 # Resolve the first one via API (ModerateContent automatically resolves queue items)
1106 with real_moderation_session(super_token) as api:
1107 api.ModerateContent(
1108 moderation_pb2.ModerateContentReq(
1109 moderation_state_id=state1_id,
1110 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1111 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1112 reason="Approved",
1113 )
1114 )
1116 # Get all items
1117 with real_moderation_session(super_token) as api:
1118 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1119 assert len(res.queue_items) == 2
1121 # Get only unresolved items
1122 with real_moderation_session(super_token) as api:
1123 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
1124 assert len(res.queue_items) == 1
1125 assert res.queue_items[0].is_resolved == False
1128def test_GetModerationQueue_filter_by_author(db):
1129 """Test filtering moderation queue by item_author_user_id"""
1130 super_user, super_token = generate_user(is_superuser=True)
1131 user1, token1 = generate_user()
1132 user2, token2 = generate_user()
1133 host_user, _ = generate_user()
1135 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1136 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1138 # Create 2 host requests by user1
1139 with requests_session(token1) as api:
1140 hr1_id = api.CreateHostRequest(
1141 requests_pb2.CreateHostRequestReq(
1142 host_user_id=host_user.id,
1143 from_date=today_plus_2,
1144 to_date=today_plus_3,
1145 text=valid_request_text(),
1146 )
1147 ).host_request_id
1149 hr2_id = api.CreateHostRequest(
1150 requests_pb2.CreateHostRequestReq(
1151 host_user_id=host_user.id,
1152 from_date=today_plus_2,
1153 to_date=today_plus_3,
1154 text=valid_request_text(),
1155 )
1156 ).host_request_id
1158 # Create 1 host request by user2
1159 with requests_session(token2) as api:
1160 hr3_id = api.CreateHostRequest(
1161 requests_pb2.CreateHostRequestReq(
1162 host_user_id=host_user.id,
1163 from_date=today_plus_2,
1164 to_date=today_plus_3,
1165 text=valid_request_text(),
1166 )
1167 ).host_request_id
1169 # Get moderation state IDs
1170 state1_id, state2_id, state3_id = None, None, None
1171 with session_scope() as session:
1172 hr1 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr1_id)).scalar_one()
1173 hr2 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr2_id)).scalar_one()
1174 hr3 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr3_id)).scalar_one()
1175 state1_id = hr1.moderation_state_id
1176 state2_id = hr2.moderation_state_id
1177 state3_id = hr3.moderation_state_id
1179 # Get all items (should be 3)
1180 with real_moderation_session(super_token) as api:
1181 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1182 assert len(res.queue_items) == 3
1184 # Filter by user1 (should get 2)
1185 with real_moderation_session(super_token) as api:
1186 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=user1.id))
1187 assert len(res.queue_items) == 2
1188 assert all(item.moderation_state.author.user_id == user1.id for item in res.queue_items)
1190 # Filter by user2 (should get 1)
1191 with real_moderation_session(super_token) as api:
1192 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=user2.id))
1193 assert len(res.queue_items) == 1
1194 assert res.queue_items[0].moderation_state.author.user_id == user2.id
1195 assert res.queue_items[0].moderation_state_id == state3_id
1197 # Filter by non-existent user (should get 0)
1198 with real_moderation_session(super_token) as api:
1199 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=999999))
1200 assert len(res.queue_items) == 0
1203def test_GetModerationQueue_ordering(db):
1204 """Test ordering moderation queue by oldest/newest first"""
1205 super_user, super_token = generate_user(is_superuser=True)
1206 normal_user, user_token = generate_user()
1207 host, _ = generate_user()
1209 # Create 3 host requests
1210 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1211 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1212 state3_id = create_test_host_request_with_moderation(user_token, host.id)
1214 # Set different times: state1 = 3 hours ago, state2 = 2 hours ago, state3 = 1 hour ago
1215 with session_scope() as session:
1216 queue_item1 = session.execute(
1217 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1218 ).scalar_one()
1219 queue_item1.time_created = now() - timedelta(hours=3)
1221 queue_item2 = session.execute(
1222 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state2_id)
1223 ).scalar_one()
1224 queue_item2.time_created = now() - timedelta(hours=2)
1226 queue_item3 = session.execute(
1227 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state3_id)
1228 ).scalar_one()
1229 queue_item3.time_created = now() - timedelta(hours=1)
1231 # Default order (oldest first)
1232 with real_moderation_session(super_token) as api:
1233 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1234 assert len(res.queue_items) == 3
1235 assert res.queue_items[0].moderation_state_id == state1_id # oldest
1236 assert res.queue_items[1].moderation_state_id == state2_id
1237 assert res.queue_items[2].moderation_state_id == state3_id # newest
1239 # Explicit oldest first
1240 with real_moderation_session(super_token) as api:
1241 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(newest_first=False))
1242 assert len(res.queue_items) == 3
1243 assert res.queue_items[0].moderation_state_id == state1_id # oldest
1244 assert res.queue_items[2].moderation_state_id == state3_id # newest
1246 # Newest first
1247 with real_moderation_session(super_token) as api:
1248 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(newest_first=True))
1249 assert len(res.queue_items) == 3
1250 assert res.queue_items[0].moderation_state_id == state3_id # newest
1251 assert res.queue_items[1].moderation_state_id == state2_id
1252 assert res.queue_items[2].moderation_state_id == state1_id # oldest
1255def test_GetModerationQueue_pagination_newest_first(db):
1256 """Test pagination with newest_first=True returns different items on each page"""
1257 super_user, super_token = generate_user(is_superuser=True)
1258 normal_user, normal_token = generate_user()
1259 host_user, _ = generate_user()
1261 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1262 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1264 # Create 5 host requests
1265 hr_ids = []
1266 with requests_session(normal_token) as api:
1267 for i in range(5):
1268 hr_id = api.CreateHostRequest(
1269 requests_pb2.CreateHostRequestReq(
1270 host_user_id=host_user.id,
1271 from_date=today_plus_2,
1272 to_date=today_plus_3,
1273 text=valid_request_text(),
1274 )
1275 ).host_request_id
1276 hr_ids.append(hr_id)
1278 # Get moderation state IDs
1279 state_ids = []
1280 with session_scope() as session:
1281 for hr_id in hr_ids:
1282 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
1283 state_ids.append(hr.moderation_state_id)
1285 # Set different times so ordering is deterministic
1286 with session_scope() as session:
1287 for i, state_id in enumerate(state_ids):
1288 queue_item = session.execute(
1289 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id)
1290 ).scalar_one()
1291 queue_item.time_created = now() - timedelta(hours=5 - i) # oldest first in list
1293 # Get first page (2 items) with newest_first=True, filtered to our user's items
1294 with real_moderation_session(super_token) as api:
1295 res1 = api.GetModerationQueue(
1296 moderation_pb2.GetModerationQueueReq(page_size=2, newest_first=True, item_author_user_id=normal_user.id)
1297 )
1298 assert len(res1.queue_items) == 2
1299 # Should get newest items: state_ids[4], state_ids[3]
1300 assert res1.queue_items[0].moderation_state_id == state_ids[4]
1301 assert res1.queue_items[1].moderation_state_id == state_ids[3]
1302 assert res1.next_page_token # should have more pages
1304 # Get second page using the token
1305 res2 = api.GetModerationQueue(
1306 moderation_pb2.GetModerationQueueReq(
1307 page_size=2, newest_first=True, page_token=res1.next_page_token, item_author_user_id=normal_user.id
1308 )
1309 )
1310 assert len(res2.queue_items) == 2
1311 # Should get next newest items: state_ids[2], state_ids[1]
1312 assert res2.queue_items[0].moderation_state_id == state_ids[2]
1313 assert res2.queue_items[1].moderation_state_id == state_ids[1]
1315 # Pages should not overlap
1316 page1_ids = {item.moderation_state_id for item in res1.queue_items}
1317 page2_ids = {item.moderation_state_id for item in res2.queue_items}
1318 assert page1_ids.isdisjoint(page2_ids), "Pages should not have overlapping items"
1321def test_GetModerationLog(db):
1322 """Test getting moderation log for a state via API"""
1323 super_user, super_token = generate_user(is_superuser=True)
1324 moderator, moderator_token = generate_user(is_superuser=True)
1325 normal_user, user_token = generate_user()
1326 host, _ = generate_user()
1328 # Create a real host request
1329 state_id = create_test_host_request_with_moderation(user_token, host.id)
1331 # Perform a moderation action via API
1332 with real_moderation_session(moderator_token) as api:
1333 api.ModerateContent(
1334 moderation_pb2.ModerateContentReq(
1335 moderation_state_id=state_id,
1336 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1337 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1338 reason="Looks good",
1339 )
1340 )
1342 with real_moderation_session(super_token) as api:
1343 res = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
1344 assert len(res.log_entries) == 2 # CREATE + APPROVE
1345 assert res.moderation_state.moderation_state_id == state_id
1346 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
1347 # Log entries are in reverse chronological order
1348 assert res.log_entries[0].action == moderation_pb2.MODERATION_ACTION_APPROVE
1349 assert res.log_entries[0].moderator_user_id == moderator.id
1350 assert res.log_entries[0].reason == "Looks good"
1351 assert res.log_entries[1].action == moderation_pb2.MODERATION_ACTION_CREATE
1352 assert res.log_entries[1].moderator_user_id == normal_user.id
1355def test_GetModerationLog_not_found(db):
1356 """Test getting moderation log for non-existent state"""
1357 super_user, super_token = generate_user(is_superuser=True)
1359 with real_moderation_session(super_token) as api:
1360 with pytest.raises(grpc.RpcError) as e:
1361 api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=999999))
1362 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1363 assert e.value.details() == "Moderation state not found."
1366def test_GetModerationState(db):
1367 """Test getting moderation state by object type and ID"""
1368 super_user, super_token = generate_user(is_superuser=True)
1369 user1, token1 = generate_user()
1370 user2, _ = generate_user()
1372 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1373 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1375 with requests_session(token1) as api:
1376 host_request_id = api.CreateHostRequest(
1377 requests_pb2.CreateHostRequestReq(
1378 host_user_id=user2.id,
1379 from_date=today_plus_2,
1380 to_date=today_plus_3,
1381 text=valid_request_text(),
1382 )
1383 ).host_request_id
1385 with real_moderation_session(super_token) as api:
1386 res = api.GetModerationState(
1387 moderation_pb2.GetModerationStateReq(
1388 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1389 object_id=host_request_id,
1390 )
1391 )
1392 assert res.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST
1393 assert res.moderation_state.object_id == host_request_id
1394 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
1395 assert res.moderation_state.moderation_state_id > 0
1398def test_GetModerationState_not_found(db):
1399 """Test getting moderation state for non-existent object"""
1400 super_user, super_token = generate_user(is_superuser=True)
1402 with real_moderation_session(super_token) as api:
1403 with pytest.raises(grpc.RpcError) as e:
1404 api.GetModerationState(
1405 moderation_pb2.GetModerationStateReq(
1406 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1407 object_id=999999,
1408 )
1409 )
1410 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1411 assert e.value.details() == "Moderation state not found."
1414def test_GetModerationState_unspecified_type(db):
1415 """Test getting moderation state with unspecified object type"""
1416 super_user, super_token = generate_user(is_superuser=True)
1418 with real_moderation_session(super_token) as api:
1419 with pytest.raises(grpc.RpcError) as e:
1420 api.GetModerationState(
1421 moderation_pb2.GetModerationStateReq(
1422 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_UNSPECIFIED,
1423 object_id=123,
1424 )
1425 )
1426 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1427 assert e.value.details() == "Object type must be specified."
1430def test_ModerateContent_approve(db):
1431 """Test approving content via unified moderation API"""
1432 super_user, super_token = generate_user(is_superuser=True)
1433 user1, token1 = generate_user()
1434 user2, _ = generate_user()
1436 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1437 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1439 # Create a host request using the API (which automatically creates moderation state)
1440 with requests_session(token1) as api:
1441 host_request_id = api.CreateHostRequest(
1442 requests_pb2.CreateHostRequestReq(
1443 host_user_id=user2.id,
1444 from_date=today_plus_2,
1445 to_date=today_plus_3,
1446 text=valid_request_text(),
1447 )
1448 ).host_request_id
1450 # Get the moderation state ID
1451 state_id = None
1452 with session_scope() as session:
1453 host_request = session.execute(
1454 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
1455 ).scalar_one()
1456 state_id = host_request.moderation_state_id
1458 with real_moderation_session(super_token) as api:
1459 res = api.ModerateContent(
1460 moderation_pb2.ModerateContentReq(
1461 moderation_state_id=state_id,
1462 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1463 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1464 reason="Approved by admin",
1465 )
1466 )
1467 assert res.moderation_state.moderation_state_id == state_id
1468 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
1470 # Verify state was updated in database
1471 with session_scope() as session:
1472 state = session.get_one(ModerationState, state_id)
1473 assert state.visibility == ModerationVisibility.visible
1476def test_ModerateContent_not_found(db):
1477 """Test moderating non-existent content"""
1478 super_user, super_token = generate_user(is_superuser=True)
1480 with real_moderation_session(super_token) as api:
1481 with pytest.raises(grpc.RpcError) as e:
1482 api.ModerateContent(
1483 moderation_pb2.ModerateContentReq(
1484 moderation_state_id=999999,
1485 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1486 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1487 reason="Test",
1488 )
1489 )
1490 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1491 assert e.value.details() == "Moderation state not found."
1494def test_ModerateContent_hide(db):
1495 """Test hiding content via unified moderation API"""
1496 super_user, super_token = generate_user(is_superuser=True)
1497 normal_user, user_token = generate_user()
1498 host, _ = generate_user()
1500 # Create a real host request
1501 state_id = create_test_host_request_with_moderation(user_token, host.id)
1503 with real_moderation_session(super_token) as api:
1504 res = api.ModerateContent(
1505 moderation_pb2.ModerateContentReq(
1506 moderation_state_id=state_id,
1507 action=moderation_pb2.MODERATION_ACTION_HIDE,
1508 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
1509 reason="Spam content",
1510 )
1511 )
1512 assert res.moderation_state.moderation_state_id == state_id
1513 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_HIDDEN
1515 # Verify state was updated in database
1516 with session_scope() as session:
1517 state = session.get_one(ModerationState, state_id)
1518 assert state.visibility == ModerationVisibility.hidden
1521def test_ModerateContent_shadow(db):
1522 """Test shadowing content via unified moderation API"""
1523 super_user, super_token = generate_user(is_superuser=True)
1524 normal_user, user_token = generate_user()
1525 host, _ = generate_user()
1527 # Create a real host request
1528 state_id = create_test_host_request_with_moderation(user_token, host.id)
1530 with real_moderation_session(super_token) as api:
1531 res = api.ModerateContent(
1532 moderation_pb2.ModerateContentReq(
1533 moderation_state_id=state_id,
1534 action=moderation_pb2.MODERATION_ACTION_HIDE,
1535 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
1536 reason="Needs further review",
1537 )
1538 )
1539 assert res.moderation_state.moderation_state_id == state_id
1540 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
1542 # Verify state was updated in database
1543 with session_scope() as session:
1544 state = session.get_one(ModerationState, state_id)
1545 assert state.visibility == ModerationVisibility.shadowed
1548def test_FlagContentForReview(db):
1549 """Test flagging content for review via admin API"""
1550 super_user, super_token = generate_user(is_superuser=True)
1551 user1, token1 = generate_user()
1552 user2, _ = generate_user()
1554 # Create a host request
1555 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1556 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1558 with requests_session(token1) as api:
1559 host_request_id = api.CreateHostRequest(
1560 requests_pb2.CreateHostRequestReq(
1561 host_user_id=user2.id,
1562 from_date=today_plus_2,
1563 to_date=today_plus_3,
1564 text=valid_request_text(),
1565 )
1566 ).host_request_id
1568 # Get the moderation state ID
1569 with session_scope() as session:
1570 host_request = session.execute(
1571 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
1572 ).scalar_one()
1573 state_id = host_request.moderation_state_id
1575 with real_moderation_session(super_token) as api:
1576 res = api.FlagContentForReview(
1577 moderation_pb2.FlagContentForReviewReq(
1578 moderation_state_id=state_id,
1579 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW,
1580 reason="Admin flagged for additional review",
1581 )
1582 )
1583 assert res.queue_item.moderation_state_id == state_id
1584 assert res.queue_item.trigger == moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW
1585 assert res.queue_item.is_resolved == False
1587 # Verify queue item was created in database
1588 with session_scope() as session:
1589 # Get the most recent queue item (the one we just created)
1590 queue_item = (
1591 session.execute(
1592 select(ModerationQueueItem)
1593 .where(ModerationQueueItem.moderation_state_id == state_id)
1594 .order_by(ModerationQueueItem.time_created.desc())
1595 )
1596 .scalars()
1597 .first()
1598 )
1599 assert queue_item
1600 assert queue_item.trigger == ModerationTrigger.moderator_review
1601 assert queue_item.resolved_by_log_id is None
1604# ============================================================================
1605# Tests for group chat moderation
1606# ============================================================================
1609def test_group_chat_created_with_moderation_state(db):
1610 """Test that group chats are created with moderation state"""
1611 user1, token1 = generate_user()
1612 user2, _ = generate_user()
1613 make_friends(user1, user2)
1615 with conversations_session(token1) as api:
1616 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
1617 group_chat_id = res.group_chat_id
1619 # Verify moderation state was created
1620 with session_scope() as session:
1621 group_chat = session.execute(select(GroupChat).where(GroupChat.conversation_id == group_chat_id)).scalar_one()
1623 assert group_chat.moderation_state.object_type == ModerationObjectType.group_chat
1624 assert group_chat.moderation_state.object_id == group_chat_id
1625 # Group chats start as SHADOWED
1626 assert group_chat.moderation_state.visibility == ModerationVisibility.shadowed
1628 # A moderation queue item should have been created
1629 queue_item = (
1630 session.execute(
1631 select(ModerationQueueItem).where(
1632 ModerationQueueItem.moderation_state_id == group_chat.moderation_state_id
1633 )
1634 )
1635 .scalars()
1636 .first()
1637 )
1638 assert queue_item is not None
1639 assert queue_item.trigger == ModerationTrigger.initial_review
1642def test_group_chat_GetModerationState(db):
1643 """Test GetModerationState API for group chats"""
1644 user1, token1 = generate_user()
1645 user2, _ = generate_user()
1646 moderator, mod_token = generate_user(is_superuser=True)
1647 make_friends(user1, user2)
1649 with conversations_session(token1) as api:
1650 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
1651 group_chat_id = res.group_chat_id
1653 # Moderator can look up the moderation state
1654 with real_moderation_session(mod_token) as api:
1655 res = api.GetModerationState(
1656 moderation_pb2.GetModerationStateReq(
1657 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
1658 object_id=group_chat_id,
1659 )
1660 )
1661 assert res.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT
1662 assert res.moderation_state.object_id == group_chat_id
1663 # Starts as SHADOWED
1664 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
1667def test_group_chat_moderation_hide(db):
1668 """Test that a moderator can hide a group chat and participants can no longer see it"""
1669 user1, token1 = generate_user()
1670 user2, token2 = generate_user()
1671 moderator, mod_token = generate_user(is_superuser=True)
1672 make_friends(user1, user2)
1674 with conversations_session(token1) as api:
1675 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
1676 group_chat_id = res.group_chat_id
1677 api.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Hello!"))
1679 # First approve the group chat so both users can see it
1680 with real_moderation_session(mod_token) as api:
1681 state_res = api.GetModerationState(
1682 moderation_pb2.GetModerationStateReq(
1683 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
1684 object_id=group_chat_id,
1685 )
1686 )
1687 api.ModerateContent(
1688 moderation_pb2.ModerateContentReq(
1689 moderation_state_id=state_res.moderation_state.moderation_state_id,
1690 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1691 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1692 reason="Approved",
1693 )
1694 )
1696 # Both users can see the chat now
1697 with conversations_session(token1) as api:
1698 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
1699 assert len(res.group_chats) == 1
1701 with conversations_session(token2) as api:
1702 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
1703 assert len(res.group_chats) == 1
1705 # Moderator hides the group chat
1706 with real_moderation_session(mod_token) as api:
1707 state_res = api.GetModerationState(
1708 moderation_pb2.GetModerationStateReq(
1709 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
1710 object_id=group_chat_id,
1711 )
1712 )
1713 api.ModerateContent(
1714 moderation_pb2.ModerateContentReq(
1715 moderation_state_id=state_res.moderation_state.moderation_state_id,
1716 action=moderation_pb2.MODERATION_ACTION_HIDE,
1717 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
1718 reason="Inappropriate content",
1719 )
1720 )
1722 # Neither user can see the chat now
1723 with conversations_session(token1) as api:
1724 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
1725 assert len(res.group_chats) == 0
1727 with conversations_session(token2) as api:
1728 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
1729 assert len(res.group_chats) == 0
1731 # Trying to get messages returns empty (chat is hidden so no messages visible)
1732 with conversations_session(token1) as api:
1733 res = api.GetGroupChatMessages(conversations_pb2.GetGroupChatMessagesReq(group_chat_id=group_chat_id))
1734 assert len(res.messages) == 0
1737def test_group_chat_moderation_shadow(db):
1738 """Test that shadowing a group chat hides it from non-creator participants"""
1739 user1, token1 = generate_user() # Creator
1740 user2, token2 = generate_user() # Participant
1741 moderator, mod_token = generate_user(is_superuser=True)
1742 make_friends(user1, user2)
1744 with conversations_session(token1) as api:
1745 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
1746 group_chat_id = res.group_chat_id
1747 api.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Hello!"))
1749 # Moderator shadows the group chat
1750 with real_moderation_session(mod_token) as api:
1751 state_res = api.GetModerationState(
1752 moderation_pb2.GetModerationStateReq(
1753 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
1754 object_id=group_chat_id,
1755 )
1756 )
1757 api.ModerateContent(
1758 moderation_pb2.ModerateContentReq(
1759 moderation_state_id=state_res.moderation_state.moderation_state_id,
1760 action=moderation_pb2.MODERATION_ACTION_HIDE,
1761 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
1762 reason="Needs review",
1763 )
1764 )
1766 # Creator can see SHADOWED content in list operations
1767 with conversations_session(token1) as api:
1768 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
1769 assert len(res.group_chats) == 1
1770 assert res.group_chats[0].group_chat_id == group_chat_id
1772 # But non-creator participant cannot see it in lists
1773 with conversations_session(token2) as api:
1774 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
1775 assert len(res.group_chats) == 0
1777 # Creator can also access it directly via GetGroupChat
1778 with conversations_session(token1) as api:
1779 res = api.GetGroupChat(conversations_pb2.GetGroupChatReq(group_chat_id=group_chat_id))
1780 assert res.group_chat_id == group_chat_id
1783# ============================================================================
1784# Tests for auto-approval background job
1785# ============================================================================
1788def test_auto_approve_moderation_queue_disabled_when_zero(db, email_collector: EmailCollector):
1789 """Test that auto-approval is disabled when deadline is 0"""
1790 moderator, mod_token = generate_user(is_superuser=True)
1791 user1, token1 = generate_user()
1792 user2, token2 = generate_user()
1794 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1795 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1797 # Create a host request
1798 with requests_session(token1) as api:
1799 host_request_id = api.CreateHostRequest(
1800 requests_pb2.CreateHostRequestReq(
1801 host_user_id=user2.id,
1802 from_date=today_plus_2,
1803 to_date=today_plus_3,
1804 text=valid_request_text(),
1805 )
1806 ).host_request_id
1808 # No email should have been sent (request is shadowed)
1809 assert email_collector.count_for_recipient(user2.email) == 0
1811 # Ensure deadline is 0 (disabled)
1812 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 0
1814 # Run the job
1815 auto_approve_moderation_queue(empty_pb2.Empty())
1817 # Surfer (author) can see the request via API
1818 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1819 assert res.host_request_id == host_request_id
1821 # Author can see their SHADOWED request in their sent list
1822 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
1823 assert len(res.host_requests) == 1
1824 assert res.host_requests[0].host_request_id == host_request_id
1826 # Host cannot see the request (it's shadowed from them)
1827 with requests_session(token2) as api:
1828 with pytest.raises(grpc.RpcError) as e:
1829 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1830 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1832 # Host doesn't see it in their received list either
1833 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
1834 assert len(res.host_requests) == 0
1836 # Moderator can still see the item in the moderation queue
1837 with real_moderation_session(mod_token) as api:
1838 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
1839 assert len(res.queue_items) == 1
1840 assert res.queue_items[0].trigger == moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW
1842 # Moderator can check the state is still SHADOWED
1843 state_res = api.GetModerationState(
1844 moderation_pb2.GetModerationStateReq(
1845 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1846 object_id=host_request_id,
1847 )
1848 )
1849 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
1852def test_auto_approve_moderation_queue_approves_old_items(
1853 db, email_collector: EmailCollector, push_collector: PushCollector
1854):
1855 """Test that auto-approval approves items older than the deadline"""
1856 moderator, mod_token = generate_user(is_superuser=True)
1857 user1, token1 = generate_user()
1858 user2, token2 = generate_user()
1860 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1861 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1863 # Create a host request
1864 with requests_session(token1) as api:
1865 host_request_id = api.CreateHostRequest(
1866 requests_pb2.CreateHostRequestReq(
1867 host_user_id=user2.id,
1868 from_date=today_plus_2,
1869 to_date=today_plus_3,
1870 text=valid_request_text("Test request for auto-approval"),
1871 )
1872 ).host_request_id
1874 # No email sent initially (shadowed)
1875 assert email_collector.count_for_recipient(user2.email) == 0
1877 # Host cannot see the request yet
1878 with requests_session(token2) as api:
1879 with pytest.raises(grpc.RpcError) as e:
1880 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1881 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1883 # Make the queue item appear old by backdating its time_created
1884 with session_scope() as session:
1885 host_request = session.execute(
1886 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
1887 ).scalar_one()
1888 queue_item = session.execute(
1889 select(ModerationQueueItem)
1890 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
1891 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
1892 ).scalar_one()
1893 # Backdate the queue item by 2 minutes
1894 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=2)
1896 # Set deadline to 60 seconds (items older than 60 seconds will be auto-approved)
1897 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
1898 config.MODERATION_BOT_USER_ID = moderator.id
1900 # Run the job
1901 auto_approve_moderation_queue(empty_pb2.Empty())
1903 # Now host can see the request via API
1904 with requests_session(token2) as api:
1905 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1906 assert res.host_request_id == host_request_id
1907 assert res.latest_message.text.text == valid_request_text("Test request for auto-approval")
1909 # Host sees it in their received list
1910 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
1911 assert len(res.host_requests) == 1
1912 assert res.host_requests[0].host_request_id == host_request_id
1914 # Surfer sees it in their sent list
1915 with requests_session(token1) as api:
1916 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
1917 assert len(res.host_requests) == 1
1918 assert res.host_requests[0].host_request_id == host_request_id
1920 # Moderator sees the queue item is now resolved
1921 with real_moderation_session(mod_token) as api:
1922 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
1923 assert len(res.queue_items) == 0
1925 # State is now VISIBLE
1926 state_res = api.GetModerationState(
1927 moderation_pb2.GetModerationStateReq(
1928 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1929 object_id=host_request_id,
1930 )
1931 )
1932 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
1934 # Check the log shows auto-approval by the bot user
1935 log_res = api.GetModerationLog(
1936 moderation_pb2.GetModerationLogReq(moderation_state_id=state_res.moderation_state.moderation_state_id)
1937 )
1938 # Find the APPROVE action
1939 approve_entries = [e for e in log_res.log_entries if e.action == moderation_pb2.MODERATION_ACTION_APPROVE]
1940 assert len(approve_entries) == 1
1941 assert "Auto-approved" in approve_entries[0].reason
1942 assert "60 seconds" in approve_entries[0].reason
1943 assert approve_entries[0].moderator_user_id == moderator.id
1946def test_auto_approve_does_not_approve_recent_items(db, email_collector: EmailCollector):
1947 """Test that auto-approval does not approve items that are newer than the deadline"""
1948 moderator, mod_token = generate_user(is_superuser=True)
1949 user1, token1 = generate_user()
1950 user2, token2 = generate_user()
1952 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1953 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1955 # Create a host request
1956 with requests_session(token1) as api:
1957 host_request_id = api.CreateHostRequest(
1958 requests_pb2.CreateHostRequestReq(
1959 host_user_id=user2.id,
1960 from_date=today_plus_2,
1961 to_date=today_plus_3,
1962 text=valid_request_text(),
1963 )
1964 ).host_request_id
1966 # No email sent (shadowed)
1967 assert email_collector.count_for_recipient(user2.email) == 0
1969 # Set deadline to 1 hour (items older than 1 hour will be auto-approved)
1970 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600
1971 config.MODERATION_BOT_USER_ID = moderator.id
1973 # Run the job - the item was just created, so it shouldn't be approved
1974 auto_approve_moderation_queue(empty_pb2.Empty())
1976 # Still no email sent
1977 assert email_collector.count_for_recipient(user2.email) == 0
1979 # Host still cannot see the request
1980 with requests_session(token2) as api:
1981 with pytest.raises(grpc.RpcError) as e:
1982 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1983 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1985 # Not in host's received list
1986 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
1987 assert len(res.host_requests) == 0
1989 # Moderator sees it still in queue unresolved
1990 with real_moderation_session(mod_token) as api:
1991 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
1992 assert len(res.queue_items) == 1
1994 # State is still SHADOWED
1995 state_res = api.GetModerationState(
1996 moderation_pb2.GetModerationStateReq(
1997 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1998 object_id=host_request_id,
1999 )
2000 )
2001 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2004def test_auto_approve_does_not_approve_already_approved(db):
2005 """Test that auto-approval does not re-approve already visible content"""
2006 moderator, mod_token = generate_user(is_superuser=True)
2007 user1, token1 = generate_user()
2008 user2, token2 = generate_user()
2010 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2011 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2013 # Create a host request
2014 with requests_session(token1) as api:
2015 host_request_id = api.CreateHostRequest(
2016 requests_pb2.CreateHostRequestReq(
2017 host_user_id=user2.id,
2018 from_date=today_plus_2,
2019 to_date=today_plus_3,
2020 text=valid_request_text(),
2021 )
2022 ).host_request_id
2024 # Moderator approves it manually
2025 with real_moderation_session(mod_token) as api:
2026 state_res = api.GetModerationState(
2027 moderation_pb2.GetModerationStateReq(
2028 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2029 object_id=host_request_id,
2030 )
2031 )
2032 state_id = state_res.moderation_state.moderation_state_id
2034 api.ModerateContent(
2035 moderation_pb2.ModerateContentReq(
2036 moderation_state_id=state_id,
2037 action=moderation_pb2.MODERATION_ACTION_APPROVE,
2038 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
2039 reason="Approved by moderator",
2040 )
2041 )
2043 # Host can now see it
2044 with requests_session(token2) as api:
2045 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2046 assert res.host_request_id == host_request_id
2048 # Get log count before auto-approval
2049 with real_moderation_session(mod_token) as api:
2050 log_res_before = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2051 log_count_before = len(log_res_before.log_entries)
2053 # Set deadline to 1 second
2054 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1
2055 config.MODERATION_BOT_USER_ID = moderator.id
2057 # Run the job
2058 auto_approve_moderation_queue(empty_pb2.Empty())
2060 # No new log entries should be created (already approved, queue item resolved)
2061 with real_moderation_session(mod_token) as api:
2062 log_res_after = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2063 assert len(log_res_after.log_entries) == log_count_before
2065 # Queue should be empty (item was resolved when moderator approved)
2066 queue_res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
2067 assert len(queue_res.queue_items) == 0
2070def test_auto_approve_does_not_approve_moderator_shadowed_items(db):
2071 """Test that auto-approval does not approve items that were explicitly shadowed by a moderator"""
2072 moderator, mod_token = generate_user(is_superuser=True)
2073 user1, token1 = generate_user()
2074 user2, token2 = generate_user()
2076 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2077 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2079 # Create a host request
2080 with requests_session(token1) as api:
2081 host_request_id = api.CreateHostRequest(
2082 requests_pb2.CreateHostRequestReq(
2083 host_user_id=user2.id,
2084 from_date=today_plus_2,
2085 to_date=today_plus_3,
2086 text=valid_request_text(),
2087 )
2088 ).host_request_id
2090 # Moderator explicitly shadows the content (keeping it shadowed but resolving the queue item)
2091 with real_moderation_session(mod_token) as api:
2092 state_res = api.GetModerationState(
2093 moderation_pb2.GetModerationStateReq(
2094 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2095 object_id=host_request_id,
2096 )
2097 )
2098 state_id = state_res.moderation_state.moderation_state_id
2100 # Set to SHADOWED explicitly - this resolves the INITIAL_REVIEW queue item
2101 api.ModerateContent(
2102 moderation_pb2.ModerateContentReq(
2103 moderation_state_id=state_id,
2104 action=moderation_pb2.MODERATION_ACTION_HIDE,
2105 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
2106 reason="Keeping shadowed for review",
2107 )
2108 )
2110 # Backdate to ensure it would be old enough for auto-approval
2111 with session_scope() as session:
2112 queue_item = session.execute(
2113 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id)
2114 ).scalar_one()
2115 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=10)
2117 # Set deadline to 1 second
2118 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1
2119 config.MODERATION_BOT_USER_ID = moderator.id
2121 # Get log count before
2122 with real_moderation_session(mod_token) as api:
2123 log_res_before = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2124 log_count_before = len(log_res_before.log_entries)
2126 # Run the job
2127 auto_approve_moderation_queue(empty_pb2.Empty())
2129 # No new log entries - the queue item was resolved when moderator shadowed it
2130 with real_moderation_session(mod_token) as api:
2131 log_res_after = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2132 assert len(log_res_after.log_entries) == log_count_before
2134 # State should still be SHADOWED (not auto-approved to VISIBLE)
2135 state_res = api.GetModerationState(
2136 moderation_pb2.GetModerationStateReq(
2137 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2138 object_id=host_request_id,
2139 )
2140 )
2141 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2143 # Host still cannot see the request
2144 with requests_session(token2) as api:
2145 with pytest.raises(grpc.RpcError) as e:
2146 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2147 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2150def test_auto_approve_skips_shadowed_user_authored_items(db):
2151 """Auto-approval must not promote content authored by a currently-shadowed user."""
2152 moderator, mod_token = generate_user(is_superuser=True)
2153 surfer, surfer_token = generate_user()
2154 host, host_token = generate_user()
2156 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2157 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2159 with requests_session(surfer_token) as api:
2160 host_request_id = api.CreateHostRequest(
2161 requests_pb2.CreateHostRequestReq(
2162 host_user_id=host.id,
2163 from_date=today_plus_2,
2164 to_date=today_plus_3,
2165 text=valid_request_text(),
2166 )
2167 ).host_request_id
2169 # Shadow the surfer after the host request was created
2170 with session_scope() as session:
2171 session.execute(select(User).where(User.id == surfer.id)).scalar_one().shadowed_at = now()
2173 # Backdate the queue item to make it eligible for auto-approval
2174 with session_scope() as session:
2175 host_request = session.execute(
2176 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
2177 ).scalar_one()
2178 queue_item = session.execute(
2179 select(ModerationQueueItem)
2180 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
2181 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
2182 ).scalar_one()
2183 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=10)
2185 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
2186 config.MODERATION_BOT_USER_ID = moderator.id
2188 auto_approve_moderation_queue(empty_pb2.Empty())
2190 # State should remain SHADOWED — the auto-approve job must skip shadowed-user content
2191 with real_moderation_session(mod_token) as api:
2192 state_res = api.GetModerationState(
2193 moderation_pb2.GetModerationStateReq(
2194 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2195 object_id=host_request_id,
2196 )
2197 )
2198 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2200 # Host still cannot see the request
2201 with requests_session(host_token) as api:
2202 with pytest.raises(grpc.RpcError) as e:
2203 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2204 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2206 # But the (shadowed) author can still see their own request
2207 with requests_session(surfer_token) as api:
2208 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2209 assert res.host_request_id == host_request_id
2212# ============================================================================
2213# Notification Suppression Tests
2214# ============================================================================
2217def test_host_request_message_notifications_suppressed_before_approval(
2218 db, email_collector: EmailCollector, push_collector: PushCollector, moderator
2219):
2220 """
2221 Test that notifications are NOT sent for messages in host requests
2222 that haven't been approved yet.
2223 """
2224 host, host_token = generate_user(complete_profile=True)
2225 surfer, surfer_token = generate_user(complete_profile=True)
2227 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2228 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2230 # Create host request (it starts in SHADOWED state)
2231 with requests_session(surfer_token) as api:
2232 hr_id = api.CreateHostRequest(
2233 requests_pb2.CreateHostRequestReq(
2234 host_user_id=host.id,
2235 from_date=today_plus_2,
2236 to_date=today_plus_3,
2237 text=valid_request_text("Initial request message"),
2238 )
2239 ).host_request_id
2241 # No notifications should have been sent to the host (request is SHADOWED)
2242 assert email_collector.count_for_recipient(host.email) == 0
2243 assert push_collector.count_for_user(host.id) == 0
2245 # Send additional messages BEFORE approval - should NOT generate notifications
2246 with requests_session(surfer_token) as api:
2247 api.SendHostRequestMessage(
2248 requests_pb2.SendHostRequestMessageReq(
2249 host_request_id=hr_id,
2250 text="Follow-up message 1",
2251 )
2252 )
2253 api.SendHostRequestMessage(
2254 requests_pb2.SendHostRequestMessageReq(
2255 host_request_id=hr_id,
2256 text="Follow-up message 2",
2257 )
2258 )
2260 # Host should STILL have no notifications (messages sent while SHADOWED)
2261 assert email_collector.count_for_recipient(host.email) == 0
2262 assert push_collector.count_for_user(host.id) == 0
2264 # Now approve the request
2265 moderator.approve_host_request(hr_id)
2266 email_collector.pop_for_recipient(host.email)
2268 # Host should now have 3 notifications (all deferred notifications are delivered on approval):
2269 # 1. host_request:create (the initial request)
2270 # 2. host_request:message (Follow-up message 1)
2271 # 3. host_request:message (Follow-up message 2)
2272 assert push_collector.count_for_user(host.id) == 3
2273 push = push_collector.pop_for_user(host.id, last=False)
2274 assert push.content.title == f"New host request from {surfer.name}"
2277def test_host_request_status_notifications_suppressed_before_approval(db, push_collector: PushCollector, moderator):
2278 """
2279 Test that status change notifications (accept/reject/etc.) are NOT sent
2280 for host requests that haven't been approved yet.
2282 Note: In practice, the host can't even SEE the request to accept/reject it
2283 when it's SHADOWED. But if they somehow did, we still shouldn't notify.
2284 """
2285 host, host_token = generate_user(complete_profile=True)
2286 surfer, surfer_token = generate_user(complete_profile=True)
2288 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2289 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2291 # Create host request
2292 with requests_session(surfer_token) as api:
2293 hr_id = api.CreateHostRequest(
2294 requests_pb2.CreateHostRequestReq(
2295 host_user_id=host.id,
2296 from_date=today_plus_2,
2297 to_date=today_plus_3,
2298 text=valid_request_text(),
2299 )
2300 ).host_request_id
2302 # No notifications should have been sent to the host (request is SHADOWED)
2303 assert push_collector.count_for_user(host.id) == 0
2305 # The surfer can cancel their own request even when SHADOWED
2306 # But this should NOT notify the host since the request isn't approved
2307 with requests_session(surfer_token) as api:
2308 api.RespondHostRequest(
2309 requests_pb2.RespondHostRequestReq(
2310 host_request_id=hr_id,
2311 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED,
2312 text="Actually, never mind",
2313 )
2314 )
2316 # Host should STILL have no notifications (cancel notification suppressed)
2317 assert push_collector.count_for_user(host.id) == 0
2320def test_host_request_notifications_sent_after_approval(
2321 db, email_collector: EmailCollector, push_collector: PushCollector, moderator
2322):
2323 """
2324 Test that after a host request is approved, all notifications work normally.
2325 """
2326 host, host_token = generate_user(complete_profile=True)
2327 surfer, surfer_token = generate_user(complete_profile=True)
2329 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2330 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2332 # Create and approve host request
2333 with requests_session(surfer_token) as api:
2334 hr_id = api.CreateHostRequest(
2335 requests_pb2.CreateHostRequestReq(
2336 host_user_id=host.id,
2337 from_date=today_plus_2,
2338 to_date=today_plus_3,
2339 text=valid_request_text(),
2340 )
2341 ).host_request_id
2343 moderator.approve_host_request(hr_id)
2345 # Host should have received 1 notification (the approval notification)
2346 email_collector.pop_for_recipient(host.email, last=True)
2347 push_collector.pop_for_user(host.id, last=True)
2349 # Host accepts the request - surfer should be notified
2350 with requests_session(host_token) as api:
2351 api.RespondHostRequest(
2352 requests_pb2.RespondHostRequestReq(
2353 host_request_id=hr_id,
2354 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
2355 text="Sure, come on over!",
2356 )
2357 )
2359 # Surfer should have 1 notification (the accept notification)
2360 email_collector.pop_for_recipient(surfer.email, last=True)
2361 push = push_collector.pop_for_user(surfer.id, last=True)
2362 assert push.content.title == f"{host.name} accepted your host request"
2364 # Surfer confirms - host should be notified
2365 with requests_session(surfer_token) as api:
2366 api.RespondHostRequest(
2367 requests_pb2.RespondHostRequestReq(
2368 host_request_id=hr_id,
2369 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED,
2370 text="See you then!",
2371 )
2372 )
2374 # Host should now have received the confirmation notifications
2375 email_collector.pop_for_recipient(host.email, last=True)
2376 push = push_collector.pop_for_user(host.id, last=True)
2377 assert push.content.title == f"{surfer.name} confirmed their host request"
2380def test_group_chat_message_notifications_suppressed_before_approval(db, push_collector: PushCollector, moderator):
2381 """
2382 Test that notifications are NOT sent for messages in group chats
2383 that haven't been approved yet.
2384 """
2385 user1, token1 = generate_user(complete_profile=True)
2386 user2, token2 = generate_user(complete_profile=True)
2388 # Create a group chat (starts in SHADOWED state)
2389 with conversations_session(token1) as api:
2390 res = api.CreateGroupChat(
2391 conversations_pb2.CreateGroupChatReq(
2392 recipient_user_ids=[user2.id],
2393 )
2394 )
2395 gc_id = res.group_chat_id
2397 # Verify initial state
2398 with session_scope() as session:
2399 gc = session.execute(select(GroupChat).where(GroupChat.conversation_id == gc_id)).scalar_one()
2400 assert gc.moderation_state.visibility == ModerationVisibility.shadowed
2402 # No notifications should have been sent yet (chat is SHADOWED)
2403 assert push_collector.count_for_user(user2.id) == 0
2405 # Send messages BEFORE approval
2406 with conversations_session(token1) as api:
2407 api.SendMessage(
2408 conversations_pb2.SendMessageReq(
2409 group_chat_id=gc_id,
2410 text="Hello before approval",
2411 )
2412 )
2414 # Process the queued notification job
2415 while process_job():
2416 pass
2418 # User2 should STILL have no notifications (chat is SHADOWED)
2419 assert push_collector.count_for_user(user2.id) == 0
2421 # Now approve the group chat
2422 moderator.approve_group_chat(gc_id)
2424 # Process the queued notification jobs from approval
2425 while process_job():
2426 pass
2428 # Verify moderation state after approval
2429 with session_scope() as session:
2430 gc = session.execute(select(GroupChat).where(GroupChat.conversation_id == gc_id)).scalar_one()
2431 assert gc.moderation_state.visibility == ModerationVisibility.visible
2433 # User2 should have received 1 notification for the first message sent before approval
2434 push = push_collector.pop_for_user(user2.id, last=True)
2435 assert push.content.title == user1.name
2436 assert push.content.body == "Hello before approval"
2438 # Send a message AFTER approval
2439 with conversations_session(token1) as api:
2440 api.SendMessage(
2441 conversations_pb2.SendMessageReq(
2442 group_chat_id=gc_id,
2443 text="Hello after approval",
2444 )
2445 )
2447 # Process the queued notification job
2448 while process_job():
2449 pass
2451 # User2 should have received another notification
2452 assert push_collector.count_for_user(user2.id) == 1
2455def test_event_moderation_state_content(db):
2456 """Test that event moderation state content includes both title and description"""
2457 super_user, super_token = generate_user(is_superuser=True)
2458 user, token = generate_user()
2460 with session_scope() as session:
2461 create_community(session, 0, 2, "Community", [user], [], None)
2463 start_time = now() + timedelta(hours=2)
2464 end_time = start_time + timedelta(hours=3)
2466 with events_session(token) as api:
2467 res = api.CreateEvent(
2468 events_pb2.CreateEventReq(
2469 title="My Event Title",
2470 content="My event description.",
2471 photo_key=None,
2472 offline_information=events_pb2.OfflineEventInformation(
2473 address="Near Null Island",
2474 lat=0.1,
2475 lng=0.2,
2476 ),
2477 start_time=Timestamp_from_datetime(start_time),
2478 end_time=Timestamp_from_datetime(end_time),
2479 timezone="UTC",
2480 )
2481 )
2482 event_id = res.event_id
2484 with session_scope() as session:
2485 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2486 state_id = occurrence.moderation_state_id
2488 with real_moderation_session(super_token) as api:
2489 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
2490 event_items = [
2491 item
2492 for item in res.queue_items
2493 if item.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_EVENT_OCCURRENCE
2494 ]
2495 assert len(event_items) == 1
2496 assert event_items[0].moderation_state.content == "My Event Title\n\nMy event description."
2499# ============================================================================
2500# Tests for SetUserContentVisibility
2501# ============================================================================
2504def _get_moderation_state(session, object_type, object_id):
2505 return session.execute(
2506 select(ModerationState)
2507 .where(ModerationState.object_type == object_type)
2508 .where(ModerationState.object_id == object_id)
2509 ).scalar_one()
2512def test_SetUserContentVisibility_host_request(db):
2513 super_user, super_token = generate_user(is_superuser=True)
2514 surfer, surfer_token = generate_user()
2515 host, _ = generate_user()
2517 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2518 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2519 with requests_session(surfer_token) as api:
2520 hr_id = api.CreateHostRequest(
2521 requests_pb2.CreateHostRequestReq(
2522 host_user_id=host.id,
2523 from_date=today_plus_2,
2524 to_date=today_plus_3,
2525 text=valid_request_text(),
2526 )
2527 ).host_request_id
2529 with real_moderation_session(super_token) as api:
2530 res = api.SetUserContentVisibility(
2531 moderation_pb2.SetUserContentVisibilityReq(
2532 user_id=surfer.id,
2533 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
2534 )
2535 )
2536 # Already shadowed by default — no-op
2537 assert res.updated_count == 0
2539 with real_moderation_session(super_token) as api:
2540 res = api.SetUserContentVisibility(
2541 moderation_pb2.SetUserContentVisibilityReq(
2542 user_id=surfer.id,
2543 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2544 reason="policy violation",
2545 )
2546 )
2547 assert res.updated_count == 1
2549 with session_scope() as session:
2550 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
2551 assert state.visibility == ModerationVisibility.hidden
2553 log_entries = (
2554 session.execute(
2555 select(ModerationLog)
2556 .where(ModerationLog.moderation_state_id == state.id)
2557 .order_by(ModerationLog.time.asc(), ModerationLog.id.asc())
2558 )
2559 .scalars()
2560 .all()
2561 )
2562 # create log + bulk update log
2563 assert len(log_entries) == 2
2564 assert log_entries[-1].new_visibility == ModerationVisibility.hidden
2565 assert log_entries[-1].moderator_user_id == super_user.id
2566 assert log_entries[-1].reason == "policy violation"
2569def test_SetUserContentVisibility_group_chat(db):
2570 super_user, super_token = generate_user(is_superuser=True)
2571 creator, creator_token = generate_user()
2572 other, _ = generate_user()
2573 make_friends(creator, other)
2575 with conversations_session(creator_token) as api:
2576 gc_id = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[other.id])).group_chat_id
2578 with real_moderation_session(super_token) as api:
2579 api.SetUserContentVisibility(
2580 moderation_pb2.SetUserContentVisibilityReq(
2581 user_id=creator.id,
2582 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2583 )
2584 )
2586 with session_scope() as session:
2587 state = _get_moderation_state(session, ModerationObjectType.group_chat, gc_id)
2588 assert state.visibility == ModerationVisibility.hidden
2591def test_SetUserContentVisibility_event_occurrence(db):
2592 super_user, super_token = generate_user(is_superuser=True)
2593 creator, creator_token = generate_user()
2595 with session_scope() as session:
2596 create_community(session, 0, 2, "Community", [creator], [], None)
2598 start_time = now() + timedelta(hours=2)
2599 end_time = start_time + timedelta(hours=3)
2600 with events_session(creator_token) as api:
2601 event_id = api.CreateEvent(
2602 events_pb2.CreateEventReq(
2603 title="Event",
2604 content="Event description.",
2605 photo_key=None,
2606 offline_information=events_pb2.OfflineEventInformation(
2607 address="Near Null Island",
2608 lat=0.1,
2609 lng=0.2,
2610 ),
2611 start_time=Timestamp_from_datetime(start_time),
2612 end_time=Timestamp_from_datetime(end_time),
2613 timezone="UTC",
2614 )
2615 ).event_id
2617 with real_moderation_session(super_token) as api:
2618 res = api.SetUserContentVisibility(
2619 moderation_pb2.SetUserContentVisibilityReq(
2620 user_id=creator.id,
2621 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2622 )
2623 )
2624 assert res.updated_count == 1
2626 with session_scope() as session:
2627 state = _get_moderation_state(session, ModerationObjectType.event_occurrence, event_id)
2628 assert state.visibility == ModerationVisibility.hidden
2631def test_SetUserContentVisibility_friend_request(db):
2632 super_user, super_token = generate_user(is_superuser=True)
2633 sender, sender_token = generate_user()
2634 recipient, _ = generate_user()
2636 with api_session(sender_token) as api:
2637 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=recipient.id))
2639 with session_scope() as session:
2640 fr_id = session.execute(
2641 select(FriendRelationship.id).where(FriendRelationship.from_user_id == sender.id)
2642 ).scalar_one()
2644 with real_moderation_session(super_token) as api:
2645 res = api.SetUserContentVisibility(
2646 moderation_pb2.SetUserContentVisibilityReq(
2647 user_id=sender.id,
2648 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2649 )
2650 )
2651 assert res.updated_count == 1
2653 with session_scope() as session:
2654 state = _get_moderation_state(session, ModerationObjectType.friend_request, fr_id)
2655 assert state.visibility == ModerationVisibility.hidden
2658def test_SetUserContentVisibility_round_trip(db):
2659 super_user, super_token = generate_user(is_superuser=True)
2660 surfer, surfer_token = generate_user()
2661 host, _ = generate_user()
2663 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2664 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2665 with requests_session(surfer_token) as api:
2666 hr_id = api.CreateHostRequest(
2667 requests_pb2.CreateHostRequestReq(
2668 host_user_id=host.id,
2669 from_date=today_plus_2,
2670 to_date=today_plus_3,
2671 text=valid_request_text(),
2672 )
2673 ).host_request_id
2675 with real_moderation_session(super_token) as api:
2676 api.SetUserContentVisibility(
2677 moderation_pb2.SetUserContentVisibilityReq(
2678 user_id=surfer.id,
2679 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2680 reason="first",
2681 )
2682 )
2683 api.SetUserContentVisibility(
2684 moderation_pb2.SetUserContentVisibilityReq(
2685 user_id=surfer.id,
2686 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
2687 reason="second",
2688 )
2689 )
2691 with session_scope() as session:
2692 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
2693 assert state.visibility == ModerationVisibility.visible
2695 bulk_log_entries = (
2696 session.execute(
2697 select(ModerationLog)
2698 .where(ModerationLog.moderation_state_id == state.id)
2699 .where(ModerationLog.reason.in_(("first", "second")))
2700 .order_by(ModerationLog.time.asc(), ModerationLog.id.asc())
2701 )
2702 .scalars()
2703 .all()
2704 )
2705 assert [entry.new_visibility for entry in bulk_log_entries] == [
2706 ModerationVisibility.hidden,
2707 ModerationVisibility.visible,
2708 ]
2711def test_SetUserContentVisibility_resolves_queue_items(db):
2712 super_user, super_token = generate_user(is_superuser=True)
2713 surfer, surfer_token = generate_user()
2714 host, _ = generate_user()
2716 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2717 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2718 with requests_session(surfer_token) as api:
2719 hr_id = api.CreateHostRequest(
2720 requests_pb2.CreateHostRequestReq(
2721 host_user_id=host.id,
2722 from_date=today_plus_2,
2723 to_date=today_plus_3,
2724 text=valid_request_text(),
2725 )
2726 ).host_request_id
2728 with session_scope() as session:
2729 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
2730 queue_item = session.execute(
2731 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id)
2732 ).scalar_one()
2733 assert queue_item.resolved_by_log_id is None
2735 with real_moderation_session(super_token) as api:
2736 api.SetUserContentVisibility(
2737 moderation_pb2.SetUserContentVisibilityReq(
2738 user_id=surfer.id,
2739 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2740 )
2741 )
2743 with session_scope() as session:
2744 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
2745 queue_item = session.execute(
2746 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id)
2747 ).scalar_one()
2748 assert queue_item.resolved_by_log_id is not None
2751def test_SetUserContentVisibility_noop_when_matches(db):
2752 super_user, super_token = generate_user(is_superuser=True)
2753 surfer, surfer_token = generate_user()
2754 host, _ = generate_user()
2756 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2757 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2758 with requests_session(surfer_token) as api:
2759 api.CreateHostRequest(
2760 requests_pb2.CreateHostRequestReq(
2761 host_user_id=host.id,
2762 from_date=today_plus_2,
2763 to_date=today_plus_3,
2764 text=valid_request_text(),
2765 )
2766 )
2768 with session_scope() as session:
2769 log_count_before = len(session.execute(select(ModerationLog)).scalars().all())
2771 with real_moderation_session(super_token) as api:
2772 res = api.SetUserContentVisibility(
2773 moderation_pb2.SetUserContentVisibilityReq(
2774 user_id=surfer.id,
2775 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
2776 )
2777 )
2778 assert res.updated_count == 0
2780 with session_scope() as session:
2781 log_count_after = len(session.execute(select(ModerationLog)).scalars().all())
2782 assert log_count_after == log_count_before
2785def test_SetUserContentVisibility_unspecified_rejected(db):
2786 super_user, super_token = generate_user(is_superuser=True)
2787 target, _ = generate_user()
2789 with real_moderation_session(super_token) as api:
2790 with pytest.raises(grpc.RpcError) as e:
2791 api.SetUserContentVisibility(
2792 moderation_pb2.SetUserContentVisibilityReq(
2793 user_id=target.id,
2794 visibility=moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED,
2795 )
2796 )
2797 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2800def test_SetUserContentVisibility_non_admin_rejected(db):
2801 normal_user, normal_token = generate_user()
2802 target, _ = generate_user()
2804 with real_moderation_session(normal_token) as api:
2805 with pytest.raises(grpc.RpcError) as e:
2806 api.SetUserContentVisibility(
2807 moderation_pb2.SetUserContentVisibilityReq(
2808 user_id=target.id,
2809 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2810 )
2811 )
2812 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2815def test_SetUserContentVisibility_writes_admin_action(db):
2816 super_user, super_token = generate_user(is_superuser=True)
2817 surfer, surfer_token = generate_user()
2818 host, _ = generate_user()
2820 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2821 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2822 with requests_session(surfer_token) as api:
2823 api.CreateHostRequest(
2824 requests_pb2.CreateHostRequestReq(
2825 host_user_id=host.id,
2826 from_date=today_plus_2,
2827 to_date=today_plus_3,
2828 text=valid_request_text(),
2829 )
2830 )
2832 with real_moderation_session(super_token) as api:
2833 api.SetUserContentVisibility(
2834 moderation_pb2.SetUserContentVisibilityReq(
2835 user_id=surfer.id,
2836 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2837 reason="bulk hide",
2838 )
2839 )
2841 with session_scope() as session:
2842 actions = (
2843 session.execute(
2844 select(AdminAction)
2845 .where(AdminAction.target_user_id == surfer.id)
2846 .where(AdminAction.action_type == "set_user_content_visibility")
2847 )
2848 .scalars()
2849 .all()
2850 )
2851 assert len(actions) == 1
2852 assert actions[0].admin_user_id == super_user.id
2853 assert actions[0].tag == "hidden"
2854 assert actions[0].note == "bulk hide"
2857def test_SetUserContentVisibility_only_touches_target(db):
2858 super_user, super_token = generate_user(is_superuser=True)
2859 target, target_token = generate_user()
2860 other, other_token = generate_user()
2861 host, _ = generate_user()
2863 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2864 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2866 with requests_session(target_token) as api:
2867 target_hr_id = api.CreateHostRequest(
2868 requests_pb2.CreateHostRequestReq(
2869 host_user_id=host.id,
2870 from_date=today_plus_2,
2871 to_date=today_plus_3,
2872 text=valid_request_text(),
2873 )
2874 ).host_request_id
2876 with requests_session(other_token) as api:
2877 other_hr_id = api.CreateHostRequest(
2878 requests_pb2.CreateHostRequestReq(
2879 host_user_id=host.id,
2880 from_date=today_plus_2,
2881 to_date=today_plus_3,
2882 text=valid_request_text(),
2883 )
2884 ).host_request_id
2886 with real_moderation_session(super_token) as api:
2887 res = api.SetUserContentVisibility(
2888 moderation_pb2.SetUserContentVisibilityReq(
2889 user_id=target.id,
2890 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2891 )
2892 )
2893 assert res.updated_count == 1
2895 with session_scope() as session:
2896 target_state = _get_moderation_state(session, ModerationObjectType.host_request, target_hr_id)
2897 other_state = _get_moderation_state(session, ModerationObjectType.host_request, other_hr_id)
2898 assert target_state.visibility == ModerationVisibility.hidden
2899 assert other_state.visibility == ModerationVisibility.shadowed
2902def test_SetUserContentVisibility_user_not_found(db):
2903 super_user, super_token = generate_user(is_superuser=True)
2905 with real_moderation_session(super_token) as api:
2906 with pytest.raises(grpc.RpcError) as e:
2907 api.SetUserContentVisibility(
2908 moderation_pb2.SetUserContentVisibilityReq(
2909 user_id=999999,
2910 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2911 )
2912 )
2913 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2916def test_SetUserContentVisibility_from_visibility_filter(db):
2917 super_user, super_token = generate_user(is_superuser=True)
2918 surfer, surfer_token = generate_user()
2919 host, _ = generate_user()
2921 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2922 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2923 with requests_session(surfer_token) as api:
2924 hr_id = api.CreateHostRequest(
2925 requests_pb2.CreateHostRequestReq(
2926 host_user_id=host.id,
2927 from_date=today_plus_2,
2928 to_date=today_plus_3,
2929 text=valid_request_text(),
2930 )
2931 ).host_request_id
2933 with real_moderation_session(super_token) as api:
2934 api.SetUserContentVisibility(
2935 moderation_pb2.SetUserContentVisibilityReq(
2936 user_id=surfer.id,
2937 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
2938 )
2939 )
2941 with real_moderation_session(super_token) as api:
2942 res = api.SetUserContentVisibility(
2943 moderation_pb2.SetUserContentVisibilityReq(
2944 user_id=surfer.id,
2945 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2946 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_SHADOWED],
2947 )
2948 )
2949 assert res.updated_count == 0
2951 with session_scope() as session:
2952 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
2953 assert state.visibility == ModerationVisibility.visible
2955 with real_moderation_session(super_token) as api:
2956 res = api.SetUserContentVisibility(
2957 moderation_pb2.SetUserContentVisibilityReq(
2958 user_id=surfer.id,
2959 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
2960 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_VISIBLE],
2961 )
2962 )
2963 assert res.updated_count == 1
2965 with session_scope() as session:
2966 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
2967 assert state.visibility == ModerationVisibility.shadowed
2970def test_SetUserContentVisibility_from_visibility_multi(db):
2971 super_user, super_token = generate_user(is_superuser=True)
2972 surfer, surfer_token = generate_user()
2973 host1, _ = generate_user()
2974 host2, _ = generate_user()
2976 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2977 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2978 with requests_session(surfer_token) as api:
2979 hr1_id = api.CreateHostRequest(
2980 requests_pb2.CreateHostRequestReq(
2981 host_user_id=host1.id,
2982 from_date=today_plus_2,
2983 to_date=today_plus_3,
2984 text=valid_request_text(),
2985 )
2986 ).host_request_id
2987 hr2_id = api.CreateHostRequest(
2988 requests_pb2.CreateHostRequestReq(
2989 host_user_id=host2.id,
2990 from_date=today_plus_2,
2991 to_date=today_plus_3,
2992 text=valid_request_text(),
2993 )
2994 ).host_request_id
2996 with session_scope() as session:
2997 state1_id = _get_moderation_state(session, ModerationObjectType.host_request, hr1_id).id
2999 with real_moderation_session(super_token) as api:
3000 api.ModerateContent(
3001 moderation_pb2.ModerateContentReq(
3002 moderation_state_id=state1_id,
3003 action=moderation_pb2.MODERATION_ACTION_APPROVE,
3004 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
3005 )
3006 )
3008 with real_moderation_session(super_token) as api:
3009 res = api.SetUserContentVisibility(
3010 moderation_pb2.SetUserContentVisibilityReq(
3011 user_id=surfer.id,
3012 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3013 from_visibility=[
3014 moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
3015 moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
3016 ],
3017 )
3018 )
3019 assert res.updated_count == 2
3021 with session_scope() as session:
3022 state1 = _get_moderation_state(session, ModerationObjectType.host_request, hr1_id)
3023 state2 = _get_moderation_state(session, ModerationObjectType.host_request, hr2_id)
3024 assert state1.visibility == ModerationVisibility.hidden
3025 assert state2.visibility == ModerationVisibility.hidden
3028def test_SetUserContentVisibility_from_visibility_empty_is_any(db):
3029 super_user, super_token = generate_user(is_superuser=True)
3030 surfer, surfer_token = generate_user()
3031 host, _ = generate_user()
3033 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3034 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3035 with requests_session(surfer_token) as api:
3036 hr_id = api.CreateHostRequest(
3037 requests_pb2.CreateHostRequestReq(
3038 host_user_id=host.id,
3039 from_date=today_plus_2,
3040 to_date=today_plus_3,
3041 text=valid_request_text(),
3042 )
3043 ).host_request_id
3045 with real_moderation_session(super_token) as api:
3046 res = api.SetUserContentVisibility(
3047 moderation_pb2.SetUserContentVisibilityReq(
3048 user_id=surfer.id,
3049 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3050 from_visibility=[],
3051 )
3052 )
3053 assert res.updated_count == 1
3055 with session_scope() as session:
3056 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3057 assert state.visibility == ModerationVisibility.hidden
3060def test_SetUserContentVisibility_from_visibility_unspecified_rejected(db):
3061 super_user, super_token = generate_user(is_superuser=True)
3062 target, _ = generate_user()
3064 with real_moderation_session(super_token) as api:
3065 with pytest.raises(grpc.RpcError) as e:
3066 api.SetUserContentVisibility(
3067 moderation_pb2.SetUserContentVisibilityReq(
3068 user_id=target.id,
3069 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3070 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED],
3071 )
3072 )
3073 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
3076def test_ListModerationStates_empty(db):
3077 super_user, super_token = generate_user(is_superuser=True)
3079 with real_moderation_session(super_token) as api:
3080 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq())
3081 assert len(res.moderation_states) == 0
3082 assert res.next_page_token == ""
3085def test_ListModerationStates_returns_states_chronologically(db):
3086 super_user, super_token = generate_user(is_superuser=True)
3087 surfer, surfer_token = generate_user()
3088 host, _ = generate_user()
3090 state1_id = create_test_host_request_with_moderation(surfer_token, host.id)
3091 state2_id = create_test_host_request_with_moderation(surfer_token, host.id)
3092 state3_id = create_test_host_request_with_moderation(surfer_token, host.id)
3094 with real_moderation_session(super_token) as api:
3095 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq())
3096 assert [s.moderation_state_id for s in res.moderation_states] == [state1_id, state2_id, state3_id]
3098 res_newest = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(newest_first=True))
3099 assert [s.moderation_state_id for s in res_newest.moderation_states] == [state3_id, state2_id, state1_id]
3102def test_ListModerationStates_filter_by_author(db):
3103 super_user, super_token = generate_user(is_superuser=True)
3104 surfer1, surfer1_token = generate_user()
3105 surfer2, surfer2_token = generate_user()
3106 host, _ = generate_user()
3108 state1_id = create_test_host_request_with_moderation(surfer1_token, host.id)
3109 state2_id = create_test_host_request_with_moderation(surfer2_token, host.id)
3110 state3_id = create_test_host_request_with_moderation(surfer1_token, host.id)
3112 with real_moderation_session(super_token) as api:
3113 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer1.id))
3114 assert {s.moderation_state_id for s in res.moderation_states} == {state1_id, state3_id}
3116 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer2.id))
3117 assert [s.moderation_state_id for s in res.moderation_states] == [state2_id]
3120def test_ListModerationStates_pagination(db):
3121 super_user, super_token = generate_user(is_superuser=True)
3122 surfer, surfer_token = generate_user()
3123 host, _ = generate_user()
3125 state_ids = [create_test_host_request_with_moderation(surfer_token, host.id) for _ in range(3)]
3127 with real_moderation_session(super_token) as api:
3128 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(page_size=2))
3129 assert [s.moderation_state_id for s in res.moderation_states] == state_ids[:2]
3130 assert res.next_page_token != ""
3132 res2 = api.ListModerationStates(
3133 moderation_pb2.ListModerationStatesReq(page_size=2, page_token=res.next_page_token)
3134 )
3135 assert [s.moderation_state_id for s in res2.moderation_states] == [state_ids[2]]
3136 assert res2.next_page_token == ""