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

1436 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +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 api_pb2, conversations_pb2, events_pb2, moderation_pb2, notifications_pb2, requests_pb2 

34from couchers.utils import Timestamp_from_datetime, now, today 

35from tests.fixtures.db import generate_user, make_friends 

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

37from tests.fixtures.sessions import ( 

38 api_session, 

39 conversations_session, 

40 events_session, 

41 notifications_session, 

42 real_moderation_session, 

43 requests_session, 

44) 

45from tests.test_communities import create_community 

46from tests.test_requests import valid_request_text 

47 

48 

49@pytest.fixture(autouse=True) 

50def _(testconfig): 

51 pass 

52 

53 

54def create_test_host_request_with_moderation(surfer_token, host_user_id): 

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

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

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

58 

59 with requests_session(surfer_token) as api: 

60 hr_id = api.CreateHostRequest( 

61 requests_pb2.CreateHostRequestReq( 

62 host_user_id=host_user_id, 

63 from_date=today_plus_2, 

64 to_date=today_plus_3, 

65 text=valid_request_text(), 

66 ) 

67 ).host_request_id 

68 

69 with session_scope() as session: 

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

71 return hr.moderation_state_id 

72 

73 

74# ============================================================================ 

75# Tests for moderation helper functions 

76# ============================================================================ 

77 

78 

79def test_create_moderation(db): 

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

81 user, _ = generate_user() 

82 

83 with session_scope() as session: 

84 # Create a moderation state 

85 moderation_state = create_moderation( 

86 session=session, 

87 object_type=ModerationObjectType.host_request, 

88 object_id=123, 

89 creator_user_id=user.id, 

90 ) 

91 

92 assert moderation_state.object_type == ModerationObjectType.host_request 

93 assert moderation_state.object_id == 123 

94 assert moderation_state.visibility == ModerationVisibility.shadowed 

95 

96 # Check that log entry was created 

97 log_entries = ( 

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

99 .scalars() 

100 .all() 

101 ) 

102 

103 assert len(log_entries) == 1 

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

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

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

107 

108 

109def test_add_to_moderation_queue(db): 

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

111 super_user, super_token = generate_user(is_superuser=True) 

112 user1, token1 = generate_user() 

113 user2, _ = generate_user() 

114 

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

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

117 

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

119 with requests_session(token1) as api: 

120 host_request_id = api.CreateHostRequest( 

121 requests_pb2.CreateHostRequestReq( 

122 host_user_id=user2.id, 

123 from_date=today_plus_2, 

124 to_date=today_plus_3, 

125 text=valid_request_text(), 

126 ) 

127 ).host_request_id 

128 

129 # Get the moderation state ID 

130 state_id = None 

131 with session_scope() as session: 

132 host_request = session.execute( 

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

134 ).scalar_one() 

135 state_id = host_request.moderation_state_id 

136 

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

138 with real_moderation_session(super_token) as api: 

139 api.ModerateContent( 

140 moderation_pb2.ModerateContentReq( 

141 moderation_state_id=state_id, 

142 action=moderation_pb2.MODERATION_ACTION_FLAG, 

143 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

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

145 ) 

146 ) 

147 

148 with session_scope() as session: 

149 flag = session.execute( 

150 select(ModerationQueueItem) 

151 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

153 ).scalar_one() 

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

155 assert flag.resolved_by_log_id is None 

156 

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

158 flag_log = session.execute( 

159 select(ModerationLog) 

160 .where(ModerationLog.moderation_state_id == state_id) 

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

162 ).scalar_one() 

163 assert flag_log.queue_item_id == flag.id 

164 

165 

166def test_moderate_content(db): 

167 """Test moderating content via API""" 

168 super_user, super_token = generate_user(is_superuser=True) 

169 user, token = generate_user() 

170 host, _ = generate_user() 

171 

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

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

174 

175 # Create a real host request 

176 state_id = None 

177 with requests_session(token) as api: 

178 hr_id = api.CreateHostRequest( 

179 requests_pb2.CreateHostRequestReq( 

180 host_user_id=host.id, 

181 from_date=today_plus_2, 

182 to_date=today_plus_3, 

183 text=valid_request_text(), 

184 ) 

185 ).host_request_id 

186 

187 with session_scope() as session: 

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

189 state_id = hr.moderation_state_id 

190 

191 # Moderate the content via API 

192 with real_moderation_session(super_token) as api: 

193 res = api.ModerateContent( 

194 moderation_pb2.ModerateContentReq( 

195 moderation_state_id=state_id, 

196 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

197 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

198 reason="Content looks good", 

199 ) 

200 ) 

201 

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

203 

204 # Check that state was updated in database 

205 with session_scope() as session: 

206 updated_state = session.get_one(ModerationState, state_id) 

207 assert updated_state.visibility == ModerationVisibility.visible 

208 

209 # Check that log entry was created 

210 log_entries = ( 

211 session.execute( 

212 select(ModerationLog) 

213 .where(ModerationLog.moderation_state_id == state_id) 

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

215 ) 

216 .scalars() 

217 .all() 

218 ) 

219 

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

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

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

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

224 

225 

226def test_resolve_queue_item(db): 

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

228 user1, token1 = generate_user() 

229 user2, _ = generate_user() 

230 moderator, moderator_token = generate_user(is_superuser=True) 

231 

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

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

234 

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

236 with requests_session(token1) as api: 

237 host_request_id = api.CreateHostRequest( 

238 requests_pb2.CreateHostRequestReq( 

239 host_user_id=user2.id, 

240 from_date=today_plus_2, 

241 to_date=today_plus_3, 

242 text=valid_request_text(), 

243 ) 

244 ).host_request_id 

245 

246 state_id = None 

247 with session_scope() as session: 

248 # Get the host request and its moderation state 

249 host_request = session.execute( 

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

251 ).scalar_one() 

252 state_id = host_request.moderation_state_id 

253 

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

255 queue_item = session.execute( 

256 select(ModerationQueueItem) 

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

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

259 ).scalar_one() 

260 

261 assert queue_item.resolved_by_log_id is None 

262 

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

264 with real_moderation_session(moderator_token) as api: 

265 api.ModerateContent( 

266 moderation_pb2.ModerateContentReq( 

267 moderation_state_id=state_id, 

268 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

269 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

270 reason="Approved after review", 

271 clear_flags=True, 

272 ) 

273 ) 

274 

275 # Check that queue item was resolved 

276 with session_scope() as session: 

277 queue_item = session.execute( 

278 select(ModerationQueueItem) 

279 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

281 ).scalar_one() 

282 assert queue_item.resolved_by_log_id is not None 

283 

284 

285def test_approve_content_via_api(db): 

286 """Test approving content via ModerateContent API""" 

287 user1, token1 = generate_user() 

288 user2, _ = generate_user() 

289 moderator, moderator_token = generate_user(is_superuser=True) 

290 

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

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

293 

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

295 with requests_session(token1) as api: 

296 host_request_id = api.CreateHostRequest( 

297 requests_pb2.CreateHostRequestReq( 

298 host_user_id=user2.id, 

299 from_date=today_plus_2, 

300 to_date=today_plus_3, 

301 text=valid_request_text(), 

302 ) 

303 ).host_request_id 

304 

305 state_id = None 

306 with session_scope() as session: 

307 # Get the host request and its moderation state 

308 host_request = session.execute( 

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

310 ).scalar_one() 

311 state_id = host_request.moderation_state_id 

312 

313 # Approve via API 

314 with real_moderation_session(moderator_token) as api: 

315 api.ModerateContent( 

316 moderation_pb2.ModerateContentReq( 

317 moderation_state_id=state_id, 

318 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

319 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

320 reason="Quick approval", 

321 ) 

322 ) 

323 

324 # Check that state was updated to VISIBLE 

325 with session_scope() as session: 

326 updated_state = session.get_one(ModerationState, state_id) 

327 assert updated_state.visibility == ModerationVisibility.visible 

328 

329 # Check log entry 

330 log_entry = session.execute( 

331 select(ModerationLog) 

332 .where(ModerationLog.moderation_state_id == state_id) 

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

334 ).scalar_one() 

335 

336 assert log_entry.moderator_user_id == moderator.id 

337 assert log_entry.reason == "Quick approval" 

338 

339 

340# ============================================================================ 

341# Tests for host request moderation integration 

342# ============================================================================ 

343 

344 

345def test_create_host_request_creates_moderation_state(db): 

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

347 user1, token1 = generate_user() 

348 user2, token2 = generate_user() 

349 

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

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

352 

353 with requests_session(token1) as api: 

354 host_request_id = api.CreateHostRequest( 

355 requests_pb2.CreateHostRequestReq( 

356 host_user_id=user2.id, 

357 from_date=today_plus_2, 

358 to_date=today_plus_3, 

359 text=valid_request_text(), 

360 ) 

361 ).host_request_id 

362 

363 with session_scope() as session: 

364 # Check that host request has a moderation state 

365 host_request = session.execute( 

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

367 ).scalar_one() 

368 

369 # Check moderation state properties 

370 moderation_state = session.execute( 

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

372 ).scalar_one() 

373 

374 assert moderation_state.object_type == ModerationObjectType.host_request 

375 assert moderation_state.object_id == host_request_id 

376 assert moderation_state.visibility == ModerationVisibility.shadowed 

377 

378 # Check that it was added to moderation queue 

379 queue_items = ( 

380 session.execute( 

381 select(ModerationQueueItem) 

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

383 .where(ModerationQueueItem.resolved_by_log_id == None) 

384 ) 

385 .scalars() 

386 .all() 

387 ) 

388 

389 assert len(queue_items) == 1 

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

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

392 

393 

394def test_host_request_no_notification_before_approval(db, push_collector: PushCollector): 

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

396 user1, token1 = generate_user() 

397 user2, token2 = generate_user() 

398 

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

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

401 

402 with requests_session(token1) as api: 

403 host_request_id = api.CreateHostRequest( 

404 requests_pb2.CreateHostRequestReq( 

405 host_user_id=user2.id, 

406 from_date=today_plus_2, 

407 to_date=today_plus_3, 

408 text=valid_request_text(), 

409 ) 

410 ).host_request_id 

411 

412 # Process all jobs (including the notification job) 

413 process_jobs() 

414 

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

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

417 

418 

419def test_shadowed_notification_not_in_list_notifications(db): 

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

421 user1, token1 = generate_user() 

422 user2, token2 = generate_user() 

423 

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

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

426 

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

428 with requests_session(token1) as api: 

429 host_request_id = api.CreateHostRequest( 

430 requests_pb2.CreateHostRequestReq( 

431 host_user_id=user2.id, 

432 from_date=today_plus_2, 

433 to_date=today_plus_3, 

434 text=valid_request_text(), 

435 ) 

436 ).host_request_id 

437 

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

439 with notifications_session(token2) as api: 

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

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

442 assert len(res.notifications) == 0 

443 

444 

445def test_notification_visible_after_approval(db): 

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

447 user1, token1 = generate_user() 

448 user2, token2 = generate_user() 

449 mod, mod_token = generate_user(is_superuser=True) 

450 

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

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

453 

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

455 with requests_session(token1) as api: 

456 host_request_id = api.CreateHostRequest( 

457 requests_pb2.CreateHostRequestReq( 

458 host_user_id=user2.id, 

459 from_date=today_plus_2, 

460 to_date=today_plus_3, 

461 text=valid_request_text(), 

462 ) 

463 ).host_request_id 

464 

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

466 with notifications_session(token2) as api: 

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

468 assert len(res.notifications) == 0 

469 

470 # Get the moderation state ID and approve 

471 with session_scope() as session: 

472 host_request = session.execute( 

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

474 ).scalar_one() 

475 state_id = host_request.moderation_state_id 

476 

477 with real_moderation_session(mod_token) as api: 

478 api.ModerateContent( 

479 moderation_pb2.ModerateContentReq( 

480 moderation_state_id=state_id, 

481 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

482 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

483 reason="Looks good", 

484 ) 

485 ) 

486 

487 # Now host SHOULD see the notification 

488 with notifications_session(token2) as api: 

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

490 assert len(res.notifications) == 1 

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

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

493 

494 

495def test_shadowed_host_request_visible_to_author_only(db): 

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

497 user1, token1 = generate_user() 

498 user2, token2 = generate_user() 

499 

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

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

502 

503 with requests_session(token1) as api: 

504 host_request_id = api.CreateHostRequest( 

505 requests_pb2.CreateHostRequestReq( 

506 host_user_id=user2.id, 

507 from_date=today_plus_2, 

508 to_date=today_plus_3, 

509 text=valid_request_text(), 

510 ) 

511 ).host_request_id 

512 

513 # Surfer (author) can see it with GetHostRequest 

514 with requests_session(token1) as api: 

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

516 assert res.host_request_id == host_request_id 

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

518 

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

520 with requests_session(token2) as api: 

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

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

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

524 

525 

526def test_unlisted_host_request_not_in_lists(db): 

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

528 user1, token1 = generate_user() 

529 user2, token2 = generate_user() 

530 

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

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

533 

534 with requests_session(token1) as api: 

535 host_request_id = api.CreateHostRequest( 

536 requests_pb2.CreateHostRequestReq( 

537 host_user_id=user2.id, 

538 from_date=today_plus_2, 

539 to_date=today_plus_3, 

540 text=valid_request_text(), 

541 ) 

542 ).host_request_id 

543 

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

545 with requests_session(token1) as api: 

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

547 assert len(res.host_requests) == 1 

548 

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

550 with requests_session(token2) as api: 

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

552 assert len(res.host_requests) == 0 

553 

554 

555def test_approved_host_request_in_lists_and_notifications(db, push_collector: PushCollector): 

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

557 user1, token1 = generate_user() 

558 user2, token2 = generate_user() 

559 mod, mod_token = generate_user(is_superuser=True) 

560 

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

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

563 

564 with requests_session(token1) as api: 

565 host_request_id = api.CreateHostRequest( 

566 requests_pb2.CreateHostRequestReq( 

567 host_user_id=user2.id, 

568 from_date=today_plus_2, 

569 to_date=today_plus_3, 

570 text=valid_request_text(), 

571 ) 

572 ).host_request_id 

573 

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

575 process_jobs() 

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

577 

578 # Get the moderation state ID 

579 state_id = None 

580 with session_scope() as session: 

581 host_request = session.execute( 

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

583 ).scalar_one() 

584 state_id = host_request.moderation_state_id 

585 

586 # Approve the host request via API 

587 with real_moderation_session(mod_token) as api: 

588 api.ModerateContent( 

589 moderation_pb2.ModerateContentReq( 

590 moderation_state_id=state_id, 

591 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

592 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

593 reason="Looks good", 

594 ) 

595 ) 

596 

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

598 process_jobs() 

599 

600 # Now surfer SHOULD see it in their sent list 

601 with requests_session(token1) as api: 

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

603 assert len(res.host_requests) == 1 

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

605 

606 # Host SHOULD see it in their received list 

607 with requests_session(token2) as api: 

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

609 assert len(res.host_requests) == 1 

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

611 

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

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

614 

615 

616def test_hidden_host_request_invisible_to_all(db): 

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

618 user1, token1 = generate_user() 

619 user2, token2 = generate_user() 

620 user3, token3 = generate_user() # Third party 

621 moderator, moderator_token = generate_user(is_superuser=True) 

622 

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

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

625 

626 with requests_session(token1) as api: 

627 host_request_id = api.CreateHostRequest( 

628 requests_pb2.CreateHostRequestReq( 

629 host_user_id=user2.id, 

630 from_date=today_plus_2, 

631 to_date=today_plus_3, 

632 text=valid_request_text(), 

633 ) 

634 ).host_request_id 

635 

636 # Get the moderation state ID 

637 state_id = None 

638 with session_scope() as session: 

639 host_request = session.execute( 

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

641 ).scalar_one() 

642 state_id = host_request.moderation_state_id 

643 

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

645 with real_moderation_session(moderator_token) as api: 

646 api.ModerateContent( 

647 moderation_pb2.ModerateContentReq( 

648 moderation_state_id=state_id, 

649 action=moderation_pb2.MODERATION_ACTION_HIDE, 

650 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

651 reason="Spam content", 

652 ) 

653 ) 

654 

655 # Surfer can't see it with GetHostRequest 

656 with requests_session(token1) as api: 

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

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

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

660 

661 # Host can't see it with GetHostRequest 

662 with requests_session(token2) as api: 

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

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

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

666 

667 # Third party definitely can't see it 

668 with requests_session(token3) as api: 

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

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

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

672 

673 # Not in any lists 

674 with requests_session(token1) as api: 

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

676 assert len(res.host_requests) == 0 

677 

678 with requests_session(token2) as api: 

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

680 assert len(res.host_requests) == 0 

681 

682 

683def test_multiple_host_requests_listing_visibility(db): 

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

685 user1, token1 = generate_user() 

686 user2, token2 = generate_user() 

687 moderator, moderator_token = generate_user(is_superuser=True) 

688 

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

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

691 

692 # Create 3 host requests 

693 host_request_ids = [] 

694 state_ids = [] 

695 with requests_session(token1) as api: 

696 for i in range(3): 

697 hr_id = api.CreateHostRequest( 

698 requests_pb2.CreateHostRequestReq( 

699 host_user_id=user2.id, 

700 from_date=today_plus_2, 

701 to_date=today_plus_3, 

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

703 ) 

704 ).host_request_id 

705 host_request_ids.append(hr_id) 

706 

707 # Get state IDs 

708 with session_scope() as session: 

709 for hr_id in host_request_ids: 

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

711 state_ids.append(host_request.moderation_state_id) 

712 

713 # Approve the first one via API 

714 with real_moderation_session(moderator_token) as api: 

715 api.ModerateContent( 

716 moderation_pb2.ModerateContentReq( 

717 moderation_state_id=state_ids[0], 

718 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

719 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

720 reason="Approved", 

721 ) 

722 ) 

723 

724 # Hide the third 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[2], 

729 action=moderation_pb2.MODERATION_ACTION_HIDE, 

730 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

731 reason="Spam", 

732 ) 

733 ) 

734 

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

736 with requests_session(token1) as api: 

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

738 assert len(res.host_requests) == 2 

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

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

741 

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

743 with requests_session(token2) as api: 

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

745 assert len(res.host_requests) == 1 

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

747 

748 

749def test_moderation_log_tracking(db): 

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

751 user, user_token = generate_user() 

752 host, _ = generate_user() 

753 moderator1, moderator1_token = generate_user(is_superuser=True) 

754 moderator2, moderator2_token = generate_user(is_superuser=True) 

755 

756 # Create a real host request 

757 state_id = create_test_host_request_with_moderation(user_token, host.id) 

758 

759 # Perform several moderation actions via API 

760 with real_moderation_session(moderator1_token) as api: 

761 api.ModerateContent( 

762 moderation_pb2.ModerateContentReq( 

763 moderation_state_id=state_id, 

764 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

765 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

766 reason="Looks good initially", 

767 ) 

768 ) 

769 

770 with real_moderation_session(moderator2_token) as api: 

771 api.ModerateContent( 

772 moderation_pb2.ModerateContentReq( 

773 moderation_state_id=state_id, 

774 action=moderation_pb2.MODERATION_ACTION_FLAG, 

775 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

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

777 ) 

778 ) 

779 # Shadow it back 

780 api.ModerateContent( 

781 moderation_pb2.ModerateContentReq( 

782 moderation_state_id=state_id, 

783 action=moderation_pb2.MODERATION_ACTION_HIDE, 

784 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

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

786 ) 

787 ) 

788 

789 with real_moderation_session(moderator1_token) as api: 

790 api.ModerateContent( 

791 moderation_pb2.ModerateContentReq( 

792 moderation_state_id=state_id, 

793 action=moderation_pb2.MODERATION_ACTION_HIDE, 

794 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

795 reason="Actually it's spam", 

796 ) 

797 ) 

798 

799 # Check all log entries 

800 with session_scope() as session: 

801 log_entries = ( 

802 session.execute( 

803 select(ModerationLog) 

804 .where(ModerationLog.moderation_state_id == state_id) 

805 .order_by(ModerationLog.time.asc()) 

806 ) 

807 .scalars() 

808 .all() 

809 ) 

810 

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

812 assert len(log_entries) >= 3 

813 

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

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

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

817 

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

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

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

821 

822 # The last action should be hiding 

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

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

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

826 

827 

828def test_moderation_queue_workflow(db): 

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

830 user1, token1 = generate_user() 

831 user2, _ = generate_user() 

832 moderator, moderator_token = generate_user(is_superuser=True) 

833 

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

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

836 

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

838 with requests_session(token1) as api: 

839 host_request_id = api.CreateHostRequest( 

840 requests_pb2.CreateHostRequestReq( 

841 host_user_id=user2.id, 

842 from_date=today_plus_2, 

843 to_date=today_plus_3, 

844 text=valid_request_text(), 

845 ) 

846 ).host_request_id 

847 

848 state_id = None 

849 queue_item_id = None 

850 with session_scope() as session: 

851 # Get the host request and its moderation state 

852 host_request = session.execute( 

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

854 ).scalar_one() 

855 state_id = host_request.moderation_state_id 

856 

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

858 queue_item = session.execute( 

859 select(ModerationQueueItem) 

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

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

862 ).scalar_one() 

863 queue_item_id = queue_item.id 

864 

865 # Verify it's in the queue 

866 unresolved_items = ( 

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

868 .scalars() 

869 .all() 

870 ) 

871 

872 assert len(unresolved_items) >= 1 

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

874 

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

876 with real_moderation_session(moderator_token) as api: 

877 api.ModerateContent( 

878 moderation_pb2.ModerateContentReq( 

879 moderation_state_id=state_id, 

880 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

881 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

882 reason="Content approved", 

883 clear_flags=True, 

884 ) 

885 ) 

886 

887 # Verify queue item was resolved 

888 with session_scope() as session: 

889 # Verify it's no longer in unresolved queue 

890 unresolved_items = ( 

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

892 .scalars() 

893 .all() 

894 ) 

895 

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

897 

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

899 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

900 assert queue_item.resolved_by_log_id is not None 

901 

902 

903# ============================================================================ 

904# Moderation API Tests (testing the gRPC servicer) 

905# ============================================================================ 

906 

907 

908def test_GetModerationQueue_empty(db): 

909 """Test getting an empty moderation queue""" 

910 super_user, super_token = generate_user(is_superuser=True) 

911 

912 with real_moderation_session(super_token) as api: 

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

914 assert len(res.queue_items) == 0 

915 assert res.next_page_token == "" 

916 

917 

918def test_GetModerationQueue_with_items(db): 

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

920 super_user, super_token = generate_user(is_superuser=True) 

921 normal_user, user_token = generate_user() 

922 host, _ = generate_user() 

923 

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

925 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

926 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

927 

928 with real_moderation_session(super_token) as api: 

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

930 assert len(res.queue_items) == 2 

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

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

933 

934 

935def test_GetModerationQueue_filter_by_trigger(db): 

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

937 super_user, super_token = generate_user(is_superuser=True) 

938 normal_user, user_token = generate_user() 

939 host, _ = generate_user() 

940 

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

942 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

943 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

944 

945 # Add USER_FLAG trigger to second item via API 

946 with real_moderation_session(super_token) as api: 

947 api.ModerateContent( 

948 moderation_pb2.ModerateContentReq( 

949 moderation_state_id=state2_id, 

950 action=moderation_pb2.MODERATION_ACTION_FLAG, 

951 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

952 reason="Reported by user", 

953 ) 

954 ) 

955 

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

957 with real_moderation_session(super_token) as api: 

958 res = api.GetModerationQueue( 

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

960 ) 

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

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

963 

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

965 with real_moderation_session(super_token) as api: 

966 res = api.GetModerationQueue( 

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

968 ) 

969 assert len(res.queue_items) == 1 

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

971 

972 

973def test_GetModerationQueue_filter_created_before(db): 

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

975 super_user, super_token = generate_user(is_superuser=True) 

976 normal_user, user_token = generate_user() 

977 host, _ = generate_user() 

978 

979 # Create host requests 

980 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

981 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

982 

983 # Backdate the first queue item 

984 with session_scope() as session: 

985 queue_item1 = session.execute( 

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

987 ).scalar_one() 

988 # Set it to 2 hours ago 

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

990 

991 # The second item remains at current time 

992 

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

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

995 with real_moderation_session(super_token) as api: 

996 res = api.GetModerationQueue( 

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

998 ) 

999 assert len(res.queue_items) == 1 

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

1001 

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

1003 with real_moderation_session(super_token) as api: 

1004 res = api.GetModerationQueue( 

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

1006 ) 

1007 assert len(res.queue_items) == 2 

1008 

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

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

1011 with real_moderation_session(super_token) as api: 

1012 res = api.GetModerationQueue( 

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

1014 ) 

1015 assert len(res.queue_items) == 0 

1016 

1017 

1018def test_GetModerationQueue_filter_created_after(db): 

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

1020 super_user, super_token = generate_user(is_superuser=True) 

1021 normal_user, user_token = generate_user() 

1022 host, _ = generate_user() 

1023 

1024 # Create host requests 

1025 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1026 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1027 

1028 # Backdate the first queue item to 2 hours ago 

1029 with session_scope() as session: 

1030 queue_item1 = session.execute( 

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

1032 ).scalar_one() 

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

1034 

1035 # The second item remains at current time 

1036 

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

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

1039 with real_moderation_session(super_token) as api: 

1040 res = api.GetModerationQueue( 

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

1042 ) 

1043 assert len(res.queue_items) == 1 

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

1045 

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

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

1048 with real_moderation_session(super_token) as api: 

1049 res = api.GetModerationQueue( 

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

1051 ) 

1052 assert len(res.queue_items) == 2 

1053 

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

1055 with real_moderation_session(super_token) as api: 

1056 res = api.GetModerationQueue( 

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

1058 ) 

1059 assert len(res.queue_items) == 0 

1060 

1061 

1062def test_GetModerationQueue_filter_created_before_and_after(db): 

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

1064 super_user, super_token = generate_user(is_superuser=True) 

1065 normal_user, user_token = generate_user() 

1066 host, _ = generate_user() 

1067 

1068 # Create 3 host requests 

1069 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1070 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1071 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1072 

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

1074 with session_scope() as session: 

1075 queue_item1 = session.execute( 

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

1077 ).scalar_one() 

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

1079 

1080 queue_item2 = session.execute( 

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

1082 ).scalar_one() 

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

1084 

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

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

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

1088 with real_moderation_session(super_token) as api: 

1089 res = api.GetModerationQueue( 

1090 moderation_pb2.GetModerationQueueReq( 

1091 created_after=Timestamp_from_datetime(after_cutoff), 

1092 created_before=Timestamp_from_datetime(before_cutoff), 

1093 ) 

1094 ) 

1095 assert len(res.queue_items) == 1 

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

1097 

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

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

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

1101 with real_moderation_session(super_token) as api: 

1102 res = api.GetModerationQueue( 

1103 moderation_pb2.GetModerationQueueReq( 

1104 created_after=Timestamp_from_datetime(after_cutoff), 

1105 created_before=Timestamp_from_datetime(before_cutoff), 

1106 ) 

1107 ) 

1108 assert len(res.queue_items) == 1 

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

1110 

1111 

1112def test_GetModerationQueue_filter_unresolved(db): 

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

1114 super_user, super_token = generate_user(is_superuser=True) 

1115 normal_user, user_token = generate_user() 

1116 host, _ = generate_user() 

1117 

1118 # Create 2 host requests 

1119 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1120 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1121 

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

1123 with real_moderation_session(super_token) as api: 

1124 api.ModerateContent( 

1125 moderation_pb2.ModerateContentReq( 

1126 moderation_state_id=state1_id, 

1127 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1128 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1129 reason="Approved", 

1130 clear_flags=True, 

1131 ) 

1132 ) 

1133 

1134 # Get all items 

1135 with real_moderation_session(super_token) as api: 

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

1137 assert len(res.queue_items) == 2 

1138 

1139 # Get only unresolved items 

1140 with real_moderation_session(super_token) as api: 

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

1142 assert len(res.queue_items) == 1 

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

1144 

1145 

1146def test_GetModerationQueue_filter_by_author(db): 

1147 """Test filtering moderation queue by item_author_user_id""" 

1148 super_user, super_token = generate_user(is_superuser=True) 

1149 user1, token1 = generate_user() 

1150 user2, token2 = generate_user() 

1151 host_user, _ = generate_user() 

1152 

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

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

1155 

1156 # Create 2 host requests by user1 

1157 with requests_session(token1) as api: 

1158 hr1_id = api.CreateHostRequest( 

1159 requests_pb2.CreateHostRequestReq( 

1160 host_user_id=host_user.id, 

1161 from_date=today_plus_2, 

1162 to_date=today_plus_3, 

1163 text=valid_request_text(), 

1164 ) 

1165 ).host_request_id 

1166 

1167 hr2_id = api.CreateHostRequest( 

1168 requests_pb2.CreateHostRequestReq( 

1169 host_user_id=host_user.id, 

1170 from_date=today_plus_2, 

1171 to_date=today_plus_3, 

1172 text=valid_request_text(), 

1173 ) 

1174 ).host_request_id 

1175 

1176 # Create 1 host request by user2 

1177 with requests_session(token2) as api: 

1178 hr3_id = api.CreateHostRequest( 

1179 requests_pb2.CreateHostRequestReq( 

1180 host_user_id=host_user.id, 

1181 from_date=today_plus_2, 

1182 to_date=today_plus_3, 

1183 text=valid_request_text(), 

1184 ) 

1185 ).host_request_id 

1186 

1187 # Get moderation state IDs 

1188 state1_id, state2_id, state3_id = None, None, None 

1189 with session_scope() as session: 

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

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

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

1193 state1_id = hr1.moderation_state_id 

1194 state2_id = hr2.moderation_state_id 

1195 state3_id = hr3.moderation_state_id 

1196 

1197 # Get all items (should be 3) 

1198 with real_moderation_session(super_token) as api: 

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

1200 assert len(res.queue_items) == 3 

1201 

1202 # Filter by user1 (should get 2) 

1203 with real_moderation_session(super_token) as api: 

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

1205 assert len(res.queue_items) == 2 

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

1207 

1208 # Filter by user2 (should get 1) 

1209 with real_moderation_session(super_token) as api: 

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

1211 assert len(res.queue_items) == 1 

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

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

1214 

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

1216 with real_moderation_session(super_token) as api: 

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

1218 assert len(res.queue_items) == 0 

1219 

1220 

1221def test_GetModerationQueue_ordering(db): 

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

1223 super_user, super_token = generate_user(is_superuser=True) 

1224 normal_user, user_token = generate_user() 

1225 host, _ = generate_user() 

1226 

1227 # Create 3 host requests 

1228 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1229 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1230 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1231 

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

1233 with session_scope() as session: 

1234 queue_item1 = session.execute( 

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

1236 ).scalar_one() 

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

1238 

1239 queue_item2 = session.execute( 

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

1241 ).scalar_one() 

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

1243 

1244 queue_item3 = session.execute( 

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

1246 ).scalar_one() 

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

1248 

1249 # Default order (oldest first) 

1250 with real_moderation_session(super_token) as api: 

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

1252 assert len(res.queue_items) == 3 

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

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

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

1256 

1257 # Explicit oldest first 

1258 with real_moderation_session(super_token) as api: 

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

1260 assert len(res.queue_items) == 3 

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

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

1263 

1264 # Newest first 

1265 with real_moderation_session(super_token) as api: 

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

1267 assert len(res.queue_items) == 3 

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

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

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

1271 

1272 

1273def test_GetModerationQueue_pagination_newest_first(db): 

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

1275 super_user, super_token = generate_user(is_superuser=True) 

1276 normal_user, normal_token = generate_user() 

1277 host_user, _ = generate_user() 

1278 

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

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

1281 

1282 # Create 5 host requests 

1283 hr_ids = [] 

1284 with requests_session(normal_token) as api: 

1285 for i in range(5): 

1286 hr_id = api.CreateHostRequest( 

1287 requests_pb2.CreateHostRequestReq( 

1288 host_user_id=host_user.id, 

1289 from_date=today_plus_2, 

1290 to_date=today_plus_3, 

1291 text=valid_request_text(), 

1292 ) 

1293 ).host_request_id 

1294 hr_ids.append(hr_id) 

1295 

1296 # Get moderation state IDs 

1297 state_ids = [] 

1298 with session_scope() as session: 

1299 for hr_id in hr_ids: 

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

1301 state_ids.append(hr.moderation_state_id) 

1302 

1303 # Set different times so ordering is deterministic 

1304 with session_scope() as session: 

1305 for i, state_id in enumerate(state_ids): 

1306 queue_item = session.execute( 

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

1308 ).scalar_one() 

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

1310 

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

1312 with real_moderation_session(super_token) as api: 

1313 res1 = api.GetModerationQueue( 

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

1315 ) 

1316 assert len(res1.queue_items) == 2 

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

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

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

1320 assert res1.next_page_token # should have more pages 

1321 

1322 # Get second page using the token 

1323 res2 = api.GetModerationQueue( 

1324 moderation_pb2.GetModerationQueueReq( 

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

1326 ) 

1327 ) 

1328 assert len(res2.queue_items) == 2 

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

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

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

1332 

1333 # Pages should not overlap 

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

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

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

1337 

1338 

1339def test_GetModerationLog(db): 

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

1341 super_user, super_token = generate_user(is_superuser=True) 

1342 moderator, moderator_token = generate_user(is_superuser=True) 

1343 normal_user, user_token = generate_user() 

1344 host, _ = generate_user() 

1345 

1346 # Create a real host request 

1347 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1348 

1349 # Perform a moderation action via API 

1350 with real_moderation_session(moderator_token) as api: 

1351 api.ModerateContent( 

1352 moderation_pb2.ModerateContentReq( 

1353 moderation_state_id=state_id, 

1354 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1355 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1356 reason="Looks good", 

1357 ) 

1358 ) 

1359 

1360 with real_moderation_session(super_token) as api: 

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

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

1363 assert res.moderation_state.moderation_state_id == state_id 

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

1365 # Log entries are in reverse chronological order 

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

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

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

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

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

1371 

1372 

1373def test_GetModerationLog_not_found(db): 

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

1375 super_user, super_token = generate_user(is_superuser=True) 

1376 

1377 with real_moderation_session(super_token) as api: 

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

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

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

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

1382 

1383 

1384def test_GetModerationState(db): 

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

1386 super_user, super_token = generate_user(is_superuser=True) 

1387 user1, token1 = generate_user() 

1388 user2, _ = generate_user() 

1389 

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

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

1392 

1393 with requests_session(token1) as api: 

1394 host_request_id = api.CreateHostRequest( 

1395 requests_pb2.CreateHostRequestReq( 

1396 host_user_id=user2.id, 

1397 from_date=today_plus_2, 

1398 to_date=today_plus_3, 

1399 text=valid_request_text(), 

1400 ) 

1401 ).host_request_id 

1402 

1403 with real_moderation_session(super_token) as api: 

1404 res = api.GetModerationState( 

1405 moderation_pb2.GetModerationStateReq( 

1406 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1407 object_id=host_request_id, 

1408 ) 

1409 ) 

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

1411 assert res.moderation_state.object_id == host_request_id 

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

1413 assert res.moderation_state.moderation_state_id > 0 

1414 

1415 

1416def test_GetModerationState_not_found(db): 

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

1418 super_user, super_token = generate_user(is_superuser=True) 

1419 

1420 with real_moderation_session(super_token) as api: 

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

1422 api.GetModerationState( 

1423 moderation_pb2.GetModerationStateReq( 

1424 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1425 object_id=999999, 

1426 ) 

1427 ) 

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

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

1430 

1431 

1432def test_GetModerationState_unspecified_type(db): 

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

1434 super_user, super_token = generate_user(is_superuser=True) 

1435 

1436 with real_moderation_session(super_token) as api: 

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

1438 api.GetModerationState( 

1439 moderation_pb2.GetModerationStateReq( 

1440 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_UNSPECIFIED, 

1441 object_id=123, 

1442 ) 

1443 ) 

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

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

1446 

1447 

1448def test_ModerateContent_approve(db): 

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

1450 super_user, super_token = generate_user(is_superuser=True) 

1451 user1, token1 = generate_user() 

1452 user2, _ = generate_user() 

1453 

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

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

1456 

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

1458 with requests_session(token1) as api: 

1459 host_request_id = api.CreateHostRequest( 

1460 requests_pb2.CreateHostRequestReq( 

1461 host_user_id=user2.id, 

1462 from_date=today_plus_2, 

1463 to_date=today_plus_3, 

1464 text=valid_request_text(), 

1465 ) 

1466 ).host_request_id 

1467 

1468 # Get the moderation state ID 

1469 state_id = None 

1470 with session_scope() as session: 

1471 host_request = session.execute( 

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

1473 ).scalar_one() 

1474 state_id = host_request.moderation_state_id 

1475 

1476 with real_moderation_session(super_token) as api: 

1477 res = api.ModerateContent( 

1478 moderation_pb2.ModerateContentReq( 

1479 moderation_state_id=state_id, 

1480 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1481 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1482 reason="Approved by admin", 

1483 ) 

1484 ) 

1485 assert res.moderation_state.moderation_state_id == state_id 

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

1487 

1488 # Verify state was updated in database 

1489 with session_scope() as session: 

1490 state = session.get_one(ModerationState, state_id) 

1491 assert state.visibility == ModerationVisibility.visible 

1492 

1493 

1494def test_ModerateContent_not_found(db): 

1495 """Test moderating non-existent content""" 

1496 super_user, super_token = generate_user(is_superuser=True) 

1497 

1498 with real_moderation_session(super_token) as api: 

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

1500 api.ModerateContent( 

1501 moderation_pb2.ModerateContentReq( 

1502 moderation_state_id=999999, 

1503 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1504 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1505 reason="Test", 

1506 ) 

1507 ) 

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

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

1510 

1511 

1512def test_ModerateContent_hide(db): 

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

1514 super_user, super_token = generate_user(is_superuser=True) 

1515 normal_user, user_token = generate_user() 

1516 host, _ = generate_user() 

1517 

1518 # Create a real host request 

1519 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1520 

1521 with real_moderation_session(super_token) as api: 

1522 res = api.ModerateContent( 

1523 moderation_pb2.ModerateContentReq( 

1524 moderation_state_id=state_id, 

1525 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1526 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1527 reason="Spam content", 

1528 ) 

1529 ) 

1530 assert res.moderation_state.moderation_state_id == state_id 

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

1532 

1533 # Verify state was updated in database 

1534 with session_scope() as session: 

1535 state = session.get_one(ModerationState, state_id) 

1536 assert state.visibility == ModerationVisibility.hidden 

1537 

1538 

1539def test_ModerateContent_shadow(db): 

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

1541 super_user, super_token = generate_user(is_superuser=True) 

1542 normal_user, user_token = generate_user() 

1543 host, _ = generate_user() 

1544 

1545 # Create a real host request 

1546 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1547 

1548 with real_moderation_session(super_token) as api: 

1549 res = api.ModerateContent( 

1550 moderation_pb2.ModerateContentReq( 

1551 moderation_state_id=state_id, 

1552 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1553 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

1554 reason="Needs further review", 

1555 ) 

1556 ) 

1557 assert res.moderation_state.moderation_state_id == state_id 

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

1559 

1560 # Verify state was updated in database 

1561 with session_scope() as session: 

1562 state = session.get_one(ModerationState, state_id) 

1563 assert state.visibility == ModerationVisibility.shadowed 

1564 

1565 

1566def test_ModerateContent_flag(db): 

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

1568 super_user, super_token = generate_user(is_superuser=True) 

1569 user1, token1 = generate_user() 

1570 user2, _ = generate_user() 

1571 

1572 # Create a host request 

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

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

1575 

1576 with requests_session(token1) as api: 

1577 host_request_id = api.CreateHostRequest( 

1578 requests_pb2.CreateHostRequestReq( 

1579 host_user_id=user2.id, 

1580 from_date=today_plus_2, 

1581 to_date=today_plus_3, 

1582 text=valid_request_text(), 

1583 ) 

1584 ).host_request_id 

1585 

1586 # Get the moderation state ID 

1587 with session_scope() as session: 

1588 host_request = session.execute( 

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

1590 ).scalar_one() 

1591 state_id = host_request.moderation_state_id 

1592 

1593 with real_moderation_session(super_token) as api: 

1594 api.ModerateContent( 

1595 moderation_pb2.ModerateContentReq( 

1596 moderation_state_id=state_id, 

1597 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1598 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

1599 reason="Admin flagged for additional review", 

1600 priority=5, 

1601 ) 

1602 ) 

1603 

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

1605 with session_scope() as session: 

1606 queue_item = session.execute( 

1607 select(ModerationQueueItem) 

1608 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

1610 ).scalar_one() 

1611 assert queue_item.resolved_by_log_id is None 

1612 assert queue_item.priority == 5 

1613 

1614 flag_log = session.execute( 

1615 select(ModerationLog) 

1616 .where(ModerationLog.moderation_state_id == state_id) 

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

1618 ).scalar_one() 

1619 assert flag_log.queue_item_id == queue_item.id 

1620 

1621 

1622def test_ModerateContent_flag_requires_trigger(db): 

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

1624 super_user, super_token = generate_user(is_superuser=True) 

1625 user1, token1 = generate_user() 

1626 user2, _ = generate_user() 

1627 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1628 

1629 with real_moderation_session(super_token) as api: 

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

1631 api.ModerateContent( 

1632 moderation_pb2.ModerateContentReq( 

1633 moderation_state_id=state_id, 

1634 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1635 reason="no trigger", 

1636 ) 

1637 ) 

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

1639 

1640 

1641def _open_queue_item_id(state_id): 

1642 with session_scope() as session: 

1643 return ( 

1644 session.execute( 

1645 select(ModerationQueueItem) 

1646 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

1648 ) 

1649 .scalars() 

1650 .one() 

1651 .id 

1652 ) 

1653 

1654 

1655def test_ModerateContent_set_priority(db): 

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

1657 moderator, mod_token = generate_user(is_superuser=True) 

1658 user1, token1 = generate_user() 

1659 user2, _ = generate_user() 

1660 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1661 queue_item_id = _open_queue_item_id(state_id) 

1662 

1663 with real_moderation_session(mod_token) as api: 

1664 api.ModerateContent( 

1665 moderation_pb2.ModerateContentReq( 

1666 moderation_state_id=state_id, 

1667 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY, 

1668 queue_item_id=queue_item_id, 

1669 priority=10, 

1670 reason="bump", 

1671 ) 

1672 ) 

1673 

1674 with session_scope() as session: 

1675 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

1676 assert queue_item.priority == 10 

1677 assert queue_item.resolved_by_log_id is None # not resolved 

1678 # State stays shadowed (visibility untouched) 

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

1680 

1681 log = session.execute( 

1682 select(ModerationLog) 

1683 .where(ModerationLog.moderation_state_id == state_id) 

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

1685 ).scalar_one() 

1686 assert log.new_priority == 10 

1687 assert log.queue_item_id == queue_item_id 

1688 

1689 # GetModerationQueue surfaces the new priority 

1690 with real_moderation_session(mod_token) as api: 

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

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

1693 assert item.priority == 10 

1694 

1695 

1696def test_ModerateContent_unflag(db): 

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

1698 moderator, mod_token = generate_user(is_superuser=True) 

1699 user1, token1 = generate_user() 

1700 user2, _ = generate_user() 

1701 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1702 queue_item_id = _open_queue_item_id(state_id) 

1703 

1704 with real_moderation_session(mod_token) as api: 

1705 api.ModerateContent( 

1706 moderation_pb2.ModerateContentReq( 

1707 moderation_state_id=state_id, 

1708 action=moderation_pb2.MODERATION_ACTION_UNFLAG, 

1709 queue_item_id=queue_item_id, 

1710 reason="dismissed", 

1711 ) 

1712 ) 

1713 

1714 with session_scope() as session: 

1715 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

1716 assert queue_item.resolved_by_log_id is not None 

1717 # Visibility unchanged (still shadowed from creation) 

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

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

1720 assert log.action == ModerationAction.unflag 

1721 assert log.queue_item_id == queue_item_id 

1722 

1723 

1724def test_ModerateContent_unflag_requires_queue_item(db): 

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

1726 moderator, mod_token = generate_user(is_superuser=True) 

1727 user1, token1 = generate_user() 

1728 user2, _ = generate_user() 

1729 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1730 

1731 with real_moderation_session(mod_token) as api: 

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

1733 api.ModerateContent( 

1734 moderation_pb2.ModerateContentReq( 

1735 moderation_state_id=state_id, 

1736 action=moderation_pb2.MODERATION_ACTION_UNFLAG, 

1737 reason="no target", 

1738 ) 

1739 ) 

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

1741 

1742 

1743def test_ModerateContent_approve_without_clear_flags_leaves_flag_open(db): 

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

1745 moderator, mod_token = generate_user(is_superuser=True) 

1746 user1, token1 = generate_user() 

1747 user2, _ = generate_user() 

1748 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1749 queue_item_id = _open_queue_item_id(state_id) 

1750 

1751 with real_moderation_session(mod_token) as api: 

1752 api.ModerateContent( 

1753 moderation_pb2.ModerateContentReq( 

1754 moderation_state_id=state_id, 

1755 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1756 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1757 reason="visible but still under review", 

1758 ) 

1759 ) 

1760 

1761 with session_scope() as session: 

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

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

1764 

1765 

1766def test_ModerateContent_flag_supersede(db): 

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

1768 moderator, mod_token = generate_user(is_superuser=True) 

1769 user1, token1 = generate_user() 

1770 user2, _ = generate_user() 

1771 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1772 initial_item_id = _open_queue_item_id(state_id) 

1773 

1774 with real_moderation_session(mod_token) as api: 

1775 api.ModerateContent( 

1776 moderation_pb2.ModerateContentReq( 

1777 moderation_state_id=state_id, 

1778 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1779 trigger=moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG, 

1780 priority=3, 

1781 supersede_queue_item_id=initial_item_id, 

1782 reason="machine re-flag", 

1783 ) 

1784 ) 

1785 

1786 with session_scope() as session: 

1787 # Old initial_review flag is resolved 

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

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

1790 open_items = ( 

1791 session.execute( 

1792 select(ModerationQueueItem) 

1793 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

1795 ) 

1796 .scalars() 

1797 .all() 

1798 ) 

1799 assert len(open_items) == 1 

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

1801 assert open_items[0].priority == 3 

1802 

1803 

1804def test_GetModerationQueue_filter_by_priority(db): 

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

1806 moderator, mod_token = generate_user(is_superuser=True) 

1807 user1, token1 = generate_user() 

1808 user2, _ = generate_user() 

1809 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1810 queue_item_id = _open_queue_item_id(state_id) 

1811 

1812 with real_moderation_session(mod_token) as api: 

1813 api.ModerateContent( 

1814 moderation_pb2.ModerateContentReq( 

1815 moderation_state_id=state_id, 

1816 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY, 

1817 queue_item_id=queue_item_id, 

1818 priority=7, 

1819 reason="raise", 

1820 ) 

1821 ) 

1822 

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

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

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

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

1827 

1828 # priority_max excludes it above the threshold 

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

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

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

1832 

1833 

1834# ============================================================================ 

1835# Tests for group chat moderation 

1836# ============================================================================ 

1837 

1838 

1839def test_group_chat_created_with_moderation_state(db): 

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

1841 user1, token1 = generate_user() 

1842 user2, _ = generate_user() 

1843 make_friends(user1, user2) 

1844 

1845 with conversations_session(token1) as api: 

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

1847 group_chat_id = res.group_chat_id 

1848 

1849 # Verify moderation state was created 

1850 with session_scope() as session: 

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

1852 

1853 assert group_chat.moderation_state.object_type == ModerationObjectType.group_chat 

1854 assert group_chat.moderation_state.object_id == group_chat_id 

1855 # Group chats start as SHADOWED 

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

1857 

1858 # A moderation queue item should have been created 

1859 queue_item = ( 

1860 session.execute( 

1861 select(ModerationQueueItem).where( 

1862 ModerationQueueItem.moderation_state_id == group_chat.moderation_state_id 

1863 ) 

1864 ) 

1865 .scalars() 

1866 .first() 

1867 ) 

1868 assert queue_item is not None 

1869 assert queue_item.trigger == ModerationTrigger.initial_review 

1870 

1871 

1872def test_group_chat_GetModerationState(db): 

1873 """Test GetModerationState API for group chats""" 

1874 user1, token1 = generate_user() 

1875 user2, _ = generate_user() 

1876 moderator, mod_token = generate_user(is_superuser=True) 

1877 make_friends(user1, user2) 

1878 

1879 with conversations_session(token1) as api: 

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

1881 group_chat_id = res.group_chat_id 

1882 

1883 # Moderator can look up the moderation state 

1884 with real_moderation_session(mod_token) as api: 

1885 res = api.GetModerationState( 

1886 moderation_pb2.GetModerationStateReq( 

1887 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1888 object_id=group_chat_id, 

1889 ) 

1890 ) 

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

1892 assert res.moderation_state.object_id == group_chat_id 

1893 # Starts as SHADOWED 

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

1895 

1896 

1897def test_group_chat_moderation_hide(db): 

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

1899 user1, token1 = generate_user() 

1900 user2, token2 = generate_user() 

1901 moderator, mod_token = generate_user(is_superuser=True) 

1902 make_friends(user1, user2) 

1903 

1904 with conversations_session(token1) as api: 

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

1906 group_chat_id = res.group_chat_id 

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

1908 

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

1910 with real_moderation_session(mod_token) as api: 

1911 state_res = api.GetModerationState( 

1912 moderation_pb2.GetModerationStateReq( 

1913 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1914 object_id=group_chat_id, 

1915 ) 

1916 ) 

1917 api.ModerateContent( 

1918 moderation_pb2.ModerateContentReq( 

1919 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1920 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1921 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1922 reason="Approved", 

1923 ) 

1924 ) 

1925 

1926 # Both users can see the chat now 

1927 with conversations_session(token1) as api: 

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

1929 assert len(res.group_chats) == 1 

1930 

1931 with conversations_session(token2) as api: 

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

1933 assert len(res.group_chats) == 1 

1934 

1935 # Moderator hides the group chat 

1936 with real_moderation_session(mod_token) as api: 

1937 state_res = api.GetModerationState( 

1938 moderation_pb2.GetModerationStateReq( 

1939 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1940 object_id=group_chat_id, 

1941 ) 

1942 ) 

1943 api.ModerateContent( 

1944 moderation_pb2.ModerateContentReq( 

1945 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1946 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1947 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1948 reason="Inappropriate content", 

1949 ) 

1950 ) 

1951 

1952 # Neither user can see the chat now 

1953 with conversations_session(token1) as api: 

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

1955 assert len(res.group_chats) == 0 

1956 

1957 with conversations_session(token2) as api: 

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

1959 assert len(res.group_chats) == 0 

1960 

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

1962 with conversations_session(token1) as api: 

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

1964 assert len(res.messages) == 0 

1965 

1966 

1967def test_group_chat_moderation_shadow(db): 

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

1969 user1, token1 = generate_user() # Creator 

1970 user2, token2 = generate_user() # Participant 

1971 moderator, mod_token = generate_user(is_superuser=True) 

1972 make_friends(user1, user2) 

1973 

1974 with conversations_session(token1) as api: 

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

1976 group_chat_id = res.group_chat_id 

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

1978 

1979 # Moderator shadows the group chat 

1980 with real_moderation_session(mod_token) as api: 

1981 state_res = api.GetModerationState( 

1982 moderation_pb2.GetModerationStateReq( 

1983 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1984 object_id=group_chat_id, 

1985 ) 

1986 ) 

1987 api.ModerateContent( 

1988 moderation_pb2.ModerateContentReq( 

1989 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1990 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1991 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

1992 reason="Needs review", 

1993 ) 

1994 ) 

1995 

1996 # Creator can see SHADOWED content in list operations 

1997 with conversations_session(token1) as api: 

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

1999 assert len(res.group_chats) == 1 

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

2001 

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

2003 with conversations_session(token2) as api: 

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

2005 assert len(res.group_chats) == 0 

2006 

2007 # Creator can also access it directly via GetGroupChat 

2008 with conversations_session(token1) as api: 

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

2010 assert res.group_chat_id == group_chat_id 

2011 

2012 

2013# ============================================================================ 

2014# Tests for auto-approval background job 

2015# ============================================================================ 

2016 

2017 

2018def test_auto_approve_moderation_queue_disabled_when_zero(db, email_collector: EmailCollector): 

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

2020 moderator, mod_token = generate_user(is_superuser=True) 

2021 user1, token1 = generate_user() 

2022 user2, token2 = generate_user() 

2023 

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

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

2026 

2027 # Create a host request 

2028 with requests_session(token1) as api: 

2029 host_request_id = api.CreateHostRequest( 

2030 requests_pb2.CreateHostRequestReq( 

2031 host_user_id=user2.id, 

2032 from_date=today_plus_2, 

2033 to_date=today_plus_3, 

2034 text=valid_request_text(), 

2035 ) 

2036 ).host_request_id 

2037 

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

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

2040 

2041 # Ensure deadline is 0 (disabled) 

2042 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 0 

2043 

2044 # Run the job 

2045 auto_approve_moderation_queue(empty_pb2.Empty()) 

2046 

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

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

2049 assert res.host_request_id == host_request_id 

2050 

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

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

2053 assert len(res.host_requests) == 1 

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

2055 

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

2057 with requests_session(token2) as api: 

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

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

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

2061 

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

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

2064 assert len(res.host_requests) == 0 

2065 

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

2067 with real_moderation_session(mod_token) as api: 

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

2069 assert len(res.queue_items) == 1 

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

2071 

2072 # Moderator can check the state is still SHADOWED 

2073 state_res = api.GetModerationState( 

2074 moderation_pb2.GetModerationStateReq( 

2075 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2076 object_id=host_request_id, 

2077 ) 

2078 ) 

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

2080 

2081 

2082def test_auto_approve_moderation_queue_approves_old_items( 

2083 db, email_collector: EmailCollector, push_collector: PushCollector 

2084): 

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

2086 moderator, mod_token = generate_user(is_superuser=True) 

2087 user1, token1 = generate_user() 

2088 user2, token2 = generate_user() 

2089 

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

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

2092 

2093 # Create a host request 

2094 with requests_session(token1) as api: 

2095 host_request_id = api.CreateHostRequest( 

2096 requests_pb2.CreateHostRequestReq( 

2097 host_user_id=user2.id, 

2098 from_date=today_plus_2, 

2099 to_date=today_plus_3, 

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

2101 ) 

2102 ).host_request_id 

2103 

2104 # No email sent initially (shadowed) 

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

2106 

2107 # Host cannot see the request yet 

2108 with requests_session(token2) as api: 

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

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

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

2112 

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

2114 with session_scope() as session: 

2115 host_request = session.execute( 

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

2117 ).scalar_one() 

2118 queue_item = session.execute( 

2119 select(ModerationQueueItem) 

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

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

2122 ).scalar_one() 

2123 # Backdate the queue item by 2 minutes 

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

2125 

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

2127 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2128 config.MODERATION_BOT_USER_ID = moderator.id 

2129 

2130 # Run the job 

2131 auto_approve_moderation_queue(empty_pb2.Empty()) 

2132 

2133 # Now host can see the request via API 

2134 with requests_session(token2) as api: 

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

2136 assert res.host_request_id == host_request_id 

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

2138 

2139 # Host sees it in their received list 

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

2141 assert len(res.host_requests) == 1 

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

2143 

2144 # Surfer sees it in their sent list 

2145 with requests_session(token1) as api: 

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

2147 assert len(res.host_requests) == 1 

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

2149 

2150 with real_moderation_session(mod_token) as api: 

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

2152 assert len(res.queue_items) == 1 

2153 flag_item = res.queue_items[0] 

2154 assert flag_item.trigger == moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG 

2155 assert flag_item.priority == MODERATION_AUTO_APPROVE_FLAG_PRIORITY 

2156 

2157 state_res = api.GetModerationState( 

2158 moderation_pb2.GetModerationStateReq( 

2159 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2160 object_id=host_request_id, 

2161 ) 

2162 ) 

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

2164 

2165 log_res = api.GetModerationLog( 

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

2167 ) 

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

2169 assert len(approve_entries) == 1 

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

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

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

2173 

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

2175 assert len(flag_entries) == 1 

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

2177 

2178 

2179def test_auto_approve_does_not_approve_recent_items(db, email_collector: EmailCollector): 

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

2181 moderator, mod_token = generate_user(is_superuser=True) 

2182 user1, token1 = generate_user() 

2183 user2, token2 = generate_user() 

2184 

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

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

2187 

2188 # Create a host request 

2189 with requests_session(token1) as api: 

2190 host_request_id = api.CreateHostRequest( 

2191 requests_pb2.CreateHostRequestReq( 

2192 host_user_id=user2.id, 

2193 from_date=today_plus_2, 

2194 to_date=today_plus_3, 

2195 text=valid_request_text(), 

2196 ) 

2197 ).host_request_id 

2198 

2199 # No email sent (shadowed) 

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

2201 

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

2203 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600 

2204 config.MODERATION_BOT_USER_ID = moderator.id 

2205 

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

2207 auto_approve_moderation_queue(empty_pb2.Empty()) 

2208 

2209 # Still no email sent 

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

2211 

2212 # Host still cannot see the request 

2213 with requests_session(token2) as api: 

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

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

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

2217 

2218 # Not in host's received list 

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

2220 assert len(res.host_requests) == 0 

2221 

2222 # Moderator sees it still in queue unresolved 

2223 with real_moderation_session(mod_token) as api: 

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

2225 assert len(res.queue_items) == 1 

2226 

2227 # State is still SHADOWED 

2228 state_res = api.GetModerationState( 

2229 moderation_pb2.GetModerationStateReq( 

2230 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2231 object_id=host_request_id, 

2232 ) 

2233 ) 

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

2235 

2236 

2237def test_auto_approve_does_not_approve_already_approved(db): 

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

2239 moderator, mod_token = generate_user(is_superuser=True) 

2240 user1, token1 = generate_user() 

2241 user2, token2 = generate_user() 

2242 

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

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

2245 

2246 # Create a host request 

2247 with requests_session(token1) as api: 

2248 host_request_id = api.CreateHostRequest( 

2249 requests_pb2.CreateHostRequestReq( 

2250 host_user_id=user2.id, 

2251 from_date=today_plus_2, 

2252 to_date=today_plus_3, 

2253 text=valid_request_text(), 

2254 ) 

2255 ).host_request_id 

2256 

2257 # Moderator approves it manually 

2258 with real_moderation_session(mod_token) as api: 

2259 state_res = api.GetModerationState( 

2260 moderation_pb2.GetModerationStateReq( 

2261 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2262 object_id=host_request_id, 

2263 ) 

2264 ) 

2265 state_id = state_res.moderation_state.moderation_state_id 

2266 

2267 api.ModerateContent( 

2268 moderation_pb2.ModerateContentReq( 

2269 moderation_state_id=state_id, 

2270 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

2271 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2272 reason="Approved by moderator", 

2273 clear_flags=True, 

2274 ) 

2275 ) 

2276 

2277 # Host can now see it 

2278 with requests_session(token2) as api: 

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

2280 assert res.host_request_id == host_request_id 

2281 

2282 # Get log count before auto-approval 

2283 with real_moderation_session(mod_token) as api: 

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

2285 log_count_before = len(log_res_before.log_entries) 

2286 

2287 # Set deadline to 1 second 

2288 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2289 config.MODERATION_BOT_USER_ID = moderator.id 

2290 

2291 # Run the job 

2292 auto_approve_moderation_queue(empty_pb2.Empty()) 

2293 

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

2295 with real_moderation_session(mod_token) as api: 

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

2297 assert len(log_res_after.log_entries) == log_count_before 

2298 

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

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

2301 assert len(queue_res.queue_items) == 0 

2302 

2303 

2304def test_auto_approve_does_not_approve_moderator_shadowed_items(db): 

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

2306 moderator, mod_token = generate_user(is_superuser=True) 

2307 user1, token1 = generate_user() 

2308 user2, token2 = generate_user() 

2309 

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

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

2312 

2313 # Create a host request 

2314 with requests_session(token1) as api: 

2315 host_request_id = api.CreateHostRequest( 

2316 requests_pb2.CreateHostRequestReq( 

2317 host_user_id=user2.id, 

2318 from_date=today_plus_2, 

2319 to_date=today_plus_3, 

2320 text=valid_request_text(), 

2321 ) 

2322 ).host_request_id 

2323 

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

2325 with real_moderation_session(mod_token) as api: 

2326 state_res = api.GetModerationState( 

2327 moderation_pb2.GetModerationStateReq( 

2328 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2329 object_id=host_request_id, 

2330 ) 

2331 ) 

2332 state_id = state_res.moderation_state.moderation_state_id 

2333 

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

2335 api.ModerateContent( 

2336 moderation_pb2.ModerateContentReq( 

2337 moderation_state_id=state_id, 

2338 action=moderation_pb2.MODERATION_ACTION_HIDE, 

2339 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2340 reason="Keeping shadowed for review", 

2341 clear_flags=True, 

2342 ) 

2343 ) 

2344 

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

2346 with session_scope() as session: 

2347 queue_item = session.execute( 

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

2349 ).scalar_one() 

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

2351 

2352 # Set deadline to 1 second 

2353 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2354 config.MODERATION_BOT_USER_ID = moderator.id 

2355 

2356 # Get log count before 

2357 with real_moderation_session(mod_token) as api: 

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

2359 log_count_before = len(log_res_before.log_entries) 

2360 

2361 # Run the job 

2362 auto_approve_moderation_queue(empty_pb2.Empty()) 

2363 

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

2365 with real_moderation_session(mod_token) as api: 

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

2367 assert len(log_res_after.log_entries) == log_count_before 

2368 

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

2370 state_res = api.GetModerationState( 

2371 moderation_pb2.GetModerationStateReq( 

2372 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2373 object_id=host_request_id, 

2374 ) 

2375 ) 

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

2377 

2378 # Host still cannot see the request 

2379 with requests_session(token2) as api: 

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

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

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

2383 

2384 

2385def test_auto_approve_skips_shadowed_user_authored_items(db): 

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

2387 moderator, mod_token = generate_user(is_superuser=True) 

2388 surfer, surfer_token = generate_user() 

2389 host, host_token = generate_user() 

2390 

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

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

2393 

2394 with requests_session(surfer_token) as api: 

2395 host_request_id = api.CreateHostRequest( 

2396 requests_pb2.CreateHostRequestReq( 

2397 host_user_id=host.id, 

2398 from_date=today_plus_2, 

2399 to_date=today_plus_3, 

2400 text=valid_request_text(), 

2401 ) 

2402 ).host_request_id 

2403 

2404 # Shadow the surfer after the host request was created 

2405 with session_scope() as session: 

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

2407 

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

2409 with session_scope() as session: 

2410 host_request = session.execute( 

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

2412 ).scalar_one() 

2413 queue_item = session.execute( 

2414 select(ModerationQueueItem) 

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

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

2417 ).scalar_one() 

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

2419 

2420 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2421 config.MODERATION_BOT_USER_ID = moderator.id 

2422 

2423 auto_approve_moderation_queue(empty_pb2.Empty()) 

2424 

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

2426 with real_moderation_session(mod_token) as api: 

2427 state_res = api.GetModerationState( 

2428 moderation_pb2.GetModerationStateReq( 

2429 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2430 object_id=host_request_id, 

2431 ) 

2432 ) 

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

2434 

2435 # Host still cannot see the request 

2436 with requests_session(host_token) as api: 

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

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

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

2440 

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

2442 with requests_session(surfer_token) as api: 

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

2444 assert res.host_request_id == host_request_id 

2445 

2446 

2447def test_auto_approve_preserves_other_open_flags(db): 

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

2449 moderator, mod_token = generate_user(is_superuser=True) 

2450 surfer, surfer_token = generate_user() 

2451 host, _ = generate_user() 

2452 

2453 state_id = create_test_host_request_with_moderation(surfer_token, host.id) 

2454 

2455 with real_moderation_session(mod_token) as api: 

2456 api.ModerateContent( 

2457 moderation_pb2.ModerateContentReq( 

2458 moderation_state_id=state_id, 

2459 action=moderation_pb2.MODERATION_ACTION_FLAG, 

2460 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

2461 priority=5, 

2462 reason="user reported this", 

2463 ) 

2464 ) 

2465 

2466 with session_scope() as session: 

2467 initial_item = session.execute( 

2468 select(ModerationQueueItem) 

2469 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

2471 ).scalar_one() 

2472 initial_item_id = initial_item.id 

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

2474 

2475 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2476 config.MODERATION_BOT_USER_ID = moderator.id 

2477 

2478 auto_approve_moderation_queue(empty_pb2.Empty()) 

2479 

2480 with session_scope() as session: 

2481 assert ( 

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

2483 .scalar_one() 

2484 .resolved_by_log_id 

2485 is not None 

2486 ) 

2487 assert ( 

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

2489 == ModerationVisibility.visible 

2490 ) 

2491 

2492 open_items = ( 

2493 session.execute( 

2494 select(ModerationQueueItem) 

2495 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

2497 ) 

2498 .scalars() 

2499 .all() 

2500 ) 

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

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

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

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

2505 

2506 

2507# ============================================================================ 

2508# Notification Suppression Tests 

2509# ============================================================================ 

2510 

2511 

2512def test_host_request_message_notifications_suppressed_before_approval( 

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

2514): 

2515 """ 

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

2517 that haven't been approved yet. 

2518 """ 

2519 host, host_token = generate_user(complete_profile=True) 

2520 surfer, surfer_token = generate_user(complete_profile=True) 

2521 

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

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

2524 

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

2526 with requests_session(surfer_token) as api: 

2527 hr_id = api.CreateHostRequest( 

2528 requests_pb2.CreateHostRequestReq( 

2529 host_user_id=host.id, 

2530 from_date=today_plus_2, 

2531 to_date=today_plus_3, 

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

2533 ) 

2534 ).host_request_id 

2535 

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

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

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

2539 

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

2541 with requests_session(surfer_token) as api: 

2542 api.SendHostRequestMessage( 

2543 requests_pb2.SendHostRequestMessageReq( 

2544 host_request_id=hr_id, 

2545 text="Follow-up message 1", 

2546 ) 

2547 ) 

2548 api.SendHostRequestMessage( 

2549 requests_pb2.SendHostRequestMessageReq( 

2550 host_request_id=hr_id, 

2551 text="Follow-up message 2", 

2552 ) 

2553 ) 

2554 

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

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

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

2558 

2559 # Now approve the request 

2560 moderator.approve_host_request(hr_id) 

2561 email_collector.pop_for_recipient(host.email) 

2562 

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

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

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

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

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

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

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

2570 

2571 

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

2573 """ 

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

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

2576 

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

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

2579 """ 

2580 host, host_token = generate_user(complete_profile=True) 

2581 surfer, surfer_token = generate_user(complete_profile=True) 

2582 

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

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

2585 

2586 # Create host request 

2587 with requests_session(surfer_token) as api: 

2588 hr_id = api.CreateHostRequest( 

2589 requests_pb2.CreateHostRequestReq( 

2590 host_user_id=host.id, 

2591 from_date=today_plus_2, 

2592 to_date=today_plus_3, 

2593 text=valid_request_text(), 

2594 ) 

2595 ).host_request_id 

2596 

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

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

2599 

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

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

2602 with requests_session(surfer_token) as api: 

2603 api.RespondHostRequest( 

2604 requests_pb2.RespondHostRequestReq( 

2605 host_request_id=hr_id, 

2606 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

2607 text="Actually, never mind", 

2608 ) 

2609 ) 

2610 

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

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

2613 

2614 

2615def test_host_request_notifications_sent_after_approval( 

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

2617): 

2618 """ 

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

2620 """ 

2621 host, host_token = generate_user(complete_profile=True) 

2622 surfer, surfer_token = generate_user(complete_profile=True) 

2623 

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

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

2626 

2627 # Create and approve host request 

2628 with requests_session(surfer_token) as api: 

2629 hr_id = api.CreateHostRequest( 

2630 requests_pb2.CreateHostRequestReq( 

2631 host_user_id=host.id, 

2632 from_date=today_plus_2, 

2633 to_date=today_plus_3, 

2634 text=valid_request_text(), 

2635 ) 

2636 ).host_request_id 

2637 

2638 moderator.approve_host_request(hr_id) 

2639 

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

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

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

2643 

2644 # Host accepts the request - surfer should be notified 

2645 with requests_session(host_token) as api: 

2646 api.RespondHostRequest( 

2647 requests_pb2.RespondHostRequestReq( 

2648 host_request_id=hr_id, 

2649 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

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

2651 ) 

2652 ) 

2653 

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

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

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

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

2658 

2659 # Surfer confirms - host should be notified 

2660 with requests_session(surfer_token) as api: 

2661 api.RespondHostRequest( 

2662 requests_pb2.RespondHostRequestReq( 

2663 host_request_id=hr_id, 

2664 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

2665 text="See you then!", 

2666 ) 

2667 ) 

2668 

2669 # Host should now have received the confirmation notifications 

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

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

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

2673 

2674 

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

2676 """ 

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

2678 that haven't been approved yet. 

2679 """ 

2680 user1, token1 = generate_user(complete_profile=True) 

2681 user2, token2 = generate_user(complete_profile=True) 

2682 

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

2684 with conversations_session(token1) as api: 

2685 res = api.CreateGroupChat( 

2686 conversations_pb2.CreateGroupChatReq( 

2687 recipient_user_ids=[user2.id], 

2688 ) 

2689 ) 

2690 gc_id = res.group_chat_id 

2691 

2692 # Verify initial state 

2693 with session_scope() as session: 

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

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

2696 

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

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

2699 

2700 # Send messages BEFORE approval 

2701 with conversations_session(token1) as api: 

2702 api.SendMessage( 

2703 conversations_pb2.SendMessageReq( 

2704 group_chat_id=gc_id, 

2705 text="Hello before approval", 

2706 ) 

2707 ) 

2708 

2709 # Process the queued notification job 

2710 while process_job(): 

2711 pass 

2712 

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

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

2715 

2716 # Now approve the group chat 

2717 moderator.approve_group_chat(gc_id) 

2718 

2719 # Process the queued notification jobs from approval 

2720 while process_job(): 

2721 pass 

2722 

2723 # Verify moderation state after approval 

2724 with session_scope() as session: 

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

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

2727 

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

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

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

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

2732 

2733 # Send a message AFTER approval 

2734 with conversations_session(token1) as api: 

2735 api.SendMessage( 

2736 conversations_pb2.SendMessageReq( 

2737 group_chat_id=gc_id, 

2738 text="Hello after approval", 

2739 ) 

2740 ) 

2741 

2742 # Process the queued notification job 

2743 while process_job(): 

2744 pass 

2745 

2746 # User2 should have received another notification 

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

2748 

2749 

2750def test_event_moderation_state_content(db): 

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

2752 super_user, super_token = generate_user(is_superuser=True) 

2753 user, token = generate_user() 

2754 

2755 with session_scope() as session: 

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

2757 

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

2759 end_time = start_time + timedelta(hours=3) 

2760 

2761 with events_session(token) as api: 

2762 res = api.CreateEvent( 

2763 events_pb2.CreateEventReq( 

2764 title="My Event Title", 

2765 content="My event description.", 

2766 photo_key=None, 

2767 offline_information=events_pb2.OfflineEventInformation( 

2768 address="Near Null Island", 

2769 lat=0.1, 

2770 lng=0.2, 

2771 ), 

2772 start_time=Timestamp_from_datetime(start_time), 

2773 end_time=Timestamp_from_datetime(end_time), 

2774 timezone="UTC", 

2775 ) 

2776 ) 

2777 event_id = res.event_id 

2778 

2779 with session_scope() as session: 

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

2781 state_id = occurrence.moderation_state_id 

2782 

2783 with real_moderation_session(super_token) as api: 

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

2785 event_items = [ 

2786 item 

2787 for item in res.queue_items 

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

2789 ] 

2790 assert len(event_items) == 1 

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

2792 

2793 

2794# ============================================================================ 

2795# Tests for SetUserContentVisibility 

2796# ============================================================================ 

2797 

2798 

2799def _get_moderation_state(session, object_type, object_id): 

2800 return session.execute( 

2801 select(ModerationState) 

2802 .where(ModerationState.object_type == object_type) 

2803 .where(ModerationState.object_id == object_id) 

2804 ).scalar_one() 

2805 

2806 

2807def test_SetUserContentVisibility_host_request(db): 

2808 super_user, super_token = generate_user(is_superuser=True) 

2809 surfer, surfer_token = generate_user() 

2810 host, _ = generate_user() 

2811 

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

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

2814 with requests_session(surfer_token) as api: 

2815 hr_id = api.CreateHostRequest( 

2816 requests_pb2.CreateHostRequestReq( 

2817 host_user_id=host.id, 

2818 from_date=today_plus_2, 

2819 to_date=today_plus_3, 

2820 text=valid_request_text(), 

2821 ) 

2822 ).host_request_id 

2823 

2824 with real_moderation_session(super_token) as api: 

2825 res = api.SetUserContentVisibility( 

2826 moderation_pb2.SetUserContentVisibilityReq( 

2827 user_id=surfer.id, 

2828 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2829 ) 

2830 ) 

2831 # Already shadowed by default — no-op 

2832 assert res.updated_count == 0 

2833 

2834 with real_moderation_session(super_token) as api: 

2835 res = api.SetUserContentVisibility( 

2836 moderation_pb2.SetUserContentVisibilityReq( 

2837 user_id=surfer.id, 

2838 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2839 reason="policy violation", 

2840 ) 

2841 ) 

2842 assert res.updated_count == 1 

2843 

2844 with session_scope() as session: 

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

2846 assert state.visibility == ModerationVisibility.hidden 

2847 

2848 log_entries = ( 

2849 session.execute( 

2850 select(ModerationLog) 

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

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

2853 ) 

2854 .scalars() 

2855 .all() 

2856 ) 

2857 # create log + bulk update log 

2858 assert len(log_entries) == 2 

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

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

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

2862 

2863 

2864def test_SetUserContentVisibility_group_chat(db): 

2865 super_user, super_token = generate_user(is_superuser=True) 

2866 creator, creator_token = generate_user() 

2867 other, _ = generate_user() 

2868 make_friends(creator, other) 

2869 

2870 with conversations_session(creator_token) as api: 

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

2872 

2873 with real_moderation_session(super_token) as api: 

2874 api.SetUserContentVisibility( 

2875 moderation_pb2.SetUserContentVisibilityReq( 

2876 user_id=creator.id, 

2877 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2878 ) 

2879 ) 

2880 

2881 with session_scope() as session: 

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

2883 assert state.visibility == ModerationVisibility.hidden 

2884 

2885 

2886def test_SetUserContentVisibility_event_occurrence(db): 

2887 super_user, super_token = generate_user(is_superuser=True) 

2888 creator, creator_token = generate_user() 

2889 

2890 with session_scope() as session: 

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

2892 

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

2894 end_time = start_time + timedelta(hours=3) 

2895 with events_session(creator_token) as api: 

2896 event_id = api.CreateEvent( 

2897 events_pb2.CreateEventReq( 

2898 title="Event", 

2899 content="Event description.", 

2900 photo_key=None, 

2901 offline_information=events_pb2.OfflineEventInformation( 

2902 address="Near Null Island", 

2903 lat=0.1, 

2904 lng=0.2, 

2905 ), 

2906 start_time=Timestamp_from_datetime(start_time), 

2907 end_time=Timestamp_from_datetime(end_time), 

2908 timezone="UTC", 

2909 ) 

2910 ).event_id 

2911 

2912 with real_moderation_session(super_token) as api: 

2913 res = api.SetUserContentVisibility( 

2914 moderation_pb2.SetUserContentVisibilityReq( 

2915 user_id=creator.id, 

2916 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2917 ) 

2918 ) 

2919 assert res.updated_count == 1 

2920 

2921 with session_scope() as session: 

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

2923 assert state.visibility == ModerationVisibility.hidden 

2924 

2925 

2926def test_SetUserContentVisibility_friend_request(db): 

2927 super_user, super_token = generate_user(is_superuser=True) 

2928 sender, sender_token = generate_user() 

2929 recipient, _ = generate_user() 

2930 

2931 with api_session(sender_token) as api: 

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

2933 

2934 with session_scope() as session: 

2935 fr_id = session.execute( 

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

2937 ).scalar_one() 

2938 

2939 with real_moderation_session(super_token) as api: 

2940 res = api.SetUserContentVisibility( 

2941 moderation_pb2.SetUserContentVisibilityReq( 

2942 user_id=sender.id, 

2943 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2944 ) 

2945 ) 

2946 assert res.updated_count == 1 

2947 

2948 with session_scope() as session: 

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

2950 assert state.visibility == ModerationVisibility.hidden 

2951 

2952 

2953def test_SetUserContentVisibility_round_trip(db): 

2954 super_user, super_token = generate_user(is_superuser=True) 

2955 surfer, surfer_token = generate_user() 

2956 host, _ = generate_user() 

2957 

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

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

2960 with requests_session(surfer_token) as api: 

2961 hr_id = api.CreateHostRequest( 

2962 requests_pb2.CreateHostRequestReq( 

2963 host_user_id=host.id, 

2964 from_date=today_plus_2, 

2965 to_date=today_plus_3, 

2966 text=valid_request_text(), 

2967 ) 

2968 ).host_request_id 

2969 

2970 with real_moderation_session(super_token) as api: 

2971 api.SetUserContentVisibility( 

2972 moderation_pb2.SetUserContentVisibilityReq( 

2973 user_id=surfer.id, 

2974 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2975 reason="first", 

2976 ) 

2977 ) 

2978 api.SetUserContentVisibility( 

2979 moderation_pb2.SetUserContentVisibilityReq( 

2980 user_id=surfer.id, 

2981 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2982 reason="second", 

2983 ) 

2984 ) 

2985 

2986 with session_scope() as session: 

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

2988 assert state.visibility == ModerationVisibility.visible 

2989 

2990 bulk_log_entries = ( 

2991 session.execute( 

2992 select(ModerationLog) 

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

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

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

2996 ) 

2997 .scalars() 

2998 .all() 

2999 ) 

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

3001 ModerationVisibility.hidden, 

3002 ModerationVisibility.visible, 

3003 ] 

3004 

3005 

3006def test_SetUserContentVisibility_resolves_queue_items(db): 

3007 super_user, super_token = generate_user(is_superuser=True) 

3008 surfer, surfer_token = generate_user() 

3009 host, _ = generate_user() 

3010 

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

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

3013 with requests_session(surfer_token) as api: 

3014 hr_id = api.CreateHostRequest( 

3015 requests_pb2.CreateHostRequestReq( 

3016 host_user_id=host.id, 

3017 from_date=today_plus_2, 

3018 to_date=today_plus_3, 

3019 text=valid_request_text(), 

3020 ) 

3021 ).host_request_id 

3022 

3023 with session_scope() as session: 

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

3025 queue_item = session.execute( 

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

3027 ).scalar_one() 

3028 assert queue_item.resolved_by_log_id is None 

3029 

3030 with real_moderation_session(super_token) as api: 

3031 api.SetUserContentVisibility( 

3032 moderation_pb2.SetUserContentVisibilityReq( 

3033 user_id=surfer.id, 

3034 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3035 ) 

3036 ) 

3037 

3038 with session_scope() as session: 

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

3040 queue_item = session.execute( 

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

3042 ).scalar_one() 

3043 assert queue_item.resolved_by_log_id is not None 

3044 

3045 

3046def test_SetUserContentVisibility_noop_when_matches(db): 

3047 super_user, super_token = generate_user(is_superuser=True) 

3048 surfer, surfer_token = generate_user() 

3049 host, _ = generate_user() 

3050 

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

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

3053 with requests_session(surfer_token) as api: 

3054 api.CreateHostRequest( 

3055 requests_pb2.CreateHostRequestReq( 

3056 host_user_id=host.id, 

3057 from_date=today_plus_2, 

3058 to_date=today_plus_3, 

3059 text=valid_request_text(), 

3060 ) 

3061 ) 

3062 

3063 with session_scope() as session: 

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

3065 

3066 with real_moderation_session(super_token) as api: 

3067 res = api.SetUserContentVisibility( 

3068 moderation_pb2.SetUserContentVisibilityReq( 

3069 user_id=surfer.id, 

3070 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3071 ) 

3072 ) 

3073 assert res.updated_count == 0 

3074 

3075 with session_scope() as session: 

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

3077 assert log_count_after == log_count_before 

3078 

3079 

3080def test_SetUserContentVisibility_unspecified_rejected(db): 

3081 super_user, super_token = generate_user(is_superuser=True) 

3082 target, _ = generate_user() 

3083 

3084 with real_moderation_session(super_token) as api: 

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

3086 api.SetUserContentVisibility( 

3087 moderation_pb2.SetUserContentVisibilityReq( 

3088 user_id=target.id, 

3089 visibility=moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED, 

3090 ) 

3091 ) 

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

3093 

3094 

3095def test_SetUserContentVisibility_non_admin_rejected(db): 

3096 normal_user, normal_token = generate_user() 

3097 target, _ = generate_user() 

3098 

3099 with real_moderation_session(normal_token) as api: 

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

3101 api.SetUserContentVisibility( 

3102 moderation_pb2.SetUserContentVisibilityReq( 

3103 user_id=target.id, 

3104 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3105 ) 

3106 ) 

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

3108 

3109 

3110def test_SetUserContentVisibility_writes_admin_action(db): 

3111 super_user, super_token = generate_user(is_superuser=True) 

3112 surfer, surfer_token = generate_user() 

3113 host, _ = generate_user() 

3114 

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

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

3117 with requests_session(surfer_token) as api: 

3118 api.CreateHostRequest( 

3119 requests_pb2.CreateHostRequestReq( 

3120 host_user_id=host.id, 

3121 from_date=today_plus_2, 

3122 to_date=today_plus_3, 

3123 text=valid_request_text(), 

3124 ) 

3125 ) 

3126 

3127 with real_moderation_session(super_token) as api: 

3128 api.SetUserContentVisibility( 

3129 moderation_pb2.SetUserContentVisibilityReq( 

3130 user_id=surfer.id, 

3131 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3132 reason="bulk hide", 

3133 ) 

3134 ) 

3135 

3136 with session_scope() as session: 

3137 actions = ( 

3138 session.execute( 

3139 select(AdminAction) 

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

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

3142 ) 

3143 .scalars() 

3144 .all() 

3145 ) 

3146 assert len(actions) == 1 

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

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

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

3150 

3151 

3152def test_SetUserContentVisibility_only_touches_target(db): 

3153 super_user, super_token = generate_user(is_superuser=True) 

3154 target, target_token = generate_user() 

3155 other, other_token = generate_user() 

3156 host, _ = generate_user() 

3157 

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

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

3160 

3161 with requests_session(target_token) as api: 

3162 target_hr_id = api.CreateHostRequest( 

3163 requests_pb2.CreateHostRequestReq( 

3164 host_user_id=host.id, 

3165 from_date=today_plus_2, 

3166 to_date=today_plus_3, 

3167 text=valid_request_text(), 

3168 ) 

3169 ).host_request_id 

3170 

3171 with requests_session(other_token) as api: 

3172 other_hr_id = api.CreateHostRequest( 

3173 requests_pb2.CreateHostRequestReq( 

3174 host_user_id=host.id, 

3175 from_date=today_plus_2, 

3176 to_date=today_plus_3, 

3177 text=valid_request_text(), 

3178 ) 

3179 ).host_request_id 

3180 

3181 with real_moderation_session(super_token) as api: 

3182 res = api.SetUserContentVisibility( 

3183 moderation_pb2.SetUserContentVisibilityReq( 

3184 user_id=target.id, 

3185 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3186 ) 

3187 ) 

3188 assert res.updated_count == 1 

3189 

3190 with session_scope() as session: 

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

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

3193 assert target_state.visibility == ModerationVisibility.hidden 

3194 assert other_state.visibility == ModerationVisibility.shadowed 

3195 

3196 

3197def test_SetUserContentVisibility_user_not_found(db): 

3198 super_user, super_token = generate_user(is_superuser=True) 

3199 

3200 with real_moderation_session(super_token) as api: 

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

3202 api.SetUserContentVisibility( 

3203 moderation_pb2.SetUserContentVisibilityReq( 

3204 user_id=999999, 

3205 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3206 ) 

3207 ) 

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

3209 

3210 

3211def test_SetUserContentVisibility_from_visibility_filter(db): 

3212 super_user, super_token = generate_user(is_superuser=True) 

3213 surfer, surfer_token = generate_user() 

3214 host, _ = generate_user() 

3215 

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

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

3218 with requests_session(surfer_token) as api: 

3219 hr_id = api.CreateHostRequest( 

3220 requests_pb2.CreateHostRequestReq( 

3221 host_user_id=host.id, 

3222 from_date=today_plus_2, 

3223 to_date=today_plus_3, 

3224 text=valid_request_text(), 

3225 ) 

3226 ).host_request_id 

3227 

3228 with real_moderation_session(super_token) as api: 

3229 api.SetUserContentVisibility( 

3230 moderation_pb2.SetUserContentVisibilityReq( 

3231 user_id=surfer.id, 

3232 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3233 ) 

3234 ) 

3235 

3236 with real_moderation_session(super_token) as api: 

3237 res = api.SetUserContentVisibility( 

3238 moderation_pb2.SetUserContentVisibilityReq( 

3239 user_id=surfer.id, 

3240 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3241 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_SHADOWED], 

3242 ) 

3243 ) 

3244 assert res.updated_count == 0 

3245 

3246 with session_scope() as session: 

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

3248 assert state.visibility == ModerationVisibility.visible 

3249 

3250 with real_moderation_session(super_token) as api: 

3251 res = api.SetUserContentVisibility( 

3252 moderation_pb2.SetUserContentVisibilityReq( 

3253 user_id=surfer.id, 

3254 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3255 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_VISIBLE], 

3256 ) 

3257 ) 

3258 assert res.updated_count == 1 

3259 

3260 with session_scope() as session: 

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

3262 assert state.visibility == ModerationVisibility.shadowed 

3263 

3264 

3265def test_SetUserContentVisibility_from_visibility_multi(db): 

3266 super_user, super_token = generate_user(is_superuser=True) 

3267 surfer, surfer_token = generate_user() 

3268 host1, _ = generate_user() 

3269 host2, _ = generate_user() 

3270 

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

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

3273 with requests_session(surfer_token) as api: 

3274 hr1_id = api.CreateHostRequest( 

3275 requests_pb2.CreateHostRequestReq( 

3276 host_user_id=host1.id, 

3277 from_date=today_plus_2, 

3278 to_date=today_plus_3, 

3279 text=valid_request_text(), 

3280 ) 

3281 ).host_request_id 

3282 hr2_id = api.CreateHostRequest( 

3283 requests_pb2.CreateHostRequestReq( 

3284 host_user_id=host2.id, 

3285 from_date=today_plus_2, 

3286 to_date=today_plus_3, 

3287 text=valid_request_text(), 

3288 ) 

3289 ).host_request_id 

3290 

3291 with session_scope() as session: 

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

3293 

3294 with real_moderation_session(super_token) as api: 

3295 api.ModerateContent( 

3296 moderation_pb2.ModerateContentReq( 

3297 moderation_state_id=state1_id, 

3298 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

3299 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3300 ) 

3301 ) 

3302 

3303 with real_moderation_session(super_token) as api: 

3304 res = api.SetUserContentVisibility( 

3305 moderation_pb2.SetUserContentVisibilityReq( 

3306 user_id=surfer.id, 

3307 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3308 from_visibility=[ 

3309 moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3310 moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3311 ], 

3312 ) 

3313 ) 

3314 assert res.updated_count == 2 

3315 

3316 with session_scope() as session: 

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

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

3319 assert state1.visibility == ModerationVisibility.hidden 

3320 assert state2.visibility == ModerationVisibility.hidden 

3321 

3322 

3323def test_SetUserContentVisibility_from_visibility_empty_is_any(db): 

3324 super_user, super_token = generate_user(is_superuser=True) 

3325 surfer, surfer_token = generate_user() 

3326 host, _ = generate_user() 

3327 

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

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

3330 with requests_session(surfer_token) as api: 

3331 hr_id = api.CreateHostRequest( 

3332 requests_pb2.CreateHostRequestReq( 

3333 host_user_id=host.id, 

3334 from_date=today_plus_2, 

3335 to_date=today_plus_3, 

3336 text=valid_request_text(), 

3337 ) 

3338 ).host_request_id 

3339 

3340 with real_moderation_session(super_token) as api: 

3341 res = api.SetUserContentVisibility( 

3342 moderation_pb2.SetUserContentVisibilityReq( 

3343 user_id=surfer.id, 

3344 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3345 from_visibility=[], 

3346 ) 

3347 ) 

3348 assert res.updated_count == 1 

3349 

3350 with session_scope() as session: 

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

3352 assert state.visibility == ModerationVisibility.hidden 

3353 

3354 

3355def test_SetUserContentVisibility_from_visibility_unspecified_rejected(db): 

3356 super_user, super_token = generate_user(is_superuser=True) 

3357 target, _ = generate_user() 

3358 

3359 with real_moderation_session(super_token) as api: 

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

3361 api.SetUserContentVisibility( 

3362 moderation_pb2.SetUserContentVisibilityReq( 

3363 user_id=target.id, 

3364 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3365 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED], 

3366 ) 

3367 ) 

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

3369 

3370 

3371def test_ListModerationStates_empty(db): 

3372 super_user, super_token = generate_user(is_superuser=True) 

3373 

3374 with real_moderation_session(super_token) as api: 

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

3376 assert len(res.moderation_states) == 0 

3377 assert res.next_page_token == "" 

3378 

3379 

3380def test_ListModerationStates_returns_states_chronologically(db): 

3381 super_user, super_token = generate_user(is_superuser=True) 

3382 surfer, surfer_token = generate_user() 

3383 host, _ = generate_user() 

3384 

3385 state1_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3386 state2_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3387 state3_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3388 

3389 with real_moderation_session(super_token) as api: 

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

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

3392 

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

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

3395 

3396 

3397def test_ListModerationStates_filter_by_author(db): 

3398 super_user, super_token = generate_user(is_superuser=True) 

3399 surfer1, surfer1_token = generate_user() 

3400 surfer2, surfer2_token = generate_user() 

3401 host, _ = generate_user() 

3402 

3403 state1_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3404 state2_id = create_test_host_request_with_moderation(surfer2_token, host.id) 

3405 state3_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3406 

3407 with real_moderation_session(super_token) as api: 

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

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

3410 

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

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

3413 

3414 

3415def test_ListModerationStates_pagination(db): 

3416 super_user, super_token = generate_user(is_superuser=True) 

3417 surfer, surfer_token = generate_user() 

3418 host, _ = generate_user() 

3419 

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

3421 

3422 with real_moderation_session(super_token) as api: 

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

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

3425 assert res.next_page_token != "" 

3426 

3427 res2 = api.ListModerationStates( 

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

3429 ) 

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

3431 assert res2.next_page_token == ""