"""Regression tests for the 2026-04-26 audit pass: every silent ``except: pass`` swallow that masked a real bug in production was either narrowed (ImportError-only for optional-domain guards) or escalated (ValidationError 422 instead of dropping a query filter). Each test pins ONE specific behavioral change so a future 'simplification' can't quietly re-introduce the swallow. """ from __future__ import annotations import uuid import pytest from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import ValidationError # ── Group 1: invalid date filters now 422, not silent broaden ────── @pytest.mark.asyncio async def test_asset_list_operations_rejects_bad_start_date( db_session: AsyncSession, ): """Pre-fix: malformed ``start_date`` was silently dropped, returning every row regardless of date. Post-fix: 422 ValidationError.""" from app.domains.asset.service import AssetService service = AssetService(db_session) with pytest.raises(ValidationError, match="start_date must be ISO"): await service.list_operations( page=1, page_size=10, start_date="not-a-date", ) @pytest.mark.asyncio async def test_asset_list_operations_rejects_bad_end_date( db_session: AsyncSession, ): from app.domains.asset.service import AssetService service = AssetService(db_session) with pytest.raises(ValidationError, match="end_date must be ISO"): await service.list_operations( page=1, page_size=10, end_date="2026/01/01", # slashes, not dashes ) @pytest.mark.asyncio async def test_asset_list_operations_accepts_valid_iso_date( db_session: AsyncSession, ): """Sanity: well-formed ISO dates still work.""" from app.domains.asset.service import AssetService service = AssetService(db_session) # Empty result is fine; we only assert it doesn't raise. rows, total = await service.list_operations( page=1, page_size=10, start_date="2026-01-01", end_date="2026-12-31", ) assert isinstance(total, int) @pytest.mark.asyncio async def test_asset_list_procurement_rejects_bad_dates( db_session: AsyncSession, ): from app.domains.asset.service import AssetService service = AssetService(db_session) with pytest.raises(ValidationError): await service.list_procurement_orders( page=1, page_size=10, start_date="garbage", ) @pytest.mark.asyncio async def test_asset_list_employee_requests_rejects_bad_dates( db_session: AsyncSession, ): from app.domains.asset.service import AssetService service = AssetService(db_session) with pytest.raises(ValidationError): await service.list_employee_requests( page=1, page_size=10, end_date="32-13-2026", # nonsense ) @pytest.mark.asyncio async def test_apikey_list_assignments_rejects_bad_dates( db_session: AsyncSession, ): from app.domains.apikey.service import ApiKeyService from app.identity.person_models import IdentityPerson service = ApiKeyService(db_session) fake_user = IdentityPerson( id=uuid.uuid4(), display_name="audit-fixture", work_email="audit@example.test", employment_status="active", ) with pytest.raises(ValidationError, match="start_date"): await service.list_assignments( current_user=fake_user, page=1, page_size=10, start_date="bogus", ) @pytest.mark.asyncio async def test_approval_list_rejects_bad_dates(db_session: AsyncSession): from app.approval.service import ApprovalService from app.identity.person_models import IdentityPerson service = ApprovalService(db_session) fake_user = IdentityPerson( id=uuid.uuid4(), display_name="audit-fixture", work_email="audit@example.test", employment_status="active", ) with pytest.raises(ValidationError, match="end_date"): await service.list_asset_approval_requests( current_user=fake_user, page=1, page_size=10, end_date="not-a-date", ) # ── Group 2: optional-domain guards narrowed to ImportError only ─── # # Pre-fix: ``except Exception: pass`` would also swallow DB errors, # masking real failures (e.g. permission table corrupt) as a clean # "no permissions" preview. Post-fix: only ImportError is swallowed. def test_provisioning_role_change_preview_narrowed_to_importerror(): """Static guard: the broad ``except Exception: pass`` around the permission-domain block in ``preview_role_change`` was narrowed to ``except ImportError`` only — DB errors during the SELECT must propagate, not be silently turned into an empty preview. Behavioural test would need a leaver fixture; pinning the narrowing in source is enough to catch a future regression.""" import inspect from app.domains.provisioning import service as prov_svc src = inspect.getsource(prov_svc) # The narrowed comment must be present. assert "permission domain not installed" in src # And the broad-Exception variant must NOT be present in the # role-change preview block. Two modules use the optional-import # pattern; pin both have the narrowed form by checking absence # of the old generic comment. assert "except Exception:\n pass" not in src @pytest.mark.asyncio async def test_offboarding_blocker_seeding_propagates_db_errors( db_session: AsyncSession, ): """Same contract for offboarding: a DB error during the SoftwarePermission SELECT must NOT be swallowed — a leaver with 'no blockers found because the SELECT crashed' is a real governance hazard.""" # Note: We can't easily exercise the entire offboarding path here # without a leaver fixture. The behavioural guarantee is asserted # by the import-only narrowing in the source; static guard: import inspect from app.domains.offboarding import service as off_svc src = inspect.getsource(off_svc) # The narrowed pattern must be present (ImportError only) and the # broad ``except Exception: pass`` for permission seeding must be # gone. assert "except ImportError:" in src # Look for the specific try/except pair around the permission # import block — the comment near it is stable. assert "permission domain not installed" in src # Verify there's no broad swallow with the old comment. assert "permission domain optional — skip if not present" not in src # ── Group 3: cloud driver swallows now log warnings ──────────────── def test_aws_cloud_local_policy_listing_logs_on_clienterror(): """``list_policies(Scope=Local)`` failure used to silently truncate the role list; now it logs a warning so support tickets can find the IAM permission gap.""" import inspect from app.integrations.drivers import aws_cloud src = inspect.getsource(aws_cloud) # The fix attaches a logger.warning to the customer-managed # policies block. Pin a stable substring of the new log line. assert ( "list_policies(Scope=Local) failed" in src ), "AWS list_policies fallback must log warning on ClientError" def test_aws_cloud_access_key_last_used_logs_on_clienterror(): """``get_access_key_last_used`` failure used to drop access-key timestamps silently from last-login. Now logs.""" import inspect from app.integrations.drivers import aws_cloud src = inspect.getsource(aws_cloud) assert ( "list_access_keys / get_access_key_last_used" in src and "logger.warning" in src ) def test_gcp_custom_roles_logs_on_httperror(): """GCP ``iam.projects().roles().list`` failure used to silently truncate the role catalog. Now logs.""" import inspect from app.integrations.drivers import gcp_cloud src = inspect.getsource(gcp_cloud) assert "GCP list custom roles for project" in src