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

1313 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-14 11:44 +0000

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.db import session_scope 

14from couchers.jobs.handlers import auto_approve_moderation_queue 

15from couchers.jobs.worker import process_job 

16from couchers.models import ( 

17 AdminAction, 

18 EventOccurrence, 

19 FriendRelationship, 

20 GroupChat, 

21 HostRequest, 

22 ModerationAction, 

23 ModerationLog, 

24 ModerationObjectType, 

25 ModerationQueueItem, 

26 ModerationState, 

27 ModerationTrigger, 

28 ModerationVisibility, 

29 User, 

30) 

31from couchers.moderation.utils import create_moderation 

32from couchers.proto import api_pb2, conversations_pb2, events_pb2, moderation_pb2, notifications_pb2, requests_pb2 

33from couchers.utils import Timestamp_from_datetime, now, today 

34from tests.fixtures.db import generate_user, make_friends 

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

36from tests.fixtures.sessions import ( 

37 api_session, 

38 conversations_session, 

39 events_session, 

40 notifications_session, 

41 real_moderation_session, 

42 requests_session, 

43) 

44from tests.test_communities import create_community 

45from tests.test_requests import valid_request_text 

46 

47 

48@pytest.fixture(autouse=True) 

49def _(testconfig): 

50 pass 

51 

52 

53def create_test_host_request_with_moderation(surfer_token, host_user_id): 

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

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

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

57 

58 with requests_session(surfer_token) as api: 

59 hr_id = api.CreateHostRequest( 

60 requests_pb2.CreateHostRequestReq( 

61 host_user_id=host_user_id, 

62 from_date=today_plus_2, 

63 to_date=today_plus_3, 

64 text=valid_request_text(), 

65 ) 

66 ).host_request_id 

67 

68 with session_scope() as session: 

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

70 return hr.moderation_state_id 

71 

72 

73# ============================================================================ 

74# Tests for moderation helper functions 

75# ============================================================================ 

76 

77 

78def test_create_moderation(db): 

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

80 user, _ = generate_user() 

81 

82 with session_scope() as session: 

83 # Create a moderation state 

84 moderation_state = create_moderation( 

85 session=session, 

86 object_type=ModerationObjectType.host_request, 

87 object_id=123, 

88 creator_user_id=user.id, 

89 ) 

90 

91 assert moderation_state.object_type == ModerationObjectType.host_request 

92 assert moderation_state.object_id == 123 

93 assert moderation_state.visibility == ModerationVisibility.shadowed 

94 

95 # Check that log entry was created 

96 log_entries = ( 

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

98 .scalars() 

99 .all() 

100 ) 

101 

102 assert len(log_entries) == 1 

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

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

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

106 

107 

108def test_add_to_moderation_queue(db): 

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

110 super_user, super_token = generate_user(is_superuser=True) 

111 user1, token1 = generate_user() 

112 user2, _ = generate_user() 

113 

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

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

116 

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

118 with requests_session(token1) as api: 

119 host_request_id = api.CreateHostRequest( 

120 requests_pb2.CreateHostRequestReq( 

121 host_user_id=user2.id, 

122 from_date=today_plus_2, 

123 to_date=today_plus_3, 

124 text=valid_request_text(), 

125 ) 

126 ).host_request_id 

127 

128 # Get the moderation state ID 

129 state_id = None 

130 with session_scope() as session: 

131 host_request = session.execute( 

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

133 ).scalar_one() 

134 state_id = host_request.moderation_state_id 

135 

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

137 with real_moderation_session(super_token) as api: 

138 res = api.FlagContentForReview( 

139 moderation_pb2.FlagContentForReviewReq( 

140 moderation_state_id=state_id, 

141 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

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

143 ) 

144 ) 

145 

146 assert res.queue_item.moderation_state_id == state_id 

147 assert res.queue_item.trigger == moderation_pb2.MODERATION_TRIGGER_USER_FLAG 

148 assert res.queue_item.reason == "Admin manually flagged for additional review" 

149 assert res.queue_item.moderation_state.author.user_id == user1.id 

150 assert res.queue_item.is_resolved == False 

151 

152 

153def test_moderate_content(db): 

154 """Test moderating content via API""" 

155 super_user, super_token = generate_user(is_superuser=True) 

156 user, token = generate_user() 

157 host, _ = generate_user() 

158 

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

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

161 

162 # Create a real host request 

163 state_id = None 

164 with requests_session(token) as api: 

165 hr_id = api.CreateHostRequest( 

166 requests_pb2.CreateHostRequestReq( 

167 host_user_id=host.id, 

168 from_date=today_plus_2, 

169 to_date=today_plus_3, 

170 text=valid_request_text(), 

171 ) 

172 ).host_request_id 

173 

174 with session_scope() as session: 

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

176 state_id = hr.moderation_state_id 

177 

178 # Moderate the content via API 

179 with real_moderation_session(super_token) as api: 

180 res = api.ModerateContent( 

181 moderation_pb2.ModerateContentReq( 

182 moderation_state_id=state_id, 

183 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

184 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

185 reason="Content looks good", 

186 ) 

187 ) 

188 

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

190 

191 # Check that state was updated in database 

192 with session_scope() as session: 

193 updated_state = session.get_one(ModerationState, state_id) 

194 assert updated_state.visibility == ModerationVisibility.visible 

195 

196 # Check that log entry was created 

197 log_entries = ( 

198 session.execute( 

199 select(ModerationLog) 

200 .where(ModerationLog.moderation_state_id == state_id) 

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

202 ) 

203 .scalars() 

204 .all() 

205 ) 

206 

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

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

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

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

211 

212 

213def test_resolve_queue_item(db): 

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

215 user1, token1 = generate_user() 

216 user2, _ = generate_user() 

217 moderator, moderator_token = generate_user(is_superuser=True) 

218 

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

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

221 

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

223 with requests_session(token1) as api: 

224 host_request_id = api.CreateHostRequest( 

225 requests_pb2.CreateHostRequestReq( 

226 host_user_id=user2.id, 

227 from_date=today_plus_2, 

228 to_date=today_plus_3, 

229 text=valid_request_text(), 

230 ) 

231 ).host_request_id 

232 

233 state_id = None 

234 with session_scope() as session: 

235 # Get the host request and its moderation state 

236 host_request = session.execute( 

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

238 ).scalar_one() 

239 state_id = host_request.moderation_state_id 

240 

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

242 queue_item = session.execute( 

243 select(ModerationQueueItem) 

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

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

246 ).scalar_one() 

247 

248 assert queue_item.resolved_by_log_id is None 

249 

250 # Approve content via API (which should resolve the queue item) 

251 with real_moderation_session(moderator_token) as api: 

252 api.ModerateContent( 

253 moderation_pb2.ModerateContentReq( 

254 moderation_state_id=state_id, 

255 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

256 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

257 reason="Approved after review", 

258 ) 

259 ) 

260 

261 # Check that queue item was resolved 

262 with session_scope() as session: 

263 queue_item = session.execute( 

264 select(ModerationQueueItem) 

265 .where(ModerationQueueItem.moderation_state_id == state_id) 

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

267 ).scalar_one() 

268 assert queue_item.resolved_by_log_id is not None 

269 

270 

271def test_approve_content_via_api(db): 

272 """Test approving content via ModerateContent API""" 

273 user1, token1 = generate_user() 

274 user2, _ = generate_user() 

275 moderator, moderator_token = generate_user(is_superuser=True) 

276 

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

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

279 

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

281 with requests_session(token1) as api: 

282 host_request_id = api.CreateHostRequest( 

283 requests_pb2.CreateHostRequestReq( 

284 host_user_id=user2.id, 

285 from_date=today_plus_2, 

286 to_date=today_plus_3, 

287 text=valid_request_text(), 

288 ) 

289 ).host_request_id 

290 

291 state_id = None 

292 with session_scope() as session: 

293 # Get the host request and its moderation state 

294 host_request = session.execute( 

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

296 ).scalar_one() 

297 state_id = host_request.moderation_state_id 

298 

299 # Approve via API 

300 with real_moderation_session(moderator_token) as api: 

301 api.ModerateContent( 

302 moderation_pb2.ModerateContentReq( 

303 moderation_state_id=state_id, 

304 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

305 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

306 reason="Quick approval", 

307 ) 

308 ) 

309 

310 # Check that state was updated to VISIBLE 

311 with session_scope() as session: 

312 updated_state = session.get_one(ModerationState, state_id) 

313 assert updated_state.visibility == ModerationVisibility.visible 

314 

315 # Check log entry 

316 log_entry = session.execute( 

317 select(ModerationLog) 

318 .where(ModerationLog.moderation_state_id == state_id) 

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

320 ).scalar_one() 

321 

322 assert log_entry.moderator_user_id == moderator.id 

323 assert log_entry.reason == "Quick approval" 

324 

325 

326# ============================================================================ 

327# Tests for host request moderation integration 

328# ============================================================================ 

329 

330 

331def test_create_host_request_creates_moderation_state(db): 

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

333 user1, token1 = generate_user() 

334 user2, token2 = generate_user() 

335 

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

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

338 

339 with requests_session(token1) as api: 

340 host_request_id = api.CreateHostRequest( 

341 requests_pb2.CreateHostRequestReq( 

342 host_user_id=user2.id, 

343 from_date=today_plus_2, 

344 to_date=today_plus_3, 

345 text=valid_request_text(), 

346 ) 

347 ).host_request_id 

348 

349 with session_scope() as session: 

350 # Check that host request has a moderation state 

351 host_request = session.execute( 

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

353 ).scalar_one() 

354 

355 # Check moderation state properties 

356 moderation_state = session.execute( 

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

358 ).scalar_one() 

359 

360 assert moderation_state.object_type == ModerationObjectType.host_request 

361 assert moderation_state.object_id == host_request_id 

362 assert moderation_state.visibility == ModerationVisibility.shadowed 

363 

364 # Check that it was added to moderation queue 

365 queue_items = ( 

366 session.execute( 

367 select(ModerationQueueItem) 

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

369 .where(ModerationQueueItem.resolved_by_log_id == None) 

370 ) 

371 .scalars() 

372 .all() 

373 ) 

374 

375 assert len(queue_items) == 1 

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

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

378 

379 

380def test_host_request_no_notification_before_approval(db, push_collector: PushCollector): 

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

382 user1, token1 = generate_user() 

383 user2, token2 = generate_user() 

384 

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

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

387 

388 with requests_session(token1) as api: 

389 host_request_id = api.CreateHostRequest( 

390 requests_pb2.CreateHostRequestReq( 

391 host_user_id=user2.id, 

392 from_date=today_plus_2, 

393 to_date=today_plus_3, 

394 text=valid_request_text(), 

395 ) 

396 ).host_request_id 

397 

398 # Process all jobs (including the notification job) 

399 process_jobs() 

400 

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

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

403 

404 

405def test_shadowed_notification_not_in_list_notifications(db): 

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

407 user1, token1 = generate_user() 

408 user2, token2 = generate_user() 

409 

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

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

412 

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

414 with requests_session(token1) as api: 

415 host_request_id = api.CreateHostRequest( 

416 requests_pb2.CreateHostRequestReq( 

417 host_user_id=user2.id, 

418 from_date=today_plus_2, 

419 to_date=today_plus_3, 

420 text=valid_request_text(), 

421 ) 

422 ).host_request_id 

423 

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

425 with notifications_session(token2) as api: 

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

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

428 assert len(res.notifications) == 0 

429 

430 

431def test_notification_visible_after_approval(db): 

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

433 user1, token1 = generate_user() 

434 user2, token2 = generate_user() 

435 mod, mod_token = generate_user(is_superuser=True) 

436 

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

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

439 

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

441 with requests_session(token1) as api: 

442 host_request_id = api.CreateHostRequest( 

443 requests_pb2.CreateHostRequestReq( 

444 host_user_id=user2.id, 

445 from_date=today_plus_2, 

446 to_date=today_plus_3, 

447 text=valid_request_text(), 

448 ) 

449 ).host_request_id 

450 

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

452 with notifications_session(token2) as api: 

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

454 assert len(res.notifications) == 0 

455 

456 # Get the moderation state ID and approve 

457 with session_scope() as session: 

458 host_request = session.execute( 

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

460 ).scalar_one() 

461 state_id = host_request.moderation_state_id 

462 

463 with real_moderation_session(mod_token) as api: 

464 api.ModerateContent( 

465 moderation_pb2.ModerateContentReq( 

466 moderation_state_id=state_id, 

467 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

468 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

469 reason="Looks good", 

470 ) 

471 ) 

472 

473 # Now host SHOULD see the notification 

474 with notifications_session(token2) as api: 

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

476 assert len(res.notifications) == 1 

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

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

479 

480 

481def test_shadowed_host_request_visible_to_author_only(db): 

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

483 user1, token1 = generate_user() 

484 user2, token2 = generate_user() 

485 

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

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

488 

489 with requests_session(token1) as api: 

490 host_request_id = api.CreateHostRequest( 

491 requests_pb2.CreateHostRequestReq( 

492 host_user_id=user2.id, 

493 from_date=today_plus_2, 

494 to_date=today_plus_3, 

495 text=valid_request_text(), 

496 ) 

497 ).host_request_id 

498 

499 # Surfer (author) can see it with GetHostRequest 

500 with requests_session(token1) as api: 

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

502 assert res.host_request_id == host_request_id 

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

504 

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

506 with requests_session(token2) as api: 

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

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

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

510 

511 

512def test_unlisted_host_request_not_in_lists(db): 

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

514 user1, token1 = generate_user() 

515 user2, token2 = generate_user() 

516 

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

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

519 

520 with requests_session(token1) as api: 

521 host_request_id = api.CreateHostRequest( 

522 requests_pb2.CreateHostRequestReq( 

523 host_user_id=user2.id, 

524 from_date=today_plus_2, 

525 to_date=today_plus_3, 

526 text=valid_request_text(), 

527 ) 

528 ).host_request_id 

529 

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

531 with requests_session(token1) as api: 

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

533 assert len(res.host_requests) == 1 

534 

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

536 with requests_session(token2) as api: 

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

538 assert len(res.host_requests) == 0 

539 

540 

541def test_approved_host_request_in_lists_and_notifications(db, push_collector: PushCollector): 

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

543 user1, token1 = generate_user() 

544 user2, token2 = generate_user() 

545 mod, mod_token = generate_user(is_superuser=True) 

546 

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

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

549 

550 with requests_session(token1) as api: 

551 host_request_id = api.CreateHostRequest( 

552 requests_pb2.CreateHostRequestReq( 

553 host_user_id=user2.id, 

554 from_date=today_plus_2, 

555 to_date=today_plus_3, 

556 text=valid_request_text(), 

557 ) 

558 ).host_request_id 

559 

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

561 process_jobs() 

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

563 

564 # Get the moderation state ID 

565 state_id = None 

566 with session_scope() as session: 

567 host_request = session.execute( 

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

569 ).scalar_one() 

570 state_id = host_request.moderation_state_id 

571 

572 # Approve the host request via API 

573 with real_moderation_session(mod_token) as api: 

574 api.ModerateContent( 

575 moderation_pb2.ModerateContentReq( 

576 moderation_state_id=state_id, 

577 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

578 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

579 reason="Looks good", 

580 ) 

581 ) 

582 

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

584 process_jobs() 

585 

586 # Now surfer SHOULD see it in their sent list 

587 with requests_session(token1) as api: 

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

589 assert len(res.host_requests) == 1 

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

591 

592 # Host SHOULD see it in their received list 

593 with requests_session(token2) as api: 

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

595 assert len(res.host_requests) == 1 

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

597 

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

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

600 

601 

602def test_hidden_host_request_invisible_to_all(db): 

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

604 user1, token1 = generate_user() 

605 user2, token2 = generate_user() 

606 user3, token3 = generate_user() # Third party 

607 moderator, moderator_token = generate_user(is_superuser=True) 

608 

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

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

611 

612 with requests_session(token1) as api: 

613 host_request_id = api.CreateHostRequest( 

614 requests_pb2.CreateHostRequestReq( 

615 host_user_id=user2.id, 

616 from_date=today_plus_2, 

617 to_date=today_plus_3, 

618 text=valid_request_text(), 

619 ) 

620 ).host_request_id 

621 

622 # Get the moderation state ID 

623 state_id = None 

624 with session_scope() as session: 

625 host_request = session.execute( 

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

627 ).scalar_one() 

628 state_id = host_request.moderation_state_id 

629 

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

631 with real_moderation_session(moderator_token) as api: 

632 api.ModerateContent( 

633 moderation_pb2.ModerateContentReq( 

634 moderation_state_id=state_id, 

635 action=moderation_pb2.MODERATION_ACTION_HIDE, 

636 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

637 reason="Spam content", 

638 ) 

639 ) 

640 

641 # Surfer can't see it with GetHostRequest 

642 with requests_session(token1) as api: 

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

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

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

646 

647 # Host can't see it with GetHostRequest 

648 with requests_session(token2) as api: 

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

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

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

652 

653 # Third party definitely can't see it 

654 with requests_session(token3) as api: 

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

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

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

658 

659 # Not in any lists 

660 with requests_session(token1) as api: 

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

662 assert len(res.host_requests) == 0 

663 

664 with requests_session(token2) as api: 

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

666 assert len(res.host_requests) == 0 

667 

668 

669def test_multiple_host_requests_listing_visibility(db): 

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

671 user1, token1 = generate_user() 

672 user2, token2 = generate_user() 

673 moderator, moderator_token = generate_user(is_superuser=True) 

674 

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

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

677 

678 # Create 3 host requests 

679 host_request_ids = [] 

680 state_ids = [] 

681 with requests_session(token1) as api: 

682 for i in range(3): 

683 hr_id = api.CreateHostRequest( 

684 requests_pb2.CreateHostRequestReq( 

685 host_user_id=user2.id, 

686 from_date=today_plus_2, 

687 to_date=today_plus_3, 

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

689 ) 

690 ).host_request_id 

691 host_request_ids.append(hr_id) 

692 

693 # Get state IDs 

694 with session_scope() as session: 

695 for hr_id in host_request_ids: 

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

697 state_ids.append(host_request.moderation_state_id) 

698 

699 # Approve the first one via API 

700 with real_moderation_session(moderator_token) as api: 

701 api.ModerateContent( 

702 moderation_pb2.ModerateContentReq( 

703 moderation_state_id=state_ids[0], 

704 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

705 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

706 reason="Approved", 

707 ) 

708 ) 

709 

710 # Hide the third one via API 

711 with real_moderation_session(moderator_token) as api: 

712 api.ModerateContent( 

713 moderation_pb2.ModerateContentReq( 

714 moderation_state_id=state_ids[2], 

715 action=moderation_pb2.MODERATION_ACTION_HIDE, 

716 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

717 reason="Spam", 

718 ) 

719 ) 

720 

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

722 with requests_session(token1) as api: 

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

724 assert len(res.host_requests) == 2 

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

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

727 

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

729 with requests_session(token2) as api: 

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

731 assert len(res.host_requests) == 1 

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

733 

734 

735def test_moderation_log_tracking(db): 

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

737 user, user_token = generate_user() 

738 host, _ = generate_user() 

739 moderator1, moderator1_token = generate_user(is_superuser=True) 

740 moderator2, moderator2_token = generate_user(is_superuser=True) 

741 

742 # Create a real host request 

743 state_id = create_test_host_request_with_moderation(user_token, host.id) 

744 

745 # Perform several moderation actions via API 

746 with real_moderation_session(moderator1_token) as api: 

747 api.ModerateContent( 

748 moderation_pb2.ModerateContentReq( 

749 moderation_state_id=state_id, 

750 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

751 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

752 reason="Looks good initially", 

753 ) 

754 ) 

755 

756 with real_moderation_session(moderator2_token) as api: 

757 api.FlagContentForReview( 

758 moderation_pb2.FlagContentForReviewReq( 

759 moderation_state_id=state_id, 

760 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

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

762 ) 

763 ) 

764 # Shadow it back 

765 api.ModerateContent( 

766 moderation_pb2.ModerateContentReq( 

767 moderation_state_id=state_id, 

768 action=moderation_pb2.MODERATION_ACTION_HIDE, 

769 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

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

771 ) 

772 ) 

773 

774 with real_moderation_session(moderator1_token) as api: 

775 api.ModerateContent( 

776 moderation_pb2.ModerateContentReq( 

777 moderation_state_id=state_id, 

778 action=moderation_pb2.MODERATION_ACTION_HIDE, 

779 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

780 reason="Actually it's spam", 

781 ) 

782 ) 

783 

784 # Check all log entries 

785 with session_scope() as session: 

786 log_entries = ( 

787 session.execute( 

788 select(ModerationLog) 

789 .where(ModerationLog.moderation_state_id == state_id) 

790 .order_by(ModerationLog.time.asc()) 

791 ) 

792 .scalars() 

793 .all() 

794 ) 

795 

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

797 assert len(log_entries) >= 3 

798 

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

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

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

802 

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

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

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

806 

807 # The last action should be hiding 

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

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

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

811 

812 

813def test_moderation_queue_workflow(db): 

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

815 user1, token1 = generate_user() 

816 user2, _ = generate_user() 

817 moderator, moderator_token = generate_user(is_superuser=True) 

818 

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

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

821 

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

823 with requests_session(token1) as api: 

824 host_request_id = api.CreateHostRequest( 

825 requests_pb2.CreateHostRequestReq( 

826 host_user_id=user2.id, 

827 from_date=today_plus_2, 

828 to_date=today_plus_3, 

829 text=valid_request_text(), 

830 ) 

831 ).host_request_id 

832 

833 state_id = None 

834 queue_item_id = None 

835 with session_scope() as session: 

836 # Get the host request and its moderation state 

837 host_request = session.execute( 

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

839 ).scalar_one() 

840 state_id = host_request.moderation_state_id 

841 

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

843 queue_item = session.execute( 

844 select(ModerationQueueItem) 

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

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

847 ).scalar_one() 

848 queue_item_id = queue_item.id 

849 

850 # Verify it's in the queue 

851 unresolved_items = ( 

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

853 .scalars() 

854 .all() 

855 ) 

856 

857 assert len(unresolved_items) >= 1 

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

859 

860 # Moderator reviews and approves via API (which also resolves the queue item) 

861 with real_moderation_session(moderator_token) as api: 

862 api.ModerateContent( 

863 moderation_pb2.ModerateContentReq( 

864 moderation_state_id=state_id, 

865 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

866 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

867 reason="Content approved", 

868 ) 

869 ) 

870 

871 # Verify queue item was resolved 

872 with session_scope() as session: 

873 # Verify it's no longer in unresolved queue 

874 unresolved_items = ( 

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

876 .scalars() 

877 .all() 

878 ) 

879 

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

881 

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

883 queue_item = session.get_one(ModerationQueueItem, queue_item_id) 

884 assert queue_item.resolved_by_log_id is not None 

885 

886 

887# ============================================================================ 

888# Moderation API Tests (testing the gRPC servicer) 

889# ============================================================================ 

890 

891 

892def test_GetModerationQueue_empty(db): 

893 """Test getting an empty moderation queue""" 

894 super_user, super_token = generate_user(is_superuser=True) 

895 

896 with real_moderation_session(super_token) as api: 

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

898 assert len(res.queue_items) == 0 

899 assert res.next_page_token == "" 

900 

901 

902def test_GetModerationQueue_with_items(db): 

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

904 super_user, super_token = generate_user(is_superuser=True) 

905 normal_user, user_token = generate_user() 

906 host, _ = generate_user() 

907 

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

909 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

910 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

911 

912 with real_moderation_session(super_token) as api: 

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

914 assert len(res.queue_items) == 2 

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

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

917 

918 

919def test_GetModerationQueue_filter_by_trigger(db): 

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

921 super_user, super_token = generate_user(is_superuser=True) 

922 normal_user, user_token = generate_user() 

923 host, _ = generate_user() 

924 

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

926 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

927 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

928 

929 # Add USER_FLAG trigger to second item via API 

930 with real_moderation_session(super_token) as api: 

931 api.FlagContentForReview( 

932 moderation_pb2.FlagContentForReviewReq( 

933 moderation_state_id=state2_id, 

934 trigger=moderation_pb2.MODERATION_TRIGGER_USER_FLAG, 

935 reason="Reported by user", 

936 ) 

937 ) 

938 

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

940 with real_moderation_session(super_token) as api: 

941 res = api.GetModerationQueue( 

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

943 ) 

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

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

946 

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

948 with real_moderation_session(super_token) as api: 

949 res = api.GetModerationQueue( 

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

951 ) 

952 assert len(res.queue_items) == 1 

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

954 

955 

956def test_GetModerationQueue_filter_created_before(db): 

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

958 super_user, super_token = generate_user(is_superuser=True) 

959 normal_user, user_token = generate_user() 

960 host, _ = generate_user() 

961 

962 # Create host requests 

963 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

964 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

965 

966 # Backdate the first queue item 

967 with session_scope() as session: 

968 queue_item1 = session.execute( 

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

970 ).scalar_one() 

971 # Set it to 2 hours ago 

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

973 

974 # The second item remains at current time 

975 

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

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

978 with real_moderation_session(super_token) as api: 

979 res = api.GetModerationQueue( 

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

981 ) 

982 assert len(res.queue_items) == 1 

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

984 

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

986 with real_moderation_session(super_token) as api: 

987 res = api.GetModerationQueue( 

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

989 ) 

990 assert len(res.queue_items) == 2 

991 

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

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

994 with real_moderation_session(super_token) as api: 

995 res = api.GetModerationQueue( 

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

997 ) 

998 assert len(res.queue_items) == 0 

999 

1000 

1001def test_GetModerationQueue_filter_created_after(db): 

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

1003 super_user, super_token = generate_user(is_superuser=True) 

1004 normal_user, user_token = generate_user() 

1005 host, _ = generate_user() 

1006 

1007 # Create host requests 

1008 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1009 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1010 

1011 # Backdate the first queue item to 2 hours ago 

1012 with session_scope() as session: 

1013 queue_item1 = session.execute( 

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

1015 ).scalar_one() 

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

1017 

1018 # The second item remains at current time 

1019 

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

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

1022 with real_moderation_session(super_token) as api: 

1023 res = api.GetModerationQueue( 

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

1025 ) 

1026 assert len(res.queue_items) == 1 

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

1028 

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

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

1031 with real_moderation_session(super_token) as api: 

1032 res = api.GetModerationQueue( 

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

1034 ) 

1035 assert len(res.queue_items) == 2 

1036 

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

1038 with real_moderation_session(super_token) as api: 

1039 res = api.GetModerationQueue( 

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

1041 ) 

1042 assert len(res.queue_items) == 0 

1043 

1044 

1045def test_GetModerationQueue_filter_created_before_and_after(db): 

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

1047 super_user, super_token = generate_user(is_superuser=True) 

1048 normal_user, user_token = generate_user() 

1049 host, _ = generate_user() 

1050 

1051 # Create 3 host requests 

1052 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1053 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1054 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1055 

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

1057 with session_scope() as session: 

1058 queue_item1 = session.execute( 

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

1060 ).scalar_one() 

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

1062 

1063 queue_item2 = session.execute( 

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

1065 ).scalar_one() 

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

1067 

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

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

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

1071 with real_moderation_session(super_token) as api: 

1072 res = api.GetModerationQueue( 

1073 moderation_pb2.GetModerationQueueReq( 

1074 created_after=Timestamp_from_datetime(after_cutoff), 

1075 created_before=Timestamp_from_datetime(before_cutoff), 

1076 ) 

1077 ) 

1078 assert len(res.queue_items) == 1 

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

1080 

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

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

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

1084 with real_moderation_session(super_token) as api: 

1085 res = api.GetModerationQueue( 

1086 moderation_pb2.GetModerationQueueReq( 

1087 created_after=Timestamp_from_datetime(after_cutoff), 

1088 created_before=Timestamp_from_datetime(before_cutoff), 

1089 ) 

1090 ) 

1091 assert len(res.queue_items) == 1 

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

1093 

1094 

1095def test_GetModerationQueue_filter_unresolved(db): 

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

1097 super_user, super_token = generate_user(is_superuser=True) 

1098 normal_user, user_token = generate_user() 

1099 host, _ = generate_user() 

1100 

1101 # Create 2 host requests 

1102 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1103 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1104 

1105 # Resolve the first one via API (ModerateContent automatically resolves queue items) 

1106 with real_moderation_session(super_token) as api: 

1107 api.ModerateContent( 

1108 moderation_pb2.ModerateContentReq( 

1109 moderation_state_id=state1_id, 

1110 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1111 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1112 reason="Approved", 

1113 ) 

1114 ) 

1115 

1116 # Get all items 

1117 with real_moderation_session(super_token) as api: 

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

1119 assert len(res.queue_items) == 2 

1120 

1121 # Get only unresolved items 

1122 with real_moderation_session(super_token) as api: 

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

1124 assert len(res.queue_items) == 1 

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

1126 

1127 

1128def test_GetModerationQueue_filter_by_author(db): 

1129 """Test filtering moderation queue by item_author_user_id""" 

1130 super_user, super_token = generate_user(is_superuser=True) 

1131 user1, token1 = generate_user() 

1132 user2, token2 = generate_user() 

1133 host_user, _ = generate_user() 

1134 

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

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

1137 

1138 # Create 2 host requests by user1 

1139 with requests_session(token1) as api: 

1140 hr1_id = api.CreateHostRequest( 

1141 requests_pb2.CreateHostRequestReq( 

1142 host_user_id=host_user.id, 

1143 from_date=today_plus_2, 

1144 to_date=today_plus_3, 

1145 text=valid_request_text(), 

1146 ) 

1147 ).host_request_id 

1148 

1149 hr2_id = api.CreateHostRequest( 

1150 requests_pb2.CreateHostRequestReq( 

1151 host_user_id=host_user.id, 

1152 from_date=today_plus_2, 

1153 to_date=today_plus_3, 

1154 text=valid_request_text(), 

1155 ) 

1156 ).host_request_id 

1157 

1158 # Create 1 host request by user2 

1159 with requests_session(token2) as api: 

1160 hr3_id = api.CreateHostRequest( 

1161 requests_pb2.CreateHostRequestReq( 

1162 host_user_id=host_user.id, 

1163 from_date=today_plus_2, 

1164 to_date=today_plus_3, 

1165 text=valid_request_text(), 

1166 ) 

1167 ).host_request_id 

1168 

1169 # Get moderation state IDs 

1170 state1_id, state2_id, state3_id = None, None, None 

1171 with session_scope() as session: 

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

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

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

1175 state1_id = hr1.moderation_state_id 

1176 state2_id = hr2.moderation_state_id 

1177 state3_id = hr3.moderation_state_id 

1178 

1179 # Get all items (should be 3) 

1180 with real_moderation_session(super_token) as api: 

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

1182 assert len(res.queue_items) == 3 

1183 

1184 # Filter by user1 (should get 2) 

1185 with real_moderation_session(super_token) as api: 

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

1187 assert len(res.queue_items) == 2 

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

1189 

1190 # Filter by user2 (should get 1) 

1191 with real_moderation_session(super_token) as api: 

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

1193 assert len(res.queue_items) == 1 

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

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

1196 

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

1198 with real_moderation_session(super_token) as api: 

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

1200 assert len(res.queue_items) == 0 

1201 

1202 

1203def test_GetModerationQueue_ordering(db): 

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

1205 super_user, super_token = generate_user(is_superuser=True) 

1206 normal_user, user_token = generate_user() 

1207 host, _ = generate_user() 

1208 

1209 # Create 3 host requests 

1210 state1_id = create_test_host_request_with_moderation(user_token, host.id) 

1211 state2_id = create_test_host_request_with_moderation(user_token, host.id) 

1212 state3_id = create_test_host_request_with_moderation(user_token, host.id) 

1213 

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

1215 with session_scope() as session: 

1216 queue_item1 = session.execute( 

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

1218 ).scalar_one() 

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

1220 

1221 queue_item2 = session.execute( 

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

1223 ).scalar_one() 

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

1225 

1226 queue_item3 = session.execute( 

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

1228 ).scalar_one() 

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

1230 

1231 # Default order (oldest first) 

1232 with real_moderation_session(super_token) as api: 

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

1234 assert len(res.queue_items) == 3 

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

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

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

1238 

1239 # Explicit oldest first 

1240 with real_moderation_session(super_token) as api: 

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

1242 assert len(res.queue_items) == 3 

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

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

1245 

1246 # Newest first 

1247 with real_moderation_session(super_token) as api: 

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

1249 assert len(res.queue_items) == 3 

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

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

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

1253 

1254 

1255def test_GetModerationQueue_pagination_newest_first(db): 

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

1257 super_user, super_token = generate_user(is_superuser=True) 

1258 normal_user, normal_token = generate_user() 

1259 host_user, _ = generate_user() 

1260 

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

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

1263 

1264 # Create 5 host requests 

1265 hr_ids = [] 

1266 with requests_session(normal_token) as api: 

1267 for i in range(5): 

1268 hr_id = api.CreateHostRequest( 

1269 requests_pb2.CreateHostRequestReq( 

1270 host_user_id=host_user.id, 

1271 from_date=today_plus_2, 

1272 to_date=today_plus_3, 

1273 text=valid_request_text(), 

1274 ) 

1275 ).host_request_id 

1276 hr_ids.append(hr_id) 

1277 

1278 # Get moderation state IDs 

1279 state_ids = [] 

1280 with session_scope() as session: 

1281 for hr_id in hr_ids: 

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

1283 state_ids.append(hr.moderation_state_id) 

1284 

1285 # Set different times so ordering is deterministic 

1286 with session_scope() as session: 

1287 for i, state_id in enumerate(state_ids): 

1288 queue_item = session.execute( 

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

1290 ).scalar_one() 

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

1292 

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

1294 with real_moderation_session(super_token) as api: 

1295 res1 = api.GetModerationQueue( 

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

1297 ) 

1298 assert len(res1.queue_items) == 2 

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

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

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

1302 assert res1.next_page_token # should have more pages 

1303 

1304 # Get second page using the token 

1305 res2 = api.GetModerationQueue( 

1306 moderation_pb2.GetModerationQueueReq( 

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

1308 ) 

1309 ) 

1310 assert len(res2.queue_items) == 2 

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

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

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

1314 

1315 # Pages should not overlap 

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

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

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

1319 

1320 

1321def test_GetModerationLog(db): 

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

1323 super_user, super_token = generate_user(is_superuser=True) 

1324 moderator, moderator_token = generate_user(is_superuser=True) 

1325 normal_user, user_token = generate_user() 

1326 host, _ = generate_user() 

1327 

1328 # Create a real host request 

1329 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1330 

1331 # Perform a moderation action via API 

1332 with real_moderation_session(moderator_token) as api: 

1333 api.ModerateContent( 

1334 moderation_pb2.ModerateContentReq( 

1335 moderation_state_id=state_id, 

1336 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1337 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1338 reason="Looks good", 

1339 ) 

1340 ) 

1341 

1342 with real_moderation_session(super_token) as api: 

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

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

1345 assert res.moderation_state.moderation_state_id == state_id 

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

1347 # Log entries are in reverse chronological order 

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

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

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

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

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

1353 

1354 

1355def test_GetModerationLog_not_found(db): 

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

1357 super_user, super_token = generate_user(is_superuser=True) 

1358 

1359 with real_moderation_session(super_token) as api: 

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

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

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

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

1364 

1365 

1366def test_GetModerationState(db): 

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

1368 super_user, super_token = generate_user(is_superuser=True) 

1369 user1, token1 = generate_user() 

1370 user2, _ = generate_user() 

1371 

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

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

1374 

1375 with requests_session(token1) as api: 

1376 host_request_id = api.CreateHostRequest( 

1377 requests_pb2.CreateHostRequestReq( 

1378 host_user_id=user2.id, 

1379 from_date=today_plus_2, 

1380 to_date=today_plus_3, 

1381 text=valid_request_text(), 

1382 ) 

1383 ).host_request_id 

1384 

1385 with real_moderation_session(super_token) as api: 

1386 res = api.GetModerationState( 

1387 moderation_pb2.GetModerationStateReq( 

1388 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1389 object_id=host_request_id, 

1390 ) 

1391 ) 

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

1393 assert res.moderation_state.object_id == host_request_id 

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

1395 assert res.moderation_state.moderation_state_id > 0 

1396 

1397 

1398def test_GetModerationState_not_found(db): 

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

1400 super_user, super_token = generate_user(is_superuser=True) 

1401 

1402 with real_moderation_session(super_token) as api: 

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

1404 api.GetModerationState( 

1405 moderation_pb2.GetModerationStateReq( 

1406 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1407 object_id=999999, 

1408 ) 

1409 ) 

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

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

1412 

1413 

1414def test_GetModerationState_unspecified_type(db): 

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

1416 super_user, super_token = generate_user(is_superuser=True) 

1417 

1418 with real_moderation_session(super_token) as api: 

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

1420 api.GetModerationState( 

1421 moderation_pb2.GetModerationStateReq( 

1422 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_UNSPECIFIED, 

1423 object_id=123, 

1424 ) 

1425 ) 

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

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

1428 

1429 

1430def test_ModerateContent_approve(db): 

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

1432 super_user, super_token = generate_user(is_superuser=True) 

1433 user1, token1 = generate_user() 

1434 user2, _ = generate_user() 

1435 

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

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

1438 

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

1440 with requests_session(token1) as api: 

1441 host_request_id = api.CreateHostRequest( 

1442 requests_pb2.CreateHostRequestReq( 

1443 host_user_id=user2.id, 

1444 from_date=today_plus_2, 

1445 to_date=today_plus_3, 

1446 text=valid_request_text(), 

1447 ) 

1448 ).host_request_id 

1449 

1450 # Get the moderation state ID 

1451 state_id = None 

1452 with session_scope() as session: 

1453 host_request = session.execute( 

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

1455 ).scalar_one() 

1456 state_id = host_request.moderation_state_id 

1457 

1458 with real_moderation_session(super_token) as api: 

1459 res = api.ModerateContent( 

1460 moderation_pb2.ModerateContentReq( 

1461 moderation_state_id=state_id, 

1462 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1463 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1464 reason="Approved by admin", 

1465 ) 

1466 ) 

1467 assert res.moderation_state.moderation_state_id == state_id 

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

1469 

1470 # Verify state was updated in database 

1471 with session_scope() as session: 

1472 state = session.get_one(ModerationState, state_id) 

1473 assert state.visibility == ModerationVisibility.visible 

1474 

1475 

1476def test_ModerateContent_not_found(db): 

1477 """Test moderating non-existent content""" 

1478 super_user, super_token = generate_user(is_superuser=True) 

1479 

1480 with real_moderation_session(super_token) as api: 

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

1482 api.ModerateContent( 

1483 moderation_pb2.ModerateContentReq( 

1484 moderation_state_id=999999, 

1485 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1486 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1487 reason="Test", 

1488 ) 

1489 ) 

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

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

1492 

1493 

1494def test_ModerateContent_hide(db): 

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

1496 super_user, super_token = generate_user(is_superuser=True) 

1497 normal_user, user_token = generate_user() 

1498 host, _ = generate_user() 

1499 

1500 # Create a real host request 

1501 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1502 

1503 with real_moderation_session(super_token) as api: 

1504 res = api.ModerateContent( 

1505 moderation_pb2.ModerateContentReq( 

1506 moderation_state_id=state_id, 

1507 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1508 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1509 reason="Spam content", 

1510 ) 

1511 ) 

1512 assert res.moderation_state.moderation_state_id == state_id 

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

1514 

1515 # Verify state was updated in database 

1516 with session_scope() as session: 

1517 state = session.get_one(ModerationState, state_id) 

1518 assert state.visibility == ModerationVisibility.hidden 

1519 

1520 

1521def test_ModerateContent_shadow(db): 

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

1523 super_user, super_token = generate_user(is_superuser=True) 

1524 normal_user, user_token = generate_user() 

1525 host, _ = generate_user() 

1526 

1527 # Create a real host request 

1528 state_id = create_test_host_request_with_moderation(user_token, host.id) 

1529 

1530 with real_moderation_session(super_token) as api: 

1531 res = api.ModerateContent( 

1532 moderation_pb2.ModerateContentReq( 

1533 moderation_state_id=state_id, 

1534 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1535 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

1536 reason="Needs further review", 

1537 ) 

1538 ) 

1539 assert res.moderation_state.moderation_state_id == state_id 

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

1541 

1542 # Verify state was updated in database 

1543 with session_scope() as session: 

1544 state = session.get_one(ModerationState, state_id) 

1545 assert state.visibility == ModerationVisibility.shadowed 

1546 

1547 

1548def test_FlagContentForReview(db): 

1549 """Test flagging content for review via admin API""" 

1550 super_user, super_token = generate_user(is_superuser=True) 

1551 user1, token1 = generate_user() 

1552 user2, _ = generate_user() 

1553 

1554 # Create a host request 

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

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

1557 

1558 with requests_session(token1) as api: 

1559 host_request_id = api.CreateHostRequest( 

1560 requests_pb2.CreateHostRequestReq( 

1561 host_user_id=user2.id, 

1562 from_date=today_plus_2, 

1563 to_date=today_plus_3, 

1564 text=valid_request_text(), 

1565 ) 

1566 ).host_request_id 

1567 

1568 # Get the moderation state ID 

1569 with session_scope() as session: 

1570 host_request = session.execute( 

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

1572 ).scalar_one() 

1573 state_id = host_request.moderation_state_id 

1574 

1575 with real_moderation_session(super_token) as api: 

1576 res = api.FlagContentForReview( 

1577 moderation_pb2.FlagContentForReviewReq( 

1578 moderation_state_id=state_id, 

1579 trigger=moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW, 

1580 reason="Admin flagged for additional review", 

1581 ) 

1582 ) 

1583 assert res.queue_item.moderation_state_id == state_id 

1584 assert res.queue_item.trigger == moderation_pb2.MODERATION_TRIGGER_MODERATOR_REVIEW 

1585 assert res.queue_item.is_resolved == False 

1586 

1587 # Verify queue item was created in database 

1588 with session_scope() as session: 

1589 # Get the most recent queue item (the one we just created) 

1590 queue_item = ( 

1591 session.execute( 

1592 select(ModerationQueueItem) 

1593 .where(ModerationQueueItem.moderation_state_id == state_id) 

1594 .order_by(ModerationQueueItem.time_created.desc()) 

1595 ) 

1596 .scalars() 

1597 .first() 

1598 ) 

1599 assert queue_item 

1600 assert queue_item.trigger == ModerationTrigger.moderator_review 

1601 assert queue_item.resolved_by_log_id is None 

1602 

1603 

1604# ============================================================================ 

1605# Tests for group chat moderation 

1606# ============================================================================ 

1607 

1608 

1609def test_group_chat_created_with_moderation_state(db): 

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

1611 user1, token1 = generate_user() 

1612 user2, _ = generate_user() 

1613 make_friends(user1, user2) 

1614 

1615 with conversations_session(token1) as api: 

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

1617 group_chat_id = res.group_chat_id 

1618 

1619 # Verify moderation state was created 

1620 with session_scope() as session: 

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

1622 

1623 assert group_chat.moderation_state.object_type == ModerationObjectType.group_chat 

1624 assert group_chat.moderation_state.object_id == group_chat_id 

1625 # Group chats start as SHADOWED 

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

1627 

1628 # A moderation queue item should have been created 

1629 queue_item = ( 

1630 session.execute( 

1631 select(ModerationQueueItem).where( 

1632 ModerationQueueItem.moderation_state_id == group_chat.moderation_state_id 

1633 ) 

1634 ) 

1635 .scalars() 

1636 .first() 

1637 ) 

1638 assert queue_item is not None 

1639 assert queue_item.trigger == ModerationTrigger.initial_review 

1640 

1641 

1642def test_group_chat_GetModerationState(db): 

1643 """Test GetModerationState API for group chats""" 

1644 user1, token1 = generate_user() 

1645 user2, _ = generate_user() 

1646 moderator, mod_token = generate_user(is_superuser=True) 

1647 make_friends(user1, user2) 

1648 

1649 with conversations_session(token1) as api: 

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

1651 group_chat_id = res.group_chat_id 

1652 

1653 # Moderator can look up the moderation state 

1654 with real_moderation_session(mod_token) as api: 

1655 res = api.GetModerationState( 

1656 moderation_pb2.GetModerationStateReq( 

1657 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1658 object_id=group_chat_id, 

1659 ) 

1660 ) 

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

1662 assert res.moderation_state.object_id == group_chat_id 

1663 # Starts as SHADOWED 

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

1665 

1666 

1667def test_group_chat_moderation_hide(db): 

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

1669 user1, token1 = generate_user() 

1670 user2, token2 = generate_user() 

1671 moderator, mod_token = generate_user(is_superuser=True) 

1672 make_friends(user1, user2) 

1673 

1674 with conversations_session(token1) as api: 

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

1676 group_chat_id = res.group_chat_id 

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

1678 

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

1680 with real_moderation_session(mod_token) as api: 

1681 state_res = api.GetModerationState( 

1682 moderation_pb2.GetModerationStateReq( 

1683 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1684 object_id=group_chat_id, 

1685 ) 

1686 ) 

1687 api.ModerateContent( 

1688 moderation_pb2.ModerateContentReq( 

1689 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1690 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

1691 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

1692 reason="Approved", 

1693 ) 

1694 ) 

1695 

1696 # Both users can see the chat now 

1697 with conversations_session(token1) as api: 

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

1699 assert len(res.group_chats) == 1 

1700 

1701 with conversations_session(token2) as api: 

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

1703 assert len(res.group_chats) == 1 

1704 

1705 # Moderator hides the group chat 

1706 with real_moderation_session(mod_token) as api: 

1707 state_res = api.GetModerationState( 

1708 moderation_pb2.GetModerationStateReq( 

1709 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1710 object_id=group_chat_id, 

1711 ) 

1712 ) 

1713 api.ModerateContent( 

1714 moderation_pb2.ModerateContentReq( 

1715 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1716 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1717 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1718 reason="Inappropriate content", 

1719 ) 

1720 ) 

1721 

1722 # Neither user can see the chat now 

1723 with conversations_session(token1) as api: 

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

1725 assert len(res.group_chats) == 0 

1726 

1727 with conversations_session(token2) as api: 

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

1729 assert len(res.group_chats) == 0 

1730 

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

1732 with conversations_session(token1) as api: 

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

1734 assert len(res.messages) == 0 

1735 

1736 

1737def test_group_chat_moderation_shadow(db): 

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

1739 user1, token1 = generate_user() # Creator 

1740 user2, token2 = generate_user() # Participant 

1741 moderator, mod_token = generate_user(is_superuser=True) 

1742 make_friends(user1, user2) 

1743 

1744 with conversations_session(token1) as api: 

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

1746 group_chat_id = res.group_chat_id 

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

1748 

1749 # Moderator shadows the group chat 

1750 with real_moderation_session(mod_token) as api: 

1751 state_res = api.GetModerationState( 

1752 moderation_pb2.GetModerationStateReq( 

1753 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_GROUP_CHAT, 

1754 object_id=group_chat_id, 

1755 ) 

1756 ) 

1757 api.ModerateContent( 

1758 moderation_pb2.ModerateContentReq( 

1759 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1760 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1761 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

1762 reason="Needs review", 

1763 ) 

1764 ) 

1765 

1766 # Creator can see SHADOWED content in list operations 

1767 with conversations_session(token1) as api: 

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

1769 assert len(res.group_chats) == 1 

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

1771 

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

1773 with conversations_session(token2) as api: 

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

1775 assert len(res.group_chats) == 0 

1776 

1777 # Creator can also access it directly via GetGroupChat 

1778 with conversations_session(token1) as api: 

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

1780 assert res.group_chat_id == group_chat_id 

1781 

1782 

1783# ============================================================================ 

1784# Tests for auto-approval background job 

1785# ============================================================================ 

1786 

1787 

1788def test_auto_approve_moderation_queue_disabled_when_zero(db, email_collector: EmailCollector): 

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

1790 moderator, mod_token = generate_user(is_superuser=True) 

1791 user1, token1 = generate_user() 

1792 user2, token2 = generate_user() 

1793 

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

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

1796 

1797 # Create a host request 

1798 with requests_session(token1) as api: 

1799 host_request_id = api.CreateHostRequest( 

1800 requests_pb2.CreateHostRequestReq( 

1801 host_user_id=user2.id, 

1802 from_date=today_plus_2, 

1803 to_date=today_plus_3, 

1804 text=valid_request_text(), 

1805 ) 

1806 ).host_request_id 

1807 

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

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

1810 

1811 # Ensure deadline is 0 (disabled) 

1812 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 0 

1813 

1814 # Run the job 

1815 auto_approve_moderation_queue(empty_pb2.Empty()) 

1816 

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

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

1819 assert res.host_request_id == host_request_id 

1820 

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

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

1823 assert len(res.host_requests) == 1 

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

1825 

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

1827 with requests_session(token2) as api: 

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

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

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

1831 

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

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

1834 assert len(res.host_requests) == 0 

1835 

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

1837 with real_moderation_session(mod_token) as api: 

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

1839 assert len(res.queue_items) == 1 

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

1841 

1842 # Moderator can check the state is still SHADOWED 

1843 state_res = api.GetModerationState( 

1844 moderation_pb2.GetModerationStateReq( 

1845 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1846 object_id=host_request_id, 

1847 ) 

1848 ) 

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

1850 

1851 

1852def test_auto_approve_moderation_queue_approves_old_items( 

1853 db, email_collector: EmailCollector, push_collector: PushCollector 

1854): 

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

1856 moderator, mod_token = generate_user(is_superuser=True) 

1857 user1, token1 = generate_user() 

1858 user2, token2 = generate_user() 

1859 

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

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

1862 

1863 # Create a host request 

1864 with requests_session(token1) as api: 

1865 host_request_id = api.CreateHostRequest( 

1866 requests_pb2.CreateHostRequestReq( 

1867 host_user_id=user2.id, 

1868 from_date=today_plus_2, 

1869 to_date=today_plus_3, 

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

1871 ) 

1872 ).host_request_id 

1873 

1874 # No email sent initially (shadowed) 

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

1876 

1877 # Host cannot see the request yet 

1878 with requests_session(token2) as api: 

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

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

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

1882 

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

1884 with session_scope() as session: 

1885 host_request = session.execute( 

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

1887 ).scalar_one() 

1888 queue_item = session.execute( 

1889 select(ModerationQueueItem) 

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

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

1892 ).scalar_one() 

1893 # Backdate the queue item by 2 minutes 

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

1895 

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

1897 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

1898 config.MODERATION_BOT_USER_ID = moderator.id 

1899 

1900 # Run the job 

1901 auto_approve_moderation_queue(empty_pb2.Empty()) 

1902 

1903 # Now host can see the request via API 

1904 with requests_session(token2) as api: 

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

1906 assert res.host_request_id == host_request_id 

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

1908 

1909 # Host sees it in their received list 

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

1911 assert len(res.host_requests) == 1 

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

1913 

1914 # Surfer sees it in their sent list 

1915 with requests_session(token1) as api: 

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

1917 assert len(res.host_requests) == 1 

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

1919 

1920 # Moderator sees the queue item is now resolved 

1921 with real_moderation_session(mod_token) as api: 

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

1923 assert len(res.queue_items) == 0 

1924 

1925 # State is now VISIBLE 

1926 state_res = api.GetModerationState( 

1927 moderation_pb2.GetModerationStateReq( 

1928 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1929 object_id=host_request_id, 

1930 ) 

1931 ) 

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

1933 

1934 # Check the log shows auto-approval by the bot user 

1935 log_res = api.GetModerationLog( 

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

1937 ) 

1938 # Find the APPROVE action 

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

1940 assert len(approve_entries) == 1 

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

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

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

1944 

1945 

1946def test_auto_approve_does_not_approve_recent_items(db, email_collector: EmailCollector): 

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

1948 moderator, mod_token = generate_user(is_superuser=True) 

1949 user1, token1 = generate_user() 

1950 user2, token2 = generate_user() 

1951 

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

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

1954 

1955 # Create a host request 

1956 with requests_session(token1) as api: 

1957 host_request_id = api.CreateHostRequest( 

1958 requests_pb2.CreateHostRequestReq( 

1959 host_user_id=user2.id, 

1960 from_date=today_plus_2, 

1961 to_date=today_plus_3, 

1962 text=valid_request_text(), 

1963 ) 

1964 ).host_request_id 

1965 

1966 # No email sent (shadowed) 

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

1968 

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

1970 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 3600 

1971 config.MODERATION_BOT_USER_ID = moderator.id 

1972 

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

1974 auto_approve_moderation_queue(empty_pb2.Empty()) 

1975 

1976 # Still no email sent 

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

1978 

1979 # Host still cannot see the request 

1980 with requests_session(token2) as api: 

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

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

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

1984 

1985 # Not in host's received list 

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

1987 assert len(res.host_requests) == 0 

1988 

1989 # Moderator sees it still in queue unresolved 

1990 with real_moderation_session(mod_token) as api: 

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

1992 assert len(res.queue_items) == 1 

1993 

1994 # State is still SHADOWED 

1995 state_res = api.GetModerationState( 

1996 moderation_pb2.GetModerationStateReq( 

1997 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

1998 object_id=host_request_id, 

1999 ) 

2000 ) 

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

2002 

2003 

2004def test_auto_approve_does_not_approve_already_approved(db): 

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

2006 moderator, mod_token = generate_user(is_superuser=True) 

2007 user1, token1 = generate_user() 

2008 user2, token2 = generate_user() 

2009 

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

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

2012 

2013 # Create a host request 

2014 with requests_session(token1) as api: 

2015 host_request_id = api.CreateHostRequest( 

2016 requests_pb2.CreateHostRequestReq( 

2017 host_user_id=user2.id, 

2018 from_date=today_plus_2, 

2019 to_date=today_plus_3, 

2020 text=valid_request_text(), 

2021 ) 

2022 ).host_request_id 

2023 

2024 # Moderator approves it manually 

2025 with real_moderation_session(mod_token) as api: 

2026 state_res = api.GetModerationState( 

2027 moderation_pb2.GetModerationStateReq( 

2028 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2029 object_id=host_request_id, 

2030 ) 

2031 ) 

2032 state_id = state_res.moderation_state.moderation_state_id 

2033 

2034 api.ModerateContent( 

2035 moderation_pb2.ModerateContentReq( 

2036 moderation_state_id=state_id, 

2037 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

2038 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2039 reason="Approved by moderator", 

2040 ) 

2041 ) 

2042 

2043 # Host can now see it 

2044 with requests_session(token2) as api: 

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

2046 assert res.host_request_id == host_request_id 

2047 

2048 # Get log count before auto-approval 

2049 with real_moderation_session(mod_token) as api: 

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

2051 log_count_before = len(log_res_before.log_entries) 

2052 

2053 # Set deadline to 1 second 

2054 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2055 config.MODERATION_BOT_USER_ID = moderator.id 

2056 

2057 # Run the job 

2058 auto_approve_moderation_queue(empty_pb2.Empty()) 

2059 

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

2061 with real_moderation_session(mod_token) as api: 

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

2063 assert len(log_res_after.log_entries) == log_count_before 

2064 

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

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

2067 assert len(queue_res.queue_items) == 0 

2068 

2069 

2070def test_auto_approve_does_not_approve_moderator_shadowed_items(db): 

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

2072 moderator, mod_token = generate_user(is_superuser=True) 

2073 user1, token1 = generate_user() 

2074 user2, token2 = generate_user() 

2075 

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

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

2078 

2079 # Create a host request 

2080 with requests_session(token1) as api: 

2081 host_request_id = api.CreateHostRequest( 

2082 requests_pb2.CreateHostRequestReq( 

2083 host_user_id=user2.id, 

2084 from_date=today_plus_2, 

2085 to_date=today_plus_3, 

2086 text=valid_request_text(), 

2087 ) 

2088 ).host_request_id 

2089 

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

2091 with real_moderation_session(mod_token) as api: 

2092 state_res = api.GetModerationState( 

2093 moderation_pb2.GetModerationStateReq( 

2094 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2095 object_id=host_request_id, 

2096 ) 

2097 ) 

2098 state_id = state_res.moderation_state.moderation_state_id 

2099 

2100 # Set to SHADOWED explicitly - this resolves the INITIAL_REVIEW queue item 

2101 api.ModerateContent( 

2102 moderation_pb2.ModerateContentReq( 

2103 moderation_state_id=state_id, 

2104 action=moderation_pb2.MODERATION_ACTION_HIDE, 

2105 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2106 reason="Keeping shadowed for review", 

2107 ) 

2108 ) 

2109 

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

2111 with session_scope() as session: 

2112 queue_item = session.execute( 

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

2114 ).scalar_one() 

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

2116 

2117 # Set deadline to 1 second 

2118 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 1 

2119 config.MODERATION_BOT_USER_ID = moderator.id 

2120 

2121 # Get log count before 

2122 with real_moderation_session(mod_token) as api: 

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

2124 log_count_before = len(log_res_before.log_entries) 

2125 

2126 # Run the job 

2127 auto_approve_moderation_queue(empty_pb2.Empty()) 

2128 

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

2130 with real_moderation_session(mod_token) as api: 

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

2132 assert len(log_res_after.log_entries) == log_count_before 

2133 

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

2135 state_res = api.GetModerationState( 

2136 moderation_pb2.GetModerationStateReq( 

2137 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2138 object_id=host_request_id, 

2139 ) 

2140 ) 

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

2142 

2143 # Host still cannot see the request 

2144 with requests_session(token2) as api: 

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

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

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

2148 

2149 

2150def test_auto_approve_skips_shadowed_user_authored_items(db): 

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

2152 moderator, mod_token = generate_user(is_superuser=True) 

2153 surfer, surfer_token = generate_user() 

2154 host, host_token = generate_user() 

2155 

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

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

2158 

2159 with requests_session(surfer_token) as api: 

2160 host_request_id = api.CreateHostRequest( 

2161 requests_pb2.CreateHostRequestReq( 

2162 host_user_id=host.id, 

2163 from_date=today_plus_2, 

2164 to_date=today_plus_3, 

2165 text=valid_request_text(), 

2166 ) 

2167 ).host_request_id 

2168 

2169 # Shadow the surfer after the host request was created 

2170 with session_scope() as session: 

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

2172 

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

2174 with session_scope() as session: 

2175 host_request = session.execute( 

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

2177 ).scalar_one() 

2178 queue_item = session.execute( 

2179 select(ModerationQueueItem) 

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

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

2182 ).scalar_one() 

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

2184 

2185 config.MODERATION_AUTO_APPROVE_DEADLINE_SECONDS = 60 

2186 config.MODERATION_BOT_USER_ID = moderator.id 

2187 

2188 auto_approve_moderation_queue(empty_pb2.Empty()) 

2189 

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

2191 with real_moderation_session(mod_token) as api: 

2192 state_res = api.GetModerationState( 

2193 moderation_pb2.GetModerationStateReq( 

2194 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_HOST_REQUEST, 

2195 object_id=host_request_id, 

2196 ) 

2197 ) 

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

2199 

2200 # Host still cannot see the request 

2201 with requests_session(host_token) as api: 

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

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

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

2205 

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

2207 with requests_session(surfer_token) as api: 

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

2209 assert res.host_request_id == host_request_id 

2210 

2211 

2212# ============================================================================ 

2213# Notification Suppression Tests 

2214# ============================================================================ 

2215 

2216 

2217def test_host_request_message_notifications_suppressed_before_approval( 

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

2219): 

2220 """ 

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

2222 that haven't been approved yet. 

2223 """ 

2224 host, host_token = generate_user(complete_profile=True) 

2225 surfer, surfer_token = generate_user(complete_profile=True) 

2226 

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

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

2229 

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

2231 with requests_session(surfer_token) as api: 

2232 hr_id = api.CreateHostRequest( 

2233 requests_pb2.CreateHostRequestReq( 

2234 host_user_id=host.id, 

2235 from_date=today_plus_2, 

2236 to_date=today_plus_3, 

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

2238 ) 

2239 ).host_request_id 

2240 

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

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

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

2244 

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

2246 with requests_session(surfer_token) as api: 

2247 api.SendHostRequestMessage( 

2248 requests_pb2.SendHostRequestMessageReq( 

2249 host_request_id=hr_id, 

2250 text="Follow-up message 1", 

2251 ) 

2252 ) 

2253 api.SendHostRequestMessage( 

2254 requests_pb2.SendHostRequestMessageReq( 

2255 host_request_id=hr_id, 

2256 text="Follow-up message 2", 

2257 ) 

2258 ) 

2259 

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

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

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

2263 

2264 # Now approve the request 

2265 moderator.approve_host_request(hr_id) 

2266 email_collector.pop_for_recipient(host.email) 

2267 

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

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

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

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

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

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

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

2275 

2276 

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

2278 """ 

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

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

2281 

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

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

2284 """ 

2285 host, host_token = generate_user(complete_profile=True) 

2286 surfer, surfer_token = generate_user(complete_profile=True) 

2287 

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

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

2290 

2291 # Create host request 

2292 with requests_session(surfer_token) as api: 

2293 hr_id = api.CreateHostRequest( 

2294 requests_pb2.CreateHostRequestReq( 

2295 host_user_id=host.id, 

2296 from_date=today_plus_2, 

2297 to_date=today_plus_3, 

2298 text=valid_request_text(), 

2299 ) 

2300 ).host_request_id 

2301 

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

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

2304 

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

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

2307 with requests_session(surfer_token) as api: 

2308 api.RespondHostRequest( 

2309 requests_pb2.RespondHostRequestReq( 

2310 host_request_id=hr_id, 

2311 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

2312 text="Actually, never mind", 

2313 ) 

2314 ) 

2315 

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

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

2318 

2319 

2320def test_host_request_notifications_sent_after_approval( 

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

2322): 

2323 """ 

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

2325 """ 

2326 host, host_token = generate_user(complete_profile=True) 

2327 surfer, surfer_token = generate_user(complete_profile=True) 

2328 

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

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

2331 

2332 # Create and approve host request 

2333 with requests_session(surfer_token) as api: 

2334 hr_id = api.CreateHostRequest( 

2335 requests_pb2.CreateHostRequestReq( 

2336 host_user_id=host.id, 

2337 from_date=today_plus_2, 

2338 to_date=today_plus_3, 

2339 text=valid_request_text(), 

2340 ) 

2341 ).host_request_id 

2342 

2343 moderator.approve_host_request(hr_id) 

2344 

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

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

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

2348 

2349 # Host accepts the request - surfer should be notified 

2350 with requests_session(host_token) as api: 

2351 api.RespondHostRequest( 

2352 requests_pb2.RespondHostRequestReq( 

2353 host_request_id=hr_id, 

2354 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

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

2356 ) 

2357 ) 

2358 

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

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

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

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

2363 

2364 # Surfer confirms - host should be notified 

2365 with requests_session(surfer_token) as api: 

2366 api.RespondHostRequest( 

2367 requests_pb2.RespondHostRequestReq( 

2368 host_request_id=hr_id, 

2369 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

2370 text="See you then!", 

2371 ) 

2372 ) 

2373 

2374 # Host should now have received the confirmation notifications 

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

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

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

2378 

2379 

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

2381 """ 

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

2383 that haven't been approved yet. 

2384 """ 

2385 user1, token1 = generate_user(complete_profile=True) 

2386 user2, token2 = generate_user(complete_profile=True) 

2387 

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

2389 with conversations_session(token1) as api: 

2390 res = api.CreateGroupChat( 

2391 conversations_pb2.CreateGroupChatReq( 

2392 recipient_user_ids=[user2.id], 

2393 ) 

2394 ) 

2395 gc_id = res.group_chat_id 

2396 

2397 # Verify initial state 

2398 with session_scope() as session: 

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

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

2401 

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

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

2404 

2405 # Send messages BEFORE approval 

2406 with conversations_session(token1) as api: 

2407 api.SendMessage( 

2408 conversations_pb2.SendMessageReq( 

2409 group_chat_id=gc_id, 

2410 text="Hello before approval", 

2411 ) 

2412 ) 

2413 

2414 # Process the queued notification job 

2415 while process_job(): 

2416 pass 

2417 

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

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

2420 

2421 # Now approve the group chat 

2422 moderator.approve_group_chat(gc_id) 

2423 

2424 # Process the queued notification jobs from approval 

2425 while process_job(): 

2426 pass 

2427 

2428 # Verify moderation state after approval 

2429 with session_scope() as session: 

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

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

2432 

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

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

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

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

2437 

2438 # Send a message AFTER approval 

2439 with conversations_session(token1) as api: 

2440 api.SendMessage( 

2441 conversations_pb2.SendMessageReq( 

2442 group_chat_id=gc_id, 

2443 text="Hello after approval", 

2444 ) 

2445 ) 

2446 

2447 # Process the queued notification job 

2448 while process_job(): 

2449 pass 

2450 

2451 # User2 should have received another notification 

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

2453 

2454 

2455def test_event_moderation_state_content(db): 

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

2457 super_user, super_token = generate_user(is_superuser=True) 

2458 user, token = generate_user() 

2459 

2460 with session_scope() as session: 

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

2462 

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

2464 end_time = start_time + timedelta(hours=3) 

2465 

2466 with events_session(token) as api: 

2467 res = api.CreateEvent( 

2468 events_pb2.CreateEventReq( 

2469 title="My Event Title", 

2470 content="My event description.", 

2471 photo_key=None, 

2472 offline_information=events_pb2.OfflineEventInformation( 

2473 address="Near Null Island", 

2474 lat=0.1, 

2475 lng=0.2, 

2476 ), 

2477 start_time=Timestamp_from_datetime(start_time), 

2478 end_time=Timestamp_from_datetime(end_time), 

2479 timezone="UTC", 

2480 ) 

2481 ) 

2482 event_id = res.event_id 

2483 

2484 with session_scope() as session: 

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

2486 state_id = occurrence.moderation_state_id 

2487 

2488 with real_moderation_session(super_token) as api: 

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

2490 event_items = [ 

2491 item 

2492 for item in res.queue_items 

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

2494 ] 

2495 assert len(event_items) == 1 

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

2497 

2498 

2499# ============================================================================ 

2500# Tests for SetUserContentVisibility 

2501# ============================================================================ 

2502 

2503 

2504def _get_moderation_state(session, object_type, object_id): 

2505 return session.execute( 

2506 select(ModerationState) 

2507 .where(ModerationState.object_type == object_type) 

2508 .where(ModerationState.object_id == object_id) 

2509 ).scalar_one() 

2510 

2511 

2512def test_SetUserContentVisibility_host_request(db): 

2513 super_user, super_token = generate_user(is_superuser=True) 

2514 surfer, surfer_token = generate_user() 

2515 host, _ = generate_user() 

2516 

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

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

2519 with requests_session(surfer_token) as api: 

2520 hr_id = api.CreateHostRequest( 

2521 requests_pb2.CreateHostRequestReq( 

2522 host_user_id=host.id, 

2523 from_date=today_plus_2, 

2524 to_date=today_plus_3, 

2525 text=valid_request_text(), 

2526 ) 

2527 ).host_request_id 

2528 

2529 with real_moderation_session(super_token) as api: 

2530 res = api.SetUserContentVisibility( 

2531 moderation_pb2.SetUserContentVisibilityReq( 

2532 user_id=surfer.id, 

2533 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2534 ) 

2535 ) 

2536 # Already shadowed by default — no-op 

2537 assert res.updated_count == 0 

2538 

2539 with real_moderation_session(super_token) as api: 

2540 res = api.SetUserContentVisibility( 

2541 moderation_pb2.SetUserContentVisibilityReq( 

2542 user_id=surfer.id, 

2543 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2544 reason="policy violation", 

2545 ) 

2546 ) 

2547 assert res.updated_count == 1 

2548 

2549 with session_scope() as session: 

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

2551 assert state.visibility == ModerationVisibility.hidden 

2552 

2553 log_entries = ( 

2554 session.execute( 

2555 select(ModerationLog) 

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

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

2558 ) 

2559 .scalars() 

2560 .all() 

2561 ) 

2562 # create log + bulk update log 

2563 assert len(log_entries) == 2 

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

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

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

2567 

2568 

2569def test_SetUserContentVisibility_group_chat(db): 

2570 super_user, super_token = generate_user(is_superuser=True) 

2571 creator, creator_token = generate_user() 

2572 other, _ = generate_user() 

2573 make_friends(creator, other) 

2574 

2575 with conversations_session(creator_token) as api: 

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

2577 

2578 with real_moderation_session(super_token) as api: 

2579 api.SetUserContentVisibility( 

2580 moderation_pb2.SetUserContentVisibilityReq( 

2581 user_id=creator.id, 

2582 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2583 ) 

2584 ) 

2585 

2586 with session_scope() as session: 

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

2588 assert state.visibility == ModerationVisibility.hidden 

2589 

2590 

2591def test_SetUserContentVisibility_event_occurrence(db): 

2592 super_user, super_token = generate_user(is_superuser=True) 

2593 creator, creator_token = generate_user() 

2594 

2595 with session_scope() as session: 

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

2597 

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

2599 end_time = start_time + timedelta(hours=3) 

2600 with events_session(creator_token) as api: 

2601 event_id = api.CreateEvent( 

2602 events_pb2.CreateEventReq( 

2603 title="Event", 

2604 content="Event description.", 

2605 photo_key=None, 

2606 offline_information=events_pb2.OfflineEventInformation( 

2607 address="Near Null Island", 

2608 lat=0.1, 

2609 lng=0.2, 

2610 ), 

2611 start_time=Timestamp_from_datetime(start_time), 

2612 end_time=Timestamp_from_datetime(end_time), 

2613 timezone="UTC", 

2614 ) 

2615 ).event_id 

2616 

2617 with real_moderation_session(super_token) as api: 

2618 res = api.SetUserContentVisibility( 

2619 moderation_pb2.SetUserContentVisibilityReq( 

2620 user_id=creator.id, 

2621 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2622 ) 

2623 ) 

2624 assert res.updated_count == 1 

2625 

2626 with session_scope() as session: 

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

2628 assert state.visibility == ModerationVisibility.hidden 

2629 

2630 

2631def test_SetUserContentVisibility_friend_request(db): 

2632 super_user, super_token = generate_user(is_superuser=True) 

2633 sender, sender_token = generate_user() 

2634 recipient, _ = generate_user() 

2635 

2636 with api_session(sender_token) as api: 

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

2638 

2639 with session_scope() as session: 

2640 fr_id = session.execute( 

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

2642 ).scalar_one() 

2643 

2644 with real_moderation_session(super_token) as api: 

2645 res = api.SetUserContentVisibility( 

2646 moderation_pb2.SetUserContentVisibilityReq( 

2647 user_id=sender.id, 

2648 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2649 ) 

2650 ) 

2651 assert res.updated_count == 1 

2652 

2653 with session_scope() as session: 

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

2655 assert state.visibility == ModerationVisibility.hidden 

2656 

2657 

2658def test_SetUserContentVisibility_round_trip(db): 

2659 super_user, super_token = generate_user(is_superuser=True) 

2660 surfer, surfer_token = generate_user() 

2661 host, _ = generate_user() 

2662 

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

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

2665 with requests_session(surfer_token) as api: 

2666 hr_id = api.CreateHostRequest( 

2667 requests_pb2.CreateHostRequestReq( 

2668 host_user_id=host.id, 

2669 from_date=today_plus_2, 

2670 to_date=today_plus_3, 

2671 text=valid_request_text(), 

2672 ) 

2673 ).host_request_id 

2674 

2675 with real_moderation_session(super_token) as api: 

2676 api.SetUserContentVisibility( 

2677 moderation_pb2.SetUserContentVisibilityReq( 

2678 user_id=surfer.id, 

2679 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2680 reason="first", 

2681 ) 

2682 ) 

2683 api.SetUserContentVisibility( 

2684 moderation_pb2.SetUserContentVisibilityReq( 

2685 user_id=surfer.id, 

2686 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2687 reason="second", 

2688 ) 

2689 ) 

2690 

2691 with session_scope() as session: 

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

2693 assert state.visibility == ModerationVisibility.visible 

2694 

2695 bulk_log_entries = ( 

2696 session.execute( 

2697 select(ModerationLog) 

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

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

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

2701 ) 

2702 .scalars() 

2703 .all() 

2704 ) 

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

2706 ModerationVisibility.hidden, 

2707 ModerationVisibility.visible, 

2708 ] 

2709 

2710 

2711def test_SetUserContentVisibility_resolves_queue_items(db): 

2712 super_user, super_token = generate_user(is_superuser=True) 

2713 surfer, surfer_token = generate_user() 

2714 host, _ = generate_user() 

2715 

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

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

2718 with requests_session(surfer_token) as api: 

2719 hr_id = api.CreateHostRequest( 

2720 requests_pb2.CreateHostRequestReq( 

2721 host_user_id=host.id, 

2722 from_date=today_plus_2, 

2723 to_date=today_plus_3, 

2724 text=valid_request_text(), 

2725 ) 

2726 ).host_request_id 

2727 

2728 with session_scope() as session: 

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

2730 queue_item = session.execute( 

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

2732 ).scalar_one() 

2733 assert queue_item.resolved_by_log_id is None 

2734 

2735 with real_moderation_session(super_token) as api: 

2736 api.SetUserContentVisibility( 

2737 moderation_pb2.SetUserContentVisibilityReq( 

2738 user_id=surfer.id, 

2739 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2740 ) 

2741 ) 

2742 

2743 with session_scope() as session: 

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

2745 queue_item = session.execute( 

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

2747 ).scalar_one() 

2748 assert queue_item.resolved_by_log_id is not None 

2749 

2750 

2751def test_SetUserContentVisibility_noop_when_matches(db): 

2752 super_user, super_token = generate_user(is_superuser=True) 

2753 surfer, surfer_token = generate_user() 

2754 host, _ = generate_user() 

2755 

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

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

2758 with requests_session(surfer_token) as api: 

2759 api.CreateHostRequest( 

2760 requests_pb2.CreateHostRequestReq( 

2761 host_user_id=host.id, 

2762 from_date=today_plus_2, 

2763 to_date=today_plus_3, 

2764 text=valid_request_text(), 

2765 ) 

2766 ) 

2767 

2768 with session_scope() as session: 

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

2770 

2771 with real_moderation_session(super_token) as api: 

2772 res = api.SetUserContentVisibility( 

2773 moderation_pb2.SetUserContentVisibilityReq( 

2774 user_id=surfer.id, 

2775 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2776 ) 

2777 ) 

2778 assert res.updated_count == 0 

2779 

2780 with session_scope() as session: 

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

2782 assert log_count_after == log_count_before 

2783 

2784 

2785def test_SetUserContentVisibility_unspecified_rejected(db): 

2786 super_user, super_token = generate_user(is_superuser=True) 

2787 target, _ = generate_user() 

2788 

2789 with real_moderation_session(super_token) as api: 

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

2791 api.SetUserContentVisibility( 

2792 moderation_pb2.SetUserContentVisibilityReq( 

2793 user_id=target.id, 

2794 visibility=moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED, 

2795 ) 

2796 ) 

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

2798 

2799 

2800def test_SetUserContentVisibility_non_admin_rejected(db): 

2801 normal_user, normal_token = generate_user() 

2802 target, _ = generate_user() 

2803 

2804 with real_moderation_session(normal_token) as api: 

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

2806 api.SetUserContentVisibility( 

2807 moderation_pb2.SetUserContentVisibilityReq( 

2808 user_id=target.id, 

2809 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2810 ) 

2811 ) 

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

2813 

2814 

2815def test_SetUserContentVisibility_writes_admin_action(db): 

2816 super_user, super_token = generate_user(is_superuser=True) 

2817 surfer, surfer_token = generate_user() 

2818 host, _ = generate_user() 

2819 

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

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

2822 with requests_session(surfer_token) as api: 

2823 api.CreateHostRequest( 

2824 requests_pb2.CreateHostRequestReq( 

2825 host_user_id=host.id, 

2826 from_date=today_plus_2, 

2827 to_date=today_plus_3, 

2828 text=valid_request_text(), 

2829 ) 

2830 ) 

2831 

2832 with real_moderation_session(super_token) as api: 

2833 api.SetUserContentVisibility( 

2834 moderation_pb2.SetUserContentVisibilityReq( 

2835 user_id=surfer.id, 

2836 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2837 reason="bulk hide", 

2838 ) 

2839 ) 

2840 

2841 with session_scope() as session: 

2842 actions = ( 

2843 session.execute( 

2844 select(AdminAction) 

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

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

2847 ) 

2848 .scalars() 

2849 .all() 

2850 ) 

2851 assert len(actions) == 1 

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

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

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

2855 

2856 

2857def test_SetUserContentVisibility_only_touches_target(db): 

2858 super_user, super_token = generate_user(is_superuser=True) 

2859 target, target_token = generate_user() 

2860 other, other_token = generate_user() 

2861 host, _ = generate_user() 

2862 

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

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

2865 

2866 with requests_session(target_token) as api: 

2867 target_hr_id = api.CreateHostRequest( 

2868 requests_pb2.CreateHostRequestReq( 

2869 host_user_id=host.id, 

2870 from_date=today_plus_2, 

2871 to_date=today_plus_3, 

2872 text=valid_request_text(), 

2873 ) 

2874 ).host_request_id 

2875 

2876 with requests_session(other_token) as api: 

2877 other_hr_id = api.CreateHostRequest( 

2878 requests_pb2.CreateHostRequestReq( 

2879 host_user_id=host.id, 

2880 from_date=today_plus_2, 

2881 to_date=today_plus_3, 

2882 text=valid_request_text(), 

2883 ) 

2884 ).host_request_id 

2885 

2886 with real_moderation_session(super_token) as api: 

2887 res = api.SetUserContentVisibility( 

2888 moderation_pb2.SetUserContentVisibilityReq( 

2889 user_id=target.id, 

2890 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2891 ) 

2892 ) 

2893 assert res.updated_count == 1 

2894 

2895 with session_scope() as session: 

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

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

2898 assert target_state.visibility == ModerationVisibility.hidden 

2899 assert other_state.visibility == ModerationVisibility.shadowed 

2900 

2901 

2902def test_SetUserContentVisibility_user_not_found(db): 

2903 super_user, super_token = generate_user(is_superuser=True) 

2904 

2905 with real_moderation_session(super_token) as api: 

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

2907 api.SetUserContentVisibility( 

2908 moderation_pb2.SetUserContentVisibilityReq( 

2909 user_id=999999, 

2910 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2911 ) 

2912 ) 

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

2914 

2915 

2916def test_SetUserContentVisibility_from_visibility_filter(db): 

2917 super_user, super_token = generate_user(is_superuser=True) 

2918 surfer, surfer_token = generate_user() 

2919 host, _ = generate_user() 

2920 

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

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

2923 with requests_session(surfer_token) as api: 

2924 hr_id = api.CreateHostRequest( 

2925 requests_pb2.CreateHostRequestReq( 

2926 host_user_id=host.id, 

2927 from_date=today_plus_2, 

2928 to_date=today_plus_3, 

2929 text=valid_request_text(), 

2930 ) 

2931 ).host_request_id 

2932 

2933 with real_moderation_session(super_token) as api: 

2934 api.SetUserContentVisibility( 

2935 moderation_pb2.SetUserContentVisibilityReq( 

2936 user_id=surfer.id, 

2937 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

2938 ) 

2939 ) 

2940 

2941 with real_moderation_session(super_token) as api: 

2942 res = api.SetUserContentVisibility( 

2943 moderation_pb2.SetUserContentVisibilityReq( 

2944 user_id=surfer.id, 

2945 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

2946 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_SHADOWED], 

2947 ) 

2948 ) 

2949 assert res.updated_count == 0 

2950 

2951 with session_scope() as session: 

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

2953 assert state.visibility == ModerationVisibility.visible 

2954 

2955 with real_moderation_session(super_token) as api: 

2956 res = api.SetUserContentVisibility( 

2957 moderation_pb2.SetUserContentVisibilityReq( 

2958 user_id=surfer.id, 

2959 visibility=moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

2960 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_VISIBLE], 

2961 ) 

2962 ) 

2963 assert res.updated_count == 1 

2964 

2965 with session_scope() as session: 

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

2967 assert state.visibility == ModerationVisibility.shadowed 

2968 

2969 

2970def test_SetUserContentVisibility_from_visibility_multi(db): 

2971 super_user, super_token = generate_user(is_superuser=True) 

2972 surfer, surfer_token = generate_user() 

2973 host1, _ = generate_user() 

2974 host2, _ = generate_user() 

2975 

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

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

2978 with requests_session(surfer_token) as api: 

2979 hr1_id = api.CreateHostRequest( 

2980 requests_pb2.CreateHostRequestReq( 

2981 host_user_id=host1.id, 

2982 from_date=today_plus_2, 

2983 to_date=today_plus_3, 

2984 text=valid_request_text(), 

2985 ) 

2986 ).host_request_id 

2987 hr2_id = api.CreateHostRequest( 

2988 requests_pb2.CreateHostRequestReq( 

2989 host_user_id=host2.id, 

2990 from_date=today_plus_2, 

2991 to_date=today_plus_3, 

2992 text=valid_request_text(), 

2993 ) 

2994 ).host_request_id 

2995 

2996 with session_scope() as session: 

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

2998 

2999 with real_moderation_session(super_token) as api: 

3000 api.ModerateContent( 

3001 moderation_pb2.ModerateContentReq( 

3002 moderation_state_id=state1_id, 

3003 action=moderation_pb2.MODERATION_ACTION_APPROVE, 

3004 visibility=moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3005 ) 

3006 ) 

3007 

3008 with real_moderation_session(super_token) as api: 

3009 res = api.SetUserContentVisibility( 

3010 moderation_pb2.SetUserContentVisibilityReq( 

3011 user_id=surfer.id, 

3012 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3013 from_visibility=[ 

3014 moderation_pb2.MODERATION_VISIBILITY_VISIBLE, 

3015 moderation_pb2.MODERATION_VISIBILITY_SHADOWED, 

3016 ], 

3017 ) 

3018 ) 

3019 assert res.updated_count == 2 

3020 

3021 with session_scope() as session: 

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

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

3024 assert state1.visibility == ModerationVisibility.hidden 

3025 assert state2.visibility == ModerationVisibility.hidden 

3026 

3027 

3028def test_SetUserContentVisibility_from_visibility_empty_is_any(db): 

3029 super_user, super_token = generate_user(is_superuser=True) 

3030 surfer, surfer_token = generate_user() 

3031 host, _ = generate_user() 

3032 

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

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

3035 with requests_session(surfer_token) as api: 

3036 hr_id = api.CreateHostRequest( 

3037 requests_pb2.CreateHostRequestReq( 

3038 host_user_id=host.id, 

3039 from_date=today_plus_2, 

3040 to_date=today_plus_3, 

3041 text=valid_request_text(), 

3042 ) 

3043 ).host_request_id 

3044 

3045 with real_moderation_session(super_token) as api: 

3046 res = api.SetUserContentVisibility( 

3047 moderation_pb2.SetUserContentVisibilityReq( 

3048 user_id=surfer.id, 

3049 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3050 from_visibility=[], 

3051 ) 

3052 ) 

3053 assert res.updated_count == 1 

3054 

3055 with session_scope() as session: 

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

3057 assert state.visibility == ModerationVisibility.hidden 

3058 

3059 

3060def test_SetUserContentVisibility_from_visibility_unspecified_rejected(db): 

3061 super_user, super_token = generate_user(is_superuser=True) 

3062 target, _ = generate_user() 

3063 

3064 with real_moderation_session(super_token) as api: 

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

3066 api.SetUserContentVisibility( 

3067 moderation_pb2.SetUserContentVisibilityReq( 

3068 user_id=target.id, 

3069 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

3070 from_visibility=[moderation_pb2.MODERATION_VISIBILITY_UNSPECIFIED], 

3071 ) 

3072 ) 

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

3074 

3075 

3076def test_ListModerationStates_empty(db): 

3077 super_user, super_token = generate_user(is_superuser=True) 

3078 

3079 with real_moderation_session(super_token) as api: 

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

3081 assert len(res.moderation_states) == 0 

3082 assert res.next_page_token == "" 

3083 

3084 

3085def test_ListModerationStates_returns_states_chronologically(db): 

3086 super_user, super_token = generate_user(is_superuser=True) 

3087 surfer, surfer_token = generate_user() 

3088 host, _ = generate_user() 

3089 

3090 state1_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3091 state2_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3092 state3_id = create_test_host_request_with_moderation(surfer_token, host.id) 

3093 

3094 with real_moderation_session(super_token) as api: 

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

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

3097 

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

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

3100 

3101 

3102def test_ListModerationStates_filter_by_author(db): 

3103 super_user, super_token = generate_user(is_superuser=True) 

3104 surfer1, surfer1_token = generate_user() 

3105 surfer2, surfer2_token = generate_user() 

3106 host, _ = generate_user() 

3107 

3108 state1_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3109 state2_id = create_test_host_request_with_moderation(surfer2_token, host.id) 

3110 state3_id = create_test_host_request_with_moderation(surfer1_token, host.id) 

3111 

3112 with real_moderation_session(super_token) as api: 

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

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

3115 

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

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

3118 

3119 

3120def test_ListModerationStates_pagination(db): 

3121 super_user, super_token = generate_user(is_superuser=True) 

3122 surfer, surfer_token = generate_user() 

3123 host, _ = generate_user() 

3124 

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

3126 

3127 with real_moderation_session(super_token) as api: 

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

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

3130 assert res.next_page_token != "" 

3131 

3132 res2 = api.ListModerationStates( 

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

3134 ) 

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

3136 assert res2.next_page_token == ""