"""离职自动挂起 Google 号(2026-07-01 ADR 翻案 B). Offboarding, once the leaver's last working day has passed, auto-suspends their Google account via the account-lifecycle coordinator — EXCEPT accounts marked keep/retained in `external_account_registry` (service / shared / CBG-retained), which are left active. Verified at the `_push_force_logout` seam (only reached post-leaver_date).""" from __future__ import annotations import uuid from unittest.mock import AsyncMock, patch import pytest from sqlalchemy.ext.asyncio import AsyncSession from app.domains.asset.models import ExternalAccountRegistry from app.domains.offboarding.service import OffboardingService from app.identity.person_models import IdentityPerson _SUSPEND = "app.core.coordinators.account_lifecycle_coordinator.suspend_google_account" async def _leaver(db: AsyncSession, *, email: str | None = None, status: str = "left") -> IdentityPerson: pid = uuid.uuid4() p = IdentityPerson( id=pid, source_system="successfactors", hris_person_id=f"sf-{pid.hex[:8]}", work_email=email or f"{pid.hex[:8]}@x.io", display_name="Leaver", employment_status=status, ) db.add(p) await db.commit() return p @pytest.mark.asyncio async def test_is_retained_helper(db_session: AsyncSession): svc = OffboardingService(db_session) p = await _leaver(db_session, email="svc@x.io") assert await svc._is_google_account_retained(p) is False # no registry row db_session.add(ExternalAccountRegistry(email="svc@x.io", classification="shared_service")) await db_session.commit() assert await svc._is_google_account_retained(p) is True p2 = await _leaver(db_session, email="u@x.io") db_session.add(ExternalAccountRegistry(email="u@x.io", classification="unknown")) await db_session.commit() assert await svc._is_google_account_retained(p2) is False # 'unknown' not retained @pytest.mark.asyncio async def test_force_logout_auto_suspends_non_retained_leaver(db_session: AsyncSession): p = await _leaver(db_session) svc = OffboardingService(db_session) with patch(_SUSPEND, new=AsyncMock(return_value={"status": "suspended", "already_absent": False})) as m: await svc._push_force_logout(p.id, trace_id="t") m.assert_awaited_once() @pytest.mark.asyncio async def test_force_logout_skips_suspend_for_retained_leaver(db_session: AsyncSession): p = await _leaver(db_session, email="keep@x.io") db_session.add(ExternalAccountRegistry(email="keep@x.io", classification="shared_service")) await db_session.commit() svc = OffboardingService(db_session) with patch(_SUSPEND, new=AsyncMock()) as m: await svc._push_force_logout(p.id, trace_id="t") m.assert_not_awaited() # retention exemption honored — never suspended @pytest.mark.asyncio async def test_cancelled_offboarding_never_suspends(db_session: AsyncSession): """CRITICAL guard: a CANCELLED offboarding (false leaver = active employee) must NEVER reach force-logout/suspend via the post-commit dispatcher, even if leaver_date has passed. Without the status guard the account-suspend added to _push_force_logout would lock out a working employee.""" import datetime as _dt import uuid from app.domains.offboarding.models import PROC_CANCELLED, OffboardingProcess p = await _leaver(db_session, email="falseleaver@x.io", status="active") proc = OffboardingProcess( id=uuid.uuid4(), person_id=p.id, status=PROC_CANCELLED, leaver_date=_dt.date.today() - _dt.timedelta(days=2), # past — gate would pass ) db_session.add(proc) await db_session.commit() svc = OffboardingService(db_session) with patch(_SUSPEND, new=AsyncMock()) as m: res = await svc.dispatch_post_commit_actions( p.id, proc.id, trace_id="t", actor_user_id=None, ) assert res.get("skipped_cancelled") is True m.assert_not_awaited() # cancelled → no vendor account action at all