Coverage for app/backend/src/tests/test_moderation.py: 100%

1440 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-04 22:32 +0000

1""" 

2Comprehensive tests for the Unified Moderation System (UMS) 

3""" 

4 

5from datetime import datetime, timedelta 

6 

7import grpc 

8import pytest 

9from google.protobuf import empty_pb2 

10from sqlalchemy.sql import select 

11 

12from couchers.config import config 

13from couchers.constants import MODERATION_AUTO_APPROVE_FLAG_PRIORITY 

14from couchers.db import session_scope 

15from couchers.jobs.handlers import auto_approve_moderation_queue 

16from couchers.jobs.worker import process_job 

17from couchers.models import ( 

18 AdminAction, 

19 EventOccurrence, 

20 FriendRelationship, 

21 GroupChat, 

22 HostRequest, 

23 ModerationAction, 

24 ModerationLog, 

25 ModerationObjectType, 

26 ModerationQueueItem, 

27 ModerationState, 

28 ModerationTrigger, 

29 ModerationVisibility, 

30 User, 

31) 

32from couchers.moderation.utils import create_moderation 

33from couchers.proto import ( 

34 api_pb2, 

35 conversations_pb2, 

36 events_pb2, 

37 messages_pb2, 

38 moderation_pb2, 

39 notifications_pb2, 

40 requests_pb2, 

41) 

42from couchers.utils import Timestamp_from_datetime, datetime_to_iso8601_local, now, today 

43from tests.fixtures.db import backdate_conversations, generate_user, make_friends 

44from tests.fixtures.misc import EmailCollector, PushCollector, process_jobs 

45from tests.fixtures.sessions import ( 

46 api_session, 

47 conversations_session, 

48 events_session, 

49 notifications_session, 

50 real_moderation_session, 

51 requests_session, 

52) 

53from tests.test_communities import create_community 

54from tests.test_requests import valid_request_text 

55 

56 

57@pytest.fixture(autouse=True) 

58def _(testconfig): 

59 pass 

60 

61 

62def create_test_host_request_with_moderation(surfer_token, host_user_id): 

63 """Helper to create a host request and return its moderation state ID""" 

64 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

65 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

66 

67 with requests_session(surfer_token) as api: 

68 hr_id = api.CreateHostRequest( 

69 requests_pb2.CreateHostRequestReq( 

70 host_user_id=host_user_id, 

71 from_date=today_plus_2, 

72 to_date=today_plus_3, 

73 text=valid_request_text(), 

74 ) 

75 ).host_request_id 

76 

77 backdate_conversations() 

78 

79 with session_scope() as session: 

80 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one() 

81 return hr.moderation_state_id 

82 

83 

84# ============================================================================ 

85# Tests for moderation helper functions 

86# ============================================================================ 

87 

88 

89def test_create_moderation(db): 

90 """Test creating a moderation state with associated log entry""" 

91 user, _ = generate_user() 

92 

93 with session_scope() as session: 

94 # Create a moderation state 

95 moderation_state = create_moderation( 

96 session=session, 

97 object_type=ModerationObjectType.host_request, 

98 object_id=123, 

99 creator_user_id=user.id, 

100 ) 

101 

102 assert moderation_state.object_type == ModerationObjectType.host_request 

103 assert moderation_state.object_id == 123 

104 assert moderation_state.visibility == ModerationVisibility.shadowed 

105 

106 # Check that log entry was created 

107 log_entries = ( 

108 session.execute(select(ModerationLog).where(ModerationLog.moderation_state_id == moderation_state.id)) 

109 .scalars() 

110 .all() 

111 ) 

112 

113 assert len(log_entries) == 1 

114 assert log_entries[0].action == ModerationAction.create 

115 assert log_entries[0].reason == "Object created." 

116 assert log_entries[0].moderator_user_id == user.id 

117 

118 

119def test_add_to_moderation_queue(db): 

120 """Test adding content to moderation queue via API""" 

121 super_user, super_token = generate_user(is_superuser=True) 

122 user1, token1 = generate_user() 

123 user2, _ = generate_user() 

124 

125 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

126 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

127 

128 # Create a real host request (which automatically creates moderation state and adds to queue) 

129 with requests_session(token1) as api: 

130 host_request_id = api.CreateHostRequest( 

131 requests_pb2.CreateHostRequestReq( 

132 host_user_id=user2.id, 

133 from_date=today_plus_2, 

134 to_date=today_plus_3, 

135 text=valid_request_text(), 

136 ) 

137 ).host_request_id 

138 

139 # Get the moderation state ID 

140 state_id = None 

141 with session_scope() as session: 

142 host_request = session.execute( 

143 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

144 ).scalar_one() 

145 state_id = host_request.moderation_state_id 

146 

147 # Add another item to moderation queue via API (the first one was created automatically) 

148 with real_moderation_session(super_token) as api: 

149 api.ModerateContent( 

150 moderation_pb2.ModerateContentReq( 

151 moderation_state_id=state_id, 

152 action=moderation_pb2.MODERATION_ACTION_FLAG, 

153 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

154 reason="Admin manually flagged for additional review", 

155 ) 

156 ) 

157 

158 with session_scope() as session: 

159 flag = session.execute( 

160 select(ModerationQueueItem) 

161 .where(ModerationQueueItem.moderation_state_id == state_id) 

162 .where(ModerationQueueItem.trigger == ModerationTrigger.user_flag) 

163 ).scalar_one() 

164 assert flag.reason == "Admin manually flagged for additional review" 

165 assert flag.resolved_by_log_id is None 

166 

167 # The FLAG action is recorded in the log, pointing at the new queue item. 

168 flag_log = session.execute( 

169 select(ModerationLog) 

170 .where(ModerationLog.moderation_state_id == state_id) 

171 .where(ModerationLog.action == ModerationAction.flag) 

172 ).scalar_one() 

173 assert flag_log.queue_item_id == flag.id 

174 

175 

176def test_moderate_content(db): 

177 """Test moderating content via API""" 

178 super_user, super_token = generate_user(is_superuser=True) 

179 user, token = generate_user() 

180 host, _ = generate_user() 

181 

182 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

183 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

184 

185 # Create a real host request 

186 state_id = None 

187 with requests_session(token) as api: 

188 hr_id = api.CreateHostRequest( 

189 requests_pb2.CreateHostRequestReq( 

190 host_user_id=host.id, 

191 from_date=today_plus_2, 

192 to_date=today_plus_3, 

193 text=valid_request_text(), 

194 ) 

195 ).host_request_id 

196 

197 with session_scope() as session: 

198 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one() 

199 state_id = hr.moderation_state_id 

200 

201 # Moderate the content via API 

202 with real_moderation_session(super_token) as api: 

203 res = api.ModerateContent( 

204 moderation_pb2.ModerateContentReq( 

205 moderation_state_id=state_id, 

206 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

207 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

208 reason="Content looks good", 

209 ) 

210 ) 

211 

212 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE 

213 

214 # Check that state was updated in database 

215 with session_scope() as session: 

216 updated_state = session.get_one(ModerationState, state_id) 

217 assert updated_state.visibility == ModerationVisibility.visible 

218 

219 # Check that log entry was created 

220 log_entries = ( 

221 session.execute( 

222 select(ModerationLog) 

223 .where(ModerationLog.moderation_state_id == state_id) 

224 .order_by(ModerationLog.time.desc(), ModerationLog.id.desc()) 

225 ) 

226 .scalars() 

227 .all() 

228 ) 

229 

230 assert len(log_entries) == 2 # CREATE + APPROVE 

231 assert log_entries[0].action == ModerationAction.approve 

232 assert log_entries[0].moderator_user_id == super_user.id 

233 assert log_entries[0].reason == "Content looks good" 

234 

235 

236def test_resolve_queue_item(db): 

237 """Test resolving a moderation queue item via ModerateContent API""" 

238 user1, token1 = generate_user() 

239 user2, _ = generate_user() 

240 moderator, moderator_token = generate_user(is_superuser=True) 

241 

242 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

243 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

244 

245 # Create a host request using the API (which automatically creates moderation state) 

246 with requests_session(token1) as api: 

247 host_request_id = api.CreateHostRequest( 

248 requests_pb2.CreateHostRequestReq( 

249 host_user_id=user2.id, 

250 from_date=today_plus_2, 

251 to_date=today_plus_3, 

252 text=valid_request_text(), 

253 ) 

254 ).host_request_id 

255 

256 state_id = None 

257 with session_scope() as session: 

258 # Get the host request and its moderation state 

259 host_request = session.execute( 

260 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

261 ).scalar_one() 

262 state_id = host_request.moderation_state_id 

263 

264 # The moderation state should already exist and be in the queue 

265 queue_item = session.execute( 

266 select(ModerationQueueItem) 

267 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id) 

268 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

269 ).scalar_one() 

270 

271 assert queue_item.resolved_by_log_id is None 

272 

273 # Approve content with clear_flags, which resolves the open queue item(s) 

274 with real_moderation_session(moderator_token) as api: 

275 api.ModerateContent( 

276 moderation_pb2.ModerateContentReq( 

277 moderation_state_id=state_id, 

278 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

279 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

280 reason="Approved after review", 

281 clear_flags=True, 

282 ) 

283 ) 

284 

285 # Check that queue item was resolved 

286 with session_scope() as session: 

287 queue_item = session.execute( 

288 select(ModerationQueueItem) 

289 .where(ModerationQueueItem.moderation_state_id == state_id) 

290 .where(ModerationQueueItem.resolved_by_log_id.is_not(None)) 

291 ).scalar_one() 

292 assert queue_item.resolved_by_log_id is not None 

293 

294 

295def test_approve_content_via_api(db): 

296 """Test approving content via ModerateContent API""" 

297 user1, token1 = generate_user() 

298 user2, _ = generate_user() 

299 moderator, moderator_token = generate_user(is_superuser=True) 

300 

301 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

302 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

303 

304 # Create a host request using the API (which automatically creates moderation state) 

305 with requests_session(token1) as api: 

306 host_request_id = api.CreateHostRequest( 

307 requests_pb2.CreateHostRequestReq( 

308 host_user_id=user2.id, 

309 from_date=today_plus_2, 

310 to_date=today_plus_3, 

311 text=valid_request_text(), 

312 ) 

313 ).host_request_id 

314 

315 state_id = None 

316 with session_scope() as session: 

317 # Get the host request and its moderation state 

318 host_request = session.execute( 

319 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

320 ).scalar_one() 

321 state_id = host_request.moderation_state_id 

322 

323 # Approve via API 

324 with real_moderation_session(moderator_token) as api: 

325 api.ModerateContent( 

326 moderation_pb2.ModerateContentReq( 

327 moderation_state_id=state_id, 

328 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

329 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

330 reason="Quick approval", 

331 ) 

332 ) 

333 

334 # Check that state was updated to VISIBLE 

335 with session_scope() as session: 

336 updated_state = session.get_one(ModerationState, state_id) 

337 assert updated_state.visibility == ModerationVisibility.visible 

338 

339 # Check log entry 

340 log_entry = session.execute( 

341 select(ModerationLog) 

342 .where(ModerationLog.moderation_state_id == state_id) 

343 .where(ModerationLog.action == ModerationAction.approve) 

344 ).scalar_one() 

345 

346 assert log_entry.moderator_user_id == moderator.id 

347 assert log_entry.reason == "Quick approval" 

348 

349 

350# ============================================================================ 

351# Tests for host request moderation integration 

352# ============================================================================ 

353 

354 

355def test_create_host_request_creates_moderation_state(db): 

356 """Test that creating a host request automatically creates a moderation state""" 

357 user1, token1 = generate_user() 

358 user2, token2 = generate_user() 

359 

360 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

361 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

362 

363 with requests_session(token1) as api: 

364 host_request_id = api.CreateHostRequest( 

365 requests_pb2.CreateHostRequestReq( 

366 host_user_id=user2.id, 

367 from_date=today_plus_2, 

368 to_date=today_plus_3, 

369 text=valid_request_text(), 

370 ) 

371 ).host_request_id 

372 

373 with session_scope() as session: 

374 # Check that host request has a moderation state 

375 host_request = session.execute( 

376 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

377 ).scalar_one() 

378 

379 # Check moderation state properties 

380 moderation_state = session.execute( 

381 select(ModerationState).where(ModerationState.id == host_request.moderation_state_id) 

382 ).scalar_one() 

383 

384 assert moderation_state.object_type == ModerationObjectType.host_request 

385 assert moderation_state.object_id == host_request_id 

386 assert moderation_state.visibility == ModerationVisibility.shadowed 

387 

388 # Check that it was added to moderation queue 

389 queue_items = ( 

390 session.execute( 

391 select(ModerationQueueItem) 

392 .where(ModerationQueueItem.moderation_state_id == moderation_state.id) 

393 .where(ModerationQueueItem.resolved_by_log_id == None) 

394 ) 

395 .scalars() 

396 .all() 

397 ) 

398 

399 assert len(queue_items) == 1 

400 assert queue_items[0].trigger == ModerationTrigger.initial_review 

401 # item_author_user_id is no longer stored in the model, it's dynamically retrieved 

402 

403 

404def test_host_request_no_notification_before_approval(db, push_collector: PushCollector): 

405 """Test that host requests don't send notifications until approved""" 

406 user1, token1 = generate_user() 

407 user2, token2 = generate_user() 

408 

409 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

410 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

411 

412 with requests_session(token1) as api: 

413 host_request_id = api.CreateHostRequest( 

414 requests_pb2.CreateHostRequestReq( 

415 host_user_id=user2.id, 

416 from_date=today_plus_2, 

417 to_date=today_plus_3, 

418 text=valid_request_text(), 

419 ) 

420 ).host_request_id 

421 

422 # Process all jobs (including the notification job) 

423 process_jobs() 

424 

425 # No push notification should be sent yet (host requests are shadowed initially) 

426 assert push_collector.count_for_user(user2.id) == 0 

427 

428 

429def test_shadowed_notification_not_in_list_notifications(db): 

430 """Test that notifications for shadowed content don't appear in ListNotifications API""" 

431 user1, token1 = generate_user() 

432 user2, token2 = generate_user() 

433 

434 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

435 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

436 

437 # Create a host request (which creates a shadowed notification for the host) 

438 with requests_session(token1) as api: 

439 host_request_id = api.CreateHostRequest( 

440 requests_pb2.CreateHostRequestReq( 

441 host_user_id=user2.id, 

442 from_date=today_plus_2, 

443 to_date=today_plus_3, 

444 text=valid_request_text(), 

445 ) 

446 ).host_request_id 

447 

448 # Host (recipient) should NOT see the notification in ListNotifications - it's for shadowed content 

449 with notifications_session(token2) as api: 

450 res = api.ListNotifications(notifications_pb2.ListNotificationsReq()) 

451 # Should be empty - the host request is still shadowed 

452 assert len(res.notifications) == 0 

453 

454 

455def test_notification_visible_after_approval(db): 

456 """Test that notifications appear in ListNotifications after content is approved""" 

457 user1, token1 = generate_user() 

458 user2, token2 = generate_user() 

459 mod, mod_token = generate_user(is_superuser=True) 

460 

461 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

462 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

463 

464 # Create a host request (which creates a shadowed notification for the host) 

465 with requests_session(token1) as api: 

466 host_request_id = api.CreateHostRequest( 

467 requests_pb2.CreateHostRequestReq( 

468 host_user_id=user2.id, 

469 from_date=today_plus_2, 

470 to_date=today_plus_3, 

471 text=valid_request_text(), 

472 ) 

473 ).host_request_id 

474 

475 # Host (recipient) should NOT see the notification initially 

476 with notifications_session(token2) as api: 

477 res = api.ListNotifications(notifications_pb2.ListNotificationsReq()) 

478 assert len(res.notifications) == 0 

479 

480 # Get the moderation state ID and approve 

481 with session_scope() as session: 

482 host_request = session.execute( 

483 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

484 ).scalar_one() 

485 state_id = host_request.moderation_state_id 

486 

487 with real_moderation_session(mod_token) as api: 

488 api.ModerateContent( 

489 moderation_pb2.ModerateContentReq( 

490 moderation_state_id=state_id, 

491 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

492 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

493 reason="Looks good", 

494 ) 

495 ) 

496 

497 # Now host SHOULD see the notification 

498 with notifications_session(token2) as api: 

499 res = api.ListNotifications(notifications_pb2.ListNotificationsReq()) 

500 assert len(res.notifications) == 1 

501 assert res.notifications[0].topic == "host_request" 

502 assert res.notifications[0].action == "create" 

503 

504 

505def test_shadowed_host_request_visible_to_author_only(db): 

506 """Test that SHADOWED host requests are visible only to the author (surfer), not the recipient (host)""" 

507 user1, token1 = generate_user() 

508 user2, token2 = generate_user() 

509 

510 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

511 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

512 

513 with requests_session(token1) as api: 

514 host_request_id = api.CreateHostRequest( 

515 requests_pb2.CreateHostRequestReq( 

516 host_user_id=user2.id, 

517 from_date=today_plus_2, 

518 to_date=today_plus_3, 

519 text=valid_request_text(), 

520 ) 

521 ).host_request_id 

522 

523 # Surfer (author) can see it with GetHostRequest 

524 with requests_session(token1) as api: 

525 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

526 assert res.host_request_id == host_request_id 

527 assert res.latest_message.text.text == valid_request_text() 

528 

529 # Host (recipient) CANNOT see it with GetHostRequest - it's shadowed 

530 with requests_session(token2) as api: 

531 with pytest.raises(grpc.RpcError) as e: 

532 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

533 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

534 

535 

536def test_unlisted_host_request_not_in_lists(db): 

537 """Test that SHADOWED host requests are visible to author but not to recipient""" 

538 user1, token1 = generate_user() 

539 user2, token2 = generate_user() 

540 

541 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

542 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

543 

544 with requests_session(token1) as api: 

545 host_request_id = api.CreateHostRequest( 

546 requests_pb2.CreateHostRequestReq( 

547 host_user_id=user2.id, 

548 from_date=today_plus_2, 

549 to_date=today_plus_3, 

550 text=valid_request_text(), 

551 ) 

552 ).host_request_id 

553 

554 # Surfer (author) should see it in their sent list even though it's SHADOWED 

555 with requests_session(token1) as api: 

556 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

557 assert len(res.host_requests) == 1 

558 

559 # Host should NOT see it in their received list (still SHADOWED from them) 

560 with requests_session(token2) as api: 

561 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

562 assert len(res.host_requests) == 0 

563 

564 

565def test_approved_host_request_in_lists_and_notifications(db, push_collector: PushCollector): 

566 """Test that approved host requests appear in lists and send notifications""" 

567 user1, token1 = generate_user() 

568 user2, token2 = generate_user() 

569 mod, mod_token = generate_user(is_superuser=True) 

570 

571 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

572 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

573 

574 with requests_session(token1) as api: 

575 host_request_id = api.CreateHostRequest( 

576 requests_pb2.CreateHostRequestReq( 

577 host_user_id=user2.id, 

578 from_date=today_plus_2, 

579 to_date=today_plus_3, 

580 text=valid_request_text(), 

581 ) 

582 ).host_request_id 

583 

584 # Process the initial notification job - should be deferred (no notification sent) 

585 process_jobs() 

586 assert push_collector.count_for_user(user2.id) == 0 

587 

588 # Get the moderation state ID 

589 state_id = None 

590 with session_scope() as session: 

591 host_request = session.execute( 

592 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

593 ).scalar_one() 

594 state_id = host_request.moderation_state_id 

595 

596 # Approve the host request via API 

597 with real_moderation_session(mod_token) as api: 

598 api.ModerateContent( 

599 moderation_pb2.ModerateContentReq( 

600 moderation_state_id=state_id, 

601 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

602 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

603 reason="Looks good", 

604 ) 

605 ) 

606 

607 # Process the re-queued notification job - should now send notification 

608 process_jobs() 

609 

610 # Now surfer SHOULD see it in their sent list 

611 with requests_session(token1) as api: 

612 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

613 assert len(res.host_requests) == 1 

614 assert res.host_requests[0].host_request_id == host_request_id 

615 

616 # Host SHOULD see it in their received list 

617 with requests_session(token2) as api: 

618 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

619 assert len(res.host_requests) == 1 

620 assert res.host_requests[0].host_request_id == host_request_id 

621 

622 # After approval, the host should have received a push notification 

623 assert push_collector.pop_for_user(user2.id, last=True).topic_action == "host_request:create" 

624 

625 

626def test_hidden_host_request_invisible_to_all(db): 

627 """Test that HIDDEN host requests are invisible to everyone except moderators""" 

628 user1, token1 = generate_user() 

629 user2, token2 = generate_user() 

630 user3, token3 = generate_user() # Third party 

631 moderator, moderator_token = generate_user(is_superuser=True) 

632 

633 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

634 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

635 

636 with requests_session(token1) as api: 

637 host_request_id = api.CreateHostRequest( 

638 requests_pb2.CreateHostRequestReq( 

639 host_user_id=user2.id, 

640 from_date=today_plus_2, 

641 to_date=today_plus_3, 

642 text=valid_request_text(), 

643 ) 

644 ).host_request_id 

645 

646 # Get the moderation state ID 

647 state_id = None 

648 with session_scope() as session: 

649 host_request = session.execute( 

650 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

651 ).scalar_one() 

652 state_id = host_request.moderation_state_id 

653 

654 # Hide the host request via API (e.g., spam/abuse) 

655 with real_moderation_session(moderator_token) as api: 

656 api.ModerateContent( 

657 moderation_pb2.ModerateContentReq( 

658 moderation_state_id=state_id, 

659 action=moderation_pb2.MODERATION_ACTION_HIDE, 

660 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

661 reason="Spam content", 

662 ) 

663 ) 

664 

665 # Surfer can't see it with GetHostRequest 

666 with requests_session(token1) as api: 

667 with pytest.raises(grpc.RpcError) as e: 

668 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

669 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

670 

671 # Host can't see it with GetHostRequest 

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 

676 

677 # Third party definitely can't see it 

678 with requests_session(token3) as api: 

679 with pytest.raises(grpc.RpcError) as e: 

680 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

681 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

682 

683 # Not in any lists 

684 with requests_session(token1) as api: 

685 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

686 assert len(res.host_requests) == 0 

687 

688 with requests_session(token2) as api: 

689 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

690 assert len(res.host_requests) == 0 

691 

692 

693def test_multiple_host_requests_listing_visibility(db): 

694 """Test that ListHostRequests correctly filters based on moderation state""" 

695 user1, token1 = generate_user() 

696 user2, token2 = generate_user() 

697 moderator, moderator_token = generate_user(is_superuser=True) 

698 

699 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

700 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

701 

702 # Create 3 host requests 

703 host_request_ids = [] 

704 state_ids = [] 

705 with requests_session(token1) as api: 

706 for i in range(3): 

707 hr_id = api.CreateHostRequest( 

708 requests_pb2.CreateHostRequestReq( 

709 host_user_id=user2.id, 

710 from_date=today_plus_2, 

711 to_date=today_plus_3, 

712 text=valid_request_text(f"Test request {i + 1}"), 

713 ) 

714 ).host_request_id 

715 host_request_ids.append(hr_id) 

716 backdate_conversations() 

717 

718 # Get state IDs 

719 with session_scope() as session: 

720 for hr_id in host_request_ids: 

721 host_request = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one() 

722 state_ids.append(host_request.moderation_state_id) 

723 

724 # Approve the first one via API 

725 with real_moderation_session(moderator_token) as api: 

726 api.ModerateContent( 

727 moderation_pb2.ModerateContentReq( 

728 moderation_state_id=state_ids[0], 

729 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

730 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

731 reason="Approved", 

732 ) 

733 ) 

734 

735 # Hide the third one via API 

736 with real_moderation_session(moderator_token) as api: 

737 api.ModerateContent( 

738 moderation_pb2.ModerateContentReq( 

739 moderation_state_id=state_ids[2], 

740 action=moderation_pb2.MODERATION_ACTION_HIDE, 

741 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

742 reason="Spam", 

743 ) 

744 ) 

745 

746 # Surfer should see the approved one and the shadowed one (author can see their SHADOWED content) 

747 with requests_session(token1) as api: 

748 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

749 assert len(res.host_requests) == 2 

750 visible_ids = {hr.host_request_id for hr in res.host_requests} 

751 assert visible_ids == {host_request_ids[0], host_request_ids[1]} 

752 

753 # Host should see only the approved one in received list 

754 with requests_session(token2) as api: 

755 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

756 assert len(res.host_requests) == 1 

757 assert res.host_requests[0].host_request_id == host_request_ids[0] 

758 

759 

760def test_moderation_log_tracking(db): 

761 """Test that moderation actions are properly logged via API""" 

762 user, user_token = generate_user() 

763 host, _ = generate_user() 

764 moderator1, moderator1_token = generate_user(is_superuser=True) 

765 moderator2, moderator2_token = generate_user(is_superuser=True) 

766 

767 # Create a real host request 

768 state_id = create_test_host_request_with_moderation(user_token, host.id) 

769 

770 # Perform several moderation actions via API 

771 with real_moderation_session(moderator1_token) as api: 

772 api.ModerateContent( 

773 moderation_pb2.ModerateContentReq( 

774 moderation_state_id=state_id, 

775 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

776 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

777 reason="Looks good initially", 

778 ) 

779 ) 

780 

781 with real_moderation_session(moderator2_token) as api: 

782 api.ModerateContent( 

783 moderation_pb2.ModerateContentReq( 

784 moderation_state_id=state_id, 

785 action=moderation_pb2.MODERATION_ACTION_FLAG, 

786 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

787 reason="Wait, this needs another look", 

788 ) 

789 ) 

790 # Shadow it back 

791 api.ModerateContent( 

792 moderation_pb2.ModerateContentReq( 

793 moderation_state_id=state_id, 

794 action=moderation_pb2.MODERATION_ACTION_HIDE, 

795 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

796 reason="Wait, this needs another look", 

797 ) 

798 ) 

799 

800 with real_moderation_session(moderator1_token) as api: 

801 api.ModerateContent( 

802 moderation_pb2.ModerateContentReq( 

803 moderation_state_id=state_id, 

804 action=moderation_pb2.MODERATION_ACTION_HIDE, 

805 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

806 reason="Actually it's spam", 

807 ) 

808 ) 

809 

810 # Check all log entries 

811 with session_scope() as session: 

812 log_entries = ( 

813 session.execute( 

814 select(ModerationLog) 

815 .where(ModerationLog.moderation_state_id == state_id) 

816 .order_by(ModerationLog.time.asc()) 

817 ) 

818 .scalars() 

819 .all() 

820 ) 

821 

822 # CREATE + APPROVE + HIDE + HIDE (shadowing back counts as HIDE action) 

823 assert len(log_entries) >= 3 

824 

825 assert log_entries[0].action == ModerationAction.create 

826 assert log_entries[0].moderator_user_id == user.id 

827 assert log_entries[0].reason == "Object created." 

828 

829 assert log_entries[1].action == ModerationAction.approve 

830 assert log_entries[1].moderator_user_id == moderator1.id 

831 assert log_entries[1].reason == "Looks good initially" 

832 

833 # The last action should be hiding 

834 assert log_entries[-1].action == ModerationAction.hide 

835 assert log_entries[-1].moderator_user_id == moderator1.id 

836 assert log_entries[-1].reason == "Actually it's spam" 

837 

838 

839def test_moderation_queue_workflow(db): 

840 """Test the full moderation queue workflow via API""" 

841 user1, token1 = generate_user() 

842 user2, _ = generate_user() 

843 moderator, moderator_token = generate_user(is_superuser=True) 

844 

845 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

846 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

847 

848 # Create a host request using the API (which automatically creates moderation state and adds to queue) 

849 with requests_session(token1) as api: 

850 host_request_id = api.CreateHostRequest( 

851 requests_pb2.CreateHostRequestReq( 

852 host_user_id=user2.id, 

853 from_date=today_plus_2, 

854 to_date=today_plus_3, 

855 text=valid_request_text(), 

856 ) 

857 ).host_request_id 

858 

859 state_id = None 

860 queue_item_id = None 

861 with session_scope() as session: 

862 # Get the host request and its moderation state 

863 host_request = session.execute( 

864 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

865 ).scalar_one() 

866 state_id = host_request.moderation_state_id 

867 

868 # The queue item should already exist (created automatically) 

869 queue_item = session.execute( 

870 select(ModerationQueueItem) 

871 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id) 

872 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

873 ).scalar_one() 

874 queue_item_id = queue_item.id 

875 

876 # Verify it's in the queue 

877 unresolved_items = ( 

878 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.resolved_by_log_id == None)) 

879 .scalars() 

880 .all() 

881 ) 

882 

883 assert len(unresolved_items) >= 1 

884 assert queue_item.id in [item.id for item in unresolved_items] 

885 

886 # Moderator reviews and approves via API, clearing the open queue item 

887 with real_moderation_session(moderator_token) as api: 

888 api.ModerateContent( 

889 moderation_pb2.ModerateContentReq( 

890 moderation_state_id=state_id, 

891 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

892 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

893 reason="Content approved", 

894 clear_flags=True, 

895 ) 

896 ) 

897 

898 # Verify queue item was resolved 

899 with session_scope() as session: 

900 # Verify it's no longer in unresolved queue 

901 unresolved_items = ( 

902 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.resolved_by_log_id == None)) 

903 .scalars() 

904 .all() 

905 ) 

906 

907 assert queue_item_id not in [item.id for item in unresolved_items] 

908 

909 # Verify the queue item was linked to a log entry 

910 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

911 assert queue_item.resolved_by_log_id is not None 

912 

913 

914# ============================================================================ 

915# Moderation API Tests (testing the gRPC servicer) 

916# ============================================================================ 

917 

918 

919def test_GetModerationQueue_empty(db): 

920 """Test getting an empty moderation queue""" 

921 super_user, super_token = generate_user(is_superuser=True) 

922 

923 with real_moderation_session(super_token) as api: 

924 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq()) 

925 assert len(res.queue_items) == 0 

926 assert res.next_page_token == "" 

927 

928 

929def test_GetModerationQueue_with_items(db): 

930 """Test getting moderation queue with items via API""" 

931 super_user, super_token = generate_user(is_superuser=True) 

932 normal_user, user_token = generate_user() 

933 host, _ = generate_user() 

934 

935 # Create some host requests (which automatically adds them to moderation queue) 

936 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

937 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

938 

939 with real_moderation_session(super_token) as api: 

940 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq()) 

941 assert len(res.queue_items) == 2 

942 assert res.queue_items[0].is_resolved == False 

943 assert res.queue_items[1].is_resolved == False 

944 

945 

946def test_GetModerationQueue_filter_by_trigger(db): 

947 """Test filtering moderation queue by trigger type via API""" 

948 super_user, super_token = generate_user(is_superuser=True) 

949 normal_user, user_token = generate_user() 

950 host, _ = generate_user() 

951 

952 # Create host requests (which automatically adds them to moderation queue with INITIAL_REVIEW) 

953 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

954 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

955 

956 # Add USER_FLAG trigger to second item via API 

957 with real_moderation_session(super_token) as api: 

958 api.ModerateContent( 

959 moderation_pb2.ModerateContentReq( 

960 moderation_state_id=state2_id, 

961 action=moderation_pb2.MODERATION_ACTION_FLAG, 

962 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

963 reason="Reported by user", 

964 ) 

965 ) 

966 

967 # Filter by INITIAL_REVIEW (should get first item and maybe both depending on how queue works) 

968 with real_moderation_session(super_token) as api: 

969 res = api.GetModerationQueue( 

970 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW]) 

971 ) 

972 assert len(res.queue_items) == 2 # Both have INITIAL_REVIEW triggers 

973 assert all(item.trigger == moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW for item in res.queue_items) 

974 

975 # Filter by USER_FLAG (should get second item only) 

976 with real_moderation_session(super_token) as api: 

977 res = api.GetModerationQueue( 

978 moderation_pb2.GetModerationQueueReq(triggers=[moderation_pb2.MODERATION_TRIGGER_USER_FLAG]) 

979 ) 

980 assert len(res.queue_items) == 1 

981 assert res.queue_items[0].trigger == moderation_pb2.MODERATION_TRIGGER_USER_FLAG 

982 

983 

984def test_GetModerationQueue_filter_created_before(db): 

985 """Test filtering moderation queue by created_before timestamp""" 

986 super_user, super_token = generate_user(is_superuser=True) 

987 normal_user, user_token = generate_user() 

988 host, _ = generate_user() 

989 

990 # Create host requests 

991 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

992 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

993 

994 # Backdate the first queue item 

995 with session_scope() as session: 

996 queue_item1 = session.execute( 

997 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id) 

998 ).scalar_one() 

999 # Set it to 2 hours ago 

1000 queue_item1.time_created = now() - timedelta(hours=2) 

1001 

1002 # The second item remains at current time 

1003 

1004 # Filter to items created before 1 hour ago (should only get the first item) 

1005 cutoff_time = now() - timedelta(hours=1) 

1006 with real_moderation_session(super_token) as api: 

1007 res = api.GetModerationQueue( 

1008 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(cutoff_time)) 

1009 ) 

1010 assert len(res.queue_items) == 1 

1011 assert res.queue_items[0].moderation_state_id == state1_id 

1012 

1013 # Filter to items created before now (should get both) 

1014 with real_moderation_session(super_token) as api: 

1015 res = api.GetModerationQueue( 

1016 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(now() + timedelta(seconds=10))) 

1017 ) 

1018 assert len(res.queue_items) == 2 

1019 

1020 # Filter to items created before 3 hours ago (should get none) 

1021 old_cutoff = now() - timedelta(hours=3) 

1022 with real_moderation_session(super_token) as api: 

1023 res = api.GetModerationQueue( 

1024 moderation_pb2.GetModerationQueueReq(created_before=Timestamp_from_datetime(old_cutoff)) 

1025 ) 

1026 assert len(res.queue_items) == 0 

1027 

1028 

1029def test_GetModerationQueue_filter_created_after(db): 

1030 """Test filtering moderation queue by created_after timestamp""" 

1031 super_user, super_token = generate_user(is_superuser=True) 

1032 normal_user, user_token = generate_user() 

1033 host, _ = generate_user() 

1034 

1035 # Create host requests 

1036 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1037 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1038 

1039 # Backdate the first queue item to 2 hours ago 

1040 with session_scope() as session: 

1041 queue_item1 = session.execute( 

1042 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id) 

1043 ).scalar_one() 

1044 queue_item1.time_created = now() - timedelta(hours=2) 

1045 

1046 # The second item remains at current time 

1047 

1048 # Filter to items created after 1 hour ago (should only get the second item) 

1049 cutoff_time = now() - timedelta(hours=1) 

1050 with real_moderation_session(super_token) as api: 

1051 res = api.GetModerationQueue( 

1052 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(cutoff_time)) 

1053 ) 

1054 assert len(res.queue_items) == 1 

1055 assert res.queue_items[0].moderation_state_id == state2_id 

1056 

1057 # Filter to items created after 3 hours ago (should get both) 

1058 old_cutoff = now() - timedelta(hours=3) 

1059 with real_moderation_session(super_token) as api: 

1060 res = api.GetModerationQueue( 

1061 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(old_cutoff)) 

1062 ) 

1063 assert len(res.queue_items) == 2 

1064 

1065 # Filter to items created after now (should get none) 

1066 with real_moderation_session(super_token) as api: 

1067 res = api.GetModerationQueue( 

1068 moderation_pb2.GetModerationQueueReq(created_after=Timestamp_from_datetime(now() + timedelta(seconds=10))) 

1069 ) 

1070 assert len(res.queue_items) == 0 

1071 

1072 

1073def test_GetModerationQueue_filter_created_before_and_after(db): 

1074 """Test filtering moderation queue by both created_before and created_after timestamps""" 

1075 super_user, super_token = generate_user(is_superuser=True) 

1076 normal_user, user_token = generate_user() 

1077 host, _ = generate_user() 

1078 

1079 # Create 3 host requests 

1080 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1081 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1082 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1083 

1084 # Set different times: state1 = 3 hours ago, state2 = 1.5 hours ago, state3 = now 

1085 with session_scope() as session: 

1086 queue_item1 = session.execute( 

1087 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id) 

1088 ).scalar_one() 

1089 queue_item1.time_created = now() - timedelta(hours=3) 

1090 

1091 queue_item2 = session.execute( 

1092 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state2_id) 

1093 ).scalar_one() 

1094 queue_item2.time_created = now() - timedelta(hours=1, minutes=30) 

1095 

1096 # Filter to items between 2 hours ago and 1 hour ago (should only get state2) 

1097 after_cutoff = now() - timedelta(hours=2) 

1098 before_cutoff = now() - timedelta(hours=1) 

1099 with real_moderation_session(super_token) as api: 

1100 res = api.GetModerationQueue( 

1101 moderation_pb2.GetModerationQueueReq( 

1102 created_after=Timestamp_from_datetime(after_cutoff), 

1103 created_before=Timestamp_from_datetime(before_cutoff), 

1104 ) 

1105 ) 

1106 assert len(res.queue_items) == 1 

1107 assert res.queue_items[0].moderation_state_id == state2_id 

1108 

1109 # Filter to items between 4 hours ago and 2.5 hours ago (should only get state1) 

1110 after_cutoff = now() - timedelta(hours=4) 

1111 before_cutoff = now() - timedelta(hours=2, minutes=30) 

1112 with real_moderation_session(super_token) as api: 

1113 res = api.GetModerationQueue( 

1114 moderation_pb2.GetModerationQueueReq( 

1115 created_after=Timestamp_from_datetime(after_cutoff), 

1116 created_before=Timestamp_from_datetime(before_cutoff), 

1117 ) 

1118 ) 

1119 assert len(res.queue_items) == 1 

1120 assert res.queue_items[0].moderation_state_id == state1_id 

1121 

1122 

1123def test_GetModerationQueue_filter_unresolved(db): 

1124 """Test filtering moderation queue for unresolved items only via API""" 

1125 super_user, super_token = generate_user(is_superuser=True) 

1126 normal_user, user_token = generate_user() 

1127 host, _ = generate_user() 

1128 

1129 # Create 2 host requests 

1130 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1131 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1132 

1133 # Resolve the first one via API (approve with clear_flags resolves the queue item) 

1134 with real_moderation_session(super_token) as api: 

1135 api.ModerateContent( 

1136 moderation_pb2.ModerateContentReq( 

1137 moderation_state_id=state1_id, 

1138 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1139 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1140 reason="Approved", 

1141 clear_flags=True, 

1142 ) 

1143 ) 

1144 

1145 # Get all items 

1146 with real_moderation_session(super_token) as api: 

1147 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq()) 

1148 assert len(res.queue_items) == 2 

1149 

1150 # Get only unresolved items 

1151 with real_moderation_session(super_token) as api: 

1152 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True)) 

1153 assert len(res.queue_items) == 1 

1154 assert res.queue_items[0].is_resolved == False 

1155 

1156 

1157def test_GetModerationQueue_filter_by_author(db): 

1158 """Test filtering moderation queue by item_author_user_id""" 

1159 super_user, super_token = generate_user(is_superuser=True) 

1160 user1, token1 = generate_user() 

1161 user2, token2 = generate_user() 

1162 host_user, _ = generate_user() 

1163 

1164 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

1165 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

1166 

1167 # Create 2 host requests by user1 

1168 with requests_session(token1) as api: 

1169 hr1_id = api.CreateHostRequest( 

1170 requests_pb2.CreateHostRequestReq( 

1171 host_user_id=host_user.id, 

1172 from_date=today_plus_2, 

1173 to_date=today_plus_3, 

1174 text=valid_request_text(), 

1175 ) 

1176 ).host_request_id 

1177 backdate_conversations() 

1178 

1179 hr2_id = api.CreateHostRequest( 

1180 requests_pb2.CreateHostRequestReq( 

1181 host_user_id=host_user.id, 

1182 from_date=today_plus_2, 

1183 to_date=today_plus_3, 

1184 text=valid_request_text(), 

1185 ) 

1186 ).host_request_id 

1187 

1188 # Create 1 host request by user2 

1189 with requests_session(token2) as api: 

1190 hr3_id = api.CreateHostRequest( 

1191 requests_pb2.CreateHostRequestReq( 

1192 host_user_id=host_user.id, 

1193 from_date=today_plus_2, 

1194 to_date=today_plus_3, 

1195 text=valid_request_text(), 

1196 ) 

1197 ).host_request_id 

1198 

1199 # Get moderation state IDs 

1200 state1_id, state2_id, state3_id = None, None, None 

1201 with session_scope() as session: 

1202 hr1 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr1_id)).scalar_one() 

1203 hr2 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr2_id)).scalar_one() 

1204 hr3 = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr3_id)).scalar_one() 

1205 state1_id = hr1.moderation_state_id 

1206 state2_id = hr2.moderation_state_id 

1207 state3_id = hr3.moderation_state_id 

1208 

1209 # Get all items (should be 3) 

1210 with real_moderation_session(super_token) as api: 

1211 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq()) 

1212 assert len(res.queue_items) == 3 

1213 

1214 # Filter by user1 (should get 2) 

1215 with real_moderation_session(super_token) as api: 

1216 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=user1.id)) 

1217 assert len(res.queue_items) == 2 

1218 assert all(item.moderation_state.author.user_id == user1.id for item in res.queue_items) 

1219 

1220 # Filter by user2 (should get 1) 

1221 with real_moderation_session(super_token) as api: 

1222 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=user2.id)) 

1223 assert len(res.queue_items) == 1 

1224 assert res.queue_items[0].moderation_state.author.user_id == user2.id 

1225 assert res.queue_items[0].moderation_state_id == state3_id 

1226 

1227 # Filter by non-existent user (should get 0) 

1228 with real_moderation_session(super_token) as api: 

1229 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(item_author_user_id=999999)) 

1230 assert len(res.queue_items) == 0 

1231 

1232 

1233def test_GetModerationQueue_ordering(db): 

1234 """Test ordering moderation queue by oldest/newest first""" 

1235 super_user, super_token = generate_user(is_superuser=True) 

1236 normal_user, user_token = generate_user() 

1237 host, _ = generate_user() 

1238 

1239 # Create 3 host requests 

1240 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1241 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1242 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1243 

1244 # Set different times: state1 = 3 hours ago, state2 = 2 hours ago, state3 = 1 hour ago 

1245 with session_scope() as session: 

1246 queue_item1 = session.execute( 

1247 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state1_id) 

1248 ).scalar_one() 

1249 queue_item1.time_created = now() - timedelta(hours=3) 

1250 

1251 queue_item2 = session.execute( 

1252 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state2_id) 

1253 ).scalar_one() 

1254 queue_item2.time_created = now() - timedelta(hours=2) 

1255 

1256 queue_item3 = session.execute( 

1257 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state3_id) 

1258 ).scalar_one() 

1259 queue_item3.time_created = now() - timedelta(hours=1) 

1260 

1261 # Default order (oldest first) 

1262 with real_moderation_session(super_token) as api: 

1263 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq()) 

1264 assert len(res.queue_items) == 3 

1265 assert res.queue_items[0].moderation_state_id == state1_id # oldest 

1266 assert res.queue_items[1].moderation_state_id == state2_id 

1267 assert res.queue_items[2].moderation_state_id == state3_id # newest 

1268 

1269 # Explicit oldest first 

1270 with real_moderation_session(super_token) as api: 

1271 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(newest_first=False)) 

1272 assert len(res.queue_items) == 3 

1273 assert res.queue_items[0].moderation_state_id == state1_id # oldest 

1274 assert res.queue_items[2].moderation_state_id == state3_id # newest 

1275 

1276 # Newest first 

1277 with real_moderation_session(super_token) as api: 

1278 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(newest_first=True)) 

1279 assert len(res.queue_items) == 3 

1280 assert res.queue_items[0].moderation_state_id == state3_id # newest 

1281 assert res.queue_items[1].moderation_state_id == state2_id 

1282 assert res.queue_items[2].moderation_state_id == state1_id # oldest 

1283 

1284 

1285def test_GetModerationQueue_pagination_newest_first(db): 

1286 """Test pagination with newest_first=True returns different items on each page""" 

1287 super_user, super_token = generate_user(is_superuser=True) 

1288 normal_user, normal_token = generate_user() 

1289 host_user, _ = generate_user() 

1290 

1291 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

1292 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

1293 

1294 # Create 5 host requests 

1295 hr_ids = [] 

1296 with requests_session(normal_token) as api: 

1297 for i in range(5): 

1298 hr_id = api.CreateHostRequest( 

1299 requests_pb2.CreateHostRequestReq( 

1300 host_user_id=host_user.id, 

1301 from_date=today_plus_2, 

1302 to_date=today_plus_3, 

1303 text=valid_request_text(), 

1304 ) 

1305 ).host_request_id 

1306 hr_ids.append(hr_id) 

1307 backdate_conversations() 

1308 

1309 # Get moderation state IDs 

1310 state_ids = [] 

1311 with session_scope() as session: 

1312 for hr_id in hr_ids: 

1313 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == hr_id)).scalar_one() 

1314 state_ids.append(hr.moderation_state_id) 

1315 

1316 # Set different times so ordering is deterministic 

1317 with session_scope() as session: 

1318 for i, state_id in enumerate(state_ids): 

1319 queue_item = session.execute( 

1320 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id) 

1321 ).scalar_one() 

1322 queue_item.time_created = now() - timedelta(hours=5 - i) # oldest first in list 

1323 

1324 # Get first page (2 items) with newest_first=True, filtered to our user's items 

1325 with real_moderation_session(super_token) as api: 

1326 res1 = api.GetModerationQueue( 

1327 moderation_pb2.GetModerationQueueReq(page_size=2, newest_first=True, item_author_user_id=normal_user.id) 

1328 ) 

1329 assert len(res1.queue_items) == 2 

1330 # Should get newest items: state_ids[4], state_ids[3] 

1331 assert res1.queue_items[0].moderation_state_id == state_ids[4] 

1332 assert res1.queue_items[1].moderation_state_id == state_ids[3] 

1333 assert res1.next_page_token # should have more pages 

1334 

1335 # Get second page using the token 

1336 res2 = api.GetModerationQueue( 

1337 moderation_pb2.GetModerationQueueReq( 

1338 page_size=2, newest_first=True, page_token=res1.next_page_token, item_author_user_id=normal_user.id 

1339 ) 

1340 ) 

1341 assert len(res2.queue_items) == 2 

1342 # Should get next newest items: state_ids[2], state_ids[1] 

1343 assert res2.queue_items[0].moderation_state_id == state_ids[2] 

1344 assert res2.queue_items[1].moderation_state_id == state_ids[1] 

1345 

1346 # Pages should not overlap 

1347 page1_ids = {item.moderation_state_id for item in res1.queue_items} 

1348 page2_ids = {item.moderation_state_id for item in res2.queue_items} 

1349 assert page1_ids.isdisjoint(page2_ids), "Pages should not have overlapping items" 

1350 

1351 

1352def test_GetModerationLog(db): 

1353 """Test getting moderation log for a state via API""" 

1354 super_user, super_token = generate_user(is_superuser=True) 

1355 moderator, moderator_token = generate_user(is_superuser=True) 

1356 normal_user, user_token = generate_user() 

1357 host, _ = generate_user() 

1358 

1359 # Create a real host request 

1360 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1361 

1362 # Perform a moderation action via API 

1363 with real_moderation_session(moderator_token) as api: 

1364 api.ModerateContent( 

1365 moderation_pb2.ModerateContentReq( 

1366 moderation_state_id=state_id, 

1367 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1368 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1369 reason="Looks good", 

1370 ) 

1371 ) 

1372 

1373 with real_moderation_session(super_token) as api: 

1374 res = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id)) 

1375 assert len(res.log_entries) == 2 # CREATE + APPROVE 

1376 assert res.moderation_state.moderation_state_id == state_id 

1377 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE 

1378 # Log entries are in reverse chronological order 

1379 assert res.log_entries[0].action == moderation_pb2.MODERATION_ACTION_APPROVE 

1380 assert res.log_entries[0].moderator_user_id == moderator.id 

1381 assert res.log_entries[0].reason == "Looks good" 

1382 assert res.log_entries[1].action == moderation_pb2.MODERATION_ACTION_CREATE 

1383 assert res.log_entries[1].moderator_user_id == normal_user.id 

1384 

1385 

1386def test_GetModerationLog_not_found(db): 

1387 """Test getting moderation log for non-existent state""" 

1388 super_user, super_token = generate_user(is_superuser=True) 

1389 

1390 with real_moderation_session(super_token) as api: 

1391 with pytest.raises(grpc.RpcError) as e: 

1392 api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=999999)) 

1393 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1394 assert e.value.details() == "Moderation state not found." 

1395 

1396 

1397def test_GetModerationState(db): 

1398 """Test getting moderation state by object type and ID""" 

1399 super_user, super_token = generate_user(is_superuser=True) 

1400 user1, token1 = generate_user() 

1401 user2, _ = generate_user() 

1402 

1403 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

1404 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

1405 

1406 with requests_session(token1) as api: 

1407 host_request_id = api.CreateHostRequest( 

1408 requests_pb2.CreateHostRequestReq( 

1409 host_user_id=user2.id, 

1410 from_date=today_plus_2, 

1411 to_date=today_plus_3, 

1412 text=valid_request_text(), 

1413 ) 

1414 ).host_request_id 

1415 

1416 with real_moderation_session(super_token) as api: 

1417 res = api.GetModerationState( 

1418 moderation_pb2.GetModerationStateReq( 

1419 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1420 object_id=host_request_id, 

1421 ) 

1422 ) 

1423 assert res.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST 

1424 assert res.moderation_state.object_id == host_request_id 

1425 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

1426 assert res.moderation_state.moderation_state_id > 0 

1427 

1428 

1429def test_GetModerationState_not_found(db): 

1430 """Test getting moderation state for non-existent object""" 

1431 super_user, super_token = generate_user(is_superuser=True) 

1432 

1433 with real_moderation_session(super_token) as api: 

1434 with pytest.raises(grpc.RpcError) as e: 

1435 api.GetModerationState( 

1436 moderation_pb2.GetModerationStateReq( 

1437 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1438 object_id=999999, 

1439 ) 

1440 ) 

1441 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1442 assert e.value.details() == "Moderation state not found." 

1443 

1444 

1445def test_GetModerationState_unspecified_type(db): 

1446 """Test getting moderation state with unspecified object type""" 

1447 super_user, super_token = generate_user(is_superuser=True) 

1448 

1449 with real_moderation_session(super_token) as api: 

1450 with pytest.raises(grpc.RpcError) as e: 

1451 api.GetModerationState( 

1452 moderation_pb2.GetModerationStateReq( 

1453 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_UNSPECIFIED, 

1454 object_id=123, 

1455 ) 

1456 ) 

1457 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1458 assert e.value.details() == "Object type must be specified." 

1459 

1460 

1461def test_ModerateContent_approve(db): 

1462 """Test approving content via unified moderation API""" 

1463 super_user, super_token = generate_user(is_superuser=True) 

1464 user1, token1 = generate_user() 

1465 user2, _ = generate_user() 

1466 

1467 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

1468 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

1469 

1470 # Create a host request using the API (which automatically creates moderation state) 

1471 with requests_session(token1) as api: 

1472 host_request_id = api.CreateHostRequest( 

1473 requests_pb2.CreateHostRequestReq( 

1474 host_user_id=user2.id, 

1475 from_date=today_plus_2, 

1476 to_date=today_plus_3, 

1477 text=valid_request_text(), 

1478 ) 

1479 ).host_request_id 

1480 

1481 # Get the moderation state ID 

1482 state_id = None 

1483 with session_scope() as session: 

1484 host_request = session.execute( 

1485 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

1486 ).scalar_one() 

1487 state_id = host_request.moderation_state_id 

1488 

1489 with real_moderation_session(super_token) as api: 

1490 res = api.ModerateContent( 

1491 moderation_pb2.ModerateContentReq( 

1492 moderation_state_id=state_id, 

1493 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1494 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1495 reason="Approved by admin", 

1496 ) 

1497 ) 

1498 assert res.moderation_state.moderation_state_id == state_id 

1499 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE 

1500 

1501 # Verify state was updated in database 

1502 with session_scope() as session: 

1503 state = session.get_one(ModerationState, state_id) 

1504 assert state.visibility == ModerationVisibility.visible 

1505 

1506 

1507def test_ModerateContent_not_found(db): 

1508 """Test moderating non-existent content""" 

1509 super_user, super_token = generate_user(is_superuser=True) 

1510 

1511 with real_moderation_session(super_token) as api: 

1512 with pytest.raises(grpc.RpcError) as e: 

1513 api.ModerateContent( 

1514 moderation_pb2.ModerateContentReq( 

1515 moderation_state_id=999999, 

1516 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1517 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1518 reason="Test", 

1519 ) 

1520 ) 

1521 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1522 assert e.value.details() == "Moderation state not found." 

1523 

1524 

1525def test_ModerateContent_hide(db): 

1526 """Test hiding content via unified moderation API""" 

1527 super_user, super_token = generate_user(is_superuser=True) 

1528 normal_user, user_token = generate_user() 

1529 host, _ = generate_user() 

1530 

1531 # Create a real host request 

1532 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1533 

1534 with real_moderation_session(super_token) as api: 

1535 res = api.ModerateContent( 

1536 moderation_pb2.ModerateContentReq( 

1537 moderation_state_id=state_id, 

1538 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1539 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1540 reason="Spam content", 

1541 ) 

1542 ) 

1543 assert res.moderation_state.moderation_state_id == state_id 

1544 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_HIDDEN 

1545 

1546 # Verify state was updated in database 

1547 with session_scope() as session: 

1548 state = session.get_one(ModerationState, state_id) 

1549 assert state.visibility == ModerationVisibility.hidden 

1550 

1551 

1552def test_ModerateContent_shadow(db): 

1553 """Test shadowing content via unified moderation API""" 

1554 super_user, super_token = generate_user(is_superuser=True) 

1555 normal_user, user_token = generate_user() 

1556 host, _ = generate_user() 

1557 

1558 # Create a real host request 

1559 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1560 

1561 with real_moderation_session(super_token) as api: 

1562 res = api.ModerateContent( 

1563 moderation_pb2.ModerateContentReq( 

1564 moderation_state_id=state_id, 

1565 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1566 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

1567 reason="Needs further review", 

1568 ) 

1569 ) 

1570 assert res.moderation_state.moderation_state_id == state_id 

1571 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

1572 

1573 # Verify state was updated in database 

1574 with session_scope() as session: 

1575 state = session.get_one(ModerationState, state_id) 

1576 assert state.visibility == ModerationVisibility.shadowed 

1577 

1578 

1579def test_ModerateContent_flag(db): 

1580 """Test opening a flag via the FLAG action""" 

1581 super_user, super_token = generate_user(is_superuser=True) 

1582 user1, token1 = generate_user() 

1583 user2, _ = generate_user() 

1584 

1585 # Create a host request 

1586 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

1587 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

1588 

1589 with requests_session(token1) as api: 

1590 host_request_id = api.CreateHostRequest( 

1591 requests_pb2.CreateHostRequestReq( 

1592 host_user_id=user2.id, 

1593 from_date=today_plus_2, 

1594 to_date=today_plus_3, 

1595 text=valid_request_text(), 

1596 ) 

1597 ).host_request_id 

1598 

1599 # Get the moderation state ID 

1600 with session_scope() as session: 

1601 host_request = session.execute( 

1602 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

1603 ).scalar_one() 

1604 state_id = host_request.moderation_state_id 

1605 

1606 with real_moderation_session(super_token) as api: 

1607 api.ModerateContent( 

1608 moderation_pb2.ModerateContentReq( 

1609 moderation_state_id=state_id, 

1610 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1611 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

1612 reason="Admin flagged for additional review", 

1613 priority=5, 

1614 ) 

1615 ) 

1616 

1617 # Verify queue item was created in database with the given priority, plus a log row referencing it 

1618 with session_scope() as session: 

1619 queue_item = session.execute( 

1620 select(ModerationQueueItem) 

1621 .where(ModerationQueueItem.moderation_state_id == state_id) 

1622 .where(ModerationQueueItem.trigger == ModerationTrigger.moderator_review) 

1623 ).scalar_one() 

1624 assert queue_item.resolved_by_log_id is None 

1625 assert queue_item.priority == 5 

1626 

1627 flag_log = session.execute( 

1628 select(ModerationLog) 

1629 .where(ModerationLog.moderation_state_id == state_id) 

1630 .where(ModerationLog.action == ModerationAction.flag) 

1631 ).scalar_one() 

1632 assert flag_log.queue_item_id == queue_item.id 

1633 

1634 

1635def test_ModerateContent_flag_requires_trigger(db): 

1636 """FLAG without a trigger is rejected.""" 

1637 super_user, super_token = generate_user(is_superuser=True) 

1638 user1, token1 = generate_user() 

1639 user2, _ = generate_user() 

1640 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1641 

1642 with real_moderation_session(super_token) as api: 

1643 with pytest.raises(grpc.RpcError) as e: 

1644 api.ModerateContent( 

1645 moderation_pb2.ModerateContentReq( 

1646 moderation_state_id=state_id, 

1647 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1648 reason="no trigger", 

1649 ) 

1650 ) 

1651 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1652 

1653 

1654def _open_queue_item_id(state_id): 

1655 with session_scope() as session: 

1656 return ( 

1657 session.execute( 

1658 select(ModerationQueueItem) 

1659 .where(ModerationQueueItem.moderation_state_id == state_id) 

1660 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

1661 ) 

1662 .scalars() 

1663 .one() 

1664 .id 

1665 ) 

1666 

1667 

1668def test_ModerateContent_set_priority(db): 

1669 """SET_PRIORITY changes a flag's priority and logs it, without touching visibility.""" 

1670 moderator, mod_token = generate_user(is_superuser=True) 

1671 user1, token1 = generate_user() 

1672 user2, _ = generate_user() 

1673 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1674 queue_item_id = _open_queue_item_id(state_id) 

1675 

1676 with real_moderation_session(mod_token) as api: 

1677 api.ModerateContent( 

1678 moderation_pb2.ModerateContentReq( 

1679 moderation_state_id=state_id, 

1680 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY, 

1681 queue_item_id=queue_item_id, 

1682 priority=10, 

1683 reason="bump", 

1684 ) 

1685 ) 

1686 

1687 with session_scope() as session: 

1688 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

1689 assert queue_item.priority == 10 

1690 assert queue_item.resolved_by_log_id is None # not resolved 

1691 # State stays shadowed (visibility untouched) 

1692 assert session.get_one(ModerationState, state_id).visibility == ModerationVisibility.shadowed 

1693 

1694 log = session.execute( 

1695 select(ModerationLog) 

1696 .where(ModerationLog.moderation_state_id == state_id) 

1697 .where(ModerationLog.action == ModerationAction.set_priority) 

1698 ).scalar_one() 

1699 assert log.new_priority == 10 

1700 assert log.queue_item_id == queue_item_id 

1701 

1702 # GetModerationQueue surfaces the new priority 

1703 with real_moderation_session(mod_token) as api: 

1704 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True)) 

1705 item = next(i for i in res.queue_items if i.queue_item_id == queue_item_id) 

1706 assert item.priority == 10 

1707 

1708 

1709def test_ModerateContent_unflag(db): 

1710 """UNFLAG resolves a single named flag and does not change visibility.""" 

1711 moderator, mod_token = generate_user(is_superuser=True) 

1712 user1, token1 = generate_user() 

1713 user2, _ = generate_user() 

1714 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1715 queue_item_id = _open_queue_item_id(state_id) 

1716 

1717 with real_moderation_session(mod_token) as api: 

1718 api.ModerateContent( 

1719 moderation_pb2.ModerateContentReq( 

1720 moderation_state_id=state_id, 

1721 action=moderation_pb2.MODERATION_ACTION_UNFLAG, 

1722 queue_item_id=queue_item_id, 

1723 reason="dismissed", 

1724 ) 

1725 ) 

1726 

1727 with session_scope() as session: 

1728 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

1729 assert queue_item.resolved_by_log_id is not None 

1730 # Visibility unchanged (still shadowed from creation) 

1731 assert session.get_one(ModerationState, state_id).visibility == ModerationVisibility.shadowed 

1732 log = session.get_one(ModerationLog, queue_item.resolved_by_log_id) 

1733 assert log.action == ModerationAction.unflag 

1734 assert log.queue_item_id == queue_item_id 

1735 

1736 

1737def test_ModerateContent_unflag_requires_queue_item(db): 

1738 """UNFLAG without a queue_item_id is rejected.""" 

1739 moderator, mod_token = generate_user(is_superuser=True) 

1740 user1, token1 = generate_user() 

1741 user2, _ = generate_user() 

1742 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1743 

1744 with real_moderation_session(mod_token) as api: 

1745 with pytest.raises(grpc.RpcError) as e: 

1746 api.ModerateContent( 

1747 moderation_pb2.ModerateContentReq( 

1748 moderation_state_id=state_id, 

1749 action=moderation_pb2.MODERATION_ACTION_UNFLAG, 

1750 reason="no target", 

1751 ) 

1752 ) 

1753 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1754 

1755 

1756def test_ModerateContent_approve_without_clear_flags_leaves_flag_open(db): 

1757 """APPROVE without clear_flags changes visibility but leaves open flags in the queue.""" 

1758 moderator, mod_token = generate_user(is_superuser=True) 

1759 user1, token1 = generate_user() 

1760 user2, _ = generate_user() 

1761 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1762 queue_item_id = _open_queue_item_id(state_id) 

1763 

1764 with real_moderation_session(mod_token) as api: 

1765 api.ModerateContent( 

1766 moderation_pb2.ModerateContentReq( 

1767 moderation_state_id=state_id, 

1768 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1769 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1770 reason="visible but still under review", 

1771 ) 

1772 ) 

1773 

1774 with session_scope() as session: 

1775 assert session.get_one(ModerationState, state_id).visibility == ModerationVisibility.visible 

1776 assert session.get_one(ModerationQueueItem, queue_item_id).resolved_by_log_id is None 

1777 

1778 

1779def test_ModerateContent_flag_supersede(db): 

1780 """FLAG with supersede_queue_item_id resolves the named flag as the new one opens.""" 

1781 moderator, mod_token = generate_user(is_superuser=True) 

1782 user1, token1 = generate_user() 

1783 user2, _ = generate_user() 

1784 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1785 initial_item_id = _open_queue_item_id(state_id) 

1786 

1787 with real_moderation_session(mod_token) as api: 

1788 api.ModerateContent( 

1789 moderation_pb2.ModerateContentReq( 

1790 moderation_state_id=state_id, 

1791 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1792 trigger=moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG, 

1793 priority=3, 

1794 supersede_queue_item_id=initial_item_id, 

1795 reason="machine re-flag", 

1796 ) 

1797 ) 

1798 

1799 with session_scope() as session: 

1800 # Old initial_review flag is resolved 

1801 assert session.get_one(ModerationQueueItem, initial_item_id).resolved_by_log_id is not None 

1802 # Exactly one open flag remains: the new machine_flag at priority 3 

1803 open_items = ( 

1804 session.execute( 

1805 select(ModerationQueueItem) 

1806 .where(ModerationQueueItem.moderation_state_id == state_id) 

1807 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

1808 ) 

1809 .scalars() 

1810 .all() 

1811 ) 

1812 assert len(open_items) == 1 

1813 assert open_items[0].trigger == ModerationTrigger.machine_flag 

1814 assert open_items[0].priority == 3 

1815 

1816 

1817def test_GetModerationQueue_filter_by_priority(db): 

1818 """priority_min / priority_max filter the queue by priority range.""" 

1819 moderator, mod_token = generate_user(is_superuser=True) 

1820 user1, token1 = generate_user() 

1821 user2, _ = generate_user() 

1822 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1823 queue_item_id = _open_queue_item_id(state_id) 

1824 

1825 with real_moderation_session(mod_token) as api: 

1826 api.ModerateContent( 

1827 moderation_pb2.ModerateContentReq( 

1828 moderation_state_id=state_id, 

1829 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY, 

1830 queue_item_id=queue_item_id, 

1831 priority=7, 

1832 reason="raise", 

1833 ) 

1834 ) 

1835 

1836 # priority_min excludes it below the threshold, includes at/above 

1837 assert len(api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_min=8)).queue_items) == 0 

1838 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_min=7)) 

1839 assert [i.queue_item_id for i in res.queue_items] == [queue_item_id] 

1840 

1841 # priority_max excludes it above the threshold 

1842 assert len(api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_max=6)).queue_items) == 0 

1843 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(priority_max=7)) 

1844 assert [i.queue_item_id for i in res.queue_items] == [queue_item_id] 

1845 

1846 

1847# ============================================================================ 

1848# Tests for group chat moderation 

1849# ============================================================================ 

1850 

1851 

1852def test_group_chat_created_with_moderation_state(db): 

1853 """Test that group chats are created with moderation state""" 

1854 user1, token1 = generate_user() 

1855 user2, _ = generate_user() 

1856 make_friends(user1, user2) 

1857 

1858 with conversations_session(token1) as api: 

1859 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])) 

1860 group_chat_id = res.group_chat_id 

1861 

1862 # Verify moderation state was created 

1863 with session_scope() as session: 

1864 group_chat = session.execute(select(GroupChat).where(GroupChat.conversation_id == group_chat_id)).scalar_one() 

1865 

1866 assert group_chat.moderation_state.object_type == ModerationObjectType.group_chat 

1867 assert group_chat.moderation_state.object_id == group_chat_id 

1868 # Group chats start as SHADOWED 

1869 assert group_chat.moderation_state.visibility == ModerationVisibility.shadowed 

1870 

1871 # A moderation queue item should have been created 

1872 queue_item = ( 

1873 session.execute( 

1874 select(ModerationQueueItem).where( 

1875 ModerationQueueItem.moderation_state_id == group_chat.moderation_state_id 

1876 ) 

1877 ) 

1878 .scalars() 

1879 .first() 

1880 ) 

1881 assert queue_item is not None 

1882 assert queue_item.trigger == ModerationTrigger.initial_review 

1883 

1884 

1885def test_group_chat_GetModerationState(db): 

1886 """Test GetModerationState API for group chats""" 

1887 user1, token1 = generate_user() 

1888 user2, _ = generate_user() 

1889 moderator, mod_token = generate_user(is_superuser=True) 

1890 make_friends(user1, user2) 

1891 

1892 with conversations_session(token1) as api: 

1893 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])) 

1894 group_chat_id = res.group_chat_id 

1895 

1896 # Moderator can look up the moderation state 

1897 with real_moderation_session(mod_token) as api: 

1898 res = api.GetModerationState( 

1899 moderation_pb2.GetModerationStateReq( 

1900 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1901 object_id=group_chat_id, 

1902 ) 

1903 ) 

1904 assert res.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT 

1905 assert res.moderation_state.object_id == group_chat_id 

1906 # Starts as SHADOWED 

1907 assert res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

1908 

1909 

1910def test_group_chat_moderation_hide(db): 

1911 """Test that a moderator can hide a group chat and participants can no longer see it""" 

1912 user1, token1 = generate_user() 

1913 user2, token2 = generate_user() 

1914 moderator, mod_token = generate_user(is_superuser=True) 

1915 make_friends(user1, user2) 

1916 

1917 with conversations_session(token1) as api: 

1918 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])) 

1919 group_chat_id = res.group_chat_id 

1920 api.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Hello!")) 

1921 

1922 # First approve the group chat so both users can see it 

1923 with real_moderation_session(mod_token) as api: 

1924 state_res = api.GetModerationState( 

1925 moderation_pb2.GetModerationStateReq( 

1926 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1927 object_id=group_chat_id, 

1928 ) 

1929 ) 

1930 api.ModerateContent( 

1931 moderation_pb2.ModerateContentReq( 

1932 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1933 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1934 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1935 reason="Approved", 

1936 ) 

1937 ) 

1938 

1939 # Both users can see the chat now 

1940 with conversations_session(token1) as api: 

1941 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq()) 

1942 assert len(res.group_chats) == 1 

1943 

1944 with conversations_session(token2) as api: 

1945 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq()) 

1946 assert len(res.group_chats) == 1 

1947 

1948 # Moderator hides the group chat 

1949 with real_moderation_session(mod_token) as api: 

1950 state_res = api.GetModerationState( 

1951 moderation_pb2.GetModerationStateReq( 

1952 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1953 object_id=group_chat_id, 

1954 ) 

1955 ) 

1956 api.ModerateContent( 

1957 moderation_pb2.ModerateContentReq( 

1958 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1959 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1960 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1961 reason="Inappropriate content", 

1962 ) 

1963 ) 

1964 

1965 # Neither user can see the chat now 

1966 with conversations_session(token1) as api: 

1967 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq()) 

1968 assert len(res.group_chats) == 0 

1969 

1970 with conversations_session(token2) as api: 

1971 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq()) 

1972 assert len(res.group_chats) == 0 

1973 

1974 # Trying to get messages returns empty (chat is hidden so no messages visible) 

1975 with conversations_session(token1) as api: 

1976 res = api.GetGroupChatMessages(conversations_pb2.GetGroupChatMessagesReq(group_chat_id=group_chat_id)) 

1977 assert len(res.messages) == 0 

1978 

1979 

1980def test_group_chat_moderation_shadow(db): 

1981 """Test that shadowing a group chat hides it from non-creator participants""" 

1982 user1, token1 = generate_user() # Creator 

1983 user2, token2 = generate_user() # Participant 

1984 moderator, mod_token = generate_user(is_superuser=True) 

1985 make_friends(user1, user2) 

1986 

1987 with conversations_session(token1) as api: 

1988 res = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[user2.id])) 

1989 group_chat_id = res.group_chat_id 

1990 api.SendMessage(conversations_pb2.SendMessageReq(group_chat_id=group_chat_id, text="Hello!")) 

1991 

1992 # Moderator shadows the group chat 

1993 with real_moderation_session(mod_token) as api: 

1994 state_res = api.GetModerationState( 

1995 moderation_pb2.GetModerationStateReq( 

1996 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1997 object_id=group_chat_id, 

1998 ) 

1999 ) 

2000 api.ModerateContent( 

2001 moderation_pb2.ModerateContentReq( 

2002 moderation_state_id=state_res.moderation_state.moderation_state_id, 

2003 action=moderation_pb2.MODERATION_ACTION_HIDE, 

2004 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2005 reason="Needs review", 

2006 ) 

2007 ) 

2008 

2009 # Creator can see SHADOWED content in list operations 

2010 with conversations_session(token1) as api: 

2011 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq()) 

2012 assert len(res.group_chats) == 1 

2013 assert res.group_chats[0].group_chat_id == group_chat_id 

2014 

2015 # But non-creator participant cannot see it in lists 

2016 with conversations_session(token2) as api: 

2017 res = api.ListGroupChats(conversations_pb2.ListGroupChatsReq()) 

2018 assert len(res.group_chats) == 0 

2019 

2020 # Creator can also access it directly via GetGroupChat 

2021 with conversations_session(token1) as api: 

2022 res = api.GetGroupChat(conversations_pb2.GetGroupChatReq(group_chat_id=group_chat_id)) 

2023 assert res.group_chat_id == group_chat_id 

2024 

2025 

2026# ============================================================================ 

2027# Tests for auto-approval background job 

2028# ============================================================================ 

2029 

2030 

2031def test_auto_approve_moderation_queue_disabled_when_zero(db, email_collector: EmailCollector): 

2032 """Test that auto-approval is disabled when deadline is 0""" 

2033 moderator, mod_token = generate_user(is_superuser=True) 

2034 user1, token1 = generate_user() 

2035 user2, token2 = generate_user() 

2036 

2037 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2038 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2039 

2040 # Create a host request 

2041 with requests_session(token1) as api: 

2042 host_request_id = api.CreateHostRequest( 

2043 requests_pb2.CreateHostRequestReq( 

2044 host_user_id=user2.id, 

2045 from_date=today_plus_2, 

2046 to_date=today_plus_3, 

2047 text=valid_request_text(), 

2048 ) 

2049 ).host_request_id 

2050 

2051 # No email should have been sent (request is shadowed) 

2052 assert email_collector.count_for_recipient(user2.email) == 0 

2053 

2054 # Ensure deadline is 0 (disabled) 

2055 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 0 

2056 

2057 # Run the job 

2058 auto_approve_moderation_queue(empty_pb2.Empty()) 

2059 

2060 # Surfer (author) can see the request via API 

2061 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2062 assert res.host_request_id == host_request_id 

2063 

2064 # Author can see their SHADOWED request in their sent list 

2065 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

2066 assert len(res.host_requests) == 1 

2067 assert res.host_requests[0].host_request_id == host_request_id 

2068 

2069 # Host cannot see the request (it's shadowed from them) 

2070 with requests_session(token2) as api: 

2071 with pytest.raises(grpc.RpcError) as e: 

2072 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2073 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2074 

2075 # Host doesn't see it in their received list either 

2076 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

2077 assert len(res.host_requests) == 0 

2078 

2079 # Moderator can still see the item in the moderation queue 

2080 with real_moderation_session(mod_token) as api: 

2081 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True)) 

2082 assert len(res.queue_items) == 1 

2083 assert res.queue_items[0].trigger == moderation_pb2.MODERATION_TRIGGER_INITIAL_REVIEW 

2084 

2085 # Moderator can check the state is still SHADOWED 

2086 state_res = api.GetModerationState( 

2087 moderation_pb2.GetModerationStateReq( 

2088 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2089 object_id=host_request_id, 

2090 ) 

2091 ) 

2092 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

2093 

2094 

2095def test_auto_approve_moderation_queue_approves_old_items( 

2096 db, email_collector: EmailCollector, push_collector: PushCollector 

2097): 

2098 """Test that auto-approval approves items older than the deadline""" 

2099 moderator, mod_token = generate_user(is_superuser=True) 

2100 user1, token1 = generate_user() 

2101 user2, token2 = generate_user() 

2102 

2103 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2104 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2105 

2106 # Create a host request 

2107 with requests_session(token1) as api: 

2108 host_request_id = api.CreateHostRequest( 

2109 requests_pb2.CreateHostRequestReq( 

2110 host_user_id=user2.id, 

2111 from_date=today_plus_2, 

2112 to_date=today_plus_3, 

2113 text=valid_request_text("Test request for auto-approval"), 

2114 ) 

2115 ).host_request_id 

2116 

2117 # No email sent initially (shadowed) 

2118 assert email_collector.count_for_recipient(user2.email) == 0 

2119 

2120 # Host cannot see the request yet 

2121 with requests_session(token2) as api: 

2122 with pytest.raises(grpc.RpcError) as e: 

2123 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2124 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2125 

2126 # Make the queue item appear old by backdating its time_created 

2127 with session_scope() as session: 

2128 host_request = session.execute( 

2129 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

2130 ).scalar_one() 

2131 queue_item = session.execute( 

2132 select(ModerationQueueItem) 

2133 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id) 

2134 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

2135 ).scalar_one() 

2136 # Backdate the queue item by 2 minutes 

2137 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=2) 

2138 

2139 # Set deadline to 60 seconds (items older than 60 seconds will be auto-approved) 

2140 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2141 config.MODERATION_BOT_USER_ID = moderator.id 

2142 

2143 # Run the job 

2144 auto_approve_moderation_queue(empty_pb2.Empty()) 

2145 

2146 # Now host can see the request via API 

2147 with requests_session(token2) as api: 

2148 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2149 assert res.host_request_id == host_request_id 

2150 assert res.latest_message.text.text == valid_request_text("Test request for auto-approval") 

2151 

2152 # Host sees it in their received list 

2153 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

2154 assert len(res.host_requests) == 1 

2155 assert res.host_requests[0].host_request_id == host_request_id 

2156 

2157 # Surfer sees it in their sent list 

2158 with requests_session(token1) as api: 

2159 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

2160 assert len(res.host_requests) == 1 

2161 assert res.host_requests[0].host_request_id == host_request_id 

2162 

2163 with real_moderation_session(mod_token) as api: 

2164 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True)) 

2165 assert len(res.queue_items) == 1 

2166 flag_item = res.queue_items[0] 

2167 assert flag_item.trigger == moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG 

2168 assert flag_item.priority == MODERATION_AUTO_APPROVE_FLAG_PRIORITY 

2169 

2170 state_res = api.GetModerationState( 

2171 moderation_pb2.GetModerationStateReq( 

2172 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2173 object_id=host_request_id, 

2174 ) 

2175 ) 

2176 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_VISIBLE 

2177 

2178 log_res = api.GetModerationLog( 

2179 moderation_pb2.GetModerationLogReq(moderation_state_id=state_res.moderation_state.moderation_state_id) 

2180 ) 

2181 approve_entries = [e for e in log_res.log_entries if e.action == moderation_pb2.MODERATION_ACTION_APPROVE] 

2182 assert len(approve_entries) == 1 

2183 assert "Auto-approved" in approve_entries[0].reason 

2184 assert "60 seconds" in approve_entries[0].reason 

2185 assert approve_entries[0].moderator_user_id == moderator.id 

2186 

2187 flag_entries = [e for e in log_res.log_entries if e.action == moderation_pb2.MODERATION_ACTION_FLAG] 

2188 assert len(flag_entries) == 1 

2189 assert flag_entries[0].moderator_user_id == moderator.id 

2190 

2191 

2192def test_auto_approve_does_not_approve_recent_items(db, email_collector: EmailCollector): 

2193 """Test that auto-approval does not approve items that are newer than the deadline""" 

2194 moderator, mod_token = generate_user(is_superuser=True) 

2195 user1, token1 = generate_user() 

2196 user2, token2 = generate_user() 

2197 

2198 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2199 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2200 

2201 # Create a host request 

2202 with requests_session(token1) as api: 

2203 host_request_id = api.CreateHostRequest( 

2204 requests_pb2.CreateHostRequestReq( 

2205 host_user_id=user2.id, 

2206 from_date=today_plus_2, 

2207 to_date=today_plus_3, 

2208 text=valid_request_text(), 

2209 ) 

2210 ).host_request_id 

2211 

2212 # No email sent (shadowed) 

2213 assert email_collector.count_for_recipient(user2.email) == 0 

2214 

2215 # Set deadline to 1 hour (items older than 1 hour will be auto-approved) 

2216 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600 

2217 config.MODERATION_BOT_USER_ID = moderator.id 

2218 

2219 # Run the job - the item was just created, so it shouldn't be approved 

2220 auto_approve_moderation_queue(empty_pb2.Empty()) 

2221 

2222 # Still no email sent 

2223 assert email_collector.count_for_recipient(user2.email) == 0 

2224 

2225 # Host still cannot see the request 

2226 with requests_session(token2) as api: 

2227 with pytest.raises(grpc.RpcError) as e: 

2228 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2229 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2230 

2231 # Not in host's received list 

2232 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

2233 assert len(res.host_requests) == 0 

2234 

2235 # Moderator sees it still in queue unresolved 

2236 with real_moderation_session(mod_token) as api: 

2237 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True)) 

2238 assert len(res.queue_items) == 1 

2239 

2240 # State is still SHADOWED 

2241 state_res = api.GetModerationState( 

2242 moderation_pb2.GetModerationStateReq( 

2243 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2244 object_id=host_request_id, 

2245 ) 

2246 ) 

2247 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

2248 

2249 

2250def test_auto_approve_does_not_approve_already_approved(db): 

2251 """Test that auto-approval does not re-approve already visible content""" 

2252 moderator, mod_token = generate_user(is_superuser=True) 

2253 user1, token1 = generate_user() 

2254 user2, token2 = generate_user() 

2255 

2256 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2257 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2258 

2259 # Create a host request 

2260 with requests_session(token1) as api: 

2261 host_request_id = api.CreateHostRequest( 

2262 requests_pb2.CreateHostRequestReq( 

2263 host_user_id=user2.id, 

2264 from_date=today_plus_2, 

2265 to_date=today_plus_3, 

2266 text=valid_request_text(), 

2267 ) 

2268 ).host_request_id 

2269 

2270 # Moderator approves it manually 

2271 with real_moderation_session(mod_token) as api: 

2272 state_res = api.GetModerationState( 

2273 moderation_pb2.GetModerationStateReq( 

2274 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2275 object_id=host_request_id, 

2276 ) 

2277 ) 

2278 state_id = state_res.moderation_state.moderation_state_id 

2279 

2280 api.ModerateContent( 

2281 moderation_pb2.ModerateContentReq( 

2282 moderation_state_id=state_id, 

2283 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

2284 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2285 reason="Approved by moderator", 

2286 clear_flags=True, 

2287 ) 

2288 ) 

2289 

2290 # Host can now see it 

2291 with requests_session(token2) as api: 

2292 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2293 assert res.host_request_id == host_request_id 

2294 

2295 # Get log count before auto-approval 

2296 with real_moderation_session(mod_token) as api: 

2297 log_res_before = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id)) 

2298 log_count_before = len(log_res_before.log_entries) 

2299 

2300 # Set deadline to 1 second 

2301 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2302 config.MODERATION_BOT_USER_ID = moderator.id 

2303 

2304 # Run the job 

2305 auto_approve_moderation_queue(empty_pb2.Empty()) 

2306 

2307 # No new log entries should be created (already approved, queue item resolved) 

2308 with real_moderation_session(mod_token) as api: 

2309 log_res_after = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id)) 

2310 assert len(log_res_after.log_entries) == log_count_before 

2311 

2312 # Queue should be empty (item was resolved when moderator approved) 

2313 queue_res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq(unresolved_only=True)) 

2314 assert len(queue_res.queue_items) == 0 

2315 

2316 

2317def test_auto_approve_does_not_approve_moderator_shadowed_items(db): 

2318 """Test that auto-approval does not approve items that were explicitly shadowed by a moderator""" 

2319 moderator, mod_token = generate_user(is_superuser=True) 

2320 user1, token1 = generate_user() 

2321 user2, token2 = generate_user() 

2322 

2323 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2324 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2325 

2326 # Create a host request 

2327 with requests_session(token1) as api: 

2328 host_request_id = api.CreateHostRequest( 

2329 requests_pb2.CreateHostRequestReq( 

2330 host_user_id=user2.id, 

2331 from_date=today_plus_2, 

2332 to_date=today_plus_3, 

2333 text=valid_request_text(), 

2334 ) 

2335 ).host_request_id 

2336 

2337 # Moderator explicitly shadows the content (keeping it shadowed but resolving the queue item) 

2338 with real_moderation_session(mod_token) as api: 

2339 state_res = api.GetModerationState( 

2340 moderation_pb2.GetModerationStateReq( 

2341 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2342 object_id=host_request_id, 

2343 ) 

2344 ) 

2345 state_id = state_res.moderation_state.moderation_state_id 

2346 

2347 # Set to SHADOWED explicitly with clear_flags - this resolves the INITIAL_REVIEW queue item 

2348 api.ModerateContent( 

2349 moderation_pb2.ModerateContentReq( 

2350 moderation_state_id=state_id, 

2351 action=moderation_pb2.MODERATION_ACTION_HIDE, 

2352 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2353 reason="Keeping shadowed for review", 

2354 clear_flags=True, 

2355 ) 

2356 ) 

2357 

2358 # Backdate to ensure it would be old enough for auto-approval 

2359 with session_scope() as session: 

2360 queue_item = session.execute( 

2361 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state_id) 

2362 ).scalar_one() 

2363 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=10) 

2364 

2365 # Set deadline to 1 second 

2366 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2367 config.MODERATION_BOT_USER_ID = moderator.id 

2368 

2369 # Get log count before 

2370 with real_moderation_session(mod_token) as api: 

2371 log_res_before = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id)) 

2372 log_count_before = len(log_res_before.log_entries) 

2373 

2374 # Run the job 

2375 auto_approve_moderation_queue(empty_pb2.Empty()) 

2376 

2377 # No new log entries - the queue item was resolved when moderator shadowed it 

2378 with real_moderation_session(mod_token) as api: 

2379 log_res_after = api.GetModerationLog(moderation_pb2.GetModerationLogReq(moderation_state_id=state_id)) 

2380 assert len(log_res_after.log_entries) == log_count_before 

2381 

2382 # State should still be SHADOWED (not auto-approved to VISIBLE) 

2383 state_res = api.GetModerationState( 

2384 moderation_pb2.GetModerationStateReq( 

2385 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2386 object_id=host_request_id, 

2387 ) 

2388 ) 

2389 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

2390 

2391 # Host still cannot see the request 

2392 with requests_session(token2) as api: 

2393 with pytest.raises(grpc.RpcError) as e: 

2394 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2395 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2396 

2397 

2398def test_auto_approve_skips_shadowed_user_authored_items(db): 

2399 """Auto-approval must not promote content authored by a currently-shadowed user.""" 

2400 moderator, mod_token = generate_user(is_superuser=True) 

2401 surfer, surfer_token = generate_user() 

2402 host, host_token = generate_user() 

2403 

2404 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2405 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2406 

2407 with requests_session(surfer_token) as api: 

2408 host_request_id = api.CreateHostRequest( 

2409 requests_pb2.CreateHostRequestReq( 

2410 host_user_id=host.id, 

2411 from_date=today_plus_2, 

2412 to_date=today_plus_3, 

2413 text=valid_request_text(), 

2414 ) 

2415 ).host_request_id 

2416 

2417 # Shadow the surfer after the host request was created 

2418 with session_scope() as session: 

2419 session.execute(select(User).where(User.id == surfer.id)).scalar_one().shadowed_at = now() 

2420 

2421 # Backdate the queue item to make it eligible for auto-approval 

2422 with session_scope() as session: 

2423 host_request = session.execute( 

2424 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

2425 ).scalar_one() 

2426 queue_item = session.execute( 

2427 select(ModerationQueueItem) 

2428 .where(ModerationQueueItem.moderation_state_id == host_request.moderation_state_id) 

2429 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

2430 ).scalar_one() 

2431 queue_item.time_created = datetime.now(queue_item.time_created.tzinfo) - timedelta(minutes=10) 

2432 

2433 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2434 config.MODERATION_BOT_USER_ID = moderator.id 

2435 

2436 auto_approve_moderation_queue(empty_pb2.Empty()) 

2437 

2438 # State should remain SHADOWED — the auto-approve job must skip shadowed-user content 

2439 with real_moderation_session(mod_token) as api: 

2440 state_res = api.GetModerationState( 

2441 moderation_pb2.GetModerationStateReq( 

2442 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2443 object_id=host_request_id, 

2444 ) 

2445 ) 

2446 assert state_res.moderation_state.visibility == moderation_pb2.MODERATION_VISIBILITY_SHADOWED 

2447 

2448 # Host still cannot see the request 

2449 with requests_session(host_token) as api: 

2450 with pytest.raises(grpc.RpcError) as e: 

2451 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2452 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2453 

2454 # But the (shadowed) author can still see their own request 

2455 with requests_session(surfer_token) as api: 

2456 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2457 assert res.host_request_id == host_request_id 

2458 

2459 

2460def test_auto_approve_preserves_other_open_flags(db): 

2461 """Auto-approval supersedes only the INITIAL_REVIEW item; other open flags survive and a high-prio flag is added.""" 

2462 moderator, mod_token = generate_user(is_superuser=True) 

2463 surfer, surfer_token = generate_user() 

2464 host, _ = generate_user() 

2465 

2466 state_id = create_test_host_request_with_moderation(surfer_token, host.id) 

2467 

2468 with real_moderation_session(mod_token) as api: 

2469 api.ModerateContent( 

2470 moderation_pb2.ModerateContentReq( 

2471 moderation_state_id=state_id, 

2472 action=moderation_pb2.MODERATION_ACTION_FLAG, 

2473 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

2474 priority=5, 

2475 reason="user reported this", 

2476 ) 

2477 ) 

2478 

2479 with session_scope() as session: 

2480 initial_item = session.execute( 

2481 select(ModerationQueueItem) 

2482 .where(ModerationQueueItem.moderation_state_id == state_id) 

2483 .where(ModerationQueueItem.trigger == ModerationTrigger.initial_review) 

2484 ).scalar_one() 

2485 initial_item_id = initial_item.id 

2486 initial_item.time_created = datetime.now(initial_item.time_created.tzinfo) - timedelta(minutes=10) 

2487 

2488 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2489 config.MODERATION_BOT_USER_ID = moderator.id 

2490 

2491 auto_approve_moderation_queue(empty_pb2.Empty()) 

2492 

2493 with session_scope() as session: 

2494 assert ( 

2495 session.execute(select(ModerationQueueItem).where(ModerationQueueItem.id == initial_item_id)) 

2496 .scalar_one() 

2497 .resolved_by_log_id 

2498 is not None 

2499 ) 

2500 assert ( 

2501 session.execute(select(ModerationState).where(ModerationState.id == state_id)).scalar_one().visibility 

2502 == ModerationVisibility.visible 

2503 ) 

2504 

2505 open_items = ( 

2506 session.execute( 

2507 select(ModerationQueueItem) 

2508 .where(ModerationQueueItem.moderation_state_id == state_id) 

2509 .where(ModerationQueueItem.resolved_by_log_id.is_(None)) 

2510 ) 

2511 .scalars() 

2512 .all() 

2513 ) 

2514 open_by_trigger = {item.trigger: item for item in open_items} 

2515 assert open_by_trigger.keys() == {ModerationTrigger.user_flag, ModerationTrigger.machine_flag} 

2516 assert open_by_trigger[ModerationTrigger.user_flag].priority == 5 

2517 assert open_by_trigger[ModerationTrigger.machine_flag].priority == MODERATION_AUTO_APPROVE_FLAG_PRIORITY 

2518 

2519 

2520# ============================================================================ 

2521# Notification Suppression Tests 

2522# ============================================================================ 

2523 

2524 

2525def test_host_request_message_notifications_suppressed_before_approval( 

2526 db, email_collector: EmailCollector, push_collector: PushCollector, moderator 

2527): 

2528 """ 

2529 Test that notifications are NOT sent for messages in host requests 

2530 that haven't been approved yet. 

2531 """ 

2532 host, host_token = generate_user(complete_profile=True) 

2533 surfer, surfer_token = generate_user(complete_profile=True) 

2534 

2535 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2536 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2537 

2538 # Create host request (it starts in SHADOWED state) 

2539 with requests_session(surfer_token) as api: 

2540 hr_id = api.CreateHostRequest( 

2541 requests_pb2.CreateHostRequestReq( 

2542 host_user_id=host.id, 

2543 from_date=today_plus_2, 

2544 to_date=today_plus_3, 

2545 text=valid_request_text("Initial request message"), 

2546 ) 

2547 ).host_request_id 

2548 

2549 # No notifications should have been sent to the host (request is SHADOWED) 

2550 assert email_collector.count_for_recipient(host.email) == 0 

2551 assert push_collector.count_for_user(host.id) == 0 

2552 

2553 # Send additional messages BEFORE approval - should NOT generate notifications 

2554 with requests_session(surfer_token) as api: 

2555 api.SendHostRequestMessage( 

2556 requests_pb2.SendHostRequestMessageReq( 

2557 host_request_id=hr_id, 

2558 text="Follow-up message 1", 

2559 ) 

2560 ) 

2561 api.SendHostRequestMessage( 

2562 requests_pb2.SendHostRequestMessageReq( 

2563 host_request_id=hr_id, 

2564 text="Follow-up message 2", 

2565 ) 

2566 ) 

2567 

2568 # Host should STILL have no notifications (messages sent while SHADOWED) 

2569 assert email_collector.count_for_recipient(host.email) == 0 

2570 assert push_collector.count_for_user(host.id) == 0 

2571 

2572 # Now approve the request 

2573 moderator.approve_host_request(hr_id) 

2574 email_collector.pop_for_recipient(host.email) 

2575 

2576 # Host should now have 3 notifications (all deferred notifications are delivered on approval): 

2577 # 1. host_request:create (the initial request) 

2578 # 2. host_request:message (Follow-up message 1) 

2579 # 3. host_request:message (Follow-up message 2) 

2580 assert push_collector.count_for_user(host.id) == 3 

2581 push = push_collector.pop_for_user(host.id, last=False) 

2582 assert push.content.title == f"New host request from {surfer.name}" 

2583 

2584 

2585def test_host_request_status_notifications_suppressed_before_approval(db, push_collector: PushCollector, moderator): 

2586 """ 

2587 Test that status change notifications (accept/reject/etc.) are NOT sent 

2588 for host requests that haven't been approved yet. 

2589 

2590 Note: In practice, the host can't even SEE the request to accept/reject it 

2591 when it's SHADOWED. But if they somehow did, we still shouldn't notify. 

2592 """ 

2593 host, host_token = generate_user(complete_profile=True) 

2594 surfer, surfer_token = generate_user(complete_profile=True) 

2595 

2596 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2597 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2598 

2599 # Create host request 

2600 with requests_session(surfer_token) as api: 

2601 hr_id = api.CreateHostRequest( 

2602 requests_pb2.CreateHostRequestReq( 

2603 host_user_id=host.id, 

2604 from_date=today_plus_2, 

2605 to_date=today_plus_3, 

2606 text=valid_request_text(), 

2607 ) 

2608 ).host_request_id 

2609 

2610 # No notifications should have been sent to the host (request is SHADOWED) 

2611 assert push_collector.count_for_user(host.id) == 0 

2612 

2613 # The surfer can cancel their own request even when SHADOWED 

2614 # But this should NOT notify the host since the request isn't approved 

2615 with requests_session(surfer_token) as api: 

2616 api.RespondHostRequest( 

2617 requests_pb2.RespondHostRequestReq( 

2618 host_request_id=hr_id, 

2619 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

2620 text="Actually, never mind", 

2621 ) 

2622 ) 

2623 

2624 # Host should STILL have no notifications (cancel notification suppressed) 

2625 assert push_collector.count_for_user(host.id) == 0 

2626 

2627 

2628def test_host_request_notifications_sent_after_approval( 

2629 db, email_collector: EmailCollector, push_collector: PushCollector, moderator 

2630): 

2631 """ 

2632 Test that after a host request is approved, all notifications work normally. 

2633 """ 

2634 host, host_token = generate_user(complete_profile=True) 

2635 surfer, surfer_token = generate_user(complete_profile=True) 

2636 

2637 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2638 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2639 

2640 # Create and approve host request 

2641 with requests_session(surfer_token) as api: 

2642 hr_id = api.CreateHostRequest( 

2643 requests_pb2.CreateHostRequestReq( 

2644 host_user_id=host.id, 

2645 from_date=today_plus_2, 

2646 to_date=today_plus_3, 

2647 text=valid_request_text(), 

2648 ) 

2649 ).host_request_id 

2650 

2651 moderator.approve_host_request(hr_id) 

2652 

2653 # Host should have received 1 notification (the approval notification) 

2654 email_collector.pop_for_recipient(host.email, last=True) 

2655 push_collector.pop_for_user(host.id, last=True) 

2656 

2657 # Host accepts the request - surfer should be notified 

2658 with requests_session(host_token) as api: 

2659 api.RespondHostRequest( 

2660 requests_pb2.RespondHostRequestReq( 

2661 host_request_id=hr_id, 

2662 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

2663 text="Sure, come on over!", 

2664 ) 

2665 ) 

2666 

2667 # Surfer should have 1 notification (the accept notification) 

2668 email_collector.pop_for_recipient(surfer.email, last=True) 

2669 push = push_collector.pop_for_user(surfer.id, last=True) 

2670 assert push.content.title == f"{host.name} accepted your host request" 

2671 

2672 # Surfer confirms - host should be notified 

2673 with requests_session(surfer_token) as api: 

2674 api.RespondHostRequest( 

2675 requests_pb2.RespondHostRequestReq( 

2676 host_request_id=hr_id, 

2677 status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

2678 text="See you then!", 

2679 ) 

2680 ) 

2681 

2682 # Host should now have received the confirmation notifications 

2683 email_collector.pop_for_recipient(host.email, last=True) 

2684 push = push_collector.pop_for_user(host.id, last=True) 

2685 assert push.content.title == f"{surfer.name} confirmed their host request" 

2686 

2687 

2688def test_group_chat_message_notifications_suppressed_before_approval(db, push_collector: PushCollector, moderator): 

2689 """ 

2690 Test that notifications are NOT sent for messages in group chats 

2691 that haven't been approved yet. 

2692 """ 

2693 user1, token1 = generate_user(complete_profile=True) 

2694 user2, token2 = generate_user(complete_profile=True) 

2695 

2696 # Create a group chat (starts in SHADOWED state) 

2697 with conversations_session(token1) as api: 

2698 res = api.CreateGroupChat( 

2699 conversations_pb2.CreateGroupChatReq( 

2700 recipient_user_ids=[user2.id], 

2701 ) 

2702 ) 

2703 gc_id = res.group_chat_id 

2704 

2705 # Verify initial state 

2706 with session_scope() as session: 

2707 gc = session.execute(select(GroupChat).where(GroupChat.conversation_id == gc_id)).scalar_one() 

2708 assert gc.moderation_state.visibility == ModerationVisibility.shadowed 

2709 

2710 # No notifications should have been sent yet (chat is SHADOWED) 

2711 assert push_collector.count_for_user(user2.id) == 0 

2712 

2713 # Send messages BEFORE approval 

2714 with conversations_session(token1) as api: 

2715 api.SendMessage( 

2716 conversations_pb2.SendMessageReq( 

2717 group_chat_id=gc_id, 

2718 text="Hello before approval", 

2719 ) 

2720 ) 

2721 

2722 # Process the queued notification job 

2723 while process_job(): 

2724 pass 

2725 

2726 # User2 should STILL have no notifications (chat is SHADOWED) 

2727 assert push_collector.count_for_user(user2.id) == 0 

2728 

2729 # Now approve the group chat 

2730 moderator.approve_group_chat(gc_id) 

2731 

2732 # Process the queued notification jobs from approval 

2733 while process_job(): 

2734 pass 

2735 

2736 # Verify moderation state after approval 

2737 with session_scope() as session: 

2738 gc = session.execute(select(GroupChat).where(GroupChat.conversation_id == gc_id)).scalar_one() 

2739 assert gc.moderation_state.visibility == ModerationVisibility.visible 

2740 

2741 # User2 should have received 1 notification for the first message sent before approval 

2742 push = push_collector.pop_for_user(user2.id, last=True) 

2743 assert push.content.title == user1.name 

2744 assert push.content.body == "Hello before approval" 

2745 

2746 # Send a message AFTER approval 

2747 with conversations_session(token1) as api: 

2748 api.SendMessage( 

2749 conversations_pb2.SendMessageReq( 

2750 group_chat_id=gc_id, 

2751 text="Hello after approval", 

2752 ) 

2753 ) 

2754 

2755 # Process the queued notification job 

2756 while process_job(): 

2757 pass 

2758 

2759 # User2 should have received another notification 

2760 assert push_collector.count_for_user(user2.id) == 1 

2761 

2762 

2763def test_event_moderation_state_content(db): 

2764 """Test that event moderation state content includes both title and description""" 

2765 super_user, super_token = generate_user(is_superuser=True) 

2766 user, token = generate_user() 

2767 

2768 with session_scope() as session: 

2769 create_community(session, 0, 2, "Community", [user], [], None) 

2770 

2771 start_time = now() + timedelta(hours=2) 

2772 end_time = start_time + timedelta(hours=3) 

2773 

2774 with events_session(token) as api: 

2775 res = api.CreateEvent( 

2776 events_pb2.CreateEventReq( 

2777 title="My Event Title", 

2778 content="My event description.", 

2779 photo_key=None, 

2780 location=events_pb2.EventLocation( 

2781 address="Near Null Island", 

2782 lat=0.1, 

2783 lng=0.2, 

2784 ), 

2785 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2786 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2787 ) 

2788 ) 

2789 event_id = res.event_id 

2790 

2791 with session_scope() as session: 

2792 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2793 state_id = occurrence.moderation_state_id 

2794 

2795 with real_moderation_session(super_token) as api: 

2796 res = api.GetModerationQueue(moderation_pb2.GetModerationQueueReq()) 

2797 event_items = [ 

2798 item 

2799 for item in res.queue_items 

2800 if item.moderation_state.object_type == moderation_pb2.MODERATION_OBJECT_TYPE_EVENT_OCCURRENCE 

2801 ] 

2802 assert len(event_items) == 1 

2803 assert event_items[0].moderation_state.content == "My Event Title\n\nMy event description." 

2804 

2805 

2806# ============================================================================ 

2807# Tests for SetUserContentVisibility 

2808# ============================================================================ 

2809 

2810 

2811def _get_moderation_state(session, object_type, object_id): 

2812 return session.execute( 

2813 select(ModerationState) 

2814 .where(ModerationState.object_type == object_type) 

2815 .where(ModerationState.object_id == object_id) 

2816 ).scalar_one() 

2817 

2818 

2819def test_SetUserContentVisibility_host_request(db): 

2820 super_user, super_token = generate_user(is_superuser=True) 

2821 surfer, surfer_token = generate_user() 

2822 host, _ = generate_user() 

2823 

2824 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2825 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2826 with requests_session(surfer_token) as api: 

2827 hr_id = api.CreateHostRequest( 

2828 requests_pb2.CreateHostRequestReq( 

2829 host_user_id=host.id, 

2830 from_date=today_plus_2, 

2831 to_date=today_plus_3, 

2832 text=valid_request_text(), 

2833 ) 

2834 ).host_request_id 

2835 

2836 with real_moderation_session(super_token) as api: 

2837 res = api.SetUserContentVisibility( 

2838 moderation_pb2.SetUserContentVisibilityReq( 

2839 user_id=surfer.id, 

2840 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2841 ) 

2842 ) 

2843 # Already shadowed by default — no-op 

2844 assert res.updated_count == 0 

2845 

2846 with real_moderation_session(super_token) as api: 

2847 res = api.SetUserContentVisibility( 

2848 moderation_pb2.SetUserContentVisibilityReq( 

2849 user_id=surfer.id, 

2850 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2851 reason="policy violation", 

2852 ) 

2853 ) 

2854 assert res.updated_count == 1 

2855 

2856 with session_scope() as session: 

2857 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

2858 assert state.visibility == ModerationVisibility.hidden 

2859 

2860 log_entries = ( 

2861 session.execute( 

2862 select(ModerationLog) 

2863 .where(ModerationLog.moderation_state_id == state.id) 

2864 .order_by(ModerationLog.time.asc(), ModerationLog.id.asc()) 

2865 ) 

2866 .scalars() 

2867 .all() 

2868 ) 

2869 # create log + bulk update log 

2870 assert len(log_entries) == 2 

2871 assert log_entries[-1].new_visibility == ModerationVisibility.hidden 

2872 assert log_entries[-1].moderator_user_id == super_user.id 

2873 assert log_entries[-1].reason == "policy violation" 

2874 

2875 

2876def test_SetUserContentVisibility_group_chat(db): 

2877 super_user, super_token = generate_user(is_superuser=True) 

2878 creator, creator_token = generate_user() 

2879 other, _ = generate_user() 

2880 make_friends(creator, other) 

2881 

2882 with conversations_session(creator_token) as api: 

2883 gc_id = api.CreateGroupChat(conversations_pb2.CreateGroupChatReq(recipient_user_ids=[other.id])).group_chat_id 

2884 

2885 with real_moderation_session(super_token) as api: 

2886 api.SetUserContentVisibility( 

2887 moderation_pb2.SetUserContentVisibilityReq( 

2888 user_id=creator.id, 

2889 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2890 ) 

2891 ) 

2892 

2893 with session_scope() as session: 

2894 state = _get_moderation_state(session, ModerationObjectType.group_chat, gc_id) 

2895 assert state.visibility == ModerationVisibility.hidden 

2896 

2897 

2898def test_SetUserContentVisibility_event_occurrence(db): 

2899 super_user, super_token = generate_user(is_superuser=True) 

2900 creator, creator_token = generate_user() 

2901 

2902 with session_scope() as session: 

2903 create_community(session, 0, 2, "Community", [creator], [], None) 

2904 

2905 start_time = now() + timedelta(hours=2) 

2906 end_time = start_time + timedelta(hours=3) 

2907 with events_session(creator_token) as api: 

2908 event_id = api.CreateEvent( 

2909 events_pb2.CreateEventReq( 

2910 title="Event", 

2911 content="Event description.", 

2912 photo_key=None, 

2913 location=events_pb2.EventLocation( 

2914 address="Near Null Island", 

2915 lat=0.1, 

2916 lng=0.2, 

2917 ), 

2918 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2919 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2920 ) 

2921 ).event_id 

2922 

2923 with real_moderation_session(super_token) as api: 

2924 res = api.SetUserContentVisibility( 

2925 moderation_pb2.SetUserContentVisibilityReq( 

2926 user_id=creator.id, 

2927 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2928 ) 

2929 ) 

2930 assert res.updated_count == 1 

2931 

2932 with session_scope() as session: 

2933 state = _get_moderation_state(session, ModerationObjectType.event_occurrence, event_id) 

2934 assert state.visibility == ModerationVisibility.hidden 

2935 

2936 

2937def test_SetUserContentVisibility_friend_request(db): 

2938 super_user, super_token = generate_user(is_superuser=True) 

2939 sender, sender_token = generate_user() 

2940 recipient, _ = generate_user() 

2941 

2942 with api_session(sender_token) as api: 

2943 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=recipient.id)) 

2944 

2945 with session_scope() as session: 

2946 fr_id = session.execute( 

2947 select(FriendRelationship.id).where(FriendRelationship.from_user_id == sender.id) 

2948 ).scalar_one() 

2949 

2950 with real_moderation_session(super_token) as api: 

2951 res = api.SetUserContentVisibility( 

2952 moderation_pb2.SetUserContentVisibilityReq( 

2953 user_id=sender.id, 

2954 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2955 ) 

2956 ) 

2957 assert res.updated_count == 1 

2958 

2959 with session_scope() as session: 

2960 state = _get_moderation_state(session, ModerationObjectType.friend_request, fr_id) 

2961 assert state.visibility == ModerationVisibility.hidden 

2962 

2963 

2964def test_SetUserContentVisibility_round_trip(db): 

2965 super_user, super_token = generate_user(is_superuser=True) 

2966 surfer, surfer_token = generate_user() 

2967 host, _ = generate_user() 

2968 

2969 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

2970 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

2971 with requests_session(surfer_token) as api: 

2972 hr_id = api.CreateHostRequest( 

2973 requests_pb2.CreateHostRequestReq( 

2974 host_user_id=host.id, 

2975 from_date=today_plus_2, 

2976 to_date=today_plus_3, 

2977 text=valid_request_text(), 

2978 ) 

2979 ).host_request_id 

2980 

2981 with real_moderation_session(super_token) as api: 

2982 api.SetUserContentVisibility( 

2983 moderation_pb2.SetUserContentVisibilityReq( 

2984 user_id=surfer.id, 

2985 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2986 reason="first", 

2987 ) 

2988 ) 

2989 api.SetUserContentVisibility( 

2990 moderation_pb2.SetUserContentVisibilityReq( 

2991 user_id=surfer.id, 

2992 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2993 reason="second", 

2994 ) 

2995 ) 

2996 

2997 with session_scope() as session: 

2998 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

2999 assert state.visibility == ModerationVisibility.visible 

3000 

3001 bulk_log_entries = ( 

3002 session.execute( 

3003 select(ModerationLog) 

3004 .where(ModerationLog.moderation_state_id == state.id) 

3005 .where(ModerationLog.reason.in_(("first", "second"))) 

3006 .order_by(ModerationLog.time.asc(), ModerationLog.id.asc()) 

3007 ) 

3008 .scalars() 

3009 .all() 

3010 ) 

3011 assert [entry.new_visibility for entry in bulk_log_entries] == [ 

3012 ModerationVisibility.hidden, 

3013 ModerationVisibility.visible, 

3014 ] 

3015 

3016 

3017def test_SetUserContentVisibility_resolves_queue_items(db): 

3018 super_user, super_token = generate_user(is_superuser=True) 

3019 surfer, surfer_token = generate_user() 

3020 host, _ = generate_user() 

3021 

3022 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

3023 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

3024 with requests_session(surfer_token) as api: 

3025 hr_id = api.CreateHostRequest( 

3026 requests_pb2.CreateHostRequestReq( 

3027 host_user_id=host.id, 

3028 from_date=today_plus_2, 

3029 to_date=today_plus_3, 

3030 text=valid_request_text(), 

3031 ) 

3032 ).host_request_id 

3033 

3034 with session_scope() as session: 

3035 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

3036 queue_item = session.execute( 

3037 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id) 

3038 ).scalar_one() 

3039 assert queue_item.resolved_by_log_id is None 

3040 

3041 with real_moderation_session(super_token) as api: 

3042 api.SetUserContentVisibility( 

3043 moderation_pb2.SetUserContentVisibilityReq( 

3044 user_id=surfer.id, 

3045 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3046 ) 

3047 ) 

3048 

3049 with session_scope() as session: 

3050 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

3051 queue_item = session.execute( 

3052 select(ModerationQueueItem).where(ModerationQueueItem.moderation_state_id == state.id) 

3053 ).scalar_one() 

3054 assert queue_item.resolved_by_log_id is not None 

3055 

3056 

3057def test_SetUserContentVisibility_noop_when_matches(db): 

3058 super_user, super_token = generate_user(is_superuser=True) 

3059 surfer, surfer_token = generate_user() 

3060 host, _ = generate_user() 

3061 

3062 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

3063 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

3064 with requests_session(surfer_token) as api: 

3065 api.CreateHostRequest( 

3066 requests_pb2.CreateHostRequestReq( 

3067 host_user_id=host.id, 

3068 from_date=today_plus_2, 

3069 to_date=today_plus_3, 

3070 text=valid_request_text(), 

3071 ) 

3072 ) 

3073 

3074 with session_scope() as session: 

3075 log_count_before = len(session.execute(select(ModerationLog)).scalars().all()) 

3076 

3077 with real_moderation_session(super_token) as api: 

3078 res = api.SetUserContentVisibility( 

3079 moderation_pb2.SetUserContentVisibilityReq( 

3080 user_id=surfer.id, 

3081 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3082 ) 

3083 ) 

3084 assert res.updated_count == 0 

3085 

3086 with session_scope() as session: 

3087 log_count_after = len(session.execute(select(ModerationLog)).scalars().all()) 

3088 assert log_count_after == log_count_before 

3089 

3090 

3091def test_SetUserContentVisibility_unspecified_rejected(db): 

3092 super_user, super_token = generate_user(is_superuser=True) 

3093 target, _ = generate_user() 

3094 

3095 with real_moderation_session(super_token) as api: 

3096 with pytest.raises(grpc.RpcError) as e: 

3097 api.SetUserContentVisibility( 

3098 moderation_pb2.SetUserContentVisibilityReq( 

3099 user_id=target.id, 

3100 visibility=moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED, 

3101 ) 

3102 ) 

3103 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

3104 

3105 

3106def test_SetUserContentVisibility_non_admin_rejected(db): 

3107 normal_user, normal_token = generate_user() 

3108 target, _ = generate_user() 

3109 

3110 with real_moderation_session(normal_token) as api: 

3111 with pytest.raises(grpc.RpcError) as e: 

3112 api.SetUserContentVisibility( 

3113 moderation_pb2.SetUserContentVisibilityReq( 

3114 user_id=target.id, 

3115 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3116 ) 

3117 ) 

3118 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

3119 

3120 

3121def test_SetUserContentVisibility_writes_admin_action(db): 

3122 super_user, super_token = generate_user(is_superuser=True) 

3123 surfer, surfer_token = generate_user() 

3124 host, _ = generate_user() 

3125 

3126 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

3127 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

3128 with requests_session(surfer_token) as api: 

3129 api.CreateHostRequest( 

3130 requests_pb2.CreateHostRequestReq( 

3131 host_user_id=host.id, 

3132 from_date=today_plus_2, 

3133 to_date=today_plus_3, 

3134 text=valid_request_text(), 

3135 ) 

3136 ) 

3137 

3138 with real_moderation_session(super_token) as api: 

3139 api.SetUserContentVisibility( 

3140 moderation_pb2.SetUserContentVisibilityReq( 

3141 user_id=surfer.id, 

3142 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3143 reason="bulk hide", 

3144 ) 

3145 ) 

3146 

3147 with session_scope() as session: 

3148 actions = ( 

3149 session.execute( 

3150 select(AdminAction) 

3151 .where(AdminAction.target_user_id == surfer.id) 

3152 .where(AdminAction.action_type == "set_user_content_visibility") 

3153 ) 

3154 .scalars() 

3155 .all() 

3156 ) 

3157 assert len(actions) == 1 

3158 assert actions[0].admin_user_id == super_user.id 

3159 assert actions[0].tag == "hidden" 

3160 assert actions[0].note == "bulk hide" 

3161 

3162 

3163def test_SetUserContentVisibility_only_touches_target(db): 

3164 super_user, super_token = generate_user(is_superuser=True) 

3165 target, target_token = generate_user() 

3166 other, other_token = generate_user() 

3167 host, _ = generate_user() 

3168 

3169 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

3170 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

3171 

3172 with requests_session(target_token) as api: 

3173 target_hr_id = api.CreateHostRequest( 

3174 requests_pb2.CreateHostRequestReq( 

3175 host_user_id=host.id, 

3176 from_date=today_plus_2, 

3177 to_date=today_plus_3, 

3178 text=valid_request_text(), 

3179 ) 

3180 ).host_request_id 

3181 

3182 with requests_session(other_token) as api: 

3183 other_hr_id = api.CreateHostRequest( 

3184 requests_pb2.CreateHostRequestReq( 

3185 host_user_id=host.id, 

3186 from_date=today_plus_2, 

3187 to_date=today_plus_3, 

3188 text=valid_request_text(), 

3189 ) 

3190 ).host_request_id 

3191 

3192 with real_moderation_session(super_token) as api: 

3193 res = api.SetUserContentVisibility( 

3194 moderation_pb2.SetUserContentVisibilityReq( 

3195 user_id=target.id, 

3196 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3197 ) 

3198 ) 

3199 assert res.updated_count == 1 

3200 

3201 with session_scope() as session: 

3202 target_state = _get_moderation_state(session, ModerationObjectType.host_request, target_hr_id) 

3203 other_state = _get_moderation_state(session, ModerationObjectType.host_request, other_hr_id) 

3204 assert target_state.visibility == ModerationVisibility.hidden 

3205 assert other_state.visibility == ModerationVisibility.shadowed 

3206 

3207 

3208def test_SetUserContentVisibility_user_not_found(db): 

3209 super_user, super_token = generate_user(is_superuser=True) 

3210 

3211 with real_moderation_session(super_token) as api: 

3212 with pytest.raises(grpc.RpcError) as e: 

3213 api.SetUserContentVisibility( 

3214 moderation_pb2.SetUserContentVisibilityReq( 

3215 user_id=999999, 

3216 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3217 ) 

3218 ) 

3219 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

3220 

3221 

3222def test_SetUserContentVisibility_from_visibility_filter(db): 

3223 super_user, super_token = generate_user(is_superuser=True) 

3224 surfer, surfer_token = generate_user() 

3225 host, _ = generate_user() 

3226 

3227 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

3228 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

3229 with requests_session(surfer_token) as api: 

3230 hr_id = api.CreateHostRequest( 

3231 requests_pb2.CreateHostRequestReq( 

3232 host_user_id=host.id, 

3233 from_date=today_plus_2, 

3234 to_date=today_plus_3, 

3235 text=valid_request_text(), 

3236 ) 

3237 ).host_request_id 

3238 

3239 with real_moderation_session(super_token) as api: 

3240 api.SetUserContentVisibility( 

3241 moderation_pb2.SetUserContentVisibilityReq( 

3242 user_id=surfer.id, 

3243 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3244 ) 

3245 ) 

3246 

3247 with real_moderation_session(super_token) as api: 

3248 res = api.SetUserContentVisibility( 

3249 moderation_pb2.SetUserContentVisibilityReq( 

3250 user_id=surfer.id, 

3251 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3252 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_SHADOWED], 

3253 ) 

3254 ) 

3255 assert res.updated_count == 0 

3256 

3257 with session_scope() as session: 

3258 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

3259 assert state.visibility == ModerationVisibility.visible 

3260 

3261 with real_moderation_session(super_token) as api: 

3262 res = api.SetUserContentVisibility( 

3263 moderation_pb2.SetUserContentVisibilityReq( 

3264 user_id=surfer.id, 

3265 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3266 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_VISIBLE], 

3267 ) 

3268 ) 

3269 assert res.updated_count == 1 

3270 

3271 with session_scope() as session: 

3272 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

3273 assert state.visibility == ModerationVisibility.shadowed 

3274 

3275 

3276def test_SetUserContentVisibility_from_visibility_multi(db): 

3277 super_user, super_token = generate_user(is_superuser=True) 

3278 surfer, surfer_token = generate_user() 

3279 host1, _ = generate_user() 

3280 host2, _ = generate_user() 

3281 

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 hr1_id = api.CreateHostRequest( 

3286 requests_pb2.CreateHostRequestReq( 

3287 host_user_id=host1.id, 

3288 from_date=today_plus_2, 

3289 to_date=today_plus_3, 

3290 text=valid_request_text(), 

3291 ) 

3292 ).host_request_id 

3293 hr2_id = api.CreateHostRequest( 

3294 requests_pb2.CreateHostRequestReq( 

3295 host_user_id=host2.id, 

3296 from_date=today_plus_2, 

3297 to_date=today_plus_3, 

3298 text=valid_request_text(), 

3299 ) 

3300 ).host_request_id 

3301 

3302 with session_scope() as session: 

3303 state1_id = _get_moderation_state(session, ModerationObjectType.host_request, hr1_id).id 

3304 

3305 with real_moderation_session(super_token) as api: 

3306 api.ModerateContent( 

3307 moderation_pb2.ModerateContentReq( 

3308 moderation_state_id=state1_id, 

3309 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

3310 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3311 ) 

3312 ) 

3313 

3314 with real_moderation_session(super_token) as api: 

3315 res = api.SetUserContentVisibility( 

3316 moderation_pb2.SetUserContentVisibilityReq( 

3317 user_id=surfer.id, 

3318 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3319 from_visibility=[ 

3320 moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3321 moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3322 ], 

3323 ) 

3324 ) 

3325 assert res.updated_count == 2 

3326 

3327 with session_scope() as session: 

3328 state1 = _get_moderation_state(session, ModerationObjectType.host_request, hr1_id) 

3329 state2 = _get_moderation_state(session, ModerationObjectType.host_request, hr2_id) 

3330 assert state1.visibility == ModerationVisibility.hidden 

3331 assert state2.visibility == ModerationVisibility.hidden 

3332 

3333 

3334def test_SetUserContentVisibility_from_visibility_empty_is_any(db): 

3335 super_user, super_token = generate_user(is_superuser=True) 

3336 surfer, surfer_token = generate_user() 

3337 host, _ = generate_user() 

3338 

3339 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

3340 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

3341 with requests_session(surfer_token) as api: 

3342 hr_id = api.CreateHostRequest( 

3343 requests_pb2.CreateHostRequestReq( 

3344 host_user_id=host.id, 

3345 from_date=today_plus_2, 

3346 to_date=today_plus_3, 

3347 text=valid_request_text(), 

3348 ) 

3349 ).host_request_id 

3350 

3351 with real_moderation_session(super_token) as api: 

3352 res = api.SetUserContentVisibility( 

3353 moderation_pb2.SetUserContentVisibilityReq( 

3354 user_id=surfer.id, 

3355 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3356 from_visibility=[], 

3357 ) 

3358 ) 

3359 assert res.updated_count == 1 

3360 

3361 with session_scope() as session: 

3362 state = _get_moderation_state(session, ModerationObjectType.host_request, hr_id) 

3363 assert state.visibility == ModerationVisibility.hidden 

3364 

3365 

3366def test_SetUserContentVisibility_from_visibility_unspecified_rejected(db): 

3367 super_user, super_token = generate_user(is_superuser=True) 

3368 target, _ = generate_user() 

3369 

3370 with real_moderation_session(super_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 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED], 

3377 ) 

3378 ) 

3379 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

3380 

3381 

3382def test_ListModerationStates_empty(db): 

3383 super_user, super_token = generate_user(is_superuser=True) 

3384 

3385 with real_moderation_session(super_token) as api: 

3386 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq()) 

3387 assert len(res.moderation_states) == 0 

3388 assert res.next_page_token == "" 

3389 

3390 

3391def test_ListModerationStates_returns_states_chronologically(db): 

3392 super_user, super_token = generate_user(is_superuser=True) 

3393 surfer, surfer_token = generate_user() 

3394 host, _ = generate_user() 

3395 

3396 state1_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3397 state2_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3398 state3_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3399 

3400 with real_moderation_session(super_token) as api: 

3401 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq()) 

3402 assert [s.moderation_state_id for s in res.moderation_states] == [state1_id, state2_id, state3_id] 

3403 

3404 res_newest = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(newest_first=True)) 

3405 assert [s.moderation_state_id for s in res_newest.moderation_states] == [state3_id, state2_id, state1_id] 

3406 

3407 

3408def test_ListModerationStates_filter_by_author(db): 

3409 super_user, super_token = generate_user(is_superuser=True) 

3410 surfer1, surfer1_token = generate_user() 

3411 surfer2, surfer2_token = generate_user() 

3412 host, _ = generate_user() 

3413 

3414 state1_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3415 state2_id = create_test_host_request_with_moderation(surfer2_token, host.id) 

3416 state3_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3417 

3418 with real_moderation_session(super_token) as api: 

3419 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer1.id)) 

3420 assert {s.moderation_state_id for s in res.moderation_states} == {state1_id, state3_id} 

3421 

3422 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(author_user_id=surfer2.id)) 

3423 assert [s.moderation_state_id for s in res.moderation_states] == [state2_id] 

3424 

3425 

3426def test_ListModerationStates_pagination(db): 

3427 super_user, super_token = generate_user(is_superuser=True) 

3428 surfer, surfer_token = generate_user() 

3429 host, _ = generate_user() 

3430 

3431 state_ids = [create_test_host_request_with_moderation(surfer_token, host.id) for _ in range(3)] 

3432 

3433 with real_moderation_session(super_token) as api: 

3434 res = api.ListModerationStates(moderation_pb2.ListModerationStatesReq(page_size=2)) 

3435 assert [s.moderation_state_id for s in res.moderation_states] == state_ids[:2] 

3436 assert res.next_page_token != "" 

3437 

3438 res2 = api.ListModerationStates( 

3439 moderation_pb2.ListModerationStatesReq(page_size=2, page_token=res.next_page_token) 

3440 ) 

3441 assert [s.moderation_state_id for s in res2.moderation_states] == [state_ids[2]] 

3442 assert res2.next_page_token == ""