"""The verify cooldown compared an aware ``now`` against a NAIVE column. ``SsoGrant.removal_last_checked_at`` is ``DateTime(timezone=True)``, which on MySQL/TiDB is a plain ``DATETIME``: it comes back naive. Every other timestamp in ``sso/removal.py`` is coerced through ``_aware`` before being compared; this one was not, so ``now - grant.removal_last_checked_at`` raised ``TypeError``. It could not show up on the first check — that one writes an aware value in memory and the column is still ``None`` when the comparison runs — and every existing test passes ``force=True``, which skips the branch outright. So the failure only appeared on the SECOND daily reconcile of any given grant, from inside the per-grant savepoint, where it was counted as one more skipped row. The whole Google verification half of the removal lifecycle would have stopped working per-grant after its first pass, with nothing on any screen saying so. This test therefore does the two things the existing ones do not: it does **not** force, and it makes the value arrive from the database. """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from typing import Any import pytest from app.domains.sso.models import ( REMOVAL_CONFIRMED, SSO_GRANT_STATUS_REVOKED, SsoGrant, ) from app.domains.sso.removal import RemovalSettings, SsoRemovalService from app.identity.person_models import IdentityPerson pytestmark = pytest.mark.asyncio class _Driver: """Never called on the cooldown path — that is the point.""" def __init__(self) -> None: self.calls = 0 async def list_saml_login_events(self, **_kw: Any) -> list[dict[str, Any]]: self.calls += 1 return [] async def _holder(db) -> IdentityPerson: p = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", employee_no=f"E{uuid.uuid4().hex[:8]}", work_email=f"h{uuid.uuid4().hex[:8]}@x.test", display_name="Holder", employment_status="active", country="CHN", ) db.add(p) await db.flush() return p async def test_a_second_verify_reads_the_stored_clock_without_a_typeerror( db_session, ): holder = await _holder(db_session) now = datetime.now(UTC) grant = SsoGrant( id=str(uuid.uuid4()), person_id=str(holder.id), sso_provider="Google", system_name="Datadog", system_owner_label="Datadog owner", status=SSO_GRANT_STATUS_REVOKED, granted_at=now - timedelta(days=30), revoked_at=now - timedelta(days=5), removal_status=REMOVAL_CONFIRMED, removal_confirmed_at=now - timedelta(days=4), # Checked an hour ago — inside the 20h floor, so the next call # must short-circuit. removal_last_checked_at=now - timedelta(hours=1), ) db_session.add(grant) await db_session.flush() # Force the round trip: expire the instance so the next attribute read # comes from MySQL, which returns a NAIVE datetime. Without this the # in-memory aware value hides the defect entirely — which is exactly # how it reached production. db_session.expire(grant) await db_session.refresh(grant) assert grant.removal_last_checked_at.tzinfo is None, ( "precondition: the DB must hand this back naive, or this test " "cannot see the bug it exists for" ) driver = _Driver() out = await SsoRemovalService(db_session).verify_grant( grant, settings=RemovalSettings(), now=datetime.now(UTC), driver=driver, trace_id="t", ) assert out["result"] == "skipped_recent" assert driver.calls == 0, "the cooldown must short-circuit before the API" async def test_past_the_cooldown_it_checks_again(db_session): """The coercion must not turn the floor into a permanent block.""" holder = await _holder(db_session) now = datetime.now(UTC) grant = SsoGrant( id=str(uuid.uuid4()), person_id=str(holder.id), sso_provider="Google", system_name="Datadog", system_owner_label="Datadog owner", status=SSO_GRANT_STATUS_REVOKED, granted_at=now - timedelta(days=30), revoked_at=now - timedelta(days=5), removal_status=REMOVAL_CONFIRMED, removal_confirmed_at=now - timedelta(days=4), removal_last_checked_at=now - timedelta(days=2), ) db_session.add(grant) await db_session.flush() db_session.expire(grant) await db_session.refresh(grant) driver = _Driver() out = await SsoRemovalService(db_session).verify_grant( grant, settings=RemovalSettings(), now=datetime.now(UTC), driver=driver, trace_id="t", ) assert out["result"] != "skipped_recent" assert driver.calls >= 1