"""DB-backed tests for cert_service: probe-target building + upsert.""" from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta import pytest import pytest_asyncio from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.cert_probe import ProbedCert from app.domains.cmdb.cert_service import ( PROBE_TARGET_GONE_ERROR, build_probe_targets, mark_orphaned_probe_targets, upsert_probed_certificate, ) from app.domains.cmdb.models import ( CERT_SOURCE_PROBED, CERT_STATUS_EXPIRED, CERT_STATUS_UNREACHABLE, CERT_STATUS_VALID, CmdbCertificate, CmdbDnsRecord, CmdbDomain, ) from app.identity.person_models import IdentityPerson from app.integrations.models import Integration @pytest_asyncio.fixture async def cloud_integ(db_session: AsyncSession) -> Integration: integ = Integration( id=uuid.uuid4(), kind="cloud", provider="aliyun", name="pytest-cert-aliyun", slug=f"pytest-cert-aliyun-{uuid.uuid4().hex[:8]}", enabled=True, is_primary=False, capabilities=["domains", "dns_records"], ) db_session.add(integ) await db_session.commit() return integ @pytest_asyncio.fixture async def owner_person(db_session: AsyncSession) -> IdentityPerson: p = IdentityPerson( id=uuid.uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", display_name="Cert Owner", work_email=f"certowner-{uuid.uuid4().hex[:6]}@pingcap.com", employment_status="active", ) db_session.add(p) await db_session.commit() return p @pytest.mark.asyncio async def test_build_probe_targets_dedups_env_inherited_no_owner_snapshot( db_session: AsyncSession, cloud_integ: Integration, owner_person: IdentityPerson, ): db_session.add(CmdbDomain( id=uuid.uuid4(), integration_id=cloud_integ.id, domain_name="example.com", environment="prod", owner_person_id=str(owner_person.id), )) db_session.add_all([ CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="example.com", record_id="r1", name="www", record_type="A", value="1.2.3.4", status="enable", ), # CNAME — probeable CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="example.com", record_id="r2", name="api", record_type="CNAME", value="lb.example.com", status="enable", owner_person_id=str(owner_person.id), ), # MX — NOT probeable, must be excluded CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="example.com", record_id="r3", name="mail", record_type="MX", value="mx.example.com", status="enable", ), # disabled — excluded CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="example.com", record_id="r4", name="old", record_type="A", value="9.9.9.9", status="disable", ), # wildcard — excluded CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="example.com", record_id="r5", name="*", record_type="A", value="1.1.1.1", status="enable", ), ]) await db_session.commit() targets = await build_probe_targets(db_session) hosts = {t["host"] for t in targets} assert hosts == {"example.com", "www.example.com", "api.example.com"} # Owner is NO LONGER snapshotted onto probe targets — ownership is # live-resolved at read/alert time. Env is still inherited. apex = next(t for t in targets if t["host"] == "example.com") assert "owner_person_id" not in apex assert apex["environment"] == "prod" www = next(t for t in targets if t["host"] == "www.example.com") assert www["environment"] == "prod" @pytest.mark.asyncio async def test_upsert_probed_created_then_updated_no_owner_snapshot( db_session: AsyncSession, ): na = datetime.now(UTC) + timedelta(days=60) probed = ProbedCert( host="svc.example.com", port=443, ok=True, common_name="svc.example.com", sans=["svc.example.com"], issuer="Let's Encrypt", serial_number="abcd", fingerprint_sha256="f" * 64, not_after=na, ) row, action = await upsert_probed_certificate( db_session, probed, environment="prod", ) await db_session.commit() assert action == "created" assert row.status == CERT_STATUS_VALID # Probed certs carry NO owner snapshot — owner is live-resolved. assert row.owner_person_id is None assert row.owner_is_manual is False assert row.dedup_key == "probed:svc.example.com:443" # Renewal: same host, new fingerprint + later not_after → update in place. probed2 = ProbedCert( host="svc.example.com", port=443, ok=True, common_name="svc.example.com", sans=["svc.example.com"], issuer="Let's Encrypt", serial_number="ef01", fingerprint_sha256="a" * 64, not_after=datetime.now(UTC) + timedelta(days=90), ) row2, action2 = await upsert_probed_certificate( db_session, probed2, environment=None, ) await db_session.commit() assert action2 == "updated" assert str(row2.id) == str(row.id) # same row assert row2.fingerprint_sha256 == "a" * 64 assert row2.owner_person_id is None rows = (await db_session.execute( select(CmdbCertificate).where( CmdbCertificate.dedup_key == "probed:svc.example.com:443" ) )).scalars().all() assert len(rows) == 1 @pytest.mark.asyncio async def test_upsert_unreachable_marks_existing_never_invents( db_session: AsyncSession, ): # Unreachable with no existing row → skipped (no row created). probed_fail = ProbedCert( host="internal.example.com", port=443, ok=False, error="timed out", ) row, action = await upsert_probed_certificate( db_session, probed_fail, environment=None, ) assert action == "skipped" assert row is None # Create a row, then a later unreachable probe marks it unreachable # WITHOUT wiping not_after. na = datetime.now(UTC) + timedelta(days=10) ok = ProbedCert( host="internal.example.com", port=443, ok=True, common_name="internal.example.com", not_after=na, fingerprint_sha256="b" * 64, ) await upsert_probed_certificate( db_session, ok, environment=None, ) await db_session.commit() row3, action3 = await upsert_probed_certificate( db_session, probed_fail, environment=None, ) await db_session.commit() assert action3 == "unreachable" assert row3.status == CERT_STATUS_UNREACHABLE assert row3.last_error == "timed out" assert row3.not_after is not None # preserved @pytest.mark.asyncio async def test_upsert_expired_status(db_session: AsyncSession): probed = ProbedCert( host="old.example.com", port=443, ok=True, common_name="old.example.com", not_after=datetime.now(UTC) - timedelta(days=2), fingerprint_sha256="c" * 64, ) row, action = await upsert_probed_certificate( db_session, probed, environment=None, ) await db_session.commit() assert action == "created" assert row.status == CERT_STATUS_EXPIRED @pytest.mark.asyncio async def test_disabled_dns_record_leaves_probe_targets_and_flags_the_cert( db_session: AsyncSession, cloud_integ: Integration, ): """A cert whose DNS record got disabled must not become a silent zombie. ``build_probe_targets`` only takes ``status='enable'`` records, so a record disabled at the provider correctly leaves the target set — the prober is not broken. But the sweep then never touches that certificate again and ``status`` is only written at probe time, so the row froze: production 2026-08-18 had ``tem.pingcap.net`` / ``tem-test.pingcap.net`` (live DNSPod record ``disable``) still displaying ``expiring`` 52 days after their certificate expired, plus ``do3.pingcap.net`` with no live record at all. """ db_session.add(CmdbDomain( id=uuid.uuid4(), integration_id=cloud_integ.id, domain_name="zombie.test", environment="prod", )) db_session.add_all([ CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="zombie.test", record_id="live-1", name="live", record_type="A", value="10.0.0.1", status="enable", ), CmdbDnsRecord( integration_id=cloud_integ.id, zone_name="zombie.test", record_id="off-1", name="switched-off", record_type="A", value="10.0.0.2", status="disable", ), ]) await db_session.flush() targets = await build_probe_targets(db_session) hosts = {t["host"] for t in targets} # The disabled record is correctly absent; the enabled one is present. assert "live.zombie.test" in hosts assert "switched-off.zombie.test" not in hosts # Two probed certs: one for a live target, one whose target just left. live_cert = CmdbCertificate( id=uuid.uuid4(), dedup_key=f"{CERT_SOURCE_PROBED}:live.zombie.test:443", source=CERT_SOURCE_PROBED, probe_host="live.zombie.test", common_name="live.zombie.test", status=CERT_STATUS_VALID, not_after=datetime.now(UTC) + timedelta(days=100), ) orphan = CmdbCertificate( id=uuid.uuid4(), dedup_key=f"{CERT_SOURCE_PROBED}:switched-off.zombie.test:443", source=CERT_SOURCE_PROBED, probe_host="switched-off.zombie.test", common_name="switched-off.zombie.test", # What the last probe wrote, before the DNS record was disabled. status="expiring", not_after=datetime.now(UTC) - timedelta(days=52), ) db_session.add_all([live_cert, orphan]) await db_session.flush() flagged = await mark_orphaned_probe_targets(db_session, hosts) assert flagged == 1 assert orphan.last_error == PROBE_TARGET_GONE_ERROR assert live_cert.last_error is None # Lifecycle data is left alone — retire-or-re-enable is a human call. assert orphan.status == "expiring" assert orphan.not_after is not None # Idempotent: a second sweep must not re-flag (and churn updated_at). assert await mark_orphaned_probe_targets(db_session, hosts) == 0 # And a successful probe clears the marker, so it can never stick after # the DNS record comes back. _row, action = await upsert_probed_certificate( db_session, ProbedCert( host="switched-off.zombie.test", port=443, ok=True, common_name="switched-off.zombie.test", sans=[], issuer="Test CA", serial_number="1", fingerprint_sha256="f" * 64, not_before=datetime.now(UTC) - timedelta(days=1), not_after=datetime.now(UTC) + timedelta(days=90), key_algorithm="RSA", key_bits=2048, signature_algorithm="sha256WithRSAEncryption", error=None, ), environment="prod", ) assert action in {"created", "updated"} assert orphan.last_error is None