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

1436 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-23 22:46 +0000

1""" 

2Comprehensive tests for the Unified Moderation System (UMS) 

3""" 

4 

5from datetime import datetime, timedelta 

6 

7import grpc 

8import pytest 

9from google.protobuf import empty_pb2 

10from sqlalchemy.sql import select 

11 

12from couchers.config import config 

13from couchers.constants import MODERATION_AUTO_APPROVE_FLAG_PRIORITY 

14from couchers.db import session_scope 

15from couchers.jobs.handlers import auto_approve_moderation_queue 

16from couchers.jobs.worker import process_job 

17from couchers.models import ( 

18 AdminAction, 

19 EventOccurrence, 

20 FriendRelationship, 

21 GroupChat, 

22 HostRequest, 

23 ModerationAction, 

24 ModerationLog, 

25 ModerationObjectType, 

26 ModerationQueueItem, 

27 ModerationState, 

28 ModerationTrigger, 

29 ModerationVisibility, 

30 User, 

31) 

32from couchers.moderation.utils import create_moderation 

33from couchers.proto import ( 

34 api_pb2, 

35 conversations_pb2, 

36 events_pb2, 

37 messages_pb2, 

38 moderation_pb2, 

39 notifications_pb2, 

40 requests_pb2, 

41) 

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

43from tests.fixtures.db import generate_user, make_friends 

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

45from tests.fixtures.sessions import ( 

46 api_session, 

47 conversations_session, 

48 events_session, 

49 notifications_session, 

50 real_moderation_session, 

51 requests_session, 

52) 

53from tests.test_communities import create_community 

54from tests.test_requests import valid_request_text 

55 

56 

57@pytest.fixture(autouse=True) 

58def _(testconfig): 

59 pass 

60 

61 

62def create_test_host_request_with_moderation(surfer_token, host_user_id): 

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

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

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

66 

67 with requests_session(surfer_token) as api: 

68 hr_id = api.CreateHostRequest( 

69 requests_pb2.CreateHostRequestReq( 

70 host_user_id=host_user_id, 

71 from_date=today_plus_2, 

72 to_date=today_plus_3, 

73 text=valid_request_text(), 

74 ) 

75 ).host_request_id 

76 

77 with session_scope() as session: 

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

79 return hr.moderation_state_id 

80 

81 

82# ============================================================================ 

83# Tests for moderation helper functions 

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

85 

86 

87def test_create_moderation(db): 

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

89 user, _ = generate_user() 

90 

91 with session_scope() as session: 

92 # Create a moderation state 

93 moderation_state = create_moderation( 

94 session=session, 

95 object_type=ModerationObjectType.host_request, 

96 object_id=123, 

97 creator_user_id=user.id, 

98 ) 

99 

100 assert moderation_state.object_type == ModerationObjectType.host_request 

101 assert moderation_state.object_id == 123 

102 assert moderation_state.visibility == ModerationVisibility.shadowed 

103 

104 # Check that log entry was created 

105 log_entries = ( 

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

107 .scalars() 

108 .all() 

109 ) 

110 

111 assert len(log_entries) == 1 

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

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

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

115 

116 

117def test_add_to_moderation_queue(db): 

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

119 super_user, super_token = generate_user(is_superuser=True) 

120 user1, token1 = generate_user() 

121 user2, _ = generate_user() 

122 

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

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

125 

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

127 with requests_session(token1) as api: 

128 host_request_id = api.CreateHostRequest( 

129 requests_pb2.CreateHostRequestReq( 

130 host_user_id=user2.id, 

131 from_date=today_plus_2, 

132 to_date=today_plus_3, 

133 text=valid_request_text(), 

134 ) 

135 ).host_request_id 

136 

137 # Get the moderation state ID 

138 state_id = None 

139 with session_scope() as session: 

140 host_request = session.execute( 

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

142 ).scalar_one() 

143 state_id = host_request.moderation_state_id 

144 

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

146 with real_moderation_session(super_token) as api: 

147 api.ModerateContent( 

148 moderation_pb2.ModerateContentReq( 

149 moderation_state_id=state_id, 

150 action=moderation_pb2.MODERATION_ACTION_FLAG, 

151 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

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

153 ) 

154 ) 

155 

156 with session_scope() as session: 

157 flag = session.execute( 

158 select(ModerationQueueItem) 

159 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

161 ).scalar_one() 

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

163 assert flag.resolved_by_log_id is None 

164 

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

166 flag_log = session.execute( 

167 select(ModerationLog) 

168 .where(ModerationLog.moderation_state_id == state_id) 

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

170 ).scalar_one() 

171 assert flag_log.queue_item_id == flag.id 

172 

173 

174def test_moderate_content(db): 

175 """Test moderating content via API""" 

176 super_user, super_token = generate_user(is_superuser=True) 

177 user, token = generate_user() 

178 host, _ = generate_user() 

179 

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

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

182 

183 # Create a real host request 

184 state_id = None 

185 with requests_session(token) as api: 

186 hr_id = api.CreateHostRequest( 

187 requests_pb2.CreateHostRequestReq( 

188 host_user_id=host.id, 

189 from_date=today_plus_2, 

190 to_date=today_plus_3, 

191 text=valid_request_text(), 

192 ) 

193 ).host_request_id 

194 

195 with session_scope() as session: 

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

197 state_id = hr.moderation_state_id 

198 

199 # Moderate the content via API 

200 with real_moderation_session(super_token) as api: 

201 res = api.ModerateContent( 

202 moderation_pb2.ModerateContentReq( 

203 moderation_state_id=state_id, 

204 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

205 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

206 reason="Content looks good", 

207 ) 

208 ) 

209 

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

211 

212 # Check that state was updated in database 

213 with session_scope() as session: 

214 updated_state = session.get_one(ModerationState, state_id) 

215 assert updated_state.visibility == ModerationVisibility.visible 

216 

217 # Check that log entry was created 

218 log_entries = ( 

219 session.execute( 

220 select(ModerationLog) 

221 .where(ModerationLog.moderation_state_id == state_id) 

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

223 ) 

224 .scalars() 

225 .all() 

226 ) 

227 

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

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

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

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

232 

233 

234def test_resolve_queue_item(db): 

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

236 user1, token1 = generate_user() 

237 user2, _ = generate_user() 

238 moderator, moderator_token = generate_user(is_superuser=True) 

239 

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

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

242 

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

244 with requests_session(token1) as api: 

245 host_request_id = api.CreateHostRequest( 

246 requests_pb2.CreateHostRequestReq( 

247 host_user_id=user2.id, 

248 from_date=today_plus_2, 

249 to_date=today_plus_3, 

250 text=valid_request_text(), 

251 ) 

252 ).host_request_id 

253 

254 state_id = None 

255 with session_scope() as session: 

256 # Get the host request and its moderation state 

257 host_request = session.execute( 

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

259 ).scalar_one() 

260 state_id = host_request.moderation_state_id 

261 

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

263 queue_item = session.execute( 

264 select(ModerationQueueItem) 

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

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

267 ).scalar_one() 

268 

269 assert queue_item.resolved_by_log_id is None 

270 

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

272 with real_moderation_session(moderator_token) as api: 

273 api.ModerateContent( 

274 moderation_pb2.ModerateContentReq( 

275 moderation_state_id=state_id, 

276 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

277 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

278 reason="Approved after review", 

279 clear_flags=True, 

280 ) 

281 ) 

282 

283 # Check that queue item was resolved 

284 with session_scope() as session: 

285 queue_item = session.execute( 

286 select(ModerationQueueItem) 

287 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

289 ).scalar_one() 

290 assert queue_item.resolved_by_log_id is not None 

291 

292 

293def test_approve_content_via_api(db): 

294 """Test approving content via ModerateContent API""" 

295 user1, token1 = generate_user() 

296 user2, _ = generate_user() 

297 moderator, moderator_token = generate_user(is_superuser=True) 

298 

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

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

301 

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

303 with requests_session(token1) as api: 

304 host_request_id = api.CreateHostRequest( 

305 requests_pb2.CreateHostRequestReq( 

306 host_user_id=user2.id, 

307 from_date=today_plus_2, 

308 to_date=today_plus_3, 

309 text=valid_request_text(), 

310 ) 

311 ).host_request_id 

312 

313 state_id = None 

314 with session_scope() as session: 

315 # Get the host request and its moderation state 

316 host_request = session.execute( 

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

318 ).scalar_one() 

319 state_id = host_request.moderation_state_id 

320 

321 # Approve via API 

322 with real_moderation_session(moderator_token) as api: 

323 api.ModerateContent( 

324 moderation_pb2.ModerateContentReq( 

325 moderation_state_id=state_id, 

326 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

327 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

328 reason="Quick approval", 

329 ) 

330 ) 

331 

332 # Check that state was updated to VISIBLE 

333 with session_scope() as session: 

334 updated_state = session.get_one(ModerationState, state_id) 

335 assert updated_state.visibility == ModerationVisibility.visible 

336 

337 # Check log entry 

338 log_entry = session.execute( 

339 select(ModerationLog) 

340 .where(ModerationLog.moderation_state_id == state_id) 

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

342 ).scalar_one() 

343 

344 assert log_entry.moderator_user_id == moderator.id 

345 assert log_entry.reason == "Quick approval" 

346 

347 

348# ============================================================================ 

349# Tests for host request moderation integration 

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

351 

352 

353def test_create_host_request_creates_moderation_state(db): 

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

355 user1, token1 = generate_user() 

356 user2, token2 = generate_user() 

357 

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

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

360 

361 with requests_session(token1) as api: 

362 host_request_id = api.CreateHostRequest( 

363 requests_pb2.CreateHostRequestReq( 

364 host_user_id=user2.id, 

365 from_date=today_plus_2, 

366 to_date=today_plus_3, 

367 text=valid_request_text(), 

368 ) 

369 ).host_request_id 

370 

371 with session_scope() as session: 

372 # Check that host request has a moderation state 

373 host_request = session.execute( 

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

375 ).scalar_one() 

376 

377 # Check moderation state properties 

378 moderation_state = session.execute( 

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

380 ).scalar_one() 

381 

382 assert moderation_state.object_type == ModerationObjectType.host_request 

383 assert moderation_state.object_id == host_request_id 

384 assert moderation_state.visibility == ModerationVisibility.shadowed 

385 

386 # Check that it was added to moderation queue 

387 queue_items = ( 

388 session.execute( 

389 select(ModerationQueueItem) 

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

391 .where(ModerationQueueItem.resolved_by_log_id == None) 

392 ) 

393 .scalars() 

394 .all() 

395 ) 

396 

397 assert len(queue_items) == 1 

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

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

400 

401 

402def test_host_request_no_notification_before_approval(db, push_collector: PushCollector): 

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

404 user1, token1 = generate_user() 

405 user2, token2 = generate_user() 

406 

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

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

409 

410 with requests_session(token1) as api: 

411 host_request_id = api.CreateHostRequest( 

412 requests_pb2.CreateHostRequestReq( 

413 host_user_id=user2.id, 

414 from_date=today_plus_2, 

415 to_date=today_plus_3, 

416 text=valid_request_text(), 

417 ) 

418 ).host_request_id 

419 

420 # Process all jobs (including the notification job) 

421 process_jobs() 

422 

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

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

425 

426 

427def test_shadowed_notification_not_in_list_notifications(db): 

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

429 user1, token1 = generate_user() 

430 user2, token2 = generate_user() 

431 

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

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

434 

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

436 with requests_session(token1) as api: 

437 host_request_id = api.CreateHostRequest( 

438 requests_pb2.CreateHostRequestReq( 

439 host_user_id=user2.id, 

440 from_date=today_plus_2, 

441 to_date=today_plus_3, 

442 text=valid_request_text(), 

443 ) 

444 ).host_request_id 

445 

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

447 with notifications_session(token2) as api: 

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

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

450 assert len(res.notifications) == 0 

451 

452 

453def test_notification_visible_after_approval(db): 

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

455 user1, token1 = generate_user() 

456 user2, token2 = generate_user() 

457 mod, mod_token = generate_user(is_superuser=True) 

458 

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

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

461 

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

463 with requests_session(token1) as api: 

464 host_request_id = api.CreateHostRequest( 

465 requests_pb2.CreateHostRequestReq( 

466 host_user_id=user2.id, 

467 from_date=today_plus_2, 

468 to_date=today_plus_3, 

469 text=valid_request_text(), 

470 ) 

471 ).host_request_id 

472 

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

474 with notifications_session(token2) as api: 

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

476 assert len(res.notifications) == 0 

477 

478 # Get the moderation state ID and approve 

479 with session_scope() as session: 

480 host_request = session.execute( 

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

482 ).scalar_one() 

483 state_id = host_request.moderation_state_id 

484 

485 with real_moderation_session(mod_token) as api: 

486 api.ModerateContent( 

487 moderation_pb2.ModerateContentReq( 

488 moderation_state_id=state_id, 

489 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

490 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

491 reason="Looks good", 

492 ) 

493 ) 

494 

495 # Now host SHOULD see the notification 

496 with notifications_session(token2) as api: 

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

498 assert len(res.notifications) == 1 

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

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

501 

502 

503def test_shadowed_host_request_visible_to_author_only(db): 

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

505 user1, token1 = generate_user() 

506 user2, token2 = generate_user() 

507 

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

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

510 

511 with requests_session(token1) as api: 

512 host_request_id = api.CreateHostRequest( 

513 requests_pb2.CreateHostRequestReq( 

514 host_user_id=user2.id, 

515 from_date=today_plus_2, 

516 to_date=today_plus_3, 

517 text=valid_request_text(), 

518 ) 

519 ).host_request_id 

520 

521 # Surfer (author) can see it with GetHostRequest 

522 with requests_session(token1) as api: 

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

524 assert res.host_request_id == host_request_id 

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

526 

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

528 with requests_session(token2) as api: 

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

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

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

532 

533 

534def test_unlisted_host_request_not_in_lists(db): 

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

536 user1, token1 = generate_user() 

537 user2, token2 = generate_user() 

538 

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

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

541 

542 with requests_session(token1) as api: 

543 host_request_id = api.CreateHostRequest( 

544 requests_pb2.CreateHostRequestReq( 

545 host_user_id=user2.id, 

546 from_date=today_plus_2, 

547 to_date=today_plus_3, 

548 text=valid_request_text(), 

549 ) 

550 ).host_request_id 

551 

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

553 with requests_session(token1) as api: 

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

555 assert len(res.host_requests) == 1 

556 

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

558 with requests_session(token2) as api: 

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

560 assert len(res.host_requests) == 0 

561 

562 

563def test_approved_host_request_in_lists_and_notifications(db, push_collector: PushCollector): 

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

565 user1, token1 = generate_user() 

566 user2, token2 = generate_user() 

567 mod, mod_token = generate_user(is_superuser=True) 

568 

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

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

571 

572 with requests_session(token1) as api: 

573 host_request_id = api.CreateHostRequest( 

574 requests_pb2.CreateHostRequestReq( 

575 host_user_id=user2.id, 

576 from_date=today_plus_2, 

577 to_date=today_plus_3, 

578 text=valid_request_text(), 

579 ) 

580 ).host_request_id 

581 

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

583 process_jobs() 

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

585 

586 # Get the moderation state ID 

587 state_id = None 

588 with session_scope() as session: 

589 host_request = session.execute( 

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

591 ).scalar_one() 

592 state_id = host_request.moderation_state_id 

593 

594 # Approve the host request via API 

595 with real_moderation_session(mod_token) as api: 

596 api.ModerateContent( 

597 moderation_pb2.ModerateContentReq( 

598 moderation_state_id=state_id, 

599 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

600 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

601 reason="Looks good", 

602 ) 

603 ) 

604 

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

606 process_jobs() 

607 

608 # Now surfer SHOULD see it in their sent list 

609 with requests_session(token1) as api: 

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

611 assert len(res.host_requests) == 1 

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

613 

614 # Host SHOULD see it in their received list 

615 with requests_session(token2) as api: 

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

617 assert len(res.host_requests) == 1 

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

619 

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

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

622 

623 

624def test_hidden_host_request_invisible_to_all(db): 

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

626 user1, token1 = generate_user() 

627 user2, token2 = generate_user() 

628 user3, token3 = generate_user() # Third party 

629 moderator, moderator_token = generate_user(is_superuser=True) 

630 

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

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

633 

634 with requests_session(token1) as api: 

635 host_request_id = api.CreateHostRequest( 

636 requests_pb2.CreateHostRequestReq( 

637 host_user_id=user2.id, 

638 from_date=today_plus_2, 

639 to_date=today_plus_3, 

640 text=valid_request_text(), 

641 ) 

642 ).host_request_id 

643 

644 # Get the moderation state ID 

645 state_id = None 

646 with session_scope() as session: 

647 host_request = session.execute( 

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

649 ).scalar_one() 

650 state_id = host_request.moderation_state_id 

651 

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

653 with real_moderation_session(moderator_token) as api: 

654 api.ModerateContent( 

655 moderation_pb2.ModerateContentReq( 

656 moderation_state_id=state_id, 

657 action=moderation_pb2.MODERATION_ACTION_HIDE, 

658 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

659 reason="Spam content", 

660 ) 

661 ) 

662 

663 # Surfer can't see it with GetHostRequest 

664 with requests_session(token1) as api: 

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

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

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

668 

669 # Host can't see it with GetHostRequest 

670 with requests_session(token2) as api: 

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

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

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

674 

675 # Third party definitely can't see it 

676 with requests_session(token3) as api: 

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

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

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

680 

681 # Not in any lists 

682 with requests_session(token1) as api: 

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

684 assert len(res.host_requests) == 0 

685 

686 with requests_session(token2) as api: 

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

688 assert len(res.host_requests) == 0 

689 

690 

691def test_multiple_host_requests_listing_visibility(db): 

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

693 user1, token1 = generate_user() 

694 user2, token2 = generate_user() 

695 moderator, moderator_token = generate_user(is_superuser=True) 

696 

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

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

699 

700 # Create 3 host requests 

701 host_request_ids = [] 

702 state_ids = [] 

703 with requests_session(token1) as api: 

704 for i in range(3): 

705 hr_id = api.CreateHostRequest( 

706 requests_pb2.CreateHostRequestReq( 

707 host_user_id=user2.id, 

708 from_date=today_plus_2, 

709 to_date=today_plus_3, 

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

711 ) 

712 ).host_request_id 

713 host_request_ids.append(hr_id) 

714 

715 # Get state IDs 

716 with session_scope() as session: 

717 for hr_id in host_request_ids: 

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

719 state_ids.append(host_request.moderation_state_id) 

720 

721 # Approve the first one via API 

722 with real_moderation_session(moderator_token) as api: 

723 api.ModerateContent( 

724 moderation_pb2.ModerateContentReq( 

725 moderation_state_id=state_ids[0], 

726 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

727 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

728 reason="Approved", 

729 ) 

730 ) 

731 

732 # Hide the third one via API 

733 with real_moderation_session(moderator_token) as api: 

734 api.ModerateContent( 

735 moderation_pb2.ModerateContentReq( 

736 moderation_state_id=state_ids[2], 

737 action=moderation_pb2.MODERATION_ACTION_HIDE, 

738 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

739 reason="Spam", 

740 ) 

741 ) 

742 

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

744 with requests_session(token1) as api: 

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

746 assert len(res.host_requests) == 2 

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

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

749 

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

751 with requests_session(token2) as api: 

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

753 assert len(res.host_requests) == 1 

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

755 

756 

757def test_moderation_log_tracking(db): 

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

759 user, user_token = generate_user() 

760 host, _ = generate_user() 

761 moderator1, moderator1_token = generate_user(is_superuser=True) 

762 moderator2, moderator2_token = generate_user(is_superuser=True) 

763 

764 # Create a real host request 

765 state_id = create_test_host_request_with_moderation(user_token, host.id) 

766 

767 # Perform several moderation actions via API 

768 with real_moderation_session(moderator1_token) as api: 

769 api.ModerateContent( 

770 moderation_pb2.ModerateContentReq( 

771 moderation_state_id=state_id, 

772 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

773 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

774 reason="Looks good initially", 

775 ) 

776 ) 

777 

778 with real_moderation_session(moderator2_token) as api: 

779 api.ModerateContent( 

780 moderation_pb2.ModerateContentReq( 

781 moderation_state_id=state_id, 

782 action=moderation_pb2.MODERATION_ACTION_FLAG, 

783 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

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

785 ) 

786 ) 

787 # Shadow it back 

788 api.ModerateContent( 

789 moderation_pb2.ModerateContentReq( 

790 moderation_state_id=state_id, 

791 action=moderation_pb2.MODERATION_ACTION_HIDE, 

792 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

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

794 ) 

795 ) 

796 

797 with real_moderation_session(moderator1_token) as api: 

798 api.ModerateContent( 

799 moderation_pb2.ModerateContentReq( 

800 moderation_state_id=state_id, 

801 action=moderation_pb2.MODERATION_ACTION_HIDE, 

802 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

803 reason="Actually it's spam", 

804 ) 

805 ) 

806 

807 # Check all log entries 

808 with session_scope() as session: 

809 log_entries = ( 

810 session.execute( 

811 select(ModerationLog) 

812 .where(ModerationLog.moderation_state_id == state_id) 

813 .order_by(ModerationLog.time.asc()) 

814 ) 

815 .scalars() 

816 .all() 

817 ) 

818 

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

820 assert len(log_entries) >= 3 

821 

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

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

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

825 

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

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

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

829 

830 # The last action should be hiding 

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

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

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

834 

835 

836def test_moderation_queue_workflow(db): 

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

838 user1, token1 = generate_user() 

839 user2, _ = generate_user() 

840 moderator, moderator_token = generate_user(is_superuser=True) 

841 

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

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

844 

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

846 with requests_session(token1) as api: 

847 host_request_id = api.CreateHostRequest( 

848 requests_pb2.CreateHostRequestReq( 

849 host_user_id=user2.id, 

850 from_date=today_plus_2, 

851 to_date=today_plus_3, 

852 text=valid_request_text(), 

853 ) 

854 ).host_request_id 

855 

856 state_id = None 

857 queue_item_id = None 

858 with session_scope() as session: 

859 # Get the host request and its moderation state 

860 host_request = session.execute( 

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

862 ).scalar_one() 

863 state_id = host_request.moderation_state_id 

864 

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

866 queue_item = session.execute( 

867 select(ModerationQueueItem) 

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

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

870 ).scalar_one() 

871 queue_item_id = queue_item.id 

872 

873 # Verify it's in the queue 

874 unresolved_items = ( 

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

876 .scalars() 

877 .all() 

878 ) 

879 

880 assert len(unresolved_items) >= 1 

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

882 

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

884 with real_moderation_session(moderator_token) as api: 

885 api.ModerateContent( 

886 moderation_pb2.ModerateContentReq( 

887 moderation_state_id=state_id, 

888 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

889 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

890 reason="Content approved", 

891 clear_flags=True, 

892 ) 

893 ) 

894 

895 # Verify queue item was resolved 

896 with session_scope() as session: 

897 # Verify it's no longer in unresolved queue 

898 unresolved_items = ( 

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

900 .scalars() 

901 .all() 

902 ) 

903 

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

905 

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

907 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

908 assert queue_item.resolved_by_log_id is not None 

909 

910 

911# ============================================================================ 

912# Moderation API Tests (testing the gRPC servicer) 

913# ============================================================================ 

914 

915 

916def test_GetModerationQueue_empty(db): 

917 """Test getting an empty moderation queue""" 

918 super_user, super_token = generate_user(is_superuser=True) 

919 

920 with real_moderation_session(super_token) as api: 

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

922 assert len(res.queue_items) == 0 

923 assert res.next_page_token == "" 

924 

925 

926def test_GetModerationQueue_with_items(db): 

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

928 super_user, super_token = generate_user(is_superuser=True) 

929 normal_user, user_token = generate_user() 

930 host, _ = generate_user() 

931 

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

933 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

934 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

935 

936 with real_moderation_session(super_token) as api: 

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

938 assert len(res.queue_items) == 2 

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

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

941 

942 

943def test_GetModerationQueue_filter_by_trigger(db): 

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

945 super_user, super_token = generate_user(is_superuser=True) 

946 normal_user, user_token = generate_user() 

947 host, _ = generate_user() 

948 

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

950 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

951 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

952 

953 # Add USER_FLAG trigger to second item via API 

954 with real_moderation_session(super_token) as api: 

955 api.ModerateContent( 

956 moderation_pb2.ModerateContentReq( 

957 moderation_state_id=state2_id, 

958 action=moderation_pb2.MODERATION_ACTION_FLAG, 

959 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

960 reason="Reported by user", 

961 ) 

962 ) 

963 

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

965 with real_moderation_session(super_token) as api: 

966 res = api.GetModerationQueue( 

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

968 ) 

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

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

971 

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

973 with real_moderation_session(super_token) as api: 

974 res = api.GetModerationQueue( 

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

976 ) 

977 assert len(res.queue_items) == 1 

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

979 

980 

981def test_GetModerationQueue_filter_created_before(db): 

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

983 super_user, super_token = generate_user(is_superuser=True) 

984 normal_user, user_token = generate_user() 

985 host, _ = generate_user() 

986 

987 # Create host requests 

988 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

989 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

990 

991 # Backdate the first queue item 

992 with session_scope() as session: 

993 queue_item1 = session.execute( 

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

995 ).scalar_one() 

996 # Set it to 2 hours ago 

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

998 

999 # The second item remains at current time 

1000 

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

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

1003 with real_moderation_session(super_token) as api: 

1004 res = api.GetModerationQueue( 

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

1006 ) 

1007 assert len(res.queue_items) == 1 

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

1009 

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

1011 with real_moderation_session(super_token) as api: 

1012 res = api.GetModerationQueue( 

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

1014 ) 

1015 assert len(res.queue_items) == 2 

1016 

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

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

1019 with real_moderation_session(super_token) as api: 

1020 res = api.GetModerationQueue( 

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

1022 ) 

1023 assert len(res.queue_items) == 0 

1024 

1025 

1026def test_GetModerationQueue_filter_created_after(db): 

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

1028 super_user, super_token = generate_user(is_superuser=True) 

1029 normal_user, user_token = generate_user() 

1030 host, _ = generate_user() 

1031 

1032 # Create host requests 

1033 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1034 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1035 

1036 # Backdate the first queue item to 2 hours ago 

1037 with session_scope() as session: 

1038 queue_item1 = session.execute( 

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

1040 ).scalar_one() 

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

1042 

1043 # The second item remains at current time 

1044 

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

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

1047 with real_moderation_session(super_token) as api: 

1048 res = api.GetModerationQueue( 

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

1050 ) 

1051 assert len(res.queue_items) == 1 

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

1053 

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

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

1056 with real_moderation_session(super_token) as api: 

1057 res = api.GetModerationQueue( 

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

1059 ) 

1060 assert len(res.queue_items) == 2 

1061 

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

1063 with real_moderation_session(super_token) as api: 

1064 res = api.GetModerationQueue( 

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

1066 ) 

1067 assert len(res.queue_items) == 0 

1068 

1069 

1070def test_GetModerationQueue_filter_created_before_and_after(db): 

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

1072 super_user, super_token = generate_user(is_superuser=True) 

1073 normal_user, user_token = generate_user() 

1074 host, _ = generate_user() 

1075 

1076 # Create 3 host requests 

1077 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1078 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1079 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1080 

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

1082 with session_scope() as session: 

1083 queue_item1 = session.execute( 

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

1085 ).scalar_one() 

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

1087 

1088 queue_item2 = session.execute( 

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

1090 ).scalar_one() 

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

1092 

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

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

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

1096 with real_moderation_session(super_token) as api: 

1097 res = api.GetModerationQueue( 

1098 moderation_pb2.GetModerationQueueReq( 

1099 created_after=Timestamp_from_datetime(after_cutoff), 

1100 created_before=Timestamp_from_datetime(before_cutoff), 

1101 ) 

1102 ) 

1103 assert len(res.queue_items) == 1 

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

1105 

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

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

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

1109 with real_moderation_session(super_token) as api: 

1110 res = api.GetModerationQueue( 

1111 moderation_pb2.GetModerationQueueReq( 

1112 created_after=Timestamp_from_datetime(after_cutoff), 

1113 created_before=Timestamp_from_datetime(before_cutoff), 

1114 ) 

1115 ) 

1116 assert len(res.queue_items) == 1 

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

1118 

1119 

1120def test_GetModerationQueue_filter_unresolved(db): 

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

1122 super_user, super_token = generate_user(is_superuser=True) 

1123 normal_user, user_token = generate_user() 

1124 host, _ = generate_user() 

1125 

1126 # Create 2 host requests 

1127 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1128 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1129 

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

1131 with real_moderation_session(super_token) as api: 

1132 api.ModerateContent( 

1133 moderation_pb2.ModerateContentReq( 

1134 moderation_state_id=state1_id, 

1135 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1136 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1137 reason="Approved", 

1138 clear_flags=True, 

1139 ) 

1140 ) 

1141 

1142 # Get all items 

1143 with real_moderation_session(super_token) as api: 

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

1145 assert len(res.queue_items) == 2 

1146 

1147 # Get only unresolved items 

1148 with real_moderation_session(super_token) as api: 

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

1150 assert len(res.queue_items) == 1 

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

1152 

1153 

1154def test_GetModerationQueue_filter_by_author(db): 

1155 """Test filtering moderation queue by item_author_user_id""" 

1156 super_user, super_token = generate_user(is_superuser=True) 

1157 user1, token1 = generate_user() 

1158 user2, token2 = generate_user() 

1159 host_user, _ = generate_user() 

1160 

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

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

1163 

1164 # Create 2 host requests by user1 

1165 with requests_session(token1) as api: 

1166 hr1_id = api.CreateHostRequest( 

1167 requests_pb2.CreateHostRequestReq( 

1168 host_user_id=host_user.id, 

1169 from_date=today_plus_2, 

1170 to_date=today_plus_3, 

1171 text=valid_request_text(), 

1172 ) 

1173 ).host_request_id 

1174 

1175 hr2_id = api.CreateHostRequest( 

1176 requests_pb2.CreateHostRequestReq( 

1177 host_user_id=host_user.id, 

1178 from_date=today_plus_2, 

1179 to_date=today_plus_3, 

1180 text=valid_request_text(), 

1181 ) 

1182 ).host_request_id 

1183 

1184 # Create 1 host request by user2 

1185 with requests_session(token2) as api: 

1186 hr3_id = api.CreateHostRequest( 

1187 requests_pb2.CreateHostRequestReq( 

1188 host_user_id=host_user.id, 

1189 from_date=today_plus_2, 

1190 to_date=today_plus_3, 

1191 text=valid_request_text(), 

1192 ) 

1193 ).host_request_id 

1194 

1195 # Get moderation state IDs 

1196 state1_id, state2_id, state3_id = None, None, None 

1197 with session_scope() as session: 

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

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

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

1201 state1_id = hr1.moderation_state_id 

1202 state2_id = hr2.moderation_state_id 

1203 state3_id = hr3.moderation_state_id 

1204 

1205 # Get all items (should be 3) 

1206 with real_moderation_session(super_token) as api: 

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

1208 assert len(res.queue_items) == 3 

1209 

1210 # Filter by user1 (should get 2) 

1211 with real_moderation_session(super_token) as api: 

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

1213 assert len(res.queue_items) == 2 

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

1215 

1216 # Filter by user2 (should get 1) 

1217 with real_moderation_session(super_token) as api: 

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

1219 assert len(res.queue_items) == 1 

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

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

1222 

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

1224 with real_moderation_session(super_token) as api: 

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

1226 assert len(res.queue_items) == 0 

1227 

1228 

1229def test_GetModerationQueue_ordering(db): 

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

1231 super_user, super_token = generate_user(is_superuser=True) 

1232 normal_user, user_token = generate_user() 

1233 host, _ = generate_user() 

1234 

1235 # Create 3 host requests 

1236 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1237 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1238 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1239 

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

1241 with session_scope() as session: 

1242 queue_item1 = session.execute( 

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

1244 ).scalar_one() 

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

1246 

1247 queue_item2 = session.execute( 

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

1249 ).scalar_one() 

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

1251 

1252 queue_item3 = session.execute( 

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

1254 ).scalar_one() 

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

1256 

1257 # Default order (oldest first) 

1258 with real_moderation_session(super_token) as api: 

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

1260 assert len(res.queue_items) == 3 

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

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

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

1264 

1265 # Explicit oldest first 

1266 with real_moderation_session(super_token) as api: 

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

1268 assert len(res.queue_items) == 3 

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

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

1271 

1272 # Newest first 

1273 with real_moderation_session(super_token) as api: 

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

1275 assert len(res.queue_items) == 3 

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

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

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

1279 

1280 

1281def test_GetModerationQueue_pagination_newest_first(db): 

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

1283 super_user, super_token = generate_user(is_superuser=True) 

1284 normal_user, normal_token = generate_user() 

1285 host_user, _ = generate_user() 

1286 

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

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

1289 

1290 # Create 5 host requests 

1291 hr_ids = [] 

1292 with requests_session(normal_token) as api: 

1293 for i in range(5): 

1294 hr_id = api.CreateHostRequest( 

1295 requests_pb2.CreateHostRequestReq( 

1296 host_user_id=host_user.id, 

1297 from_date=today_plus_2, 

1298 to_date=today_plus_3, 

1299 text=valid_request_text(), 

1300 ) 

1301 ).host_request_id 

1302 hr_ids.append(hr_id) 

1303 

1304 # Get moderation state IDs 

1305 state_ids = [] 

1306 with session_scope() as session: 

1307 for hr_id in hr_ids: 

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

1309 state_ids.append(hr.moderation_state_id) 

1310 

1311 # Set different times so ordering is deterministic 

1312 with session_scope() as session: 

1313 for i, state_id in enumerate(state_ids): 

1314 queue_item = session.execute( 

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

1316 ).scalar_one() 

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

1318 

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

1320 with real_moderation_session(super_token) as api: 

1321 res1 = api.GetModerationQueue( 

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

1323 ) 

1324 assert len(res1.queue_items) == 2 

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

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

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

1328 assert res1.next_page_token # should have more pages 

1329 

1330 # Get second page using the token 

1331 res2 = api.GetModerationQueue( 

1332 moderation_pb2.GetModerationQueueReq( 

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

1334 ) 

1335 ) 

1336 assert len(res2.queue_items) == 2 

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

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

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

1340 

1341 # Pages should not overlap 

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

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

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

1345 

1346 

1347def test_GetModerationLog(db): 

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

1349 super_user, super_token = generate_user(is_superuser=True) 

1350 moderator, moderator_token = generate_user(is_superuser=True) 

1351 normal_user, user_token = generate_user() 

1352 host, _ = generate_user() 

1353 

1354 # Create a real host request 

1355 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1356 

1357 # Perform a moderation action via API 

1358 with real_moderation_session(moderator_token) as api: 

1359 api.ModerateContent( 

1360 moderation_pb2.ModerateContentReq( 

1361 moderation_state_id=state_id, 

1362 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1363 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1364 reason="Looks good", 

1365 ) 

1366 ) 

1367 

1368 with real_moderation_session(super_token) as api: 

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

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

1371 assert res.moderation_state.moderation_state_id == state_id 

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

1373 # Log entries are in reverse chronological order 

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

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

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

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

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

1379 

1380 

1381def test_GetModerationLog_not_found(db): 

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

1383 super_user, super_token = generate_user(is_superuser=True) 

1384 

1385 with real_moderation_session(super_token) as api: 

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

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

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

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

1390 

1391 

1392def test_GetModerationState(db): 

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

1394 super_user, super_token = generate_user(is_superuser=True) 

1395 user1, token1 = generate_user() 

1396 user2, _ = generate_user() 

1397 

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

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

1400 

1401 with requests_session(token1) as api: 

1402 host_request_id = api.CreateHostRequest( 

1403 requests_pb2.CreateHostRequestReq( 

1404 host_user_id=user2.id, 

1405 from_date=today_plus_2, 

1406 to_date=today_plus_3, 

1407 text=valid_request_text(), 

1408 ) 

1409 ).host_request_id 

1410 

1411 with real_moderation_session(super_token) as api: 

1412 res = api.GetModerationState( 

1413 moderation_pb2.GetModerationStateReq( 

1414 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1415 object_id=host_request_id, 

1416 ) 

1417 ) 

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

1419 assert res.moderation_state.object_id == host_request_id 

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

1421 assert res.moderation_state.moderation_state_id > 0 

1422 

1423 

1424def test_GetModerationState_not_found(db): 

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

1426 super_user, super_token = generate_user(is_superuser=True) 

1427 

1428 with real_moderation_session(super_token) as api: 

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

1430 api.GetModerationState( 

1431 moderation_pb2.GetModerationStateReq( 

1432 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1433 object_id=999999, 

1434 ) 

1435 ) 

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

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

1438 

1439 

1440def test_GetModerationState_unspecified_type(db): 

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

1442 super_user, super_token = generate_user(is_superuser=True) 

1443 

1444 with real_moderation_session(super_token) as api: 

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

1446 api.GetModerationState( 

1447 moderation_pb2.GetModerationStateReq( 

1448 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_UNSPECIFIED, 

1449 object_id=123, 

1450 ) 

1451 ) 

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

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

1454 

1455 

1456def test_ModerateContent_approve(db): 

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

1458 super_user, super_token = generate_user(is_superuser=True) 

1459 user1, token1 = generate_user() 

1460 user2, _ = generate_user() 

1461 

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

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

1464 

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

1466 with requests_session(token1) as api: 

1467 host_request_id = api.CreateHostRequest( 

1468 requests_pb2.CreateHostRequestReq( 

1469 host_user_id=user2.id, 

1470 from_date=today_plus_2, 

1471 to_date=today_plus_3, 

1472 text=valid_request_text(), 

1473 ) 

1474 ).host_request_id 

1475 

1476 # Get the moderation state ID 

1477 state_id = None 

1478 with session_scope() as session: 

1479 host_request = session.execute( 

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

1481 ).scalar_one() 

1482 state_id = host_request.moderation_state_id 

1483 

1484 with real_moderation_session(super_token) as api: 

1485 res = api.ModerateContent( 

1486 moderation_pb2.ModerateContentReq( 

1487 moderation_state_id=state_id, 

1488 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1489 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1490 reason="Approved by admin", 

1491 ) 

1492 ) 

1493 assert res.moderation_state.moderation_state_id == state_id 

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

1495 

1496 # Verify state was updated in database 

1497 with session_scope() as session: 

1498 state = session.get_one(ModerationState, state_id) 

1499 assert state.visibility == ModerationVisibility.visible 

1500 

1501 

1502def test_ModerateContent_not_found(db): 

1503 """Test moderating non-existent content""" 

1504 super_user, super_token = generate_user(is_superuser=True) 

1505 

1506 with real_moderation_session(super_token) as api: 

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

1508 api.ModerateContent( 

1509 moderation_pb2.ModerateContentReq( 

1510 moderation_state_id=999999, 

1511 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1512 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1513 reason="Test", 

1514 ) 

1515 ) 

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

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

1518 

1519 

1520def test_ModerateContent_hide(db): 

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

1522 super_user, super_token = generate_user(is_superuser=True) 

1523 normal_user, user_token = generate_user() 

1524 host, _ = generate_user() 

1525 

1526 # Create a real host request 

1527 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1528 

1529 with real_moderation_session(super_token) as api: 

1530 res = api.ModerateContent( 

1531 moderation_pb2.ModerateContentReq( 

1532 moderation_state_id=state_id, 

1533 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1534 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1535 reason="Spam content", 

1536 ) 

1537 ) 

1538 assert res.moderation_state.moderation_state_id == state_id 

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

1540 

1541 # Verify state was updated in database 

1542 with session_scope() as session: 

1543 state = session.get_one(ModerationState, state_id) 

1544 assert state.visibility == ModerationVisibility.hidden 

1545 

1546 

1547def test_ModerateContent_shadow(db): 

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

1549 super_user, super_token = generate_user(is_superuser=True) 

1550 normal_user, user_token = generate_user() 

1551 host, _ = generate_user() 

1552 

1553 # Create a real host request 

1554 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1555 

1556 with real_moderation_session(super_token) as api: 

1557 res = api.ModerateContent( 

1558 moderation_pb2.ModerateContentReq( 

1559 moderation_state_id=state_id, 

1560 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1561 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

1562 reason="Needs further review", 

1563 ) 

1564 ) 

1565 assert res.moderation_state.moderation_state_id == state_id 

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

1567 

1568 # Verify state was updated in database 

1569 with session_scope() as session: 

1570 state = session.get_one(ModerationState, state_id) 

1571 assert state.visibility == ModerationVisibility.shadowed 

1572 

1573 

1574def test_ModerateContent_flag(db): 

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

1576 super_user, super_token = generate_user(is_superuser=True) 

1577 user1, token1 = generate_user() 

1578 user2, _ = generate_user() 

1579 

1580 # Create a host request 

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

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

1583 

1584 with requests_session(token1) as api: 

1585 host_request_id = api.CreateHostRequest( 

1586 requests_pb2.CreateHostRequestReq( 

1587 host_user_id=user2.id, 

1588 from_date=today_plus_2, 

1589 to_date=today_plus_3, 

1590 text=valid_request_text(), 

1591 ) 

1592 ).host_request_id 

1593 

1594 # Get the moderation state ID 

1595 with session_scope() as session: 

1596 host_request = session.execute( 

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

1598 ).scalar_one() 

1599 state_id = host_request.moderation_state_id 

1600 

1601 with real_moderation_session(super_token) as api: 

1602 api.ModerateContent( 

1603 moderation_pb2.ModerateContentReq( 

1604 moderation_state_id=state_id, 

1605 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1606 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

1607 reason="Admin flagged for additional review", 

1608 priority=5, 

1609 ) 

1610 ) 

1611 

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

1613 with session_scope() as session: 

1614 queue_item = session.execute( 

1615 select(ModerationQueueItem) 

1616 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

1618 ).scalar_one() 

1619 assert queue_item.resolved_by_log_id is None 

1620 assert queue_item.priority == 5 

1621 

1622 flag_log = session.execute( 

1623 select(ModerationLog) 

1624 .where(ModerationLog.moderation_state_id == state_id) 

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

1626 ).scalar_one() 

1627 assert flag_log.queue_item_id == queue_item.id 

1628 

1629 

1630def test_ModerateContent_flag_requires_trigger(db): 

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

1632 super_user, super_token = generate_user(is_superuser=True) 

1633 user1, token1 = generate_user() 

1634 user2, _ = generate_user() 

1635 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1636 

1637 with real_moderation_session(super_token) as api: 

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

1639 api.ModerateContent( 

1640 moderation_pb2.ModerateContentReq( 

1641 moderation_state_id=state_id, 

1642 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1643 reason="no trigger", 

1644 ) 

1645 ) 

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

1647 

1648 

1649def _open_queue_item_id(state_id): 

1650 with session_scope() as session: 

1651 return ( 

1652 session.execute( 

1653 select(ModerationQueueItem) 

1654 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

1656 ) 

1657 .scalars() 

1658 .one() 

1659 .id 

1660 ) 

1661 

1662 

1663def test_ModerateContent_set_priority(db): 

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

1665 moderator, mod_token = generate_user(is_superuser=True) 

1666 user1, token1 = generate_user() 

1667 user2, _ = generate_user() 

1668 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1669 queue_item_id = _open_queue_item_id(state_id) 

1670 

1671 with real_moderation_session(mod_token) as api: 

1672 api.ModerateContent( 

1673 moderation_pb2.ModerateContentReq( 

1674 moderation_state_id=state_id, 

1675 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY, 

1676 queue_item_id=queue_item_id, 

1677 priority=10, 

1678 reason="bump", 

1679 ) 

1680 ) 

1681 

1682 with session_scope() as session: 

1683 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

1684 assert queue_item.priority == 10 

1685 assert queue_item.resolved_by_log_id is None # not resolved 

1686 # State stays shadowed (visibility untouched) 

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

1688 

1689 log = session.execute( 

1690 select(ModerationLog) 

1691 .where(ModerationLog.moderation_state_id == state_id) 

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

1693 ).scalar_one() 

1694 assert log.new_priority == 10 

1695 assert log.queue_item_id == queue_item_id 

1696 

1697 # GetModerationQueue surfaces the new priority 

1698 with real_moderation_session(mod_token) as api: 

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

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

1701 assert item.priority == 10 

1702 

1703 

1704def test_ModerateContent_unflag(db): 

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

1706 moderator, mod_token = generate_user(is_superuser=True) 

1707 user1, token1 = generate_user() 

1708 user2, _ = generate_user() 

1709 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1710 queue_item_id = _open_queue_item_id(state_id) 

1711 

1712 with real_moderation_session(mod_token) as api: 

1713 api.ModerateContent( 

1714 moderation_pb2.ModerateContentReq( 

1715 moderation_state_id=state_id, 

1716 action=moderation_pb2.MODERATION_ACTION_UNFLAG, 

1717 queue_item_id=queue_item_id, 

1718 reason="dismissed", 

1719 ) 

1720 ) 

1721 

1722 with session_scope() as session: 

1723 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

1724 assert queue_item.resolved_by_log_id is not None 

1725 # Visibility unchanged (still shadowed from creation) 

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

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

1728 assert log.action == ModerationAction.unflag 

1729 assert log.queue_item_id == queue_item_id 

1730 

1731 

1732def test_ModerateContent_unflag_requires_queue_item(db): 

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

1734 moderator, mod_token = generate_user(is_superuser=True) 

1735 user1, token1 = generate_user() 

1736 user2, _ = generate_user() 

1737 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1738 

1739 with real_moderation_session(mod_token) as api: 

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

1741 api.ModerateContent( 

1742 moderation_pb2.ModerateContentReq( 

1743 moderation_state_id=state_id, 

1744 action=moderation_pb2.MODERATION_ACTION_UNFLAG, 

1745 reason="no target", 

1746 ) 

1747 ) 

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

1749 

1750 

1751def test_ModerateContent_approve_without_clear_flags_leaves_flag_open(db): 

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

1753 moderator, mod_token = generate_user(is_superuser=True) 

1754 user1, token1 = generate_user() 

1755 user2, _ = generate_user() 

1756 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1757 queue_item_id = _open_queue_item_id(state_id) 

1758 

1759 with real_moderation_session(mod_token) as api: 

1760 api.ModerateContent( 

1761 moderation_pb2.ModerateContentReq( 

1762 moderation_state_id=state_id, 

1763 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1764 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1765 reason="visible but still under review", 

1766 ) 

1767 ) 

1768 

1769 with session_scope() as session: 

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

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

1772 

1773 

1774def test_ModerateContent_flag_supersede(db): 

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

1776 moderator, mod_token = generate_user(is_superuser=True) 

1777 user1, token1 = generate_user() 

1778 user2, _ = generate_user() 

1779 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1780 initial_item_id = _open_queue_item_id(state_id) 

1781 

1782 with real_moderation_session(mod_token) as api: 

1783 api.ModerateContent( 

1784 moderation_pb2.ModerateContentReq( 

1785 moderation_state_id=state_id, 

1786 action=moderation_pb2.MODERATION_ACTION_FLAG, 

1787 trigger=moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG, 

1788 priority=3, 

1789 supersede_queue_item_id=initial_item_id, 

1790 reason="machine re-flag", 

1791 ) 

1792 ) 

1793 

1794 with session_scope() as session: 

1795 # Old initial_review flag is resolved 

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

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

1798 open_items = ( 

1799 session.execute( 

1800 select(ModerationQueueItem) 

1801 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

1803 ) 

1804 .scalars() 

1805 .all() 

1806 ) 

1807 assert len(open_items) == 1 

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

1809 assert open_items[0].priority == 3 

1810 

1811 

1812def test_GetModerationQueue_filter_by_priority(db): 

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

1814 moderator, mod_token = generate_user(is_superuser=True) 

1815 user1, token1 = generate_user() 

1816 user2, _ = generate_user() 

1817 state_id = create_test_host_request_with_moderation(token1, user2.id) 

1818 queue_item_id = _open_queue_item_id(state_id) 

1819 

1820 with real_moderation_session(mod_token) as api: 

1821 api.ModerateContent( 

1822 moderation_pb2.ModerateContentReq( 

1823 moderation_state_id=state_id, 

1824 action=moderation_pb2.MODERATION_ACTION_SET_PRIORITY, 

1825 queue_item_id=queue_item_id, 

1826 priority=7, 

1827 reason="raise", 

1828 ) 

1829 ) 

1830 

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

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

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

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

1835 

1836 # priority_max excludes it above the threshold 

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

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

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

1840 

1841 

1842# ============================================================================ 

1843# Tests for group chat moderation 

1844# ============================================================================ 

1845 

1846 

1847def test_group_chat_created_with_moderation_state(db): 

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

1849 user1, token1 = generate_user() 

1850 user2, _ = generate_user() 

1851 make_friends(user1, user2) 

1852 

1853 with conversations_session(token1) as api: 

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

1855 group_chat_id = res.group_chat_id 

1856 

1857 # Verify moderation state was created 

1858 with session_scope() as session: 

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

1860 

1861 assert group_chat.moderation_state.object_type == ModerationObjectType.group_chat 

1862 assert group_chat.moderation_state.object_id == group_chat_id 

1863 # Group chats start as SHADOWED 

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

1865 

1866 # A moderation queue item should have been created 

1867 queue_item = ( 

1868 session.execute( 

1869 select(ModerationQueueItem).where( 

1870 ModerationQueueItem.moderation_state_id == group_chat.moderation_state_id 

1871 ) 

1872 ) 

1873 .scalars() 

1874 .first() 

1875 ) 

1876 assert queue_item is not None 

1877 assert queue_item.trigger == ModerationTrigger.initial_review 

1878 

1879 

1880def test_group_chat_GetModerationState(db): 

1881 """Test GetModerationState API for group chats""" 

1882 user1, token1 = generate_user() 

1883 user2, _ = generate_user() 

1884 moderator, mod_token = generate_user(is_superuser=True) 

1885 make_friends(user1, user2) 

1886 

1887 with conversations_session(token1) as api: 

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

1889 group_chat_id = res.group_chat_id 

1890 

1891 # Moderator can look up the moderation state 

1892 with real_moderation_session(mod_token) as api: 

1893 res = api.GetModerationState( 

1894 moderation_pb2.GetModerationStateReq( 

1895 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1896 object_id=group_chat_id, 

1897 ) 

1898 ) 

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

1900 assert res.moderation_state.object_id == group_chat_id 

1901 # Starts as SHADOWED 

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

1903 

1904 

1905def test_group_chat_moderation_hide(db): 

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

1907 user1, token1 = generate_user() 

1908 user2, token2 = generate_user() 

1909 moderator, mod_token = generate_user(is_superuser=True) 

1910 make_friends(user1, user2) 

1911 

1912 with conversations_session(token1) as api: 

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

1914 group_chat_id = res.group_chat_id 

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

1916 

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

1918 with real_moderation_session(mod_token) as api: 

1919 state_res = api.GetModerationState( 

1920 moderation_pb2.GetModerationStateReq( 

1921 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1922 object_id=group_chat_id, 

1923 ) 

1924 ) 

1925 api.ModerateContent( 

1926 moderation_pb2.ModerateContentReq( 

1927 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1928 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1929 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1930 reason="Approved", 

1931 ) 

1932 ) 

1933 

1934 # Both users can see the chat now 

1935 with conversations_session(token1) as api: 

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

1937 assert len(res.group_chats) == 1 

1938 

1939 with conversations_session(token2) as api: 

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

1941 assert len(res.group_chats) == 1 

1942 

1943 # Moderator hides the group chat 

1944 with real_moderation_session(mod_token) as api: 

1945 state_res = api.GetModerationState( 

1946 moderation_pb2.GetModerationStateReq( 

1947 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1948 object_id=group_chat_id, 

1949 ) 

1950 ) 

1951 api.ModerateContent( 

1952 moderation_pb2.ModerateContentReq( 

1953 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1954 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1955 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1956 reason="Inappropriate content", 

1957 ) 

1958 ) 

1959 

1960 # Neither user can see the chat now 

1961 with conversations_session(token1) as api: 

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

1963 assert len(res.group_chats) == 0 

1964 

1965 with conversations_session(token2) as api: 

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

1967 assert len(res.group_chats) == 0 

1968 

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

1970 with conversations_session(token1) as api: 

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

1972 assert len(res.messages) == 0 

1973 

1974 

1975def test_group_chat_moderation_shadow(db): 

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

1977 user1, token1 = generate_user() # Creator 

1978 user2, token2 = generate_user() # Participant 

1979 moderator, mod_token = generate_user(is_superuser=True) 

1980 make_friends(user1, user2) 

1981 

1982 with conversations_session(token1) as api: 

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

1984 group_chat_id = res.group_chat_id 

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

1986 

1987 # Moderator shadows the group chat 

1988 with real_moderation_session(mod_token) as api: 

1989 state_res = api.GetModerationState( 

1990 moderation_pb2.GetModerationStateReq( 

1991 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1992 object_id=group_chat_id, 

1993 ) 

1994 ) 

1995 api.ModerateContent( 

1996 moderation_pb2.ModerateContentReq( 

1997 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1998 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1999 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2000 reason="Needs review", 

2001 ) 

2002 ) 

2003 

2004 # Creator can see SHADOWED content in list operations 

2005 with conversations_session(token1) as api: 

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

2007 assert len(res.group_chats) == 1 

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

2009 

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

2011 with conversations_session(token2) as api: 

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

2013 assert len(res.group_chats) == 0 

2014 

2015 # Creator can also access it directly via GetGroupChat 

2016 with conversations_session(token1) as api: 

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

2018 assert res.group_chat_id == group_chat_id 

2019 

2020 

2021# ============================================================================ 

2022# Tests for auto-approval background job 

2023# ============================================================================ 

2024 

2025 

2026def test_auto_approve_moderation_queue_disabled_when_zero(db, email_collector: EmailCollector): 

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

2028 moderator, mod_token = generate_user(is_superuser=True) 

2029 user1, token1 = generate_user() 

2030 user2, token2 = generate_user() 

2031 

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

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

2034 

2035 # Create a host request 

2036 with requests_session(token1) as api: 

2037 host_request_id = api.CreateHostRequest( 

2038 requests_pb2.CreateHostRequestReq( 

2039 host_user_id=user2.id, 

2040 from_date=today_plus_2, 

2041 to_date=today_plus_3, 

2042 text=valid_request_text(), 

2043 ) 

2044 ).host_request_id 

2045 

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

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

2048 

2049 # Ensure deadline is 0 (disabled) 

2050 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 0 

2051 

2052 # Run the job 

2053 auto_approve_moderation_queue(empty_pb2.Empty()) 

2054 

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

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

2057 assert res.host_request_id == host_request_id 

2058 

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

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

2061 assert len(res.host_requests) == 1 

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

2063 

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

2065 with requests_session(token2) as api: 

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

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

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

2069 

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

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

2072 assert len(res.host_requests) == 0 

2073 

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

2075 with real_moderation_session(mod_token) as api: 

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

2077 assert len(res.queue_items) == 1 

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

2079 

2080 # Moderator can check the state is still SHADOWED 

2081 state_res = api.GetModerationState( 

2082 moderation_pb2.GetModerationStateReq( 

2083 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2084 object_id=host_request_id, 

2085 ) 

2086 ) 

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

2088 

2089 

2090def test_auto_approve_moderation_queue_approves_old_items( 

2091 db, email_collector: EmailCollector, push_collector: PushCollector 

2092): 

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

2094 moderator, mod_token = generate_user(is_superuser=True) 

2095 user1, token1 = generate_user() 

2096 user2, token2 = generate_user() 

2097 

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

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

2100 

2101 # Create a host request 

2102 with requests_session(token1) as api: 

2103 host_request_id = api.CreateHostRequest( 

2104 requests_pb2.CreateHostRequestReq( 

2105 host_user_id=user2.id, 

2106 from_date=today_plus_2, 

2107 to_date=today_plus_3, 

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

2109 ) 

2110 ).host_request_id 

2111 

2112 # No email sent initially (shadowed) 

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

2114 

2115 # Host cannot see the request yet 

2116 with requests_session(token2) as api: 

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

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

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

2120 

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

2122 with session_scope() as session: 

2123 host_request = session.execute( 

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

2125 ).scalar_one() 

2126 queue_item = session.execute( 

2127 select(ModerationQueueItem) 

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

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

2130 ).scalar_one() 

2131 # Backdate the queue item by 2 minutes 

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

2133 

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

2135 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2136 config.MODERATION_BOT_USER_ID = moderator.id 

2137 

2138 # Run the job 

2139 auto_approve_moderation_queue(empty_pb2.Empty()) 

2140 

2141 # Now host can see the request via API 

2142 with requests_session(token2) as api: 

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

2144 assert res.host_request_id == host_request_id 

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

2146 

2147 # Host sees it in their received list 

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

2149 assert len(res.host_requests) == 1 

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

2151 

2152 # Surfer sees it in their sent list 

2153 with requests_session(token1) as api: 

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

2155 assert len(res.host_requests) == 1 

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

2157 

2158 with real_moderation_session(mod_token) as api: 

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

2160 assert len(res.queue_items) == 1 

2161 flag_item = res.queue_items[0] 

2162 assert flag_item.trigger == moderation_pb2.MODERATION_TRIGGER_MACHINE_FLAG 

2163 assert flag_item.priority == MODERATION_AUTO_APPROVE_FLAG_PRIORITY 

2164 

2165 state_res = api.GetModerationState( 

2166 moderation_pb2.GetModerationStateReq( 

2167 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2168 object_id=host_request_id, 

2169 ) 

2170 ) 

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

2172 

2173 log_res = api.GetModerationLog( 

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

2175 ) 

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

2177 assert len(approve_entries) == 1 

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

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

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

2181 

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

2183 assert len(flag_entries) == 1 

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

2185 

2186 

2187def test_auto_approve_does_not_approve_recent_items(db, email_collector: EmailCollector): 

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

2189 moderator, mod_token = generate_user(is_superuser=True) 

2190 user1, token1 = generate_user() 

2191 user2, token2 = generate_user() 

2192 

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

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

2195 

2196 # Create a host request 

2197 with requests_session(token1) as api: 

2198 host_request_id = api.CreateHostRequest( 

2199 requests_pb2.CreateHostRequestReq( 

2200 host_user_id=user2.id, 

2201 from_date=today_plus_2, 

2202 to_date=today_plus_3, 

2203 text=valid_request_text(), 

2204 ) 

2205 ).host_request_id 

2206 

2207 # No email sent (shadowed) 

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

2209 

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

2211 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600 

2212 config.MODERATION_BOT_USER_ID = moderator.id 

2213 

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

2215 auto_approve_moderation_queue(empty_pb2.Empty()) 

2216 

2217 # Still no email sent 

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

2219 

2220 # Host still cannot see the request 

2221 with requests_session(token2) as api: 

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

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

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

2225 

2226 # Not in host's received list 

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

2228 assert len(res.host_requests) == 0 

2229 

2230 # Moderator sees it still in queue unresolved 

2231 with real_moderation_session(mod_token) as api: 

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

2233 assert len(res.queue_items) == 1 

2234 

2235 # State is still SHADOWED 

2236 state_res = api.GetModerationState( 

2237 moderation_pb2.GetModerationStateReq( 

2238 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2239 object_id=host_request_id, 

2240 ) 

2241 ) 

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

2243 

2244 

2245def test_auto_approve_does_not_approve_already_approved(db): 

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

2247 moderator, mod_token = generate_user(is_superuser=True) 

2248 user1, token1 = generate_user() 

2249 user2, token2 = generate_user() 

2250 

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

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

2253 

2254 # Create a host request 

2255 with requests_session(token1) as api: 

2256 host_request_id = api.CreateHostRequest( 

2257 requests_pb2.CreateHostRequestReq( 

2258 host_user_id=user2.id, 

2259 from_date=today_plus_2, 

2260 to_date=today_plus_3, 

2261 text=valid_request_text(), 

2262 ) 

2263 ).host_request_id 

2264 

2265 # Moderator approves it manually 

2266 with real_moderation_session(mod_token) as api: 

2267 state_res = api.GetModerationState( 

2268 moderation_pb2.GetModerationStateReq( 

2269 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2270 object_id=host_request_id, 

2271 ) 

2272 ) 

2273 state_id = state_res.moderation_state.moderation_state_id 

2274 

2275 api.ModerateContent( 

2276 moderation_pb2.ModerateContentReq( 

2277 moderation_state_id=state_id, 

2278 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

2279 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2280 reason="Approved by moderator", 

2281 clear_flags=True, 

2282 ) 

2283 ) 

2284 

2285 # Host can now see it 

2286 with requests_session(token2) as api: 

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

2288 assert res.host_request_id == host_request_id 

2289 

2290 # Get log count before auto-approval 

2291 with real_moderation_session(mod_token) as api: 

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

2293 log_count_before = len(log_res_before.log_entries) 

2294 

2295 # Set deadline to 1 second 

2296 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2297 config.MODERATION_BOT_USER_ID = moderator.id 

2298 

2299 # Run the job 

2300 auto_approve_moderation_queue(empty_pb2.Empty()) 

2301 

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

2303 with real_moderation_session(mod_token) as api: 

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

2305 assert len(log_res_after.log_entries) == log_count_before 

2306 

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

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

2309 assert len(queue_res.queue_items) == 0 

2310 

2311 

2312def test_auto_approve_does_not_approve_moderator_shadowed_items(db): 

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

2314 moderator, mod_token = generate_user(is_superuser=True) 

2315 user1, token1 = generate_user() 

2316 user2, token2 = generate_user() 

2317 

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

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

2320 

2321 # Create a host request 

2322 with requests_session(token1) as api: 

2323 host_request_id = api.CreateHostRequest( 

2324 requests_pb2.CreateHostRequestReq( 

2325 host_user_id=user2.id, 

2326 from_date=today_plus_2, 

2327 to_date=today_plus_3, 

2328 text=valid_request_text(), 

2329 ) 

2330 ).host_request_id 

2331 

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

2333 with real_moderation_session(mod_token) as api: 

2334 state_res = api.GetModerationState( 

2335 moderation_pb2.GetModerationStateReq( 

2336 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2337 object_id=host_request_id, 

2338 ) 

2339 ) 

2340 state_id = state_res.moderation_state.moderation_state_id 

2341 

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

2343 api.ModerateContent( 

2344 moderation_pb2.ModerateContentReq( 

2345 moderation_state_id=state_id, 

2346 action=moderation_pb2.MODERATION_ACTION_HIDE, 

2347 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2348 reason="Keeping shadowed for review", 

2349 clear_flags=True, 

2350 ) 

2351 ) 

2352 

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

2354 with session_scope() as session: 

2355 queue_item = session.execute( 

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

2357 ).scalar_one() 

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

2359 

2360 # Set deadline to 1 second 

2361 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2362 config.MODERATION_BOT_USER_ID = moderator.id 

2363 

2364 # Get log count before 

2365 with real_moderation_session(mod_token) as api: 

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

2367 log_count_before = len(log_res_before.log_entries) 

2368 

2369 # Run the job 

2370 auto_approve_moderation_queue(empty_pb2.Empty()) 

2371 

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

2373 with real_moderation_session(mod_token) as api: 

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

2375 assert len(log_res_after.log_entries) == log_count_before 

2376 

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

2378 state_res = api.GetModerationState( 

2379 moderation_pb2.GetModerationStateReq( 

2380 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2381 object_id=host_request_id, 

2382 ) 

2383 ) 

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

2385 

2386 # Host still cannot see the request 

2387 with requests_session(token2) as api: 

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

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

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

2391 

2392 

2393def test_auto_approve_skips_shadowed_user_authored_items(db): 

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

2395 moderator, mod_token = generate_user(is_superuser=True) 

2396 surfer, surfer_token = generate_user() 

2397 host, host_token = generate_user() 

2398 

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

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

2401 

2402 with requests_session(surfer_token) as api: 

2403 host_request_id = api.CreateHostRequest( 

2404 requests_pb2.CreateHostRequestReq( 

2405 host_user_id=host.id, 

2406 from_date=today_plus_2, 

2407 to_date=today_plus_3, 

2408 text=valid_request_text(), 

2409 ) 

2410 ).host_request_id 

2411 

2412 # Shadow the surfer after the host request was created 

2413 with session_scope() as session: 

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

2415 

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

2417 with session_scope() as session: 

2418 host_request = session.execute( 

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

2420 ).scalar_one() 

2421 queue_item = session.execute( 

2422 select(ModerationQueueItem) 

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

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

2425 ).scalar_one() 

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

2427 

2428 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2429 config.MODERATION_BOT_USER_ID = moderator.id 

2430 

2431 auto_approve_moderation_queue(empty_pb2.Empty()) 

2432 

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

2434 with real_moderation_session(mod_token) as api: 

2435 state_res = api.GetModerationState( 

2436 moderation_pb2.GetModerationStateReq( 

2437 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2438 object_id=host_request_id, 

2439 ) 

2440 ) 

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

2442 

2443 # Host still cannot see the request 

2444 with requests_session(host_token) as api: 

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

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

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

2448 

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

2450 with requests_session(surfer_token) as api: 

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

2452 assert res.host_request_id == host_request_id 

2453 

2454 

2455def test_auto_approve_preserves_other_open_flags(db): 

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

2457 moderator, mod_token = generate_user(is_superuser=True) 

2458 surfer, surfer_token = generate_user() 

2459 host, _ = generate_user() 

2460 

2461 state_id = create_test_host_request_with_moderation(surfer_token, host.id) 

2462 

2463 with real_moderation_session(mod_token) as api: 

2464 api.ModerateContent( 

2465 moderation_pb2.ModerateContentReq( 

2466 moderation_state_id=state_id, 

2467 action=moderation_pb2.MODERATION_ACTION_FLAG, 

2468 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

2469 priority=5, 

2470 reason="user reported this", 

2471 ) 

2472 ) 

2473 

2474 with session_scope() as session: 

2475 initial_item = session.execute( 

2476 select(ModerationQueueItem) 

2477 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

2479 ).scalar_one() 

2480 initial_item_id = initial_item.id 

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

2482 

2483 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2484 config.MODERATION_BOT_USER_ID = moderator.id 

2485 

2486 auto_approve_moderation_queue(empty_pb2.Empty()) 

2487 

2488 with session_scope() as session: 

2489 assert ( 

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

2491 .scalar_one() 

2492 .resolved_by_log_id 

2493 is not None 

2494 ) 

2495 assert ( 

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

2497 == ModerationVisibility.visible 

2498 ) 

2499 

2500 open_items = ( 

2501 session.execute( 

2502 select(ModerationQueueItem) 

2503 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

2505 ) 

2506 .scalars() 

2507 .all() 

2508 ) 

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

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

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

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

2513 

2514 

2515# ============================================================================ 

2516# Notification Suppression Tests 

2517# ============================================================================ 

2518 

2519 

2520def test_host_request_message_notifications_suppressed_before_approval( 

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

2522): 

2523 """ 

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

2525 that haven't been approved yet. 

2526 """ 

2527 host, host_token = generate_user(complete_profile=True) 

2528 surfer, surfer_token = generate_user(complete_profile=True) 

2529 

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

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

2532 

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

2534 with requests_session(surfer_token) as api: 

2535 hr_id = api.CreateHostRequest( 

2536 requests_pb2.CreateHostRequestReq( 

2537 host_user_id=host.id, 

2538 from_date=today_plus_2, 

2539 to_date=today_plus_3, 

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

2541 ) 

2542 ).host_request_id 

2543 

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

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

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

2547 

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

2549 with requests_session(surfer_token) as api: 

2550 api.SendHostRequestMessage( 

2551 requests_pb2.SendHostRequestMessageReq( 

2552 host_request_id=hr_id, 

2553 text="Follow-up message 1", 

2554 ) 

2555 ) 

2556 api.SendHostRequestMessage( 

2557 requests_pb2.SendHostRequestMessageReq( 

2558 host_request_id=hr_id, 

2559 text="Follow-up message 2", 

2560 ) 

2561 ) 

2562 

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

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

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

2566 

2567 # Now approve the request 

2568 moderator.approve_host_request(hr_id) 

2569 email_collector.pop_for_recipient(host.email) 

2570 

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

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

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

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

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

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

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

2578 

2579 

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

2581 """ 

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

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

2584 

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

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

2587 """ 

2588 host, host_token = generate_user(complete_profile=True) 

2589 surfer, surfer_token = generate_user(complete_profile=True) 

2590 

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

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

2593 

2594 # Create host request 

2595 with requests_session(surfer_token) as api: 

2596 hr_id = api.CreateHostRequest( 

2597 requests_pb2.CreateHostRequestReq( 

2598 host_user_id=host.id, 

2599 from_date=today_plus_2, 

2600 to_date=today_plus_3, 

2601 text=valid_request_text(), 

2602 ) 

2603 ).host_request_id 

2604 

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

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

2607 

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

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

2610 with requests_session(surfer_token) as api: 

2611 api.RespondHostRequest( 

2612 requests_pb2.RespondHostRequestReq( 

2613 host_request_id=hr_id, 

2614 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

2615 text="Actually, never mind", 

2616 ) 

2617 ) 

2618 

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

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

2621 

2622 

2623def test_host_request_notifications_sent_after_approval( 

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

2625): 

2626 """ 

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

2628 """ 

2629 host, host_token = generate_user(complete_profile=True) 

2630 surfer, surfer_token = generate_user(complete_profile=True) 

2631 

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

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

2634 

2635 # Create and approve host request 

2636 with requests_session(surfer_token) as api: 

2637 hr_id = api.CreateHostRequest( 

2638 requests_pb2.CreateHostRequestReq( 

2639 host_user_id=host.id, 

2640 from_date=today_plus_2, 

2641 to_date=today_plus_3, 

2642 text=valid_request_text(), 

2643 ) 

2644 ).host_request_id 

2645 

2646 moderator.approve_host_request(hr_id) 

2647 

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

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

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

2651 

2652 # Host accepts the request - surfer should be notified 

2653 with requests_session(host_token) as api: 

2654 api.RespondHostRequest( 

2655 requests_pb2.RespondHostRequestReq( 

2656 host_request_id=hr_id, 

2657 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

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

2659 ) 

2660 ) 

2661 

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

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

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

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

2666 

2667 # Surfer confirms - host should be notified 

2668 with requests_session(surfer_token) as api: 

2669 api.RespondHostRequest( 

2670 requests_pb2.RespondHostRequestReq( 

2671 host_request_id=hr_id, 

2672 status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

2673 text="See you then!", 

2674 ) 

2675 ) 

2676 

2677 # Host should now have received the confirmation notifications 

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

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

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

2681 

2682 

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

2684 """ 

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

2686 that haven't been approved yet. 

2687 """ 

2688 user1, token1 = generate_user(complete_profile=True) 

2689 user2, token2 = generate_user(complete_profile=True) 

2690 

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

2692 with conversations_session(token1) as api: 

2693 res = api.CreateGroupChat( 

2694 conversations_pb2.CreateGroupChatReq( 

2695 recipient_user_ids=[user2.id], 

2696 ) 

2697 ) 

2698 gc_id = res.group_chat_id 

2699 

2700 # Verify initial state 

2701 with session_scope() as session: 

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

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

2704 

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

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

2707 

2708 # Send messages BEFORE approval 

2709 with conversations_session(token1) as api: 

2710 api.SendMessage( 

2711 conversations_pb2.SendMessageReq( 

2712 group_chat_id=gc_id, 

2713 text="Hello before approval", 

2714 ) 

2715 ) 

2716 

2717 # Process the queued notification job 

2718 while process_job(): 

2719 pass 

2720 

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

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

2723 

2724 # Now approve the group chat 

2725 moderator.approve_group_chat(gc_id) 

2726 

2727 # Process the queued notification jobs from approval 

2728 while process_job(): 

2729 pass 

2730 

2731 # Verify moderation state after approval 

2732 with session_scope() as session: 

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

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

2735 

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

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

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

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

2740 

2741 # Send a message AFTER approval 

2742 with conversations_session(token1) as api: 

2743 api.SendMessage( 

2744 conversations_pb2.SendMessageReq( 

2745 group_chat_id=gc_id, 

2746 text="Hello after approval", 

2747 ) 

2748 ) 

2749 

2750 # Process the queued notification job 

2751 while process_job(): 

2752 pass 

2753 

2754 # User2 should have received another notification 

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

2756 

2757 

2758def test_event_moderation_state_content(db): 

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

2760 super_user, super_token = generate_user(is_superuser=True) 

2761 user, token = generate_user() 

2762 

2763 with session_scope() as session: 

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

2765 

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

2767 end_time = start_time + timedelta(hours=3) 

2768 

2769 with events_session(token) as api: 

2770 res = api.CreateEvent( 

2771 events_pb2.CreateEventReq( 

2772 title="My Event Title", 

2773 content="My event description.", 

2774 photo_key=None, 

2775 location=events_pb2.EventLocation( 

2776 address="Near Null Island", 

2777 lat=0.1, 

2778 lng=0.2, 

2779 ), 

2780 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2781 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2782 ) 

2783 ) 

2784 event_id = res.event_id 

2785 

2786 with session_scope() as session: 

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

2788 state_id = occurrence.moderation_state_id 

2789 

2790 with real_moderation_session(super_token) as api: 

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

2792 event_items = [ 

2793 item 

2794 for item in res.queue_items 

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

2796 ] 

2797 assert len(event_items) == 1 

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

2799 

2800 

2801# ============================================================================ 

2802# Tests for SetUserContentVisibility 

2803# ============================================================================ 

2804 

2805 

2806def _get_moderation_state(session, object_type, object_id): 

2807 return session.execute( 

2808 select(ModerationState) 

2809 .where(ModerationState.object_type == object_type) 

2810 .where(ModerationState.object_id == object_id) 

2811 ).scalar_one() 

2812 

2813 

2814def test_SetUserContentVisibility_host_request(db): 

2815 super_user, super_token = generate_user(is_superuser=True) 

2816 surfer, surfer_token = generate_user() 

2817 host, _ = generate_user() 

2818 

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

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

2821 with requests_session(surfer_token) as api: 

2822 hr_id = api.CreateHostRequest( 

2823 requests_pb2.CreateHostRequestReq( 

2824 host_user_id=host.id, 

2825 from_date=today_plus_2, 

2826 to_date=today_plus_3, 

2827 text=valid_request_text(), 

2828 ) 

2829 ).host_request_id 

2830 

2831 with real_moderation_session(super_token) as api: 

2832 res = api.SetUserContentVisibility( 

2833 moderation_pb2.SetUserContentVisibilityReq( 

2834 user_id=surfer.id, 

2835 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2836 ) 

2837 ) 

2838 # Already shadowed by default — no-op 

2839 assert res.updated_count == 0 

2840 

2841 with real_moderation_session(super_token) as api: 

2842 res = api.SetUserContentVisibility( 

2843 moderation_pb2.SetUserContentVisibilityReq( 

2844 user_id=surfer.id, 

2845 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2846 reason="policy violation", 

2847 ) 

2848 ) 

2849 assert res.updated_count == 1 

2850 

2851 with session_scope() as session: 

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

2853 assert state.visibility == ModerationVisibility.hidden 

2854 

2855 log_entries = ( 

2856 session.execute( 

2857 select(ModerationLog) 

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

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

2860 ) 

2861 .scalars() 

2862 .all() 

2863 ) 

2864 # create log + bulk update log 

2865 assert len(log_entries) == 2 

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

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

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

2869 

2870 

2871def test_SetUserContentVisibility_group_chat(db): 

2872 super_user, super_token = generate_user(is_superuser=True) 

2873 creator, creator_token = generate_user() 

2874 other, _ = generate_user() 

2875 make_friends(creator, other) 

2876 

2877 with conversations_session(creator_token) as api: 

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

2879 

2880 with real_moderation_session(super_token) as api: 

2881 api.SetUserContentVisibility( 

2882 moderation_pb2.SetUserContentVisibilityReq( 

2883 user_id=creator.id, 

2884 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2885 ) 

2886 ) 

2887 

2888 with session_scope() as session: 

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

2890 assert state.visibility == ModerationVisibility.hidden 

2891 

2892 

2893def test_SetUserContentVisibility_event_occurrence(db): 

2894 super_user, super_token = generate_user(is_superuser=True) 

2895 creator, creator_token = generate_user() 

2896 

2897 with session_scope() as session: 

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

2899 

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

2901 end_time = start_time + timedelta(hours=3) 

2902 with events_session(creator_token) as api: 

2903 event_id = api.CreateEvent( 

2904 events_pb2.CreateEventReq( 

2905 title="Event", 

2906 content="Event description.", 

2907 photo_key=None, 

2908 location=events_pb2.EventLocation( 

2909 address="Near Null Island", 

2910 lat=0.1, 

2911 lng=0.2, 

2912 ), 

2913 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2914 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2915 ) 

2916 ).event_id 

2917 

2918 with real_moderation_session(super_token) as api: 

2919 res = api.SetUserContentVisibility( 

2920 moderation_pb2.SetUserContentVisibilityReq( 

2921 user_id=creator.id, 

2922 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2923 ) 

2924 ) 

2925 assert res.updated_count == 1 

2926 

2927 with session_scope() as session: 

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

2929 assert state.visibility == ModerationVisibility.hidden 

2930 

2931 

2932def test_SetUserContentVisibility_friend_request(db): 

2933 super_user, super_token = generate_user(is_superuser=True) 

2934 sender, sender_token = generate_user() 

2935 recipient, _ = generate_user() 

2936 

2937 with api_session(sender_token) as api: 

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

2939 

2940 with session_scope() as session: 

2941 fr_id = session.execute( 

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

2943 ).scalar_one() 

2944 

2945 with real_moderation_session(super_token) as api: 

2946 res = api.SetUserContentVisibility( 

2947 moderation_pb2.SetUserContentVisibilityReq( 

2948 user_id=sender.id, 

2949 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2950 ) 

2951 ) 

2952 assert res.updated_count == 1 

2953 

2954 with session_scope() as session: 

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

2956 assert state.visibility == ModerationVisibility.hidden 

2957 

2958 

2959def test_SetUserContentVisibility_round_trip(db): 

2960 super_user, super_token = generate_user(is_superuser=True) 

2961 surfer, surfer_token = generate_user() 

2962 host, _ = generate_user() 

2963 

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

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

2966 with requests_session(surfer_token) as api: 

2967 hr_id = api.CreateHostRequest( 

2968 requests_pb2.CreateHostRequestReq( 

2969 host_user_id=host.id, 

2970 from_date=today_plus_2, 

2971 to_date=today_plus_3, 

2972 text=valid_request_text(), 

2973 ) 

2974 ).host_request_id 

2975 

2976 with real_moderation_session(super_token) as api: 

2977 api.SetUserContentVisibility( 

2978 moderation_pb2.SetUserContentVisibilityReq( 

2979 user_id=surfer.id, 

2980 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2981 reason="first", 

2982 ) 

2983 ) 

2984 api.SetUserContentVisibility( 

2985 moderation_pb2.SetUserContentVisibilityReq( 

2986 user_id=surfer.id, 

2987 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2988 reason="second", 

2989 ) 

2990 ) 

2991 

2992 with session_scope() as session: 

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

2994 assert state.visibility == ModerationVisibility.visible 

2995 

2996 bulk_log_entries = ( 

2997 session.execute( 

2998 select(ModerationLog) 

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

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

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

3002 ) 

3003 .scalars() 

3004 .all() 

3005 ) 

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

3007 ModerationVisibility.hidden, 

3008 ModerationVisibility.visible, 

3009 ] 

3010 

3011 

3012def test_SetUserContentVisibility_resolves_queue_items(db): 

3013 super_user, super_token = generate_user(is_superuser=True) 

3014 surfer, surfer_token = generate_user() 

3015 host, _ = generate_user() 

3016 

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

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

3019 with requests_session(surfer_token) as api: 

3020 hr_id = api.CreateHostRequest( 

3021 requests_pb2.CreateHostRequestReq( 

3022 host_user_id=host.id, 

3023 from_date=today_plus_2, 

3024 to_date=today_plus_3, 

3025 text=valid_request_text(), 

3026 ) 

3027 ).host_request_id 

3028 

3029 with session_scope() as session: 

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

3031 queue_item = session.execute( 

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

3033 ).scalar_one() 

3034 assert queue_item.resolved_by_log_id is None 

3035 

3036 with real_moderation_session(super_token) as api: 

3037 api.SetUserContentVisibility( 

3038 moderation_pb2.SetUserContentVisibilityReq( 

3039 user_id=surfer.id, 

3040 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3041 ) 

3042 ) 

3043 

3044 with session_scope() as session: 

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

3046 queue_item = session.execute( 

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

3048 ).scalar_one() 

3049 assert queue_item.resolved_by_log_id is not None 

3050 

3051 

3052def test_SetUserContentVisibility_noop_when_matches(db): 

3053 super_user, super_token = generate_user(is_superuser=True) 

3054 surfer, surfer_token = generate_user() 

3055 host, _ = generate_user() 

3056 

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

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

3059 with requests_session(surfer_token) as api: 

3060 api.CreateHostRequest( 

3061 requests_pb2.CreateHostRequestReq( 

3062 host_user_id=host.id, 

3063 from_date=today_plus_2, 

3064 to_date=today_plus_3, 

3065 text=valid_request_text(), 

3066 ) 

3067 ) 

3068 

3069 with session_scope() as session: 

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

3071 

3072 with real_moderation_session(super_token) as api: 

3073 res = api.SetUserContentVisibility( 

3074 moderation_pb2.SetUserContentVisibilityReq( 

3075 user_id=surfer.id, 

3076 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3077 ) 

3078 ) 

3079 assert res.updated_count == 0 

3080 

3081 with session_scope() as session: 

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

3083 assert log_count_after == log_count_before 

3084 

3085 

3086def test_SetUserContentVisibility_unspecified_rejected(db): 

3087 super_user, super_token = generate_user(is_superuser=True) 

3088 target, _ = generate_user() 

3089 

3090 with real_moderation_session(super_token) as api: 

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

3092 api.SetUserContentVisibility( 

3093 moderation_pb2.SetUserContentVisibilityReq( 

3094 user_id=target.id, 

3095 visibility=moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED, 

3096 ) 

3097 ) 

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

3099 

3100 

3101def test_SetUserContentVisibility_non_admin_rejected(db): 

3102 normal_user, normal_token = generate_user() 

3103 target, _ = generate_user() 

3104 

3105 with real_moderation_session(normal_token) as api: 

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

3107 api.SetUserContentVisibility( 

3108 moderation_pb2.SetUserContentVisibilityReq( 

3109 user_id=target.id, 

3110 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3111 ) 

3112 ) 

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

3114 

3115 

3116def test_SetUserContentVisibility_writes_admin_action(db): 

3117 super_user, super_token = generate_user(is_superuser=True) 

3118 surfer, surfer_token = generate_user() 

3119 host, _ = generate_user() 

3120 

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

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

3123 with requests_session(surfer_token) as api: 

3124 api.CreateHostRequest( 

3125 requests_pb2.CreateHostRequestReq( 

3126 host_user_id=host.id, 

3127 from_date=today_plus_2, 

3128 to_date=today_plus_3, 

3129 text=valid_request_text(), 

3130 ) 

3131 ) 

3132 

3133 with real_moderation_session(super_token) as api: 

3134 api.SetUserContentVisibility( 

3135 moderation_pb2.SetUserContentVisibilityReq( 

3136 user_id=surfer.id, 

3137 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3138 reason="bulk hide", 

3139 ) 

3140 ) 

3141 

3142 with session_scope() as session: 

3143 actions = ( 

3144 session.execute( 

3145 select(AdminAction) 

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

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

3148 ) 

3149 .scalars() 

3150 .all() 

3151 ) 

3152 assert len(actions) == 1 

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

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

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

3156 

3157 

3158def test_SetUserContentVisibility_only_touches_target(db): 

3159 super_user, super_token = generate_user(is_superuser=True) 

3160 target, target_token = generate_user() 

3161 other, other_token = generate_user() 

3162 host, _ = generate_user() 

3163 

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

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

3166 

3167 with requests_session(target_token) as api: 

3168 target_hr_id = api.CreateHostRequest( 

3169 requests_pb2.CreateHostRequestReq( 

3170 host_user_id=host.id, 

3171 from_date=today_plus_2, 

3172 to_date=today_plus_3, 

3173 text=valid_request_text(), 

3174 ) 

3175 ).host_request_id 

3176 

3177 with requests_session(other_token) as api: 

3178 other_hr_id = api.CreateHostRequest( 

3179 requests_pb2.CreateHostRequestReq( 

3180 host_user_id=host.id, 

3181 from_date=today_plus_2, 

3182 to_date=today_plus_3, 

3183 text=valid_request_text(), 

3184 ) 

3185 ).host_request_id 

3186 

3187 with real_moderation_session(super_token) as api: 

3188 res = api.SetUserContentVisibility( 

3189 moderation_pb2.SetUserContentVisibilityReq( 

3190 user_id=target.id, 

3191 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3192 ) 

3193 ) 

3194 assert res.updated_count == 1 

3195 

3196 with session_scope() as session: 

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

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

3199 assert target_state.visibility == ModerationVisibility.hidden 

3200 assert other_state.visibility == ModerationVisibility.shadowed 

3201 

3202 

3203def test_SetUserContentVisibility_user_not_found(db): 

3204 super_user, super_token = generate_user(is_superuser=True) 

3205 

3206 with real_moderation_session(super_token) as api: 

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

3208 api.SetUserContentVisibility( 

3209 moderation_pb2.SetUserContentVisibilityReq( 

3210 user_id=999999, 

3211 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3212 ) 

3213 ) 

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

3215 

3216 

3217def test_SetUserContentVisibility_from_visibility_filter(db): 

3218 super_user, super_token = generate_user(is_superuser=True) 

3219 surfer, surfer_token = generate_user() 

3220 host, _ = generate_user() 

3221 

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

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

3224 with requests_session(surfer_token) as api: 

3225 hr_id = api.CreateHostRequest( 

3226 requests_pb2.CreateHostRequestReq( 

3227 host_user_id=host.id, 

3228 from_date=today_plus_2, 

3229 to_date=today_plus_3, 

3230 text=valid_request_text(), 

3231 ) 

3232 ).host_request_id 

3233 

3234 with real_moderation_session(super_token) as api: 

3235 api.SetUserContentVisibility( 

3236 moderation_pb2.SetUserContentVisibilityReq( 

3237 user_id=surfer.id, 

3238 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3239 ) 

3240 ) 

3241 

3242 with real_moderation_session(super_token) as api: 

3243 res = api.SetUserContentVisibility( 

3244 moderation_pb2.SetUserContentVisibilityReq( 

3245 user_id=surfer.id, 

3246 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3247 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_SHADOWED], 

3248 ) 

3249 ) 

3250 assert res.updated_count == 0 

3251 

3252 with session_scope() as session: 

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

3254 assert state.visibility == ModerationVisibility.visible 

3255 

3256 with real_moderation_session(super_token) as api: 

3257 res = api.SetUserContentVisibility( 

3258 moderation_pb2.SetUserContentVisibilityReq( 

3259 user_id=surfer.id, 

3260 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3261 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_VISIBLE], 

3262 ) 

3263 ) 

3264 assert res.updated_count == 1 

3265 

3266 with session_scope() as session: 

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

3268 assert state.visibility == ModerationVisibility.shadowed 

3269 

3270 

3271def test_SetUserContentVisibility_from_visibility_multi(db): 

3272 super_user, super_token = generate_user(is_superuser=True) 

3273 surfer, surfer_token = generate_user() 

3274 host1, _ = generate_user() 

3275 host2, _ = generate_user() 

3276 

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

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

3279 with requests_session(surfer_token) as api: 

3280 hr1_id = api.CreateHostRequest( 

3281 requests_pb2.CreateHostRequestReq( 

3282 host_user_id=host1.id, 

3283 from_date=today_plus_2, 

3284 to_date=today_plus_3, 

3285 text=valid_request_text(), 

3286 ) 

3287 ).host_request_id 

3288 hr2_id = api.CreateHostRequest( 

3289 requests_pb2.CreateHostRequestReq( 

3290 host_user_id=host2.id, 

3291 from_date=today_plus_2, 

3292 to_date=today_plus_3, 

3293 text=valid_request_text(), 

3294 ) 

3295 ).host_request_id 

3296 

3297 with session_scope() as session: 

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

3299 

3300 with real_moderation_session(super_token) as api: 

3301 api.ModerateContent( 

3302 moderation_pb2.ModerateContentReq( 

3303 moderation_state_id=state1_id, 

3304 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

3305 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3306 ) 

3307 ) 

3308 

3309 with real_moderation_session(super_token) as api: 

3310 res = api.SetUserContentVisibility( 

3311 moderation_pb2.SetUserContentVisibilityReq( 

3312 user_id=surfer.id, 

3313 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3314 from_visibility=[ 

3315 moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3316 moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3317 ], 

3318 ) 

3319 ) 

3320 assert res.updated_count == 2 

3321 

3322 with session_scope() as session: 

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

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

3325 assert state1.visibility == ModerationVisibility.hidden 

3326 assert state2.visibility == ModerationVisibility.hidden 

3327 

3328 

3329def test_SetUserContentVisibility_from_visibility_empty_is_any(db): 

3330 super_user, super_token = generate_user(is_superuser=True) 

3331 surfer, surfer_token = generate_user() 

3332 host, _ = generate_user() 

3333 

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

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

3336 with requests_session(surfer_token) as api: 

3337 hr_id = api.CreateHostRequest( 

3338 requests_pb2.CreateHostRequestReq( 

3339 host_user_id=host.id, 

3340 from_date=today_plus_2, 

3341 to_date=today_plus_3, 

3342 text=valid_request_text(), 

3343 ) 

3344 ).host_request_id 

3345 

3346 with real_moderation_session(super_token) as api: 

3347 res = api.SetUserContentVisibility( 

3348 moderation_pb2.SetUserContentVisibilityReq( 

3349 user_id=surfer.id, 

3350 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3351 from_visibility=[], 

3352 ) 

3353 ) 

3354 assert res.updated_count == 1 

3355 

3356 with session_scope() as session: 

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

3358 assert state.visibility == ModerationVisibility.hidden 

3359 

3360 

3361def test_SetUserContentVisibility_from_visibility_unspecified_rejected(db): 

3362 super_user, super_token = generate_user(is_superuser=True) 

3363 target, _ = generate_user() 

3364 

3365 with real_moderation_session(super_token) as api: 

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

3367 api.SetUserContentVisibility( 

3368 moderation_pb2.SetUserContentVisibilityReq( 

3369 user_id=target.id, 

3370 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3371 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED], 

3372 ) 

3373 ) 

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

3375 

3376 

3377def test_ListModerationStates_empty(db): 

3378 super_user, super_token = generate_user(is_superuser=True) 

3379 

3380 with real_moderation_session(super_token) as api: 

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

3382 assert len(res.moderation_states) == 0 

3383 assert res.next_page_token == "" 

3384 

3385 

3386def test_ListModerationStates_returns_states_chronologically(db): 

3387 super_user, super_token = generate_user(is_superuser=True) 

3388 surfer, surfer_token = generate_user() 

3389 host, _ = generate_user() 

3390 

3391 state1_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3392 state2_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3393 state3_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3394 

3395 with real_moderation_session(super_token) as api: 

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

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

3398 

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

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

3401 

3402 

3403def test_ListModerationStates_filter_by_author(db): 

3404 super_user, super_token = generate_user(is_superuser=True) 

3405 surfer1, surfer1_token = generate_user() 

3406 surfer2, surfer2_token = generate_user() 

3407 host, _ = generate_user() 

3408 

3409 state1_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3410 state2_id = create_test_host_request_with_moderation(surfer2_token, host.id) 

3411 state3_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3412 

3413 with real_moderation_session(super_token) as api: 

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

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

3416 

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

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

3419 

3420 

3421def test_ListModerationStates_pagination(db): 

3422 super_user, super_token = generate_user(is_superuser=True) 

3423 surfer, surfer_token = generate_user() 

3424 host, _ = generate_user() 

3425 

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

3427 

3428 with real_moderation_session(super_token) as api: 

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

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

3431 assert res.next_page_token != "" 

3432 

3433 res2 = api.ListModerationStates( 

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

3435 ) 

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

3437 assert res2.next_page_token == ""