Coverage for app/backend/src/tests/test_moderation.py: 100%
1925 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +0000
1"""
2Comprehensive tests for the Unified Moderation System (UMS)
3"""
5import json
6from datetime import datetime, timedelta
8import grpc
9import pytest
10from google.protobuf import empty_pb2, wrappers_pb2
11from sqlalchemy.exc import IntegrityError
12from sqlalchemy.sql import select
14from couchers import urls
15from couchers.config import config
16from couchers.constants import MODERATION_AUTO_APPROVE_FLAG_PRIORITY
17from couchers.db import session_scope
18from couchers.jobs.handlers import (
19 DatabaseInconsistencyError,
20 auto_approve_moderation_queue,
21 check_database_consistency,
22)
23from couchers.jobs.worker import process_job
24from couchers.models import (
25 AdminAction,
26 EventOccurrence,
27 FriendRelationship,
28 GroupChat,
29 HostRequest,
30 ModerationAction,
31 ModerationLog,
32 ModerationObjectType,
33 ModerationQueueItem,
34 ModerationState,
35 ModerationTrigger,
36 ModerationVisibility,
37 Page,
38 PageType,
39 Reply,
40 User,
41 get_moderated_models,
42)
43from couchers.moderation.utils import create_moderation
44from couchers.proto import (
45 admin_pb2,
46 api_pb2,
47 conversations_pb2,
48 discussions_pb2,
49 messages_pb2,
50 moderation_pb2,
51 notifications_pb2,
52 pages_pb2,
53 public_trips_pb2,
54 requests_pb2,
55 threads_pb2,
56)
57from couchers.servicers.threads import pack_thread_id
58from couchers.utils import Timestamp_from_datetime, now, today
59from tests.fixtures.api import events as events_api
60from tests.fixtures.db import backdate_conversations, generate_user, make_friends
61from tests.fixtures.misc import EmailCollector, PushCollector, process_jobs
62from tests.fixtures.sessions import (
63 api_session,
64 conversations_session,
65 discussions_session,
66 notifications_session,
67 pages_session,
68 public_trips_session,
69 real_admin_session,
70 real_moderation_session,
71 requests_session,
72 threads_session,
73)
74from tests.test_communities import create_community, create_group
75from tests.test_public_trips import VALID_DESCRIPTION as VALID_TRIP_DESCRIPTION
76from tests.test_references import create_friend_reference
77from tests.test_requests import valid_request_text
80def create_test_host_request_with_moderation(surfer_token, host_user_id):
81 """Helper to create a host request and return its moderation state ID"""
82 today_plus_2 = (today() + timedelta(days=2)).isoformat()
83 today_plus_3 = (today() + timedelta(days=3)).isoformat()
85 with requests_session(surfer_token) as api:
86 hr_id = api.CreateHostRequest(
87 requests_pb2.CreateHostRequestReq(
88 host_user_id=host_user_id,
89 from_date=today_plus_2,
90 to_date=today_plus_3,
91 text=valid_request_text(),
92 )
93 ).host_request_id
95 backdate_conversations()
97 with session_scope() as session:
98 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
99 return hr.moderation_state_id
102# ============================================================================
103# Tests for moderation helper functions
104# ============================================================================
107def test_create_moderation(db):
108 """Test creating a moderation state with associated log entry"""
109 user, _ = generate_user()
111 with session_scope() as session:
112 # Create a moderation state
113 moderation_state = create_moderation(
114 session=session,
115 object_type=ModerationObjectType.host_request,
116 object_id=123,
117 creator_user_id=user.id,
118 )
120 assert moderation_state.object_type == ModerationObjectType.host_request
121 assert moderation_state.object_id == 123
122 assert moderation_state.visibility == ModerationVisibility.shadowed
124 # Check that log entry was created
125 log_entries = (
126 session.execute(select(ModerationLog).where(ModerationLog.moderation_state_id == moderation_state.id))
127 .scalars()
128 .all()
129 )
131 assert len(log_entries) == 1
132 assert log_entries[0].action == ModerationAction.create
133 assert log_entries[0].reason == "Object created."
134 assert log_entries[0].moderator_user_id == user.id
137def test_add_to_moderation_queue(db):
138 """Test adding content to moderation queue via API"""
139 super_user, super_token = generate_user(is_superuser=True)
140 user1, token1 = generate_user()
141 user2, _ = generate_user()
143 today_plus_2 = (today() + timedelta(days=2)).isoformat()
144 today_plus_3 = (today() + timedelta(days=3)).isoformat()
146 # Create a real host request (which automatically creates moderation state and adds to queue)
147 with requests_session(token1) as api:
148 host_request_id = api.CreateHostRequest(
149 requests_pb2.CreateHostRequestReq(
150 host_user_id=user2.id,
151 from_date=today_plus_2,
152 to_date=today_plus_3,
153 text=valid_request_text(),
154 )
155 ).host_request_id
157 # Get the moderation state ID
158 state_id = None
159 with session_scope() as session:
160 host_request = session.execute(
161 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
162 ).scalar_one()
163 state_id = host_request.moderation_state_id
165 # Add another item to moderation queue via API (the first one was created automatically)
166 with real_moderation_session(super_token) as api:
167 api.ModerateContent(
168 moderation_pb2.ModerateContentReq(
169 moderation_state_id=state_id,
170 action=moderation_pb2.MODERATION_ACTION_FLAG,
171 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG,
172 reason="Admin manually flagged for additional review",
173 )
174 )
176 with session_scope() as session:
177 flag = session.execute(
178 select(ModerationQueueItem)
179 .where(ModerationQueueItem.moderation_state_id == state_id)
180 .where(ModerationQueueItem.trigger == ModerationTrigger.user_flag)
181 ).scalar_one()
182 assert flag.reason == "Admin manually flagged for additional review"
183 assert flag.resolved_by_log_id is None
185 # The FLAG action is recorded in the log, pointing at the new queue item.
186 flag_log = session.execute(
187 select(ModerationLog)
188 .where(ModerationLog.moderation_state_id == state_id)
189 .where(ModerationLog.action == ModerationAction.flag)
190 ).scalar_one()
191 assert flag_log.queue_item_id == flag.id
194def test_flag_with_data(db):
195 """A flag can carry a JSON payload"""
196 super_user, super_token = generate_user(is_superuser=True)
197 user1, token1 = generate_user()
198 user2, _ = generate_user()
200 state_id = create_test_host_request_with_moderation(token1, user2.id)
202 with real_moderation_session(super_token) as api:
203 api.ModerateContent(
204 moderation_pb2.ModerateContentReq(
205 moderation_state_id=state_id,
206 action=moderation_pb2.MODERATION_ACTION_FLAG,
207 trigger=moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG,
208 reason="Automod",
209 data=json.dumps({"classifier": "spam", "score": 0.97, "matches": ["buy now"]}),
210 )
211 )
213 res = api.GetModerationQueue(
214 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG])
215 )
216 assert len(res.queue_items) == 1
217 assert json.loads(res.queue_items[0].data) == {
218 "classifier": "spam",
219 "score": 0.97,
220 "matches": ["buy now"],
221 }
223 with session_scope() as session:
224 flag = session.execute(
225 select(ModerationQueueItem)
226 .where(ModerationQueueItem.moderation_state_id == state_id)
227 .where(ModerationQueueItem.trigger == ModerationTrigger.machine_flag)
228 ).scalar_one()
229 assert flag.data == {"classifier": "spam", "score": 0.97, "matches": ["buy now"]}
232def test_flag_without_data(db):
233 """A flag with no data serializes as an empty string"""
234 super_user, super_token = generate_user(is_superuser=True)
235 user1, token1 = generate_user()
236 user2, _ = generate_user()
238 state_id = create_test_host_request_with_moderation(token1, user2.id)
240 with real_moderation_session(super_token) as api:
241 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
242 assert len(res.queue_items) == 1
243 assert res.queue_items[0].data == ""
245 with session_scope() as session:
246 item = session.execute(
247 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id)
248 ).scalar_one()
249 assert item.data is None
252def test_flag_with_invalid_data(db):
253 """Flag data must be valid JSON"""
254 super_user, super_token = generate_user(is_superuser=True)
255 user1, token1 = generate_user()
256 user2, _ = generate_user()
258 state_id = create_test_host_request_with_moderation(token1, user2.id)
260 with real_moderation_session(super_token) as api:
261 with pytest.raises(grpc.RpcError) as e:
262 api.ModerateContent(
263 moderation_pb2.ModerateContentReq(
264 moderation_state_id=state_id,
265 action=moderation_pb2.MODERATION_ACTION_FLAG,
266 trigger=moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG,
267 reason="Automod",
268 data="not json",
269 )
270 )
271 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
272 assert e.value.details() == "The moderation data must be valid JSON."
275def test_data_rejected_on_non_flag_action(db):
276 """Data has nowhere to go on actions that don't create a queue item"""
277 super_user, super_token = generate_user(is_superuser=True)
278 user1, token1 = generate_user()
279 user2, _ = generate_user()
281 state_id = create_test_host_request_with_moderation(token1, user2.id)
283 with real_moderation_session(super_token) as api:
284 with pytest.raises(grpc.RpcError) as e:
285 api.ModerateContent(
286 moderation_pb2.ModerateContentReq(
287 moderation_state_id=state_id,
288 action=moderation_pb2.MODERATION_ACTION_APPROVE,
289 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
290 reason="Looks fine",
291 data=json.dumps({"score": 0.1}),
292 )
293 )
294 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
295 assert e.value.details() == "Moderation data can only be set when flagging."
298def test_create_moderation_with_data(db):
299 """create_moderation can attach data to the initial review item"""
300 user, _ = generate_user()
302 with session_scope() as session:
303 moderation_state = create_moderation(
304 session=session,
305 object_type=ModerationObjectType.host_request,
306 object_id=123,
307 creator_user_id=user.id,
308 data={"classifier": "spam", "score": 0.42},
309 )
311 item = session.execute(
312 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == moderation_state.id)
313 ).scalar_one()
314 assert item.trigger == ModerationTrigger.initial_review
315 assert item.data == {"classifier": "spam", "score": 0.42}
318def test_moderate_content(db):
319 """Test moderating content via API"""
320 super_user, super_token = generate_user(is_superuser=True)
321 user, token = generate_user()
322 host, _ = generate_user()
324 today_plus_2 = (today() + timedelta(days=2)).isoformat()
325 today_plus_3 = (today() + timedelta(days=3)).isoformat()
327 # Create a real host request
328 state_id = None
329 with requests_session(token) as api:
330 hr_id = api.CreateHostRequest(
331 requests_pb2.CreateHostRequestReq(
332 host_user_id=host.id,
333 from_date=today_plus_2,
334 to_date=today_plus_3,
335 text=valid_request_text(),
336 )
337 ).host_request_id
339 with session_scope() as session:
340 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
341 state_id = hr.moderation_state_id
343 # Moderate the content via API
344 with real_moderation_session(super_token) as api:
345 res = api.ModerateContent(
346 moderation_pb2.ModerateContentReq(
347 moderation_state_id=state_id,
348 action=moderation_pb2.MODERATION_ACTION_APPROVE,
349 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
350 reason="Content looks good",
351 )
352 )
354 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
356 # Check that state was updated in database
357 with session_scope() as session:
358 updated_state = session.get_one(ModerationState, state_id)
359 assert updated_state.visibility == ModerationVisibility.visible
361 # Check that log entry was created
362 log_entries = (
363 session.execute(
364 select(ModerationLog)
365 .where(ModerationLog.moderation_state_id == state_id)
366 .order_by(ModerationLog.time.desc(), ModerationLog.id.desc())
367 )
368 .scalars()
369 .all()
370 )
372 assert len(log_entries) == 2 # CREATE + APPROVE
373 assert log_entries[0].action == ModerationAction.approve
374 assert log_entries[0].moderator_user_id == super_user.id
375 assert log_entries[0].reason == "Content looks good"
378def test_resolve_queue_item(db):
379 """Test resolving a moderation queue item via ModerateContent API"""
380 user1, token1 = generate_user()
381 user2, _ = generate_user()
382 moderator, moderator_token = generate_user(is_superuser=True)
384 today_plus_2 = (today() + timedelta(days=2)).isoformat()
385 today_plus_3 = (today() + timedelta(days=3)).isoformat()
387 # Create a host request using the API (which automatically creates moderation state)
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 state_id = None
399 with session_scope() as session:
400 # Get the host request and its moderation state
401 host_request = session.execute(
402 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
403 ).scalar_one()
404 state_id = host_request.moderation_state_id
406 # The moderation state should already exist and be in the queue
407 queue_item = session.execute(
408 select(ModerationQueueItem)
409 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
410 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
411 ).scalar_one()
413 assert queue_item.resolved_by_log_id is None
415 # Approve content with clear_flags, which resolves the open queue item(s)
416 with real_moderation_session(moderator_token) as api:
417 api.ModerateContent(
418 moderation_pb2.ModerateContentReq(
419 moderation_state_id=state_id,
420 action=moderation_pb2.MODERATION_ACTION_APPROVE,
421 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
422 reason="Approved after review",
423 clear_flags=True,
424 )
425 )
427 # Check that queue item was resolved
428 with session_scope() as session:
429 queue_item = session.execute(
430 select(ModerationQueueItem)
431 .where(ModerationQueueItem.moderation_state_id == state_id)
432 .where(ModerationQueueItem.resolved_by_log_id.is_not(None))
433 ).scalar_one()
434 assert queue_item.resolved_by_log_id is not None
437def test_approve_content_via_api(db):
438 """Test approving content via ModerateContent API"""
439 user1, token1 = generate_user()
440 user2, _ = generate_user()
441 moderator, moderator_token = generate_user(is_superuser=True)
443 today_plus_2 = (today() + timedelta(days=2)).isoformat()
444 today_plus_3 = (today() + timedelta(days=3)).isoformat()
446 # Create a host request using the API (which automatically creates moderation state)
447 with requests_session(token1) as api:
448 host_request_id = api.CreateHostRequest(
449 requests_pb2.CreateHostRequestReq(
450 host_user_id=user2.id,
451 from_date=today_plus_2,
452 to_date=today_plus_3,
453 text=valid_request_text(),
454 )
455 ).host_request_id
457 state_id = None
458 with session_scope() as session:
459 # Get the host request and its moderation state
460 host_request = session.execute(
461 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
462 ).scalar_one()
463 state_id = host_request.moderation_state_id
465 # Approve via API
466 with real_moderation_session(moderator_token) as api:
467 api.ModerateContent(
468 moderation_pb2.ModerateContentReq(
469 moderation_state_id=state_id,
470 action=moderation_pb2.MODERATION_ACTION_APPROVE,
471 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
472 reason="Quick approval",
473 )
474 )
476 # Check that state was updated to VISIBLE
477 with session_scope() as session:
478 updated_state = session.get_one(ModerationState, state_id)
479 assert updated_state.visibility == ModerationVisibility.visible
481 # Check log entry
482 log_entry = session.execute(
483 select(ModerationLog)
484 .where(ModerationLog.moderation_state_id == state_id)
485 .where(ModerationLog.action == ModerationAction.approve)
486 ).scalar_one()
488 assert log_entry.moderator_user_id == moderator.id
489 assert log_entry.reason == "Quick approval"
492# ============================================================================
493# Tests for host request moderation integration
494# ============================================================================
497def test_create_host_request_creates_moderation_state(db):
498 """Test that creating a host request automatically creates a moderation state"""
499 user1, token1 = generate_user()
500 user2, token2 = generate_user()
502 today_plus_2 = (today() + timedelta(days=2)).isoformat()
503 today_plus_3 = (today() + timedelta(days=3)).isoformat()
505 with requests_session(token1) as api:
506 host_request_id = api.CreateHostRequest(
507 requests_pb2.CreateHostRequestReq(
508 host_user_id=user2.id,
509 from_date=today_plus_2,
510 to_date=today_plus_3,
511 text=valid_request_text(),
512 )
513 ).host_request_id
515 with session_scope() as session:
516 # Check that host request has a moderation state
517 host_request = session.execute(
518 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
519 ).scalar_one()
521 # Check moderation state properties
522 moderation_state = session.execute(
523 select(ModerationState).where(ModerationState.id == host_request.moderation_state_id)
524 ).scalar_one()
526 assert moderation_state.object_type == ModerationObjectType.host_request
527 assert moderation_state.object_id == host_request_id
528 assert moderation_state.visibility == ModerationVisibility.shadowed
530 # Check that it was added to moderation queue
531 queue_items = (
532 session.execute(
533 select(ModerationQueueItem)
534 .where(ModerationQueueItem.moderation_state_id == moderation_state.id)
535 .where(ModerationQueueItem.resolved_by_log_id == None)
536 )
537 .scalars()
538 .all()
539 )
541 assert len(queue_items) == 1
542 assert queue_items[0].trigger == ModerationTrigger.initial_review
543 # item_author_user_id is no longer stored in the model, it's dynamically retrieved
546def test_host_request_no_notification_before_approval(db, push_collector: PushCollector):
547 """Test that host requests don't send notifications until approved"""
548 user1, token1 = generate_user()
549 user2, token2 = generate_user()
551 today_plus_2 = (today() + timedelta(days=2)).isoformat()
552 today_plus_3 = (today() + timedelta(days=3)).isoformat()
554 with requests_session(token1) as api:
555 host_request_id = api.CreateHostRequest(
556 requests_pb2.CreateHostRequestReq(
557 host_user_id=user2.id,
558 from_date=today_plus_2,
559 to_date=today_plus_3,
560 text=valid_request_text(),
561 )
562 ).host_request_id
564 # Process all jobs (including the notification job)
565 process_jobs()
567 # No push notification should be sent yet (host requests are shadowed initially)
568 assert push_collector.count_for_user(user2.id) == 0
571def test_shadowed_notification_not_in_list_notifications(db):
572 """Test that notifications for shadowed content don't appear in ListNotifications API"""
573 user1, token1 = generate_user()
574 user2, token2 = generate_user()
576 today_plus_2 = (today() + timedelta(days=2)).isoformat()
577 today_plus_3 = (today() + timedelta(days=3)).isoformat()
579 # Create a host request (which creates a shadowed notification for the host)
580 with requests_session(token1) as api:
581 host_request_id = api.CreateHostRequest(
582 requests_pb2.CreateHostRequestReq(
583 host_user_id=user2.id,
584 from_date=today_plus_2,
585 to_date=today_plus_3,
586 text=valid_request_text(),
587 )
588 ).host_request_id
590 # Host (recipient) should NOT see the notification in ListNotifications - it's for shadowed content
591 with notifications_session(token2) as api:
592 res = api.ListNotifications(notifications_pb2.ListNotificationsReq())
593 # Should be empty - the host request is still shadowed
594 assert len(res.notifications) == 0
597def test_notification_visible_after_approval(db):
598 """Test that notifications appear in ListNotifications after content is approved"""
599 user1, token1 = generate_user()
600 user2, token2 = generate_user()
601 mod, mod_token = generate_user(is_superuser=True)
603 today_plus_2 = (today() + timedelta(days=2)).isoformat()
604 today_plus_3 = (today() + timedelta(days=3)).isoformat()
606 # Create a host request (which creates a shadowed notification for the host)
607 with requests_session(token1) as api:
608 host_request_id = api.CreateHostRequest(
609 requests_pb2.CreateHostRequestReq(
610 host_user_id=user2.id,
611 from_date=today_plus_2,
612 to_date=today_plus_3,
613 text=valid_request_text(),
614 )
615 ).host_request_id
617 # Host (recipient) should NOT see the notification initially
618 with notifications_session(token2) as api:
619 res = api.ListNotifications(notifications_pb2.ListNotificationsReq())
620 assert len(res.notifications) == 0
622 # Get the moderation state ID and approve
623 with session_scope() as session:
624 host_request = session.execute(
625 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
626 ).scalar_one()
627 state_id = host_request.moderation_state_id
629 with real_moderation_session(mod_token) as api:
630 api.ModerateContent(
631 moderation_pb2.ModerateContentReq(
632 moderation_state_id=state_id,
633 action=moderation_pb2.MODERATION_ACTION_APPROVE,
634 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
635 reason="Looks good",
636 )
637 )
639 # Now host SHOULD see the notification
640 with notifications_session(token2) as api:
641 res = api.ListNotifications(notifications_pb2.ListNotificationsReq())
642 assert len(res.notifications) == 1
643 assert res.notifications[0].topic == "host_request"
644 assert res.notifications[0].action == "create"
647def test_shadowed_host_request_visible_to_author_only(db):
648 """Test that SHADOWED host requests are visible only to the author (surfer), not the recipient (host)"""
649 user1, token1 = generate_user()
650 user2, token2 = generate_user()
652 today_plus_2 = (today() + timedelta(days=2)).isoformat()
653 today_plus_3 = (today() + timedelta(days=3)).isoformat()
655 with requests_session(token1) as api:
656 host_request_id = api.CreateHostRequest(
657 requests_pb2.CreateHostRequestReq(
658 host_user_id=user2.id,
659 from_date=today_plus_2,
660 to_date=today_plus_3,
661 text=valid_request_text(),
662 )
663 ).host_request_id
665 # Surfer (author) can see it with GetHostRequest
666 with requests_session(token1) as api:
667 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
668 assert res.host_request_id == host_request_id
669 assert res.latest_message.text.text == valid_request_text()
671 # Host (recipient) CANNOT see it with GetHostRequest - it's shadowed
672 with requests_session(token2) as api:
673 with pytest.raises(grpc.RpcError) as e:
674 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
675 assert e.value.code() == grpc.StatusCode.NOT_FOUND
678def test_unlisted_host_request_not_in_lists(db):
679 """Test that SHADOWED host requests are visible to author but not to recipient"""
680 user1, token1 = generate_user()
681 user2, token2 = generate_user()
683 today_plus_2 = (today() + timedelta(days=2)).isoformat()
684 today_plus_3 = (today() + timedelta(days=3)).isoformat()
686 with requests_session(token1) as api:
687 host_request_id = api.CreateHostRequest(
688 requests_pb2.CreateHostRequestReq(
689 host_user_id=user2.id,
690 from_date=today_plus_2,
691 to_date=today_plus_3,
692 text=valid_request_text(),
693 )
694 ).host_request_id
696 # Surfer (author) should see it in their sent list even though it's SHADOWED
697 with requests_session(token1) as api:
698 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
699 assert len(res.host_requests) == 1
701 # Host should NOT see it in their received list (still SHADOWED from them)
702 with requests_session(token2) as api:
703 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
704 assert len(res.host_requests) == 0
707def test_approved_host_request_in_lists_and_notifications(db, push_collector: PushCollector):
708 """Test that approved host requests appear in lists and send notifications"""
709 user1, token1 = generate_user()
710 user2, token2 = generate_user()
711 mod, mod_token = generate_user(is_superuser=True)
713 today_plus_2 = (today() + timedelta(days=2)).isoformat()
714 today_plus_3 = (today() + timedelta(days=3)).isoformat()
716 with requests_session(token1) as api:
717 host_request_id = api.CreateHostRequest(
718 requests_pb2.CreateHostRequestReq(
719 host_user_id=user2.id,
720 from_date=today_plus_2,
721 to_date=today_plus_3,
722 text=valid_request_text(),
723 )
724 ).host_request_id
726 # Process the initial notification job - should be deferred (no notification sent)
727 process_jobs()
728 assert push_collector.count_for_user(user2.id) == 0
730 # Get the moderation state ID
731 state_id = None
732 with session_scope() as session:
733 host_request = session.execute(
734 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
735 ).scalar_one()
736 state_id = host_request.moderation_state_id
738 # Approve the host request via API
739 with real_moderation_session(mod_token) as api:
740 api.ModerateContent(
741 moderation_pb2.ModerateContentReq(
742 moderation_state_id=state_id,
743 action=moderation_pb2.MODERATION_ACTION_APPROVE,
744 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
745 reason="Looks good",
746 )
747 )
749 # Process the re-queued notification job - should now send notification
750 process_jobs()
752 # Now surfer SHOULD see it in their sent list
753 with requests_session(token1) as api:
754 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
755 assert len(res.host_requests) == 1
756 assert res.host_requests[0].host_request_id == host_request_id
758 # Host SHOULD see it in their received list
759 with requests_session(token2) as api:
760 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
761 assert len(res.host_requests) == 1
762 assert res.host_requests[0].host_request_id == host_request_id
764 # After approval, the host should have received a push notification
765 assert push_collector.pop_for_user(user2.id, last=True).topic_action == "host_request:create"
768def test_hidden_host_request_invisible_to_all(db):
769 """Test that HIDDEN host requests are invisible to everyone except moderators"""
770 user1, token1 = generate_user()
771 user2, token2 = generate_user()
772 user3, token3 = generate_user() # Third party
773 moderator, moderator_token = generate_user(is_superuser=True)
775 today_plus_2 = (today() + timedelta(days=2)).isoformat()
776 today_plus_3 = (today() + timedelta(days=3)).isoformat()
778 with requests_session(token1) as api:
779 host_request_id = api.CreateHostRequest(
780 requests_pb2.CreateHostRequestReq(
781 host_user_id=user2.id,
782 from_date=today_plus_2,
783 to_date=today_plus_3,
784 text=valid_request_text(),
785 )
786 ).host_request_id
788 # Get the moderation state ID
789 state_id = None
790 with session_scope() as session:
791 host_request = session.execute(
792 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
793 ).scalar_one()
794 state_id = host_request.moderation_state_id
796 # Hide the host request via API (e.g., spam/abuse)
797 with real_moderation_session(moderator_token) as api:
798 api.ModerateContent(
799 moderation_pb2.ModerateContentReq(
800 moderation_state_id=state_id,
801 action=moderation_pb2.MODERATION_ACTION_HIDE,
802 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
803 reason="Spam content",
804 )
805 )
807 # Surfer can't see it with GetHostRequest
808 with requests_session(token1) as api:
809 with pytest.raises(grpc.RpcError) as e:
810 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
811 assert e.value.code() == grpc.StatusCode.NOT_FOUND
813 # Host can't see it with GetHostRequest
814 with requests_session(token2) as api:
815 with pytest.raises(grpc.RpcError) as e:
816 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
817 assert e.value.code() == grpc.StatusCode.NOT_FOUND
819 # Third party definitely can't see it
820 with requests_session(token3) as api:
821 with pytest.raises(grpc.RpcError) as e:
822 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
823 assert e.value.code() == grpc.StatusCode.NOT_FOUND
825 # Not in any lists
826 with requests_session(token1) as api:
827 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
828 assert len(res.host_requests) == 0
830 with requests_session(token2) as api:
831 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
832 assert len(res.host_requests) == 0
835def test_multiple_host_requests_listing_visibility(db):
836 """Test that ListHostRequests correctly filters based on moderation state"""
837 user1, token1 = generate_user()
838 user2, token2 = generate_user()
839 moderator, moderator_token = generate_user(is_superuser=True)
841 today_plus_2 = (today() + timedelta(days=2)).isoformat()
842 today_plus_3 = (today() + timedelta(days=3)).isoformat()
844 # Create 3 host requests
845 host_request_ids = []
846 state_ids = []
847 with requests_session(token1) as api:
848 for i in range(3):
849 hr_id = api.CreateHostRequest(
850 requests_pb2.CreateHostRequestReq(
851 host_user_id=user2.id,
852 from_date=today_plus_2,
853 to_date=today_plus_3,
854 text=valid_request_text(f"Test request {i + 1}"),
855 )
856 ).host_request_id
857 host_request_ids.append(hr_id)
858 backdate_conversations()
860 # Get state IDs
861 with session_scope() as session:
862 for hr_id in host_request_ids:
863 host_request = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
864 state_ids.append(host_request.moderation_state_id)
866 # Approve the first one via API
867 with real_moderation_session(moderator_token) as api:
868 api.ModerateContent(
869 moderation_pb2.ModerateContentReq(
870 moderation_state_id=state_ids[0],
871 action=moderation_pb2.MODERATION_ACTION_APPROVE,
872 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
873 reason="Approved",
874 )
875 )
877 # Hide the third one via API
878 with real_moderation_session(moderator_token) as api:
879 api.ModerateContent(
880 moderation_pb2.ModerateContentReq(
881 moderation_state_id=state_ids[2],
882 action=moderation_pb2.MODERATION_ACTION_HIDE,
883 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
884 reason="Spam",
885 )
886 )
888 # Surfer should see the approved one and the shadowed one (author can see their SHADOWED content)
889 with requests_session(token1) as api:
890 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
891 assert len(res.host_requests) == 2
892 visible_ids = {hr.host_request_id for hr in res.host_requests}
893 assert visible_ids == {host_request_ids[0], host_request_ids[1]}
895 # Host should see only the approved one in received list
896 with requests_session(token2) as api:
897 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
898 assert len(res.host_requests) == 1
899 assert res.host_requests[0].host_request_id == host_request_ids[0]
902def test_moderation_log_tracking(db):
903 """Test that moderation actions are properly logged via API"""
904 user, user_token = generate_user()
905 host, _ = generate_user()
906 moderator1, moderator1_token = generate_user(is_superuser=True)
907 moderator2, moderator2_token = generate_user(is_superuser=True)
909 # Create a real host request
910 state_id = create_test_host_request_with_moderation(user_token, host.id)
912 # Perform several moderation actions via API
913 with real_moderation_session(moderator1_token) as api:
914 api.ModerateContent(
915 moderation_pb2.ModerateContentReq(
916 moderation_state_id=state_id,
917 action=moderation_pb2.MODERATION_ACTION_APPROVE,
918 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
919 reason="Looks good initially",
920 )
921 )
923 with real_moderation_session(moderator2_token) as api:
924 api.ModerateContent(
925 moderation_pb2.ModerateContentReq(
926 moderation_state_id=state_id,
927 action=moderation_pb2.MODERATION_ACTION_FLAG,
928 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW,
929 reason="Wait, this needs another look",
930 )
931 )
932 # Shadow it back
933 api.ModerateContent(
934 moderation_pb2.ModerateContentReq(
935 moderation_state_id=state_id,
936 action=moderation_pb2.MODERATION_ACTION_HIDE,
937 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
938 reason="Wait, this needs another look",
939 )
940 )
942 with real_moderation_session(moderator1_token) as api:
943 api.ModerateContent(
944 moderation_pb2.ModerateContentReq(
945 moderation_state_id=state_id,
946 action=moderation_pb2.MODERATION_ACTION_HIDE,
947 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
948 reason="Actually it's spam",
949 )
950 )
952 # Check all log entries
953 with session_scope() as session:
954 log_entries = (
955 session.execute(
956 select(ModerationLog)
957 .where(ModerationLog.moderation_state_id == state_id)
958 .order_by(ModerationLog.time.asc())
959 )
960 .scalars()
961 .all()
962 )
964 # CREATE + APPROVE + HIDE + HIDE (shadowing back counts as HIDE action)
965 assert len(log_entries) >= 3
967 assert log_entries[0].action == ModerationAction.create
968 assert log_entries[0].moderator_user_id == user.id
969 assert log_entries[0].reason == "Object created."
971 assert log_entries[1].action == ModerationAction.approve
972 assert log_entries[1].moderator_user_id == moderator1.id
973 assert log_entries[1].reason == "Looks good initially"
975 # The last action should be hiding
976 assert log_entries[-1].action == ModerationAction.hide
977 assert log_entries[-1].moderator_user_id == moderator1.id
978 assert log_entries[-1].reason == "Actually it's spam"
981def test_moderation_queue_workflow(db):
982 """Test the full moderation queue workflow via API"""
983 user1, token1 = generate_user()
984 user2, _ = generate_user()
985 moderator, moderator_token = generate_user(is_superuser=True)
987 today_plus_2 = (today() + timedelta(days=2)).isoformat()
988 today_plus_3 = (today() + timedelta(days=3)).isoformat()
990 # Create a host request using the API (which automatically creates moderation state and adds to queue)
991 with requests_session(token1) as api:
992 host_request_id = api.CreateHostRequest(
993 requests_pb2.CreateHostRequestReq(
994 host_user_id=user2.id,
995 from_date=today_plus_2,
996 to_date=today_plus_3,
997 text=valid_request_text(),
998 )
999 ).host_request_id
1001 state_id = None
1002 queue_item_id = None
1003 with session_scope() as session:
1004 # Get the host request and its moderation state
1005 host_request = session.execute(
1006 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
1007 ).scalar_one()
1008 state_id = host_request.moderation_state_id
1010 # The queue item should already exist (created automatically)
1011 queue_item = session.execute(
1012 select(ModerationQueueItem)
1013 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
1014 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
1015 ).scalar_one()
1016 queue_item_id = queue_item.id
1018 # Verify it's in the queue
1019 unresolved_items = (
1020 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.resolved_by_log_id == None))
1021 .scalars()
1022 .all()
1023 )
1025 assert len(unresolved_items) >= 1
1026 assert queue_item.id in [item.id for item in unresolved_items]
1028 # Moderator reviews and approves via API, clearing the open queue item
1029 with real_moderation_session(moderator_token) as api:
1030 api.ModerateContent(
1031 moderation_pb2.ModerateContentReq(
1032 moderation_state_id=state_id,
1033 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1034 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1035 reason="Content approved",
1036 clear_flags=True,
1037 )
1038 )
1040 # Verify queue item was resolved
1041 with session_scope() as session:
1042 # Verify it's no longer in unresolved queue
1043 unresolved_items = (
1044 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.resolved_by_log_id == None))
1045 .scalars()
1046 .all()
1047 )
1049 assert queue_item_id not in [item.id for item in unresolved_items]
1051 # Verify the queue item was linked to a log entry
1052 queue_item = session.get_one(ModerationQueueItem, queue_item_id)
1053 assert queue_item.resolved_by_log_id is not None
1056# ============================================================================
1057# Moderation API Tests (testing the gRPC servicer)
1058# ============================================================================
1061def test_GetModerationQueue_empty(db):
1062 """Test getting an empty moderation queue"""
1063 super_user, super_token = generate_user(is_superuser=True)
1065 with real_moderation_session(super_token) as api:
1066 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1067 assert len(res.queue_items) == 0
1068 assert res.next_page_token == ""
1071def test_GetModerationQueue_with_items(db):
1072 """Test getting moderation queue with items via API"""
1073 super_user, super_token = generate_user(is_superuser=True)
1074 normal_user, user_token = generate_user()
1075 host, _ = generate_user()
1077 # Create some host requests (which automatically adds them to moderation queue)
1078 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1079 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1081 with real_moderation_session(super_token) as api:
1082 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1083 assert len(res.queue_items) == 2
1084 assert res.queue_items[0].is_resolved == False
1085 assert res.queue_items[1].is_resolved == False
1088def test_GetModerationQueue_filter_by_trigger(db):
1089 """Test filtering moderation queue by trigger type via API"""
1090 super_user, super_token = generate_user(is_superuser=True)
1091 normal_user, user_token = generate_user()
1092 host, _ = generate_user()
1094 # Create host requests (which automatically adds them to moderation queue with INITIAL_REVIEW)
1095 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1096 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1098 # Add USER_FLAG trigger to second item via API
1099 with real_moderation_session(super_token) as api:
1100 api.ModerateContent(
1101 moderation_pb2.ModerateContentReq(
1102 moderation_state_id=state2_id,
1103 action=moderation_pb2.MODERATION_ACTION_FLAG,
1104 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG,
1105 reason="Reported by user",
1106 )
1107 )
1109 # Filter by INITIAL_REVIEW (should get first item and maybe both depending on how queue works)
1110 with real_moderation_session(super_token) as api:
1111 res = api.GetModerationQueue(
1112 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW])
1113 )
1114 assert len(res.queue_items) == 2 # Both have INITIAL_REVIEW triggers
1115 assert all(item.trigger == moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW for item in res.queue_items)
1117 # Filter by USER_FLAG (should get second item only)
1118 with real_moderation_session(super_token) as api:
1119 res = api.GetModerationQueue(
1120 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_USER_FLAG])
1121 )
1122 assert len(res.queue_items) == 1
1123 assert res.queue_items[0].trigger == moderation_pb2.MODERATION_TRIGGER_USER_FLAG
1126def test_GetModerationQueue_filter_created_before(db):
1127 """Test filtering moderation queue by created_before timestamp"""
1128 super_user, super_token = generate_user(is_superuser=True)
1129 normal_user, user_token = generate_user()
1130 host, _ = generate_user()
1132 # Create host requests
1133 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1134 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1136 # Backdate the first queue item
1137 with session_scope() as session:
1138 queue_item1 = session.execute(
1139 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1140 ).scalar_one()
1141 # Set it to 2 hours ago
1142 queue_item1.time_created = now() - timedelta(hours=2)
1144 # The second item remains at current time
1146 # Filter to items created before 1 hour ago (should only get the first item)
1147 cutoff_time = now() - timedelta(hours=1)
1148 with real_moderation_session(super_token) as api:
1149 res = api.GetModerationQueue(
1150 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(cutoff_time))
1151 )
1152 assert len(res.queue_items) == 1
1153 assert res.queue_items[0].moderation_state_id == state1_id
1155 # Filter to items created before now (should get both)
1156 with real_moderation_session(super_token) as api:
1157 res = api.GetModerationQueue(
1158 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(now() + timedelta(seconds=10)))
1159 )
1160 assert len(res.queue_items) == 2
1162 # Filter to items created before 3 hours ago (should get none)
1163 old_cutoff = now() - timedelta(hours=3)
1164 with real_moderation_session(super_token) as api:
1165 res = api.GetModerationQueue(
1166 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(old_cutoff))
1167 )
1168 assert len(res.queue_items) == 0
1171def test_GetModerationQueue_filter_created_after(db):
1172 """Test filtering moderation queue by created_after timestamp"""
1173 super_user, super_token = generate_user(is_superuser=True)
1174 normal_user, user_token = generate_user()
1175 host, _ = generate_user()
1177 # Create host requests
1178 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1179 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1181 # Backdate the first queue item to 2 hours ago
1182 with session_scope() as session:
1183 queue_item1 = session.execute(
1184 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1185 ).scalar_one()
1186 queue_item1.time_created = now() - timedelta(hours=2)
1188 # The second item remains at current time
1190 # Filter to items created after 1 hour ago (should only get the second item)
1191 cutoff_time = now() - timedelta(hours=1)
1192 with real_moderation_session(super_token) as api:
1193 res = api.GetModerationQueue(
1194 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(cutoff_time))
1195 )
1196 assert len(res.queue_items) == 1
1197 assert res.queue_items[0].moderation_state_id == state2_id
1199 # Filter to items created after 3 hours ago (should get both)
1200 old_cutoff = now() - timedelta(hours=3)
1201 with real_moderation_session(super_token) as api:
1202 res = api.GetModerationQueue(
1203 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(old_cutoff))
1204 )
1205 assert len(res.queue_items) == 2
1207 # Filter to items created after now (should get none)
1208 with real_moderation_session(super_token) as api:
1209 res = api.GetModerationQueue(
1210 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(now() + timedelta(seconds=10)))
1211 )
1212 assert len(res.queue_items) == 0
1215def test_GetModerationQueue_filter_created_before_and_after(db):
1216 """Test filtering moderation queue by both created_before and created_after timestamps"""
1217 super_user, super_token = generate_user(is_superuser=True)
1218 normal_user, user_token = generate_user()
1219 host, _ = generate_user()
1221 # Create 3 host requests
1222 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1223 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1224 state3_id = create_test_host_request_with_moderation(user_token, host.id)
1226 # Set different times: state1 = 3 hours ago, state2 = 1.5 hours ago, state3 = now
1227 with session_scope() as session:
1228 queue_item1 = session.execute(
1229 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1230 ).scalar_one()
1231 queue_item1.time_created = now() - timedelta(hours=3)
1233 queue_item2 = session.execute(
1234 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state2_id)
1235 ).scalar_one()
1236 queue_item2.time_created = now() - timedelta(hours=1, minutes=30)
1238 # Filter to items between 2 hours ago and 1 hour ago (should only get state2)
1239 after_cutoff = now() - timedelta(hours=2)
1240 before_cutoff = now() - timedelta(hours=1)
1241 with real_moderation_session(super_token) as api:
1242 res = api.GetModerationQueue(
1243 moderation_pb2.GetModerationQueueReq(
1244 created_after=Timestamp_from_datetime(after_cutoff),
1245 created_before=Timestamp_from_datetime(before_cutoff),
1246 )
1247 )
1248 assert len(res.queue_items) == 1
1249 assert res.queue_items[0].moderation_state_id == state2_id
1251 # Filter to items between 4 hours ago and 2.5 hours ago (should only get state1)
1252 after_cutoff = now() - timedelta(hours=4)
1253 before_cutoff = now() - timedelta(hours=2, minutes=30)
1254 with real_moderation_session(super_token) as api:
1255 res = api.GetModerationQueue(
1256 moderation_pb2.GetModerationQueueReq(
1257 created_after=Timestamp_from_datetime(after_cutoff),
1258 created_before=Timestamp_from_datetime(before_cutoff),
1259 )
1260 )
1261 assert len(res.queue_items) == 1
1262 assert res.queue_items[0].moderation_state_id == state1_id
1265def test_GetModerationQueue_filter_unresolved(db):
1266 """Test filtering moderation queue for unresolved items only via API"""
1267 super_user, super_token = generate_user(is_superuser=True)
1268 normal_user, user_token = generate_user()
1269 host, _ = generate_user()
1271 # Create 2 host requests
1272 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1273 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1275 # Resolve the first one via API (approve with clear_flags resolves the queue item)
1276 with real_moderation_session(super_token) as api:
1277 api.ModerateContent(
1278 moderation_pb2.ModerateContentReq(
1279 moderation_state_id=state1_id,
1280 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1281 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1282 reason="Approved",
1283 clear_flags=True,
1284 )
1285 )
1287 # Get all items
1288 with real_moderation_session(super_token) as api:
1289 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1290 assert len(res.queue_items) == 2
1292 # Get only unresolved items
1293 with real_moderation_session(super_token) as api:
1294 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
1295 assert len(res.queue_items) == 1
1296 assert res.queue_items[0].is_resolved == False
1299def test_GetModerationQueue_filter_by_author(db):
1300 """Test filtering moderation queue by item_author_user_id"""
1301 super_user, super_token = generate_user(is_superuser=True)
1302 user1, token1 = generate_user()
1303 user2, token2 = generate_user()
1304 host_user, _ = generate_user()
1306 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1307 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1309 # Create 2 host requests by user1
1310 with requests_session(token1) as api:
1311 hr1_id = api.CreateHostRequest(
1312 requests_pb2.CreateHostRequestReq(
1313 host_user_id=host_user.id,
1314 from_date=today_plus_2,
1315 to_date=today_plus_3,
1316 text=valid_request_text(),
1317 )
1318 ).host_request_id
1319 backdate_conversations()
1321 hr2_id = api.CreateHostRequest(
1322 requests_pb2.CreateHostRequestReq(
1323 host_user_id=host_user.id,
1324 from_date=today_plus_2,
1325 to_date=today_plus_3,
1326 text=valid_request_text(),
1327 )
1328 ).host_request_id
1330 # Create 1 host request by user2
1331 with requests_session(token2) as api:
1332 hr3_id = api.CreateHostRequest(
1333 requests_pb2.CreateHostRequestReq(
1334 host_user_id=host_user.id,
1335 from_date=today_plus_2,
1336 to_date=today_plus_3,
1337 text=valid_request_text(),
1338 )
1339 ).host_request_id
1341 # Get moderation state IDs
1342 state1_id, state2_id, state3_id = None, None, None
1343 with session_scope() as session:
1344 hr1 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr1_id)).scalar_one()
1345 hr2 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr2_id)).scalar_one()
1346 hr3 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr3_id)).scalar_one()
1347 state1_id = hr1.moderation_state_id
1348 state2_id = hr2.moderation_state_id
1349 state3_id = hr3.moderation_state_id
1351 # Get all items (should be 3)
1352 with real_moderation_session(super_token) as api:
1353 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1354 assert len(res.queue_items) == 3
1356 # Filter by user1 (should get 2)
1357 with real_moderation_session(super_token) as api:
1358 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=user1.id))
1359 assert len(res.queue_items) == 2
1360 assert all(item.moderation_state.author.user_id == user1.id for item in res.queue_items)
1362 # Filter by user2 (should get 1)
1363 with real_moderation_session(super_token) as api:
1364 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=user2.id))
1365 assert len(res.queue_items) == 1
1366 assert res.queue_items[0].moderation_state.author.user_id == user2.id
1367 assert res.queue_items[0].moderation_state_id == state3_id
1369 # Filter by non-existent user (should get 0)
1370 with real_moderation_session(super_token) as api:
1371 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=999999))
1372 assert len(res.queue_items) == 0
1375def test_GetModerationQueue_ordering(db):
1376 """Test ordering moderation queue by oldest/newest first"""
1377 super_user, super_token = generate_user(is_superuser=True)
1378 normal_user, user_token = generate_user()
1379 host, _ = generate_user()
1381 # Create 3 host requests
1382 state1_id = create_test_host_request_with_moderation(user_token, host.id)
1383 state2_id = create_test_host_request_with_moderation(user_token, host.id)
1384 state3_id = create_test_host_request_with_moderation(user_token, host.id)
1386 # Set different times: state1 = 3 hours ago, state2 = 2 hours ago, state3 = 1 hour ago
1387 with session_scope() as session:
1388 queue_item1 = session.execute(
1389 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id)
1390 ).scalar_one()
1391 queue_item1.time_created = now() - timedelta(hours=3)
1393 queue_item2 = session.execute(
1394 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state2_id)
1395 ).scalar_one()
1396 queue_item2.time_created = now() - timedelta(hours=2)
1398 queue_item3 = session.execute(
1399 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state3_id)
1400 ).scalar_one()
1401 queue_item3.time_created = now() - timedelta(hours=1)
1403 # Default order (oldest first)
1404 with real_moderation_session(super_token) as api:
1405 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
1406 assert len(res.queue_items) == 3
1407 assert res.queue_items[0].moderation_state_id == state1_id # oldest
1408 assert res.queue_items[1].moderation_state_id == state2_id
1409 assert res.queue_items[2].moderation_state_id == state3_id # newest
1411 # Explicit oldest first
1412 with real_moderation_session(super_token) as api:
1413 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(newest_first=False))
1414 assert len(res.queue_items) == 3
1415 assert res.queue_items[0].moderation_state_id == state1_id # oldest
1416 assert res.queue_items[2].moderation_state_id == state3_id # newest
1418 # Newest first
1419 with real_moderation_session(super_token) as api:
1420 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(newest_first=True))
1421 assert len(res.queue_items) == 3
1422 assert res.queue_items[0].moderation_state_id == state3_id # newest
1423 assert res.queue_items[1].moderation_state_id == state2_id
1424 assert res.queue_items[2].moderation_state_id == state1_id # oldest
1427def test_GetModerationQueue_pagination_newest_first(db):
1428 """Test pagination with newest_first=True returns different items on each page"""
1429 super_user, super_token = generate_user(is_superuser=True)
1430 normal_user, normal_token = generate_user()
1431 host_user, _ = generate_user()
1433 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1434 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1436 # Create 5 host requests
1437 hr_ids = []
1438 with requests_session(normal_token) as api:
1439 for i in range(5):
1440 hr_id = api.CreateHostRequest(
1441 requests_pb2.CreateHostRequestReq(
1442 host_user_id=host_user.id,
1443 from_date=today_plus_2,
1444 to_date=today_plus_3,
1445 text=valid_request_text(),
1446 )
1447 ).host_request_id
1448 hr_ids.append(hr_id)
1449 backdate_conversations()
1451 # Get moderation state IDs
1452 state_ids = []
1453 with session_scope() as session:
1454 for hr_id in hr_ids:
1455 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one()
1456 state_ids.append(hr.moderation_state_id)
1458 # Set different times so ordering is deterministic
1459 with session_scope() as session:
1460 for i, state_id in enumerate(state_ids):
1461 queue_item = session.execute(
1462 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id)
1463 ).scalar_one()
1464 queue_item.time_created = now() - timedelta(hours=5 - i) # oldest first in list
1466 # Get first page (2 items) with newest_first=True, filtered to our user's items
1467 with real_moderation_session(super_token) as api:
1468 res1 = api.GetModerationQueue(
1469 moderation_pb2.GetModerationQueueReq(page_size=2, newest_first=True, item_author_user_id=normal_user.id)
1470 )
1471 assert len(res1.queue_items) == 2
1472 # Should get newest items: state_ids[4], state_ids[3]
1473 assert res1.queue_items[0].moderation_state_id == state_ids[4]
1474 assert res1.queue_items[1].moderation_state_id == state_ids[3]
1475 assert res1.next_page_token # should have more pages
1477 # Get second page using the token
1478 res2 = api.GetModerationQueue(
1479 moderation_pb2.GetModerationQueueReq(
1480 page_size=2, newest_first=True, page_token=res1.next_page_token, item_author_user_id=normal_user.id
1481 )
1482 )
1483 assert len(res2.queue_items) == 2
1484 # Should get next newest items: state_ids[2], state_ids[1]
1485 assert res2.queue_items[0].moderation_state_id == state_ids[2]
1486 assert res2.queue_items[1].moderation_state_id == state_ids[1]
1488 # Pages should not overlap
1489 page1_ids = {item.moderation_state_id for item in res1.queue_items}
1490 page2_ids = {item.moderation_state_id for item in res2.queue_items}
1491 assert page1_ids.isdisjoint(page2_ids), "Pages should not have overlapping items"
1494def test_GetModerationQueue_pagination_ignores_time_created(db):
1495 """Backfilled queue items were given ids out of a separate range, so time_created and id disagree."""
1496 super_user, super_token = generate_user(is_superuser=True)
1497 normal_user, normal_token = generate_user()
1498 host, _ = generate_user()
1500 first_state_id = create_test_host_request_with_moderation(normal_token, host.id)
1501 create_test_host_request_with_moderation(normal_token, host.id)
1502 create_test_host_request_with_moderation(normal_token, host.id)
1504 with session_scope() as session:
1505 item = session.execute(
1506 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == first_state_id)
1507 ).scalar_one()
1508 item.time_created = now() + timedelta(days=1)
1510 paged_ids = []
1511 page_token = ""
1512 with real_moderation_session(super_token) as api:
1513 while True:
1514 res = api.GetModerationQueue(
1515 moderation_pb2.GetModerationQueueReq(page_size=2, page_token=page_token, newest_first=True)
1516 )
1517 paged_ids += [item.queue_item_id for item in res.queue_items]
1518 page_token = res.next_page_token
1519 if not page_token:
1520 break
1522 with session_scope() as session:
1523 expected = list(
1524 session.execute(select(ModerationQueueItem.id).order_by(ModerationQueueItem.id.desc())).scalars().all()
1525 )
1527 assert paged_ids == expected
1530def test_GetModerationLog(db):
1531 """Test getting moderation log for a state via API"""
1532 super_user, super_token = generate_user(is_superuser=True)
1533 moderator, moderator_token = generate_user(is_superuser=True)
1534 normal_user, user_token = generate_user()
1535 host, _ = generate_user()
1537 # Create a real host request
1538 state_id = create_test_host_request_with_moderation(user_token, host.id)
1540 # Perform a moderation action via API
1541 with real_moderation_session(moderator_token) as api:
1542 api.ModerateContent(
1543 moderation_pb2.ModerateContentReq(
1544 moderation_state_id=state_id,
1545 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1546 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1547 reason="Looks good",
1548 )
1549 )
1551 with real_moderation_session(super_token) as api:
1552 res = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
1553 assert len(res.log_entries) == 2 # CREATE + APPROVE
1554 assert res.moderation_state.moderation_state_id == state_id
1555 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
1556 # Log entries are in reverse chronological order
1557 assert res.log_entries[0].action == moderation_pb2.MODERATION_ACTION_APPROVE
1558 assert res.log_entries[0].moderator_user_id == moderator.id
1559 assert res.log_entries[0].reason == "Looks good"
1560 assert res.log_entries[1].action == moderation_pb2.MODERATION_ACTION_CREATE
1561 assert res.log_entries[1].moderator_user_id == normal_user.id
1564def test_GetModerationLog_not_found(db):
1565 """Test getting moderation log for non-existent state"""
1566 super_user, super_token = generate_user(is_superuser=True)
1568 with real_moderation_session(super_token) as api:
1569 with pytest.raises(grpc.RpcError) as e:
1570 api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=999999))
1571 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1572 assert e.value.details() == "Moderation state not found."
1575def test_GetModerationState(db):
1576 """Test getting moderation state by object type and ID"""
1577 super_user, super_token = generate_user(is_superuser=True)
1578 user1, token1 = generate_user()
1579 user2, _ = generate_user()
1581 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1582 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1584 with requests_session(token1) as api:
1585 host_request_id = api.CreateHostRequest(
1586 requests_pb2.CreateHostRequestReq(
1587 host_user_id=user2.id,
1588 from_date=today_plus_2,
1589 to_date=today_plus_3,
1590 text=valid_request_text(),
1591 )
1592 ).host_request_id
1594 with real_moderation_session(super_token) as api:
1595 res = api.GetModerationState(
1596 moderation_pb2.GetModerationStateReq(
1597 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1598 object_id=host_request_id,
1599 )
1600 )
1601 assert res.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST
1602 assert res.moderation_state.object_id == host_request_id
1603 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
1604 assert res.moderation_state.moderation_state_id > 0
1607def test_GetModerationState_not_found(db):
1608 """Test getting moderation state for non-existent object"""
1609 super_user, super_token = generate_user(is_superuser=True)
1611 with real_moderation_session(super_token) as api:
1612 with pytest.raises(grpc.RpcError) as e:
1613 api.GetModerationState(
1614 moderation_pb2.GetModerationStateReq(
1615 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
1616 object_id=999999,
1617 )
1618 )
1619 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1620 assert e.value.details() == "Moderation state not found."
1623def test_GetModerationState_unspecified_type(db):
1624 """Test getting moderation state with unspecified object type"""
1625 super_user, super_token = generate_user(is_superuser=True)
1627 with real_moderation_session(super_token) as api:
1628 with pytest.raises(grpc.RpcError) as e:
1629 api.GetModerationState(
1630 moderation_pb2.GetModerationStateReq(
1631 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_UNSPECIFIED,
1632 object_id=123,
1633 )
1634 )
1635 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1636 assert e.value.details() == "Object type must be specified."
1639def test_ModerateContent_approve(db):
1640 """Test approving content via unified moderation API"""
1641 super_user, super_token = generate_user(is_superuser=True)
1642 user1, token1 = generate_user()
1643 user2, _ = generate_user()
1645 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1646 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1648 # Create a host request using the API (which automatically creates moderation state)
1649 with requests_session(token1) as api:
1650 host_request_id = api.CreateHostRequest(
1651 requests_pb2.CreateHostRequestReq(
1652 host_user_id=user2.id,
1653 from_date=today_plus_2,
1654 to_date=today_plus_3,
1655 text=valid_request_text(),
1656 )
1657 ).host_request_id
1659 # Get the moderation state ID
1660 state_id = None
1661 with session_scope() as session:
1662 host_request = session.execute(
1663 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
1664 ).scalar_one()
1665 state_id = host_request.moderation_state_id
1667 with real_moderation_session(super_token) as api:
1668 res = api.ModerateContent(
1669 moderation_pb2.ModerateContentReq(
1670 moderation_state_id=state_id,
1671 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1672 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1673 reason="Approved by admin",
1674 )
1675 )
1676 assert res.moderation_state.moderation_state_id == state_id
1677 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
1679 # Verify state was updated in database
1680 with session_scope() as session:
1681 state = session.get_one(ModerationState, state_id)
1682 assert state.visibility == ModerationVisibility.visible
1685def test_ModerateContent_not_found(db):
1686 """Test moderating non-existent content"""
1687 super_user, super_token = generate_user(is_superuser=True)
1689 with real_moderation_session(super_token) as api:
1690 with pytest.raises(grpc.RpcError) as e:
1691 api.ModerateContent(
1692 moderation_pb2.ModerateContentReq(
1693 moderation_state_id=999999,
1694 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1695 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1696 reason="Test",
1697 )
1698 )
1699 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1700 assert e.value.details() == "Moderation state not found."
1703def test_ModerateContent_hide(db):
1704 """Test hiding content via unified moderation API"""
1705 super_user, super_token = generate_user(is_superuser=True)
1706 normal_user, user_token = generate_user()
1707 host, _ = generate_user()
1709 # Create a real host request
1710 state_id = create_test_host_request_with_moderation(user_token, host.id)
1712 with real_moderation_session(super_token) as api:
1713 res = api.ModerateContent(
1714 moderation_pb2.ModerateContentReq(
1715 moderation_state_id=state_id,
1716 action=moderation_pb2.MODERATION_ACTION_HIDE,
1717 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
1718 reason="Spam content",
1719 )
1720 )
1721 assert res.moderation_state.moderation_state_id == state_id
1722 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_HIDDEN
1724 # Verify state was updated in database
1725 with session_scope() as session:
1726 state = session.get_one(ModerationState, state_id)
1727 assert state.visibility == ModerationVisibility.hidden
1730def test_ModerateContent_shadow(db):
1731 """Test shadowing content via unified moderation API"""
1732 super_user, super_token = generate_user(is_superuser=True)
1733 normal_user, user_token = generate_user()
1734 host, _ = generate_user()
1736 # Create a real host request
1737 state_id = create_test_host_request_with_moderation(user_token, host.id)
1739 with real_moderation_session(super_token) as api:
1740 res = api.ModerateContent(
1741 moderation_pb2.ModerateContentReq(
1742 moderation_state_id=state_id,
1743 action=moderation_pb2.MODERATION_ACTION_HIDE,
1744 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
1745 reason="Needs further review",
1746 )
1747 )
1748 assert res.moderation_state.moderation_state_id == state_id
1749 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
1751 # Verify state was updated in database
1752 with session_scope() as session:
1753 state = session.get_one(ModerationState, state_id)
1754 assert state.visibility == ModerationVisibility.shadowed
1757def test_ModerateContent_flag(db):
1758 """Test opening a flag via the FLAG action"""
1759 super_user, super_token = generate_user(is_superuser=True)
1760 user1, token1 = generate_user()
1761 user2, _ = generate_user()
1763 # Create a host request
1764 today_plus_2 = (today() + timedelta(days=2)).isoformat()
1765 today_plus_3 = (today() + timedelta(days=3)).isoformat()
1767 with requests_session(token1) as api:
1768 host_request_id = api.CreateHostRequest(
1769 requests_pb2.CreateHostRequestReq(
1770 host_user_id=user2.id,
1771 from_date=today_plus_2,
1772 to_date=today_plus_3,
1773 text=valid_request_text(),
1774 )
1775 ).host_request_id
1777 # Get the moderation state ID
1778 with session_scope() as session:
1779 host_request = session.execute(
1780 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
1781 ).scalar_one()
1782 state_id = host_request.moderation_state_id
1784 with real_moderation_session(super_token) as api:
1785 api.ModerateContent(
1786 moderation_pb2.ModerateContentReq(
1787 moderation_state_id=state_id,
1788 action=moderation_pb2.MODERATION_ACTION_FLAG,
1789 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW,
1790 reason="Admin flagged for additional review",
1791 priority=5,
1792 )
1793 )
1795 # Verify queue item was created in database with the given priority, plus a log row referencing it
1796 with session_scope() as session:
1797 queue_item = session.execute(
1798 select(ModerationQueueItem)
1799 .where(ModerationQueueItem.moderation_state_id == state_id)
1800 .where(ModerationQueueItem.trigger == ModerationTrigger.moderator_review)
1801 ).scalar_one()
1802 assert queue_item.resolved_by_log_id is None
1803 assert queue_item.priority == 5
1805 flag_log = session.execute(
1806 select(ModerationLog)
1807 .where(ModerationLog.moderation_state_id == state_id)
1808 .where(ModerationLog.action == ModerationAction.flag)
1809 ).scalar_one()
1810 assert flag_log.queue_item_id == queue_item.id
1813def test_ModerateContent_flag_requires_trigger(db):
1814 """FLAG without a trigger is rejected."""
1815 super_user, super_token = generate_user(is_superuser=True)
1816 user1, token1 = generate_user()
1817 user2, _ = generate_user()
1818 state_id = create_test_host_request_with_moderation(token1, user2.id)
1820 with real_moderation_session(super_token) as api:
1821 with pytest.raises(grpc.RpcError) as e:
1822 api.ModerateContent(
1823 moderation_pb2.ModerateContentReq(
1824 moderation_state_id=state_id,
1825 action=moderation_pb2.MODERATION_ACTION_FLAG,
1826 reason="no trigger",
1827 )
1828 )
1829 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1832def _open_queue_item_id(state_id):
1833 with session_scope() as session:
1834 return (
1835 session.execute(
1836 select(ModerationQueueItem)
1837 .where(ModerationQueueItem.moderation_state_id == state_id)
1838 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
1839 )
1840 .scalars()
1841 .one()
1842 .id
1843 )
1846def test_ModerateContent_set_priority(db):
1847 """SET_PRIORITY changes a flag's priority and logs it, without touching visibility."""
1848 moderator, mod_token = generate_user(is_superuser=True)
1849 user1, token1 = generate_user()
1850 user2, _ = generate_user()
1851 state_id = create_test_host_request_with_moderation(token1, user2.id)
1852 queue_item_id = _open_queue_item_id(state_id)
1854 with real_moderation_session(mod_token) as api:
1855 api.ModerateContent(
1856 moderation_pb2.ModerateContentReq(
1857 moderation_state_id=state_id,
1858 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY,
1859 queue_item_id=queue_item_id,
1860 priority=10,
1861 reason="bump",
1862 )
1863 )
1865 with session_scope() as session:
1866 queue_item = session.get_one(ModerationQueueItem, queue_item_id)
1867 assert queue_item.priority == 10
1868 assert queue_item.resolved_by_log_id is None # not resolved
1869 # State stays shadowed (visibility untouched)
1870 assert session.get_one(ModerationState, state_id).visibility == ModerationVisibility.shadowed
1872 log = session.execute(
1873 select(ModerationLog)
1874 .where(ModerationLog.moderation_state_id == state_id)
1875 .where(ModerationLog.action == ModerationAction.set_priority)
1876 ).scalar_one()
1877 assert log.new_priority == 10
1878 assert log.queue_item_id == queue_item_id
1880 # GetModerationQueue surfaces the new priority
1881 with real_moderation_session(mod_token) as api:
1882 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
1883 item = next(i for i in res.queue_items if i.queue_item_id == queue_item_id)
1884 assert item.priority == 10
1887def test_ModerateContent_unflag(db):
1888 """UNFLAG resolves a single named flag and does not change visibility."""
1889 moderator, mod_token = generate_user(is_superuser=True)
1890 user1, token1 = generate_user()
1891 user2, _ = generate_user()
1892 state_id = create_test_host_request_with_moderation(token1, user2.id)
1893 queue_item_id = _open_queue_item_id(state_id)
1895 with real_moderation_session(mod_token) as api:
1896 api.ModerateContent(
1897 moderation_pb2.ModerateContentReq(
1898 moderation_state_id=state_id,
1899 action=moderation_pb2.MODERATION_ACTION_UNFLAG,
1900 queue_item_id=queue_item_id,
1901 reason="dismissed",
1902 )
1903 )
1905 with session_scope() as session:
1906 queue_item = session.get_one(ModerationQueueItem, queue_item_id)
1907 assert queue_item.resolved_by_log_id is not None
1908 # Visibility unchanged (still shadowed from creation)
1909 assert session.get_one(ModerationState, state_id).visibility == ModerationVisibility.shadowed
1910 log = session.get_one(ModerationLog, queue_item.resolved_by_log_id)
1911 assert log.action == ModerationAction.unflag
1912 assert log.queue_item_id == queue_item_id
1915def test_ModerateContent_unflag_requires_queue_item(db):
1916 """UNFLAG without a queue_item_id is rejected."""
1917 moderator, mod_token = generate_user(is_superuser=True)
1918 user1, token1 = generate_user()
1919 user2, _ = generate_user()
1920 state_id = create_test_host_request_with_moderation(token1, user2.id)
1922 with real_moderation_session(mod_token) as api:
1923 with pytest.raises(grpc.RpcError) as e:
1924 api.ModerateContent(
1925 moderation_pb2.ModerateContentReq(
1926 moderation_state_id=state_id,
1927 action=moderation_pb2.MODERATION_ACTION_UNFLAG,
1928 reason="no target",
1929 )
1930 )
1931 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1934def test_ModerateContent_approve_without_clear_flags_leaves_flag_open(db):
1935 """APPROVE without clear_flags changes visibility but leaves open flags in the queue."""
1936 moderator, mod_token = generate_user(is_superuser=True)
1937 user1, token1 = generate_user()
1938 user2, _ = generate_user()
1939 state_id = create_test_host_request_with_moderation(token1, user2.id)
1940 queue_item_id = _open_queue_item_id(state_id)
1942 with real_moderation_session(mod_token) as api:
1943 api.ModerateContent(
1944 moderation_pb2.ModerateContentReq(
1945 moderation_state_id=state_id,
1946 action=moderation_pb2.MODERATION_ACTION_APPROVE,
1947 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
1948 reason="visible but still under review",
1949 )
1950 )
1952 with session_scope() as session:
1953 assert session.get_one(ModerationState, state_id).visibility == ModerationVisibility.visible
1954 assert session.get_one(ModerationQueueItem, queue_item_id).resolved_by_log_id is None
1957def test_ModerateContent_flag_supersede(db):
1958 """FLAG with supersede_queue_item_id resolves the named flag as the new one opens."""
1959 moderator, mod_token = generate_user(is_superuser=True)
1960 user1, token1 = generate_user()
1961 user2, _ = generate_user()
1962 state_id = create_test_host_request_with_moderation(token1, user2.id)
1963 initial_item_id = _open_queue_item_id(state_id)
1965 with real_moderation_session(mod_token) as api:
1966 api.ModerateContent(
1967 moderation_pb2.ModerateContentReq(
1968 moderation_state_id=state_id,
1969 action=moderation_pb2.MODERATION_ACTION_FLAG,
1970 trigger=moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG,
1971 priority=3,
1972 supersede_queue_item_id=initial_item_id,
1973 reason="machine re-flag",
1974 )
1975 )
1977 with session_scope() as session:
1978 # Old initial_review flag is resolved
1979 assert session.get_one(ModerationQueueItem, initial_item_id).resolved_by_log_id is not None
1980 # Exactly one open flag remains: the new machine_flag at priority 3
1981 open_items = (
1982 session.execute(
1983 select(ModerationQueueItem)
1984 .where(ModerationQueueItem.moderation_state_id == state_id)
1985 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
1986 )
1987 .scalars()
1988 .all()
1989 )
1990 assert len(open_items) == 1
1991 assert open_items[0].trigger == ModerationTrigger.machine_flag
1992 assert open_items[0].priority == 3
1995def test_GetModerationQueue_filter_by_priority(db):
1996 """priority_min / priority_max filter the queue by priority range."""
1997 moderator, mod_token = generate_user(is_superuser=True)
1998 user1, token1 = generate_user()
1999 user2, _ = generate_user()
2000 state_id = create_test_host_request_with_moderation(token1, user2.id)
2001 queue_item_id = _open_queue_item_id(state_id)
2003 with real_moderation_session(mod_token) as api:
2004 api.ModerateContent(
2005 moderation_pb2.ModerateContentReq(
2006 moderation_state_id=state_id,
2007 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY,
2008 queue_item_id=queue_item_id,
2009 priority=7,
2010 reason="raise",
2011 )
2012 )
2014 # priority_min excludes it below the threshold, includes at/above
2015 assert len(api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_min=8)).queue_items) == 0
2016 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_min=7))
2017 assert [i.queue_item_id for i in res.queue_items] == [queue_item_id]
2019 # priority_max excludes it above the threshold
2020 assert len(api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_max=6)).queue_items) == 0
2021 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_max=7))
2022 assert [i.queue_item_id for i in res.queue_items] == [queue_item_id]
2025# ============================================================================
2026# Tests for group chat moderation
2027# ============================================================================
2030def test_group_chat_created_with_moderation_state(db):
2031 """Test that group chats are created with moderation state"""
2032 user1, token1 = generate_user()
2033 user2, _ = generate_user()
2034 make_friends(user1, user2)
2036 with conversations_session(token1) as api:
2037 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
2038 group_chat_id = res.group_chat_id
2040 # Verify moderation state was created
2041 with session_scope() as session:
2042 group_chat = session.execute(select(GroupChat).where(GroupChat.conversation_id == group_chat_id)).scalar_one()
2044 assert group_chat.moderation_state.object_type == ModerationObjectType.group_chat
2045 assert group_chat.moderation_state.object_id == group_chat_id
2046 # Group chats start as SHADOWED
2047 assert group_chat.moderation_state.visibility == ModerationVisibility.shadowed
2049 # A moderation queue item should have been created
2050 queue_item = (
2051 session.execute(
2052 select(ModerationQueueItem).where(
2053 ModerationQueueItem.moderation_state_id == group_chat.moderation_state_id
2054 )
2055 )
2056 .scalars()
2057 .first()
2058 )
2059 assert queue_item is not None
2060 assert queue_item.trigger == ModerationTrigger.initial_review
2063def test_group_chat_GetModerationState(db):
2064 """Test GetModerationState API for group chats"""
2065 user1, token1 = generate_user()
2066 user2, _ = generate_user()
2067 moderator, mod_token = generate_user(is_superuser=True)
2068 make_friends(user1, user2)
2070 with conversations_session(token1) as api:
2071 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
2072 group_chat_id = res.group_chat_id
2074 # Moderator can look up the moderation state
2075 with real_moderation_session(mod_token) as api:
2076 res = api.GetModerationState(
2077 moderation_pb2.GetModerationStateReq(
2078 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
2079 object_id=group_chat_id,
2080 )
2081 )
2082 assert res.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT
2083 assert res.moderation_state.object_id == group_chat_id
2084 # Starts as SHADOWED
2085 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2088def test_group_chat_moderation_hide(db):
2089 """Test that a moderator can hide a group chat and participants can no longer see it"""
2090 user1, token1 = generate_user()
2091 user2, token2 = generate_user()
2092 moderator, mod_token = generate_user(is_superuser=True)
2093 make_friends(user1, user2)
2095 with conversations_session(token1) as api:
2096 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
2097 group_chat_id = res.group_chat_id
2098 api.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Hello!"))
2100 # First approve the group chat so both users can see it
2101 with real_moderation_session(mod_token) as api:
2102 state_res = api.GetModerationState(
2103 moderation_pb2.GetModerationStateReq(
2104 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
2105 object_id=group_chat_id,
2106 )
2107 )
2108 api.ModerateContent(
2109 moderation_pb2.ModerateContentReq(
2110 moderation_state_id=state_res.moderation_state.moderation_state_id,
2111 action=moderation_pb2.MODERATION_ACTION_APPROVE,
2112 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
2113 reason="Approved",
2114 )
2115 )
2117 # Both users can see the chat now
2118 with conversations_session(token1) as api:
2119 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
2120 assert len(res.group_chats) == 1
2122 with conversations_session(token2) as api:
2123 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
2124 assert len(res.group_chats) == 1
2126 # Moderator hides the group chat
2127 with real_moderation_session(mod_token) as api:
2128 state_res = api.GetModerationState(
2129 moderation_pb2.GetModerationStateReq(
2130 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
2131 object_id=group_chat_id,
2132 )
2133 )
2134 api.ModerateContent(
2135 moderation_pb2.ModerateContentReq(
2136 moderation_state_id=state_res.moderation_state.moderation_state_id,
2137 action=moderation_pb2.MODERATION_ACTION_HIDE,
2138 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2139 reason="Inappropriate content",
2140 )
2141 )
2143 # Neither user can see the chat now
2144 with conversations_session(token1) as api:
2145 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
2146 assert len(res.group_chats) == 0
2148 with conversations_session(token2) as api:
2149 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
2150 assert len(res.group_chats) == 0
2152 # Trying to get messages returns empty (chat is hidden so no messages visible)
2153 with conversations_session(token1) as api:
2154 res = api.GetGroupChatMessages(conversations_pb2.GetGroupChatMessagesReq(group_chat_id=group_chat_id))
2155 assert len(res.messages) == 0
2158def test_group_chat_moderation_shadow(db):
2159 """Test that shadowing a group chat hides it from non-creator participants"""
2160 user1, token1 = generate_user() # Creator
2161 user2, token2 = generate_user() # Participant
2162 moderator, mod_token = generate_user(is_superuser=True)
2163 make_friends(user1, user2)
2165 with conversations_session(token1) as api:
2166 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id]))
2167 group_chat_id = res.group_chat_id
2168 api.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Hello!"))
2170 # Moderator shadows the group chat
2171 with real_moderation_session(mod_token) as api:
2172 state_res = api.GetModerationState(
2173 moderation_pb2.GetModerationStateReq(
2174 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT,
2175 object_id=group_chat_id,
2176 )
2177 )
2178 api.ModerateContent(
2179 moderation_pb2.ModerateContentReq(
2180 moderation_state_id=state_res.moderation_state.moderation_state_id,
2181 action=moderation_pb2.MODERATION_ACTION_HIDE,
2182 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
2183 reason="Needs review",
2184 )
2185 )
2187 # Creator can see SHADOWED content in list operations
2188 with conversations_session(token1) as api:
2189 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
2190 assert len(res.group_chats) == 1
2191 assert res.group_chats[0].group_chat_id == group_chat_id
2193 # But non-creator participant cannot see it in lists
2194 with conversations_session(token2) as api:
2195 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq())
2196 assert len(res.group_chats) == 0
2198 # Creator can also access it directly via GetGroupChat
2199 with conversations_session(token1) as api:
2200 res = api.GetGroupChat(conversations_pb2.GetGroupChatReq(group_chat_id=group_chat_id))
2201 assert res.group_chat_id == group_chat_id
2204# ============================================================================
2205# Tests for auto-approval background job
2206# ============================================================================
2209def test_auto_approve_moderation_queue_disabled_when_zero(db, email_collector: EmailCollector):
2210 """Test that auto-approval is disabled when deadline is 0"""
2211 moderator, mod_token = generate_user(is_superuser=True)
2212 user1, token1 = generate_user()
2213 user2, token2 = generate_user()
2215 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2216 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2218 # Create a host request
2219 with requests_session(token1) as api:
2220 host_request_id = api.CreateHostRequest(
2221 requests_pb2.CreateHostRequestReq(
2222 host_user_id=user2.id,
2223 from_date=today_plus_2,
2224 to_date=today_plus_3,
2225 text=valid_request_text(),
2226 )
2227 ).host_request_id
2229 # No email should have been sent (request is shadowed)
2230 assert email_collector.count_for_recipient(user2.email) == 0
2232 # Ensure deadline is 0 (disabled)
2233 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 0
2235 # Run the job
2236 auto_approve_moderation_queue(empty_pb2.Empty())
2238 # Surfer (author) can see the request via API
2239 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2240 assert res.host_request_id == host_request_id
2242 # Author can see their SHADOWED request in their sent list
2243 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
2244 assert len(res.host_requests) == 1
2245 assert res.host_requests[0].host_request_id == host_request_id
2247 # Host cannot see the request (it's shadowed from them)
2248 with requests_session(token2) as api:
2249 with pytest.raises(grpc.RpcError) as e:
2250 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2251 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2253 # Host doesn't see it in their received list either
2254 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
2255 assert len(res.host_requests) == 0
2257 # Moderator can still see the item in the moderation queue
2258 with real_moderation_session(mod_token) as api:
2259 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
2260 assert len(res.queue_items) == 1
2261 assert res.queue_items[0].trigger == moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW
2263 # Moderator can check the state is still SHADOWED
2264 state_res = api.GetModerationState(
2265 moderation_pb2.GetModerationStateReq(
2266 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2267 object_id=host_request_id,
2268 )
2269 )
2270 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2273def test_auto_approve_moderation_queue_approves_old_items(
2274 db, email_collector: EmailCollector, push_collector: PushCollector
2275):
2276 """Test that auto-approval approves items older than the deadline"""
2277 moderator, mod_token = generate_user(is_superuser=True)
2278 user1, token1 = generate_user()
2279 user2, token2 = generate_user()
2281 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2282 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2284 # Create a host request
2285 with requests_session(token1) as api:
2286 host_request_id = api.CreateHostRequest(
2287 requests_pb2.CreateHostRequestReq(
2288 host_user_id=user2.id,
2289 from_date=today_plus_2,
2290 to_date=today_plus_3,
2291 text=valid_request_text("Test request for auto-approval"),
2292 )
2293 ).host_request_id
2295 # No email sent initially (shadowed)
2296 assert email_collector.count_for_recipient(user2.email) == 0
2298 # Host cannot see the request yet
2299 with requests_session(token2) as api:
2300 with pytest.raises(grpc.RpcError) as e:
2301 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2302 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2304 # Make the queue item appear old by backdating its time_created
2305 with session_scope() as session:
2306 host_request = session.execute(
2307 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
2308 ).scalar_one()
2309 queue_item = session.execute(
2310 select(ModerationQueueItem)
2311 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
2312 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
2313 ).scalar_one()
2314 # Backdate the queue item by 2 minutes
2315 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=2)
2317 # Set deadline to 60 seconds (items older than 60 seconds will be auto-approved)
2318 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
2319 config.MODERATION_BOT_USER_ID = moderator.id
2321 # Run the job
2322 auto_approve_moderation_queue(empty_pb2.Empty())
2324 # Now host can see the request via API
2325 with requests_session(token2) as api:
2326 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2327 assert res.host_request_id == host_request_id
2328 assert res.latest_message.text.text == valid_request_text("Test request for auto-approval")
2330 # Host sees it in their received list
2331 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
2332 assert len(res.host_requests) == 1
2333 assert res.host_requests[0].host_request_id == host_request_id
2335 # Surfer sees it in their sent list
2336 with requests_session(token1) as api:
2337 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
2338 assert len(res.host_requests) == 1
2339 assert res.host_requests[0].host_request_id == host_request_id
2341 with real_moderation_session(mod_token) as api:
2342 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
2343 assert len(res.queue_items) == 1
2344 flag_item = res.queue_items[0]
2345 assert flag_item.trigger == moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG
2346 assert flag_item.priority == MODERATION_AUTO_APPROVE_FLAG_PRIORITY
2348 state_res = api.GetModerationState(
2349 moderation_pb2.GetModerationStateReq(
2350 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2351 object_id=host_request_id,
2352 )
2353 )
2354 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE
2356 log_res = api.GetModerationLog(
2357 moderation_pb2.GetModerationLogReq(moderation_state_id=state_res.moderation_state.moderation_state_id)
2358 )
2359 approve_entries = [e for e in log_res.log_entries if e.action == moderation_pb2.MODERATION_ACTION_APPROVE]
2360 assert len(approve_entries) == 1
2361 assert "Auto-approved" in approve_entries[0].reason
2362 assert "60 seconds" in approve_entries[0].reason
2363 assert approve_entries[0].moderator_user_id == moderator.id
2365 flag_entries = [e for e in log_res.log_entries if e.action == moderation_pb2.MODERATION_ACTION_FLAG]
2366 assert len(flag_entries) == 1
2367 assert flag_entries[0].moderator_user_id == moderator.id
2370def test_auto_approve_does_not_approve_recent_items(db, email_collector: EmailCollector):
2371 """Test that auto-approval does not approve items that are newer than the deadline"""
2372 moderator, mod_token = generate_user(is_superuser=True)
2373 user1, token1 = generate_user()
2374 user2, token2 = generate_user()
2376 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2377 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2379 # Create a host request
2380 with requests_session(token1) as api:
2381 host_request_id = api.CreateHostRequest(
2382 requests_pb2.CreateHostRequestReq(
2383 host_user_id=user2.id,
2384 from_date=today_plus_2,
2385 to_date=today_plus_3,
2386 text=valid_request_text(),
2387 )
2388 ).host_request_id
2390 # No email sent (shadowed)
2391 assert email_collector.count_for_recipient(user2.email) == 0
2393 # Set deadline to 1 hour (items older than 1 hour will be auto-approved)
2394 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600
2395 config.MODERATION_BOT_USER_ID = moderator.id
2397 # Run the job - the item was just created, so it shouldn't be approved
2398 auto_approve_moderation_queue(empty_pb2.Empty())
2400 # Still no email sent
2401 assert email_collector.count_for_recipient(user2.email) == 0
2403 # Host still cannot see the request
2404 with requests_session(token2) as api:
2405 with pytest.raises(grpc.RpcError) as e:
2406 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2407 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2409 # Not in host's received list
2410 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
2411 assert len(res.host_requests) == 0
2413 # Moderator sees it still in queue unresolved
2414 with real_moderation_session(mod_token) as api:
2415 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
2416 assert len(res.queue_items) == 1
2418 # State is still SHADOWED
2419 state_res = api.GetModerationState(
2420 moderation_pb2.GetModerationStateReq(
2421 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2422 object_id=host_request_id,
2423 )
2424 )
2425 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2428def test_auto_approve_does_not_approve_already_approved(db):
2429 """Test that auto-approval does not re-approve already visible content"""
2430 moderator, mod_token = generate_user(is_superuser=True)
2431 user1, token1 = generate_user()
2432 user2, token2 = generate_user()
2434 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2435 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2437 # Create a host request
2438 with requests_session(token1) as api:
2439 host_request_id = api.CreateHostRequest(
2440 requests_pb2.CreateHostRequestReq(
2441 host_user_id=user2.id,
2442 from_date=today_plus_2,
2443 to_date=today_plus_3,
2444 text=valid_request_text(),
2445 )
2446 ).host_request_id
2448 # Moderator approves it manually
2449 with real_moderation_session(mod_token) as api:
2450 state_res = api.GetModerationState(
2451 moderation_pb2.GetModerationStateReq(
2452 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2453 object_id=host_request_id,
2454 )
2455 )
2456 state_id = state_res.moderation_state.moderation_state_id
2458 api.ModerateContent(
2459 moderation_pb2.ModerateContentReq(
2460 moderation_state_id=state_id,
2461 action=moderation_pb2.MODERATION_ACTION_APPROVE,
2462 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
2463 reason="Approved by moderator",
2464 clear_flags=True,
2465 )
2466 )
2468 # Host can now see it
2469 with requests_session(token2) as api:
2470 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2471 assert res.host_request_id == host_request_id
2473 # Get log count before auto-approval
2474 with real_moderation_session(mod_token) as api:
2475 log_res_before = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2476 log_count_before = len(log_res_before.log_entries)
2478 # Set deadline to 1 second
2479 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1
2480 config.MODERATION_BOT_USER_ID = moderator.id
2482 # Run the job
2483 auto_approve_moderation_queue(empty_pb2.Empty())
2485 # No new log entries should be created (already approved, queue item resolved)
2486 with real_moderation_session(mod_token) as api:
2487 log_res_after = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2488 assert len(log_res_after.log_entries) == log_count_before
2490 # Queue should be empty (item was resolved when moderator approved)
2491 queue_res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True))
2492 assert len(queue_res.queue_items) == 0
2495def test_auto_approve_does_not_approve_moderator_shadowed_items(db):
2496 """Test that auto-approval does not approve items that were explicitly shadowed by a moderator"""
2497 moderator, mod_token = generate_user(is_superuser=True)
2498 user1, token1 = generate_user()
2499 user2, token2 = generate_user()
2501 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2502 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2504 # Create a host request
2505 with requests_session(token1) as api:
2506 host_request_id = api.CreateHostRequest(
2507 requests_pb2.CreateHostRequestReq(
2508 host_user_id=user2.id,
2509 from_date=today_plus_2,
2510 to_date=today_plus_3,
2511 text=valid_request_text(),
2512 )
2513 ).host_request_id
2515 # Moderator explicitly shadows the content (keeping it shadowed but resolving the queue item)
2516 with real_moderation_session(mod_token) as api:
2517 state_res = api.GetModerationState(
2518 moderation_pb2.GetModerationStateReq(
2519 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2520 object_id=host_request_id,
2521 )
2522 )
2523 state_id = state_res.moderation_state.moderation_state_id
2525 # Set to SHADOWED explicitly with clear_flags - this resolves the INITIAL_REVIEW queue item
2526 api.ModerateContent(
2527 moderation_pb2.ModerateContentReq(
2528 moderation_state_id=state_id,
2529 action=moderation_pb2.MODERATION_ACTION_HIDE,
2530 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
2531 reason="Keeping shadowed for review",
2532 clear_flags=True,
2533 )
2534 )
2536 # Backdate to ensure it would be old enough for auto-approval
2537 with session_scope() as session:
2538 queue_item = session.execute(
2539 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id)
2540 ).scalar_one()
2541 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=10)
2543 # Set deadline to 1 second
2544 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1
2545 config.MODERATION_BOT_USER_ID = moderator.id
2547 # Get log count before
2548 with real_moderation_session(mod_token) as api:
2549 log_res_before = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2550 log_count_before = len(log_res_before.log_entries)
2552 # Run the job
2553 auto_approve_moderation_queue(empty_pb2.Empty())
2555 # No new log entries - the queue item was resolved when moderator shadowed it
2556 with real_moderation_session(mod_token) as api:
2557 log_res_after = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2558 assert len(log_res_after.log_entries) == log_count_before
2560 # State should still be SHADOWED (not auto-approved to VISIBLE)
2561 state_res = api.GetModerationState(
2562 moderation_pb2.GetModerationStateReq(
2563 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2564 object_id=host_request_id,
2565 )
2566 )
2567 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2569 # Host still cannot see the request
2570 with requests_session(token2) as api:
2571 with pytest.raises(grpc.RpcError) as e:
2572 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2573 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2576def test_auto_approve_skips_shadowed_user_authored_items(db):
2577 """Auto-approval must not promote content authored by a currently-shadowed user."""
2578 moderator, mod_token = generate_user(is_superuser=True)
2579 surfer, surfer_token = generate_user()
2580 host, host_token = generate_user()
2582 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2583 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2585 with requests_session(surfer_token) as api:
2586 host_request_id = api.CreateHostRequest(
2587 requests_pb2.CreateHostRequestReq(
2588 host_user_id=host.id,
2589 from_date=today_plus_2,
2590 to_date=today_plus_3,
2591 text=valid_request_text(),
2592 )
2593 ).host_request_id
2595 # Shadow the surfer after the host request was created
2596 with session_scope() as session:
2597 session.execute(select(User).where(User.id == surfer.id)).scalar_one().shadowed_at = now()
2599 # Backdate the queue item to make it eligible for auto-approval
2600 with session_scope() as session:
2601 host_request = session.execute(
2602 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
2603 ).scalar_one()
2604 queue_item = session.execute(
2605 select(ModerationQueueItem)
2606 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id)
2607 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
2608 ).scalar_one()
2609 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=10)
2611 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
2612 config.MODERATION_BOT_USER_ID = moderator.id
2614 auto_approve_moderation_queue(empty_pb2.Empty())
2616 # State should remain SHADOWED — the auto-approve job must skip shadowed-user content
2617 with real_moderation_session(mod_token) as api:
2618 state_res = api.GetModerationState(
2619 moderation_pb2.GetModerationStateReq(
2620 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST,
2621 object_id=host_request_id,
2622 )
2623 )
2624 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED
2626 # Host still cannot see the request
2627 with requests_session(host_token) as api:
2628 with pytest.raises(grpc.RpcError) as e:
2629 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2630 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2632 # But the (shadowed) author can still see their own request
2633 with requests_session(surfer_token) as api:
2634 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2635 assert res.host_request_id == host_request_id
2638def test_auto_approve_preserves_other_open_flags(db):
2639 """Auto-approval supersedes only the INITIAL_REVIEW item; other open flags survive and a high-prio flag is added."""
2640 moderator, mod_token = generate_user(is_superuser=True)
2641 surfer, surfer_token = generate_user()
2642 host, _ = generate_user()
2644 state_id = create_test_host_request_with_moderation(surfer_token, host.id)
2646 with real_moderation_session(mod_token) as api:
2647 api.ModerateContent(
2648 moderation_pb2.ModerateContentReq(
2649 moderation_state_id=state_id,
2650 action=moderation_pb2.MODERATION_ACTION_FLAG,
2651 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG,
2652 priority=5,
2653 reason="user reported this",
2654 )
2655 )
2657 with session_scope() as session:
2658 initial_item = session.execute(
2659 select(ModerationQueueItem)
2660 .where(ModerationQueueItem.moderation_state_id == state_id)
2661 .where(ModerationQueueItem.trigger == ModerationTrigger.initial_review)
2662 ).scalar_one()
2663 initial_item_id = initial_item.id
2664 initial_item.time_created = datetime.now(initial_item.time_created.tzinfo) - timedelta(minutes=10)
2666 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
2667 config.MODERATION_BOT_USER_ID = moderator.id
2669 auto_approve_moderation_queue(empty_pb2.Empty())
2671 with session_scope() as session:
2672 assert (
2673 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.id == initial_item_id))
2674 .scalar_one()
2675 .resolved_by_log_id
2676 is not None
2677 )
2678 assert (
2679 session.execute(select(ModerationState).where(ModerationState.id == state_id)).scalar_one().visibility
2680 == ModerationVisibility.visible
2681 )
2683 open_items = (
2684 session.execute(
2685 select(ModerationQueueItem)
2686 .where(ModerationQueueItem.moderation_state_id == state_id)
2687 .where(ModerationQueueItem.resolved_by_log_id.is_(None))
2688 )
2689 .scalars()
2690 .all()
2691 )
2692 open_by_trigger = {item.trigger: item for item in open_items}
2693 assert open_by_trigger.keys() == {ModerationTrigger.user_flag, ModerationTrigger.machine_flag}
2694 assert open_by_trigger[ModerationTrigger.user_flag].priority == 5
2695 assert open_by_trigger[ModerationTrigger.machine_flag].priority == MODERATION_AUTO_APPROVE_FLAG_PRIORITY
2698def _backdate_initial_review(state_id, delta):
2699 with session_scope() as session:
2700 queue_item = session.execute(
2701 select(ModerationQueueItem)
2702 .where(ModerationQueueItem.moderation_state_id == state_id)
2703 .where(ModerationQueueItem.trigger == ModerationTrigger.initial_review)
2704 ).scalar_one()
2705 queue_item.time_created -= delta
2708def _visibility(state_id):
2709 with session_scope() as session:
2710 return session.execute(select(ModerationState.visibility).where(ModerationState.id == state_id)).scalar_one()
2713def _set_moderation_auto_approve_deadline(token, user, seconds):
2714 with real_admin_session(token) as api:
2715 api.SetModerationAutoApproveDeadline(
2716 admin_pb2.SetModerationAutoApproveDeadlineReq(
2717 user=user.username,
2718 auto_approve_seconds=wrappers_pb2.Int64Value(value=seconds) if seconds is not None else None,
2719 )
2720 )
2723def test_auto_approve_uses_author_deadline(db):
2724 """Lowering an author's deadline applies to their already pending content without touching other authors"""
2725 moderator, mod_token = generate_user(is_superuser=True)
2726 surfer, surfer_token = generate_user()
2727 other_surfer, other_surfer_token = generate_user()
2728 host, _ = generate_user()
2729 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600
2730 config.MODERATION_BOT_USER_ID = moderator.id
2732 state_id = create_test_host_request_with_moderation(surfer_token, host.id)
2733 other_state_id = create_test_host_request_with_moderation(other_surfer_token, host.id)
2734 _backdate_initial_review(state_id, timedelta(minutes=10))
2735 _backdate_initial_review(other_state_id, timedelta(minutes=10))
2737 auto_approve_moderation_queue(empty_pb2.Empty())
2738 assert _visibility(state_id) == ModerationVisibility.shadowed
2739 assert _visibility(other_state_id) == ModerationVisibility.shadowed
2741 _set_moderation_auto_approve_deadline(mod_token, surfer, 300)
2743 auto_approve_moderation_queue(empty_pb2.Empty())
2744 assert _visibility(state_id) == ModerationVisibility.visible
2745 assert _visibility(other_state_id) == ModerationVisibility.shadowed
2747 with real_moderation_session(mod_token) as api:
2748 log_res = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id))
2749 approve_entries = [e for e in log_res.log_entries if e.action == moderation_pb2.MODERATION_ACTION_APPROVE]
2750 assert len(approve_entries) == 1
2751 assert "300 seconds" in approve_entries[0].reason
2754@pytest.mark.parametrize("seconds", [0, 86400])
2755def test_auto_approve_author_deadline_holds_content(db, seconds):
2756 """A long or zero (never) author deadline holds content past the default without tripping the consistency check"""
2757 moderator, mod_token = generate_user(is_superuser=True)
2758 surfer, surfer_token = generate_user()
2759 host, _ = generate_user()
2760 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
2761 config.MODERATION_BOT_USER_ID = moderator.id
2763 _set_moderation_auto_approve_deadline(mod_token, surfer, seconds)
2764 state_id = create_test_host_request_with_moderation(surfer_token, host.id)
2765 _backdate_initial_review(state_id, timedelta(minutes=10))
2767 auto_approve_moderation_queue(empty_pb2.Empty())
2768 assert _visibility(state_id) == ModerationVisibility.shadowed
2769 check_database_consistency(empty_pb2.Empty())
2771 _set_moderation_auto_approve_deadline(mod_token, surfer, None)
2772 with pytest.raises(DatabaseInconsistencyError, match="exceeding auto-approve deadline"):
2773 check_database_consistency(empty_pb2.Empty())
2775 auto_approve_moderation_queue(empty_pb2.Empty())
2776 assert _visibility(state_id) == ModerationVisibility.visible
2777 check_database_consistency(empty_pb2.Empty())
2780def test_auto_approve_does_not_unhide_hidden_content(db):
2781 """Content a moderator hid without resolving its initial review is left hidden"""
2782 moderator, mod_token = generate_user(is_superuser=True)
2783 surfer, surfer_token = generate_user()
2784 host, _ = generate_user()
2785 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60
2786 config.MODERATION_BOT_USER_ID = moderator.id
2788 state_id = create_test_host_request_with_moderation(surfer_token, host.id)
2789 with real_moderation_session(mod_token) as api:
2790 api.ModerateContent(
2791 moderation_pb2.ModerateContentReq(
2792 moderation_state_id=state_id,
2793 action=moderation_pb2.MODERATION_ACTION_HIDE,
2794 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
2795 reason="Hidden",
2796 clear_flags=False,
2797 )
2798 )
2799 _backdate_initial_review(state_id, timedelta(minutes=10))
2801 auto_approve_moderation_queue(empty_pb2.Empty())
2803 assert _visibility(state_id) == ModerationVisibility.hidden
2804 with session_scope() as session:
2805 initial_item = session.execute(
2806 select(ModerationQueueItem)
2807 .where(ModerationQueueItem.moderation_state_id == state_id)
2808 .where(ModerationQueueItem.trigger == ModerationTrigger.initial_review)
2809 ).scalar_one()
2810 assert initial_item.resolved_by_log_id is None
2813# ============================================================================
2814# Notification Suppression Tests
2815# ============================================================================
2818def test_host_request_message_notifications_suppressed_before_approval(
2819 db, email_collector: EmailCollector, push_collector: PushCollector, moderator
2820):
2821 """
2822 Test that notifications are NOT sent for messages in host requests
2823 that haven't been approved yet.
2824 """
2825 host, host_token = generate_user(complete_profile=True)
2826 surfer, surfer_token = generate_user(complete_profile=True)
2828 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2829 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2831 # Create host request (it starts in SHADOWED state)
2832 with requests_session(surfer_token) as api:
2833 hr_id = api.CreateHostRequest(
2834 requests_pb2.CreateHostRequestReq(
2835 host_user_id=host.id,
2836 from_date=today_plus_2,
2837 to_date=today_plus_3,
2838 text=valid_request_text("Initial request message"),
2839 )
2840 ).host_request_id
2842 # No notifications should have been sent to the host (request is SHADOWED)
2843 assert email_collector.count_for_recipient(host.email) == 0
2844 assert push_collector.count_for_user(host.id) == 0
2846 # Send additional messages BEFORE approval - should NOT generate notifications
2847 with requests_session(surfer_token) as api:
2848 api.SendHostRequestMessage(
2849 requests_pb2.SendHostRequestMessageReq(
2850 host_request_id=hr_id,
2851 text="Follow-up message 1",
2852 )
2853 )
2854 api.SendHostRequestMessage(
2855 requests_pb2.SendHostRequestMessageReq(
2856 host_request_id=hr_id,
2857 text="Follow-up message 2",
2858 )
2859 )
2861 # Host should STILL have no notifications (messages sent while SHADOWED)
2862 assert email_collector.count_for_recipient(host.email) == 0
2863 assert push_collector.count_for_user(host.id) == 0
2865 # Now approve the request
2866 moderator.approve_host_request(hr_id)
2867 email_collector.pop_for_recipient(host.email)
2869 # Host should now have 3 notifications (all deferred notifications are delivered on approval):
2870 # 1. host_request:create (the initial request)
2871 # 2. host_request:message (Follow-up message 1)
2872 # 3. host_request:message (Follow-up message 2)
2873 assert push_collector.count_for_user(host.id) == 3
2874 push = push_collector.pop_for_user(host.id, last=False)
2875 assert push.content.title == f"New host request from {surfer.name}"
2878def test_host_request_status_notifications_suppressed_before_approval(db, push_collector: PushCollector, moderator):
2879 """
2880 Test that status change notifications (accept/reject/etc.) are NOT sent
2881 for host requests that haven't been approved yet.
2883 Note: In practice, the host can't even SEE the request to accept/reject it
2884 when it's SHADOWED. But if they somehow did, we still shouldn't notify.
2885 """
2886 host, host_token = generate_user(complete_profile=True)
2887 surfer, surfer_token = generate_user(complete_profile=True)
2889 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2890 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2892 # Create host request
2893 with requests_session(surfer_token) as api:
2894 hr_id = api.CreateHostRequest(
2895 requests_pb2.CreateHostRequestReq(
2896 host_user_id=host.id,
2897 from_date=today_plus_2,
2898 to_date=today_plus_3,
2899 text=valid_request_text(),
2900 )
2901 ).host_request_id
2903 # No notifications should have been sent to the host (request is SHADOWED)
2904 assert push_collector.count_for_user(host.id) == 0
2906 # The surfer can cancel their own request even when SHADOWED
2907 # But this should NOT notify the host since the request isn't approved
2908 with requests_session(surfer_token) as api:
2909 api.RespondHostRequest(
2910 requests_pb2.RespondHostRequestReq(
2911 host_request_id=hr_id,
2912 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
2913 text="Actually, never mind",
2914 )
2915 )
2917 # Host should STILL have no notifications (cancel notification suppressed)
2918 assert push_collector.count_for_user(host.id) == 0
2921def test_host_request_notifications_sent_after_approval(
2922 db, email_collector: EmailCollector, push_collector: PushCollector, moderator
2923):
2924 """
2925 Test that after a host request is approved, all notifications work normally.
2926 """
2927 host, host_token = generate_user(complete_profile=True)
2928 surfer, surfer_token = generate_user(complete_profile=True)
2930 today_plus_2 = (today() + timedelta(days=2)).isoformat()
2931 today_plus_3 = (today() + timedelta(days=3)).isoformat()
2933 # Create and approve host request
2934 with requests_session(surfer_token) as api:
2935 hr_id = api.CreateHostRequest(
2936 requests_pb2.CreateHostRequestReq(
2937 host_user_id=host.id,
2938 from_date=today_plus_2,
2939 to_date=today_plus_3,
2940 text=valid_request_text(),
2941 )
2942 ).host_request_id
2944 moderator.approve_host_request(hr_id)
2946 # Host should have received 1 notification (the approval notification)
2947 email_collector.pop_for_recipient(host.email, last=True)
2948 push_collector.pop_for_user(host.id, last=True)
2950 # Host accepts the request - surfer should be notified
2951 with requests_session(host_token) as api:
2952 api.RespondHostRequest(
2953 requests_pb2.RespondHostRequestReq(
2954 host_request_id=hr_id,
2955 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
2956 text="Sure, come on over!",
2957 )
2958 )
2960 # Surfer should have 1 notification (the accept notification)
2961 email_collector.pop_for_recipient(surfer.email, last=True)
2962 push = push_collector.pop_for_user(surfer.id, last=True)
2963 assert push.content.title == f"{host.name} accepted your host request"
2965 # Surfer confirms - host should be notified
2966 with requests_session(surfer_token) as api:
2967 api.RespondHostRequest(
2968 requests_pb2.RespondHostRequestReq(
2969 host_request_id=hr_id,
2970 status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED,
2971 text="See you then!",
2972 )
2973 )
2975 # Host should now have received the confirmation notifications
2976 email_collector.pop_for_recipient(host.email, last=True)
2977 push = push_collector.pop_for_user(host.id, last=True)
2978 assert push.content.title == f"{surfer.name} confirmed their host request"
2981def test_group_chat_message_notifications_suppressed_before_approval(db, push_collector: PushCollector, moderator):
2982 """
2983 Test that notifications are NOT sent for messages in group chats
2984 that haven't been approved yet.
2985 """
2986 user1, token1 = generate_user(complete_profile=True)
2987 user2, token2 = generate_user(complete_profile=True)
2989 # Create a group chat (starts in SHADOWED state)
2990 with conversations_session(token1) as api:
2991 res = api.CreateGroupChat(
2992 conversations_pb2.CreateGroupChatReq(
2993 recipient_user_ids=[user2.id],
2994 )
2995 )
2996 gc_id = res.group_chat_id
2998 # Verify initial state
2999 with session_scope() as session:
3000 gc = session.execute(select(GroupChat).where(GroupChat.conversation_id == gc_id)).scalar_one()
3001 assert gc.moderation_state.visibility == ModerationVisibility.shadowed
3003 # No notifications should have been sent yet (chat is SHADOWED)
3004 assert push_collector.count_for_user(user2.id) == 0
3006 # Send messages BEFORE approval
3007 with conversations_session(token1) as api:
3008 api.SendMessage(
3009 conversations_pb2.SendMessageReq(
3010 group_chat_id=gc_id,
3011 text="Hello before approval",
3012 )
3013 )
3015 # Process the queued notification job
3016 while process_job():
3017 pass
3019 # User2 should STILL have no notifications (chat is SHADOWED)
3020 assert push_collector.count_for_user(user2.id) == 0
3022 # Now approve the group chat
3023 moderator.approve_group_chat(gc_id)
3025 # Process the queued notification jobs from approval
3026 while process_job():
3027 pass
3029 # Verify moderation state after approval
3030 with session_scope() as session:
3031 gc = session.execute(select(GroupChat).where(GroupChat.conversation_id == gc_id)).scalar_one()
3032 assert gc.moderation_state.visibility == ModerationVisibility.visible
3034 # User2 should have received 1 notification for the first message sent before approval
3035 push = push_collector.pop_for_user(user2.id, last=True)
3036 assert push.content.title == user1.name
3037 assert push.content.body == "Hello before approval"
3039 # Send a message AFTER approval
3040 with conversations_session(token1) as api:
3041 api.SendMessage(
3042 conversations_pb2.SendMessageReq(
3043 group_chat_id=gc_id,
3044 text="Hello after approval",
3045 )
3046 )
3048 # Process the queued notification job
3049 while process_job():
3050 pass
3052 # User2 should have received another notification
3053 assert push_collector.count_for_user(user2.id) == 1
3056def test_event_moderation_state_content(db):
3057 """Test that event moderation state content includes both title and description"""
3058 super_user, super_token = generate_user(is_superuser=True)
3059 user, token = generate_user()
3061 with session_scope() as session:
3062 create_community(session, 0, 2, "Community", [user], [], None)
3064 res = events_api.create_event(token=token, title="My Event Title", content="My event description.")
3065 event_id = res.event_id
3067 with session_scope() as session:
3068 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3069 state_id = occurrence.moderation_state_id
3071 with real_moderation_session(super_token) as api:
3072 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq())
3073 event_items = [
3074 item
3075 for item in res.queue_items
3076 if item.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_EVENT_OCCURRENCE
3077 ]
3078 assert len(event_items) == 1
3079 assert event_items[0].moderation_state.content == "My Event Title\n\nMy event description."
3082# ============================================================================
3083# Tests for SetUserContentVisibility
3084# ============================================================================
3087def _get_moderation_state(session, object_type, object_id):
3088 return session.execute(
3089 select(ModerationState)
3090 .where(ModerationState.object_type == object_type)
3091 .where(ModerationState.object_id == object_id)
3092 ).scalar_one()
3095def test_SetUserContentVisibility_host_request(db):
3096 super_user, super_token = generate_user(is_superuser=True)
3097 surfer, surfer_token = generate_user()
3098 host, _ = generate_user()
3100 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3101 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3102 with requests_session(surfer_token) as api:
3103 hr_id = api.CreateHostRequest(
3104 requests_pb2.CreateHostRequestReq(
3105 host_user_id=host.id,
3106 from_date=today_plus_2,
3107 to_date=today_plus_3,
3108 text=valid_request_text(),
3109 )
3110 ).host_request_id
3112 with real_moderation_session(super_token) as api:
3113 res = api.SetUserContentVisibility(
3114 moderation_pb2.SetUserContentVisibilityReq(
3115 user_id=surfer.id,
3116 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
3117 )
3118 )
3119 # Already shadowed by default — no-op
3120 assert res.updated_count == 0
3122 with real_moderation_session(super_token) as api:
3123 res = api.SetUserContentVisibility(
3124 moderation_pb2.SetUserContentVisibilityReq(
3125 user_id=surfer.id,
3126 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3127 reason="policy violation",
3128 )
3129 )
3130 assert res.updated_count == 1
3132 with session_scope() as session:
3133 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3134 assert state.visibility == ModerationVisibility.hidden
3136 log_entries = (
3137 session.execute(
3138 select(ModerationLog)
3139 .where(ModerationLog.moderation_state_id == state.id)
3140 .order_by(ModerationLog.time.asc(), ModerationLog.id.asc())
3141 )
3142 .scalars()
3143 .all()
3144 )
3145 # create log + bulk update log
3146 assert len(log_entries) == 2
3147 assert log_entries[-1].new_visibility == ModerationVisibility.hidden
3148 assert log_entries[-1].moderator_user_id == super_user.id
3149 assert log_entries[-1].reason == "policy violation"
3152def test_SetUserContentVisibility_group_chat(db):
3153 super_user, super_token = generate_user(is_superuser=True)
3154 creator, creator_token = generate_user()
3155 other, _ = generate_user()
3156 make_friends(creator, other)
3158 with conversations_session(creator_token) as api:
3159 gc_id = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[other.id])).group_chat_id
3161 with real_moderation_session(super_token) as api:
3162 api.SetUserContentVisibility(
3163 moderation_pb2.SetUserContentVisibilityReq(
3164 user_id=creator.id,
3165 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3166 )
3167 )
3169 with session_scope() as session:
3170 state = _get_moderation_state(session, ModerationObjectType.group_chat, gc_id)
3171 assert state.visibility == ModerationVisibility.hidden
3174def test_SetUserContentVisibility_event_occurrence(db):
3175 super_user, super_token = generate_user(is_superuser=True)
3176 creator, creator_token = generate_user()
3178 with session_scope() as session:
3179 create_community(session, 0, 2, "Community", [creator], [], None)
3181 event_id = events_api.create_event(token=creator_token).event_id
3183 with real_moderation_session(super_token) as api:
3184 res = api.SetUserContentVisibility(
3185 moderation_pb2.SetUserContentVisibilityReq(
3186 user_id=creator.id,
3187 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3188 )
3189 )
3190 assert res.updated_count == 1
3192 with session_scope() as session:
3193 state = _get_moderation_state(session, ModerationObjectType.event_occurrence, event_id)
3194 assert state.visibility == ModerationVisibility.hidden
3197def test_SetUserContentVisibility_friend_request(db):
3198 super_user, super_token = generate_user(is_superuser=True)
3199 sender, sender_token = generate_user()
3200 recipient, _ = generate_user()
3202 with api_session(sender_token) as api:
3203 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=recipient.id))
3205 with session_scope() as session:
3206 fr_id = session.execute(
3207 select(FriendRelationship.id).where(FriendRelationship.from_user_id == sender.id)
3208 ).scalar_one()
3210 with real_moderation_session(super_token) as api:
3211 res = api.SetUserContentVisibility(
3212 moderation_pb2.SetUserContentVisibilityReq(
3213 user_id=sender.id,
3214 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3215 )
3216 )
3217 assert res.updated_count == 1
3219 with session_scope() as session:
3220 state = _get_moderation_state(session, ModerationObjectType.friend_request, fr_id)
3221 assert state.visibility == ModerationVisibility.hidden
3224def test_SetUserContentVisibility_round_trip(db):
3225 super_user, super_token = generate_user(is_superuser=True)
3226 surfer, surfer_token = generate_user()
3227 host, _ = generate_user()
3229 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3230 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3231 with requests_session(surfer_token) as api:
3232 hr_id = api.CreateHostRequest(
3233 requests_pb2.CreateHostRequestReq(
3234 host_user_id=host.id,
3235 from_date=today_plus_2,
3236 to_date=today_plus_3,
3237 text=valid_request_text(),
3238 )
3239 ).host_request_id
3241 with real_moderation_session(super_token) as api:
3242 api.SetUserContentVisibility(
3243 moderation_pb2.SetUserContentVisibilityReq(
3244 user_id=surfer.id,
3245 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3246 reason="first",
3247 )
3248 )
3249 api.SetUserContentVisibility(
3250 moderation_pb2.SetUserContentVisibilityReq(
3251 user_id=surfer.id,
3252 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
3253 reason="second",
3254 )
3255 )
3257 with session_scope() as session:
3258 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3259 assert state.visibility == ModerationVisibility.visible
3261 bulk_log_entries = (
3262 session.execute(
3263 select(ModerationLog)
3264 .where(ModerationLog.moderation_state_id == state.id)
3265 .where(ModerationLog.reason.in_(("first", "second")))
3266 .order_by(ModerationLog.time.asc(), ModerationLog.id.asc())
3267 )
3268 .scalars()
3269 .all()
3270 )
3271 assert [entry.new_visibility for entry in bulk_log_entries] == [
3272 ModerationVisibility.hidden,
3273 ModerationVisibility.visible,
3274 ]
3277def test_SetUserContentVisibility_resolves_queue_items(db):
3278 super_user, super_token = generate_user(is_superuser=True)
3279 surfer, surfer_token = generate_user()
3280 host, _ = generate_user()
3282 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3283 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3284 with requests_session(surfer_token) as api:
3285 hr_id = api.CreateHostRequest(
3286 requests_pb2.CreateHostRequestReq(
3287 host_user_id=host.id,
3288 from_date=today_plus_2,
3289 to_date=today_plus_3,
3290 text=valid_request_text(),
3291 )
3292 ).host_request_id
3294 with session_scope() as session:
3295 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3296 queue_item = session.execute(
3297 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id)
3298 ).scalar_one()
3299 assert queue_item.resolved_by_log_id is None
3301 with real_moderation_session(super_token) as api:
3302 api.SetUserContentVisibility(
3303 moderation_pb2.SetUserContentVisibilityReq(
3304 user_id=surfer.id,
3305 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3306 )
3307 )
3309 with session_scope() as session:
3310 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3311 queue_item = session.execute(
3312 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id)
3313 ).scalar_one()
3314 assert queue_item.resolved_by_log_id is not None
3317def test_SetUserContentVisibility_noop_when_matches(db):
3318 super_user, super_token = generate_user(is_superuser=True)
3319 surfer, surfer_token = generate_user()
3320 host, _ = generate_user()
3322 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3323 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3324 with requests_session(surfer_token) as api:
3325 api.CreateHostRequest(
3326 requests_pb2.CreateHostRequestReq(
3327 host_user_id=host.id,
3328 from_date=today_plus_2,
3329 to_date=today_plus_3,
3330 text=valid_request_text(),
3331 )
3332 )
3334 with session_scope() as session:
3335 log_count_before = len(session.execute(select(ModerationLog)).scalars().all())
3337 with real_moderation_session(super_token) as api:
3338 res = api.SetUserContentVisibility(
3339 moderation_pb2.SetUserContentVisibilityReq(
3340 user_id=surfer.id,
3341 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
3342 )
3343 )
3344 assert res.updated_count == 0
3346 with session_scope() as session:
3347 log_count_after = len(session.execute(select(ModerationLog)).scalars().all())
3348 assert log_count_after == log_count_before
3351def test_SetUserContentVisibility_unspecified_rejected(db):
3352 super_user, super_token = generate_user(is_superuser=True)
3353 target, _ = generate_user()
3355 with real_moderation_session(super_token) as api:
3356 with pytest.raises(grpc.RpcError) as e:
3357 api.SetUserContentVisibility(
3358 moderation_pb2.SetUserContentVisibilityReq(
3359 user_id=target.id,
3360 visibility=moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED,
3361 )
3362 )
3363 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
3366def test_SetUserContentVisibility_non_admin_rejected(db):
3367 normal_user, normal_token = generate_user()
3368 target, _ = generate_user()
3370 with real_moderation_session(normal_token) as api:
3371 with pytest.raises(grpc.RpcError) as e:
3372 api.SetUserContentVisibility(
3373 moderation_pb2.SetUserContentVisibilityReq(
3374 user_id=target.id,
3375 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3376 )
3377 )
3378 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
3381def test_SetUserContentVisibility_writes_admin_action(db):
3382 super_user, super_token = generate_user(is_superuser=True)
3383 surfer, surfer_token = generate_user()
3384 host, _ = generate_user()
3386 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3387 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3388 with requests_session(surfer_token) as api:
3389 api.CreateHostRequest(
3390 requests_pb2.CreateHostRequestReq(
3391 host_user_id=host.id,
3392 from_date=today_plus_2,
3393 to_date=today_plus_3,
3394 text=valid_request_text(),
3395 )
3396 )
3398 with real_moderation_session(super_token) as api:
3399 api.SetUserContentVisibility(
3400 moderation_pb2.SetUserContentVisibilityReq(
3401 user_id=surfer.id,
3402 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3403 reason="bulk hide",
3404 )
3405 )
3407 with session_scope() as session:
3408 actions = (
3409 session.execute(
3410 select(AdminAction)
3411 .where(AdminAction.target_user_id == surfer.id)
3412 .where(AdminAction.action_type == "set_user_content_visibility")
3413 )
3414 .scalars()
3415 .all()
3416 )
3417 assert len(actions) == 1
3418 assert actions[0].admin_user_id == super_user.id
3419 assert actions[0].tag == "hidden"
3420 assert actions[0].note == "bulk hide"
3423def test_SetUserContentVisibility_only_touches_target(db):
3424 super_user, super_token = generate_user(is_superuser=True)
3425 target, target_token = generate_user()
3426 other, other_token = generate_user()
3427 host, _ = generate_user()
3429 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3430 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3432 with requests_session(target_token) as api:
3433 target_hr_id = api.CreateHostRequest(
3434 requests_pb2.CreateHostRequestReq(
3435 host_user_id=host.id,
3436 from_date=today_plus_2,
3437 to_date=today_plus_3,
3438 text=valid_request_text(),
3439 )
3440 ).host_request_id
3442 with requests_session(other_token) as api:
3443 other_hr_id = api.CreateHostRequest(
3444 requests_pb2.CreateHostRequestReq(
3445 host_user_id=host.id,
3446 from_date=today_plus_2,
3447 to_date=today_plus_3,
3448 text=valid_request_text(),
3449 )
3450 ).host_request_id
3452 with real_moderation_session(super_token) as api:
3453 res = api.SetUserContentVisibility(
3454 moderation_pb2.SetUserContentVisibilityReq(
3455 user_id=target.id,
3456 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3457 )
3458 )
3459 assert res.updated_count == 1
3461 with session_scope() as session:
3462 target_state = _get_moderation_state(session, ModerationObjectType.host_request, target_hr_id)
3463 other_state = _get_moderation_state(session, ModerationObjectType.host_request, other_hr_id)
3464 assert target_state.visibility == ModerationVisibility.hidden
3465 assert other_state.visibility == ModerationVisibility.shadowed
3468def test_SetUserContentVisibility_user_not_found(db):
3469 super_user, super_token = generate_user(is_superuser=True)
3471 with real_moderation_session(super_token) as api:
3472 with pytest.raises(grpc.RpcError) as e:
3473 api.SetUserContentVisibility(
3474 moderation_pb2.SetUserContentVisibilityReq(
3475 user_id=999999,
3476 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3477 )
3478 )
3479 assert e.value.code() == grpc.StatusCode.NOT_FOUND
3482def test_SetUserContentVisibility_from_visibility_filter(db):
3483 super_user, super_token = generate_user(is_superuser=True)
3484 surfer, surfer_token = generate_user()
3485 host, _ = generate_user()
3487 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3488 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3489 with requests_session(surfer_token) as api:
3490 hr_id = api.CreateHostRequest(
3491 requests_pb2.CreateHostRequestReq(
3492 host_user_id=host.id,
3493 from_date=today_plus_2,
3494 to_date=today_plus_3,
3495 text=valid_request_text(),
3496 )
3497 ).host_request_id
3499 with real_moderation_session(super_token) as api:
3500 api.SetUserContentVisibility(
3501 moderation_pb2.SetUserContentVisibilityReq(
3502 user_id=surfer.id,
3503 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
3504 )
3505 )
3507 with real_moderation_session(super_token) as api:
3508 res = api.SetUserContentVisibility(
3509 moderation_pb2.SetUserContentVisibilityReq(
3510 user_id=surfer.id,
3511 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3512 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_SHADOWED],
3513 )
3514 )
3515 assert res.updated_count == 0
3517 with session_scope() as session:
3518 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3519 assert state.visibility == ModerationVisibility.visible
3521 with real_moderation_session(super_token) as api:
3522 res = api.SetUserContentVisibility(
3523 moderation_pb2.SetUserContentVisibilityReq(
3524 user_id=surfer.id,
3525 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
3526 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_VISIBLE],
3527 )
3528 )
3529 assert res.updated_count == 1
3531 with session_scope() as session:
3532 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3533 assert state.visibility == ModerationVisibility.shadowed
3536def test_SetUserContentVisibility_from_visibility_multi(db):
3537 super_user, super_token = generate_user(is_superuser=True)
3538 surfer, surfer_token = generate_user()
3539 host1, _ = generate_user()
3540 host2, _ = generate_user()
3542 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3543 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3544 with requests_session(surfer_token) as api:
3545 hr1_id = api.CreateHostRequest(
3546 requests_pb2.CreateHostRequestReq(
3547 host_user_id=host1.id,
3548 from_date=today_plus_2,
3549 to_date=today_plus_3,
3550 text=valid_request_text(),
3551 )
3552 ).host_request_id
3553 hr2_id = api.CreateHostRequest(
3554 requests_pb2.CreateHostRequestReq(
3555 host_user_id=host2.id,
3556 from_date=today_plus_2,
3557 to_date=today_plus_3,
3558 text=valid_request_text(),
3559 )
3560 ).host_request_id
3562 with session_scope() as session:
3563 state1_id = _get_moderation_state(session, ModerationObjectType.host_request, hr1_id).id
3565 with real_moderation_session(super_token) as api:
3566 api.ModerateContent(
3567 moderation_pb2.ModerateContentReq(
3568 moderation_state_id=state1_id,
3569 action=moderation_pb2.MODERATION_ACTION_APPROVE,
3570 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
3571 )
3572 )
3574 with real_moderation_session(super_token) as api:
3575 res = api.SetUserContentVisibility(
3576 moderation_pb2.SetUserContentVisibilityReq(
3577 user_id=surfer.id,
3578 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3579 from_visibility=[
3580 moderation_pb2.MODERATION_VISIBILITY_VISIBLE,
3581 moderation_pb2.MODERATION_VISIBILITY_SHADOWED,
3582 ],
3583 )
3584 )
3585 assert res.updated_count == 2
3587 with session_scope() as session:
3588 state1 = _get_moderation_state(session, ModerationObjectType.host_request, hr1_id)
3589 state2 = _get_moderation_state(session, ModerationObjectType.host_request, hr2_id)
3590 assert state1.visibility == ModerationVisibility.hidden
3591 assert state2.visibility == ModerationVisibility.hidden
3594def test_SetUserContentVisibility_from_visibility_empty_is_any(db):
3595 super_user, super_token = generate_user(is_superuser=True)
3596 surfer, surfer_token = generate_user()
3597 host, _ = generate_user()
3599 today_plus_2 = (today() + timedelta(days=2)).isoformat()
3600 today_plus_3 = (today() + timedelta(days=3)).isoformat()
3601 with requests_session(surfer_token) as api:
3602 hr_id = api.CreateHostRequest(
3603 requests_pb2.CreateHostRequestReq(
3604 host_user_id=host.id,
3605 from_date=today_plus_2,
3606 to_date=today_plus_3,
3607 text=valid_request_text(),
3608 )
3609 ).host_request_id
3611 with real_moderation_session(super_token) as api:
3612 res = api.SetUserContentVisibility(
3613 moderation_pb2.SetUserContentVisibilityReq(
3614 user_id=surfer.id,
3615 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3616 from_visibility=[],
3617 )
3618 )
3619 assert res.updated_count == 1
3621 with session_scope() as session:
3622 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id)
3623 assert state.visibility == ModerationVisibility.hidden
3626def test_SetUserContentVisibility_from_visibility_unspecified_rejected(db):
3627 super_user, super_token = generate_user(is_superuser=True)
3628 target, _ = generate_user()
3630 with real_moderation_session(super_token) as api:
3631 with pytest.raises(grpc.RpcError) as e:
3632 api.SetUserContentVisibility(
3633 moderation_pb2.SetUserContentVisibilityReq(
3634 user_id=target.id,
3635 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3636 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED],
3637 )
3638 )
3639 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
3642def _content_state_ids(res: moderation_pb2.ListModerationStatesRes) -> list[int]:
3643 """Every user has a moderation state, so listings mix them in with the content states."""
3644 return [
3645 s.moderation_state_id
3646 for s in res.moderation_states
3647 if s.object_type != moderation_pb2.MODERATION_OBJECT_TYPE_USER
3648 ]
3651def test_ListModerationStates_with_no_content(db):
3652 super_user, super_token = generate_user(is_superuser=True)
3654 with real_moderation_session(super_token) as api:
3655 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq())
3656 assert [s.object_id for s in res.moderation_states] == [super_user.id]
3657 assert _content_state_ids(res) == []
3658 assert res.next_page_token == ""
3661def test_ListModerationStates_returns_states_chronologically(db):
3662 super_user, super_token = generate_user(is_superuser=True)
3663 surfer, surfer_token = generate_user()
3664 host, _ = generate_user()
3666 state1_id = create_test_host_request_with_moderation(surfer_token, host.id)
3667 state2_id = create_test_host_request_with_moderation(surfer_token, host.id)
3668 state3_id = create_test_host_request_with_moderation(surfer_token, host.id)
3670 with real_moderation_session(super_token) as api:
3671 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq())
3672 assert _content_state_ids(res) == [state1_id, state2_id, state3_id]
3674 res_newest = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(newest_first=True))
3675 assert _content_state_ids(res_newest) == [state3_id, state2_id, state1_id]
3678def test_ListModerationStates_filter_by_author(db):
3679 super_user, super_token = generate_user(is_superuser=True)
3680 surfer1, surfer1_token = generate_user()
3681 surfer2, surfer2_token = generate_user()
3682 host, _ = generate_user()
3684 state1_id = create_test_host_request_with_moderation(surfer1_token, host.id)
3685 state2_id = create_test_host_request_with_moderation(surfer2_token, host.id)
3686 state3_id = create_test_host_request_with_moderation(surfer1_token, host.id)
3688 with real_moderation_session(super_token) as api:
3689 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer1.id))
3690 assert set(_content_state_ids(res)) == {state1_id, state3_id}
3691 user_states = [s for s in res.moderation_states if s.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_USER]
3692 assert [s.object_id for s in user_states] == [surfer1.id]
3694 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer2.id))
3695 assert _content_state_ids(res) == [state2_id]
3698def test_ListModerationStates_pagination(db):
3699 super_user, super_token = generate_user(is_superuser=True)
3700 surfer, surfer_token = generate_user()
3701 host, _ = generate_user()
3703 for _ in range(3):
3704 create_test_host_request_with_moderation(surfer_token, host.id)
3706 with real_moderation_session(super_token) as api:
3707 # 3 users plus 3 host requests
3708 all_ids = [
3709 s.moderation_state_id
3710 for s in api.ListModerationStates(moderation_pb2.ListModerationStatesReq()).moderation_states
3711 ]
3712 assert len(all_ids) == 6
3714 paged_ids = []
3715 page_token = ""
3716 while True:
3717 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(page_size=2, page_token=page_token))
3718 assert len(res.moderation_states) == 2
3719 paged_ids += [s.moderation_state_id for s in res.moderation_states]
3720 page_token = res.next_page_token
3721 if not page_token:
3722 break
3724 assert paged_ids == all_ids
3727def test_ListModerationStates_pagination_ignores_created(db):
3728 """Backfilled states carry the migration's timestamp, so created and id disagree on their order."""
3729 super_user, super_token = generate_user(is_superuser=True)
3730 surfer, surfer_token = generate_user()
3731 host, _ = generate_user()
3733 first_state_id = create_test_host_request_with_moderation(surfer_token, host.id)
3734 create_test_host_request_with_moderation(surfer_token, host.id)
3735 create_test_host_request_with_moderation(surfer_token, host.id)
3737 with session_scope() as session:
3738 state = session.execute(select(ModerationState).where(ModerationState.id == first_state_id)).scalar_one()
3739 state.created = now() + timedelta(days=1)
3741 paged_ids = []
3742 page_token = ""
3743 with real_moderation_session(super_token) as api:
3744 while True:
3745 res = api.ListModerationStates(
3746 moderation_pb2.ListModerationStatesReq(page_size=2, page_token=page_token, newest_first=True)
3747 )
3748 paged_ids += [s.moderation_state_id for s in res.moderation_states]
3749 page_token = res.next_page_token
3750 if not page_token:
3751 break
3753 with session_scope() as session:
3754 expected = list(session.execute(select(ModerationState.id).order_by(ModerationState.id.desc())).scalars().all())
3756 assert paged_ids == expected
3759# ============================================================================
3760# Tests for user account moderation states
3761# ============================================================================
3764def test_user_moderation_state_created_with_the_account(db):
3765 user, _ = generate_user()
3767 with session_scope() as session:
3768 state = session.execute(
3769 select(ModerationState)
3770 .where(ModerationState.object_type == ModerationObjectType.user)
3771 .where(ModerationState.object_id == user.id)
3772 ).scalar_one()
3774 assert state.visibility is None
3775 assert session.execute(select(User.moderation_state_id).where(User.id == user.id)).scalar_one() == state.id
3777 log_entries = (
3778 session.execute(select(ModerationLog).where(ModerationLog.moderation_state_id == state.id)).scalars().all()
3779 )
3780 assert len(log_entries) == 1
3781 assert log_entries[0].action == ModerationAction.create
3782 assert log_entries[0].moderator_user_id == user.id
3784 queue_items = (
3785 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id))
3786 .scalars()
3787 .all()
3788 )
3789 assert queue_items == []
3792def test_GetModerationState_for_user(db):
3793 super_user, super_token = generate_user(is_superuser=True)
3794 user, _ = generate_user()
3796 with real_moderation_session(super_token) as api:
3797 state = api.GetModerationState(
3798 moderation_pb2.GetModerationStateReq(
3799 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_USER,
3800 object_id=user.id,
3801 )
3802 ).moderation_state
3803 assert state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_USER
3804 assert state.object_id == user.id
3805 assert state.author_user_id == user.id
3806 assert state.author.user_id == user.id
3807 assert state.content == f"@{user.username} / {user.id}"
3808 assert state.visibility == moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED
3811def test_GetModerationState_for_nonexistent_user(db):
3812 super_user, super_token = generate_user(is_superuser=True)
3814 with real_moderation_session(super_token) as api:
3815 with pytest.raises(grpc.RpcError) as e:
3816 api.GetModerationState(
3817 moderation_pb2.GetModerationStateReq(
3818 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_USER,
3819 object_id=999999,
3820 )
3821 )
3822 assert e.value.code() == grpc.StatusCode.NOT_FOUND
3825def test_flag_user_shows_up_in_queue_and_author_filters(db):
3826 super_user, super_token = generate_user(is_superuser=True)
3827 surfer, surfer_token = generate_user()
3828 host, _ = generate_user()
3829 other, _ = generate_user()
3831 content_state_id = create_test_host_request_with_moderation(surfer_token, host.id)
3833 with real_moderation_session(super_token) as api:
3834 user_state_id = api.GetModerationState(
3835 moderation_pb2.GetModerationStateReq(
3836 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_USER,
3837 object_id=surfer.id,
3838 )
3839 ).moderation_state.moderation_state_id
3841 api.ModerateContent(
3842 moderation_pb2.ModerateContentReq(
3843 moderation_state_id=user_state_id,
3844 action=moderation_pb2.MODERATION_ACTION_FLAG,
3845 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG,
3846 reason="Reported for spamming hosts",
3847 )
3848 )
3850 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=surfer.id))
3851 assert {item.moderation_state_id for item in res.queue_items} == {content_state_id, user_state_id}
3852 user_item = next(item for item in res.queue_items if item.moderation_state_id == user_state_id)
3853 assert user_item.trigger == moderation_pb2.MODERATION_TRIGGER_USER_FLAG
3854 assert user_item.reason == "Reported for spamming hosts"
3855 assert user_item.moderation_state.author.user_id == surfer.id
3857 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=other.id))
3858 assert len(res.queue_items) == 0
3860 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer.id))
3861 assert {s.moderation_state_id for s in res.moderation_states} == {content_state_id, user_state_id}
3863 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=other.id))
3864 assert [s.object_id for s in res.moderation_states] == [other.id]
3867@pytest.mark.parametrize("action", [moderation_pb2.MODERATION_ACTION_APPROVE, moderation_pb2.MODERATION_ACTION_HIDE])
3868def test_ModerateContent_cannot_set_visibility_on_user_state(db, action):
3869 super_user, super_token = generate_user(is_superuser=True)
3870 user, _ = generate_user()
3872 with real_moderation_session(super_token) as api:
3873 user_state_id = api.GetModerationState(
3874 moderation_pb2.GetModerationStateReq(
3875 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_USER,
3876 object_id=user.id,
3877 )
3878 ).moderation_state.moderation_state_id
3880 with pytest.raises(grpc.RpcError) as e:
3881 api.ModerateContent(
3882 moderation_pb2.ModerateContentReq(
3883 moderation_state_id=user_state_id,
3884 action=action,
3885 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3886 )
3887 )
3888 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
3890 with session_scope() as session:
3891 state = session.execute(select(ModerationState).where(ModerationState.id == user_state_id)).scalar_one()
3892 assert state.visibility is None
3895def test_SetUserContentVisibility_leaves_user_state_alone(db):
3896 super_user, super_token = generate_user(is_superuser=True)
3897 surfer, surfer_token = generate_user()
3898 host, _ = generate_user()
3900 content_state_id = create_test_host_request_with_moderation(surfer_token, host.id)
3902 with real_moderation_session(super_token) as api:
3903 user_state_id = api.GetModerationState(
3904 moderation_pb2.GetModerationStateReq(
3905 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_USER,
3906 object_id=surfer.id,
3907 )
3908 ).moderation_state.moderation_state_id
3910 res = api.SetUserContentVisibility(
3911 moderation_pb2.SetUserContentVisibilityReq(
3912 user_id=surfer.id,
3913 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
3914 )
3915 )
3916 assert res.updated_count == 1
3918 with session_scope() as session:
3919 content_state = session.execute(
3920 select(ModerationState).where(ModerationState.id == content_state_id)
3921 ).scalar_one()
3922 assert content_state.visibility == ModerationVisibility.hidden
3924 user_state = session.execute(select(ModerationState).where(ModerationState.id == user_state_id)).scalar_one()
3925 assert user_state.visibility is None
3928# ============================================================================
3929# Tests for the moderation model metadata
3930# ============================================================================
3933def test_every_object_type_has_a_model():
3934 """A type in the enum with no model behind it is accepted everywhere and understood nowhere."""
3935 assert set(get_moderated_models()) == set(ModerationObjectType)
3938@pytest.mark.parametrize("object_type", list(ModerationObjectType))
3939def test_visibility_check_constraint_matches_the_models(db, object_type):
3940 """The check constraint spells out the types with their own visibility mechanism; this makes sure it agrees with the models."""
3941 has_own_visibility_mechanism = get_moderated_models()[object_type].has_own_visibility_mechanism
3943 def add_state(object_id: int, visibility: ModerationVisibility | None) -> None:
3944 with session_scope() as session:
3945 session.add(ModerationState(object_type=object_type, object_id=object_id, visibility=visibility))
3947 if has_own_visibility_mechanism:
3948 add_state(1, None)
3949 with pytest.raises(IntegrityError):
3950 add_state(2, ModerationVisibility.visible)
3951 else:
3952 add_state(1, ModerationVisibility.visible)
3953 with pytest.raises(IntegrityError):
3954 add_state(2, None)
3957# ============================================================================
3958# Tests for GetModeratedObject
3959# ============================================================================
3962def _get_moderated_object(token, moderation_state_id):
3963 with real_moderation_session(token) as api:
3964 return api.GetModeratedObject(moderation_pb2.GetModeratedObjectReq(moderation_state_id=moderation_state_id))
3967def _post_reply(token, thread_id, content):
3968 with threads_session(token) as api:
3969 return api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content=content)).thread_id
3972def _moderation_state_id(object_type, object_id):
3973 with session_scope() as session:
3974 return _get_moderation_state(session, object_type, object_id).id
3977def test_GetModeratedObject_host_request(db):
3978 super_user, super_token = generate_user(is_superuser=True)
3979 surfer, surfer_token = generate_user()
3980 host, _ = generate_user()
3982 state_id = create_test_host_request_with_moderation(surfer_token, host.id)
3984 res = _get_moderated_object(super_token, state_id)
3985 assert res.moderation_state.moderation_state_id == state_id
3986 assert res.involved_user_ids == [host.id]
3987 assert res.url == ""
3988 assert not res.HasField("community")
3989 assert not res.HasField("thread_parent")
3992def test_GetModeratedObject_event_occurrence_in_group(db):
3993 super_user, super_token = generate_user(is_superuser=True)
3994 user, token = generate_user()
3996 with session_scope() as session:
3997 community = create_community(session, 0, 2, "Event Community", [user], [], None)
3998 group = create_group(session, "Event Group", [user], [], community)
3999 community_id = community.id
4000 group_id = group.id
4002 event = events_api.create_event(token=token)
4003 events_api.transfer_event(token=token, event_id=event.event_id, new_owner_group_id=group_id)
4005 res = _get_moderated_object(
4006 super_token, _moderation_state_id(ModerationObjectType.event_occurrence, event.event_id)
4007 )
4008 assert res.url == urls.event_link(occurrence_id=event.event_id, slug=event.slug)
4009 assert res.community.community_id == community_id
4010 assert res.community.community_name == "Event Community"
4011 assert res.community.group_id == group_id
4012 assert res.community.group_name == "Event Group"
4015def test_GetModeratedObject_event_occurrence_transferred_to_group_elsewhere(db):
4016 super_user, super_token = generate_user(is_superuser=True)
4017 user, token = generate_user()
4019 with session_scope() as session:
4020 create_community(session, 0, 2, "Location Community", [user], [], None)
4021 other_community = create_community(session, 5, 7, "Other Community", [user], [], None)
4022 group = create_group(session, "Other Group", [user], [], other_community)
4023 other_community_id = other_community.id
4024 group_id = group.id
4026 event = events_api.create_event(token=token)
4027 events_api.transfer_event(token=token, event_id=event.event_id, new_owner_group_id=group_id)
4029 res = _get_moderated_object(
4030 super_token, _moderation_state_id(ModerationObjectType.event_occurrence, event.event_id)
4031 )
4032 assert res.community.community_id == other_community_id
4033 assert res.community.community_name == "Other Community"
4034 assert res.community.group_id == group_id
4035 assert res.community.group_name == "Other Group"
4038def test_GetModeratedObject_comment_on_discussion(db):
4039 super_user, super_token = generate_user(is_superuser=True)
4040 user, token = generate_user()
4042 with session_scope() as session:
4043 community_id = create_community(session, 0, 2, "Discussion Community", [user], [], None).id
4045 with discussions_session(token) as api:
4046 discussion = api.CreateDiscussion(
4047 discussions_pb2.CreateDiscussionReq(
4048 title="Where to eat?", content="Discussion body.", owner_community_id=community_id
4049 )
4050 )
4051 comment_thread_id = _post_reply(token, discussion.thread.thread_id, "Try the noodle place")
4053 res = _get_moderated_object(
4054 super_token, _moderation_state_id(ModerationObjectType.comment, comment_thread_id // 10)
4055 )
4056 assert res.thread_parent.discussion_id == discussion.discussion_id
4057 assert res.thread_parent.title == "Where to eat?"
4058 assert res.url == urls.discussion_link(discussion_id=str(discussion.discussion_id), slug=discussion.slug)
4059 assert res.community.community_id == community_id
4060 assert res.community.group_id == 0
4063def test_GetModeratedObject_comment_on_page(db):
4064 super_user, super_token = generate_user(is_superuser=True)
4065 user, token = generate_user()
4067 with session_scope() as session:
4068 community_id = create_community(session, 0, 2, "Page Community", [user], [], None).id
4069 page_id, page_thread_id = session.execute(
4070 select(Page.id, Page.thread_id)
4071 .where(Page.parent_node_id == community_id)
4072 .where(Page.type == PageType.main_page)
4073 ).one()
4075 comment_thread_id = _post_reply(token, pack_thread_id(page_thread_id, 0), "Nice page")
4077 res = _get_moderated_object(
4078 super_token, _moderation_state_id(ModerationObjectType.comment, comment_thread_id // 10)
4079 )
4080 assert res.thread_parent.page_id == page_id
4081 assert res.thread_parent.title == "Main page for the Page Community community"
4082 assert res.url == ""
4083 assert res.community.community_id == community_id
4086def test_GetModeratedObject_comment_on_page_transferred_to_group_elsewhere(db):
4087 super_user, super_token = generate_user(is_superuser=True)
4088 user, token = generate_user()
4090 with session_scope() as session:
4091 create_community(session, 0, 2, "Location Community", [user], [], None)
4092 other_community = create_community(session, 5, 7, "Other Community", [user], [], None)
4093 group = create_group(session, "Other Group", [user], [], other_community)
4094 other_community_id = other_community.id
4095 group_id = group.id
4097 with pages_session(token) as api:
4098 place = api.CreatePlace(
4099 pages_pb2.CreatePlaceReq(
4100 title="Old Lighthouse",
4101 content="Place description.",
4102 address="Near Null Island",
4103 location=pages_pb2.Coordinate(lat=1, lng=1),
4104 )
4105 )
4106 api.TransferPage(pages_pb2.TransferPageReq(page_id=place.page_id, new_owner_group_id=group_id))
4108 comment_thread_id = _post_reply(token, place.thread.thread_id, "Is it open to visitors?")
4110 res = _get_moderated_object(
4111 super_token, _moderation_state_id(ModerationObjectType.comment, comment_thread_id // 10)
4112 )
4113 assert res.thread_parent.page_id == place.page_id
4114 assert res.thread_parent.title == "Old Lighthouse"
4115 assert res.community.community_id == other_community_id
4116 assert res.community.community_name == "Other Community"
4117 assert res.community.group_id == group_id
4118 assert res.community.group_name == "Other Group"
4121def test_GetModeratedObject_reply_on_event(db):
4122 super_user, super_token = generate_user(is_superuser=True)
4123 user, token = generate_user()
4125 with session_scope() as session:
4126 community_id = create_community(session, 0, 2, "Event Community", [user], [], None).id
4128 event = events_api.create_event(token=token, title="Board Games")
4129 comment_thread_id = _post_reply(token, event.thread.thread_id, "Can I bring a friend?")
4130 reply_thread_id = _post_reply(token, comment_thread_id, "Sure")
4132 with session_scope() as session:
4133 reply_state_id = session.execute(
4134 select(Reply.moderation_state_id).where(Reply.id == reply_thread_id // 10)
4135 ).scalar_one()
4137 res = _get_moderated_object(super_token, reply_state_id)
4138 assert res.parent_comment_id == comment_thread_id // 10
4139 assert res.parent_comment_content == "Can I bring a friend?"
4140 assert res.thread_parent.event_occurrence_id == event.event_id
4141 assert res.thread_parent.title == "Board Games"
4142 assert res.url == urls.event_link(occurrence_id=event.event_id, slug=event.slug)
4143 assert res.community.community_id == community_id
4146def test_GetModeratedObject_group_chat(db):
4147 super_user, super_token = generate_user(is_superuser=True)
4148 creator, creator_token = generate_user()
4149 member, _ = generate_user()
4150 removed, _ = generate_user()
4151 make_friends(creator, member)
4152 make_friends(creator, removed)
4154 with conversations_session(creator_token) as api:
4155 group_chat_id = api.CreateGroupChat(
4156 conversations_pb2.CreateGroupChatReq(recipient_user_ids=[member.id, removed.id])
4157 ).group_chat_id
4158 api.RemoveGroupChatUser(
4159 conversations_pb2.RemoveGroupChatUserReq(group_chat_id=group_chat_id, user_id=removed.id)
4160 )
4162 res = _get_moderated_object(super_token, _moderation_state_id(ModerationObjectType.group_chat, group_chat_id))
4163 assert res.moderation_state.author_user_id == creator.id
4164 assert res.involved_user_ids == [member.id]
4165 assert res.url == ""
4166 assert not res.HasField("community")
4169def test_GetModeratedObject_friend_request(db):
4170 super_user, super_token = generate_user(is_superuser=True)
4171 sender, sender_token = generate_user()
4172 recipient, _ = generate_user()
4174 with api_session(sender_token) as api:
4175 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=recipient.id))
4177 with session_scope() as session:
4178 friend_request_id = session.execute(
4179 select(FriendRelationship.id).where(FriendRelationship.from_user_id == sender.id)
4180 ).scalar_one()
4182 res = _get_moderated_object(
4183 super_token, _moderation_state_id(ModerationObjectType.friend_request, friend_request_id)
4184 )
4185 assert res.involved_user_ids == [recipient.id]
4186 assert res.url == ""
4187 assert not res.HasField("community")
4190def test_GetModeratedObject_discussion_in_group(db):
4191 super_user, super_token = generate_user(is_superuser=True)
4192 user, token = generate_user()
4194 with session_scope() as session:
4195 community = create_community(session, 0, 2, "Discussion Community", [user], [], None)
4196 group = create_group(session, "Discussion Group", [user], [], community)
4197 community_id = community.id
4198 group_id = group.id
4200 with discussions_session(token) as api:
4201 discussion = api.CreateDiscussion(
4202 discussions_pb2.CreateDiscussionReq(
4203 title="Group hike?", content="Discussion body.", owner_group_id=group_id
4204 )
4205 )
4207 res = _get_moderated_object(
4208 super_token, _moderation_state_id(ModerationObjectType.discussion, discussion.discussion_id)
4209 )
4210 assert res.url == urls.discussion_link(discussion_id=str(discussion.discussion_id), slug=discussion.slug)
4211 assert res.community.community_id == community_id
4212 assert res.community.community_name == "Discussion Community"
4213 assert res.community.group_id == group_id
4214 assert res.community.group_name == "Discussion Group"
4215 assert not res.HasField("thread_parent")
4218def test_GetModeratedObject_reference(db):
4219 super_user, super_token = generate_user(is_superuser=True)
4220 from_user, _ = generate_user()
4221 to_user, _ = generate_user()
4223 with session_scope() as session:
4224 reference_id = create_friend_reference(session, from_user.id, to_user.id, timedelta(days=1))
4226 res = _get_moderated_object(super_token, _moderation_state_id(ModerationObjectType.reference, reference_id))
4227 assert res.moderation_state.author_user_id == from_user.id
4228 assert res.url == urls.user_link(username=to_user.username)
4229 assert res.involved_user_ids == [to_user.id]
4230 assert not res.HasField("community")
4233def test_GetModeratedObject_public_trip(db):
4234 super_user, super_token = generate_user(is_superuser=True)
4235 user, token = generate_user()
4237 with session_scope() as session:
4238 community_id = create_community(session, 0, 2, "Trip Community", [user], [], None).id
4240 with public_trips_session(token) as api:
4241 trip_id = api.CreatePublicTrip(
4242 public_trips_pb2.CreatePublicTripReq(
4243 community_id=community_id,
4244 from_date=(today() + timedelta(days=5)).isoformat(),
4245 to_date=(today() + timedelta(days=10)).isoformat(),
4246 description=VALID_TRIP_DESCRIPTION,
4247 )
4248 ).trip_id
4250 res = _get_moderated_object(super_token, _moderation_state_id(ModerationObjectType.public_trip, trip_id))
4251 assert res.community.community_id == community_id
4252 assert res.community.community_name == "Trip Community"
4253 assert res.community.group_id == 0
4254 assert res.url == ""
4255 assert not res.involved_user_ids
4258def test_GetModeratedObject_user(db):
4259 super_user, super_token = generate_user(is_superuser=True)
4260 user, _ = generate_user()
4262 res = _get_moderated_object(super_token, _moderation_state_id(ModerationObjectType.user, user.id))
4263 assert res.moderation_state.author_user_id == user.id
4264 assert res.url == urls.user_link(username=user.username)
4265 assert not res.involved_user_ids
4266 assert not res.HasField("community")
4269def test_GetModeratedObject_event_occurrence_owned_by_user(db):
4270 super_user, super_token = generate_user(is_superuser=True)
4271 user, token = generate_user()
4273 with session_scope() as session:
4274 community_id = create_community(session, 0, 2, "Event Community", [user], [], None).id
4276 event = events_api.create_event(token=token)
4278 res = _get_moderated_object(
4279 super_token, _moderation_state_id(ModerationObjectType.event_occurrence, event.event_id)
4280 )
4281 assert res.url == urls.event_link(occurrence_id=event.event_id, slug=event.slug)
4282 assert res.community.community_id == community_id
4283 assert res.community.community_name == "Event Community"
4284 assert res.community.group_id == 0
4285 assert not res.HasField("thread_parent")
4288def test_GetModeratedObject_event_occurrence_transferred_to_community(db):
4289 super_user, super_token = generate_user(is_superuser=True)
4290 user, token = generate_user()
4292 with session_scope() as session:
4293 create_community(session, 0, 2, "Location Community", [user], [], None)
4294 other_community_id = create_community(session, 5, 7, "Other Community", [user], [], None).id
4296 event = events_api.create_event(token=token)
4297 events_api.transfer_event(token=token, event_id=event.event_id, new_owner_community_id=other_community_id)
4299 res = _get_moderated_object(
4300 super_token, _moderation_state_id(ModerationObjectType.event_occurrence, event.event_id)
4301 )
4302 assert res.community.community_id == other_community_id
4303 assert res.community.community_name == "Other Community"
4304 assert res.community.group_id == 0
4307def test_GetModeratedObject_not_found(db):
4308 super_user, super_token = generate_user(is_superuser=True)
4310 with real_moderation_session(super_token) as api:
4311 with pytest.raises(grpc.RpcError) as e:
4312 api.GetModeratedObject(moderation_pb2.GetModeratedObjectReq(moderation_state_id=999_999_999))
4313 assert e.value.code() == grpc.StatusCode.NOT_FOUND