"""Regression: a feed @-mention must not leak a restricted post's body to a mentioned person who isn't allowed to view the post. A department-visibility post can mention any active employee, but the mention notification (inbox row + Feishu DM, carrying up to 500 chars of the body) must be gated on the SAME visibility check as the post-detail page — otherwise the body leaks to out-of-scope recipients even though the /feed/p/{id} deep-link would 403 them. """ from __future__ import annotations import uuid import pytest from sqlalchemy.ext.asyncio import AsyncSession from app.domains.feed import notifications as feed_notifications from app.domains.feed.service import FeedService from app.identity.person_models import IdentityPerson @pytest.mark.asyncio async def test_department_post_mention_skips_out_of_scope_recipient( db_session: AsyncSession, admin_user, employee_user, monkeypatch, ) -> None: admin, _ = admin_user # global_admin → can view any post author, _ = employee_user await db_session.refresh(author, attribute_names=["org_memberships"]) post, _ = await FeedService(db_session).create_user_post( author=author, body="confidential team-only body", visibility="department", category="general", mentioned_person_ids=[], attachments=[], ) # Pin a department snapshot that the out-of-scope employee can't match. post.department_snapshot = "QA-ONLY-DEPT" post.visibility = "department" # A plain employee with no org membership → primary_department is None, # so they are NOT in the post's department scope. outsider = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", work_email=f"out{uuid.uuid4().hex[:6]}@x.test", display_name="Outsider", employment_status="active", ) db_session.add(outsider) await db_session.flush() await db_session.commit() pushed: list[str] = [] async def _spy(self, env, **_kw): # noqa: ANN001 pushed.append(str(env.recipient.id)) monkeypatch.setattr( "app.notifications.service.NotificationService.push_inbox_and_feishu", _spy, ) await feed_notifications.notify_mentions_on_post( db_session, post=post, mentioned_person_ids=[str(admin.id), str(outsider.id)], ) # Admin can view any post → notified. The out-of-department employee # must be skipped — the body must not reach them. assert str(admin.id) in pushed assert str(outsider.id) not in pushed @pytest.mark.asyncio async def test_post_mention_fanout_is_capped( db_session: AsyncSession, employee_user, monkeypatch, ) -> None: """2026-07-07: a public post with >MAX_MENTIONS_PER_POST mentions must fan out at most MAX_MENTIONS_PER_POST Feishu DMs. Storage truncates to the cap, but the router passed the RAW (uncapped) list to the notifier, so N mentions → N DMs while only 20 were stored (org-wide spam vector).""" from app.domains.feed.service import MAX_MENTIONS_PER_POST author, _ = employee_user await db_session.refresh(author, attribute_names=["org_memberships"]) post, _ = await FeedService(db_session).create_user_post( author=author, body="hi everyone", visibility="public", category="general", mentioned_person_ids=[], attachments=[], ) mids: list[str] = [] for _ in range(MAX_MENTIONS_PER_POST + 10): # 30 mentions p = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", work_email=f"m{uuid.uuid4().hex[:6]}@x.test", display_name="Mentioned", employment_status="active", ) db_session.add(p) mids.append(str(p.id)) await db_session.flush() await db_session.commit() pushed: list[str] = [] async def _spy(self, env, **_kw): # noqa: ANN001 pushed.append(str(env.recipient.id)) monkeypatch.setattr( "app.notifications.service.NotificationService.push_inbox_and_feishu", _spy, ) await feed_notifications.notify_mentions_on_post( db_session, post=post, mentioned_person_ids=mids, ) # All 30 are active + can view a public post; without the cap the buggy # code would push 30. The fix bounds it to the storage cap. assert len(pushed) == MAX_MENTIONS_PER_POST