"""Smoke tests: every service constructor must not touch external systems eagerly. Motivation — a bug shipped in #218 and its hotfix: ``ApiKeyService.__init__`` eagerly constructed ``FeishuApprovalClient()`` with no args, which raised after the client was made credential-injected. Every apikey endpoint then crashed before ever reaching its handler. Tests didn't catch this because (a) we had no apikey tests at all and (b) the approval tests stubbed the client. This module exists so any future regression of the same pattern ("service touches an external system in its ``__init__``") fails loudly and immediately. The tests construct each service directly and hit the read-only endpoints that have zero business touching Feishu / SF etc. """ from __future__ import annotations import uuid import pytest from app.domains.apikey.models import ApiKeyProvider def _admin_headers(admin_user): _, token = admin_user return {"Authorization": f"Bearer {token}"} @pytest.mark.asyncio async def test_apikey_service_constructs_without_feishu(db_session): """Constructing ApiKeyService must not require Feishu credentials. This is the direct regression for the production bug where ApiKeyService() threw before any handler ran, breaking /apikeys/providers, /apikeys/assignments, and every other apikey route with an opaque 404. """ from app.domains.apikey.service import ApiKeyService svc = ApiKeyService(db_session) # must NOT raise # Nothing in list_providers should touch Feishu either. providers = await svc.list_providers() assert isinstance(providers, list) @pytest.mark.asyncio async def test_approval_service_constructs_without_feishu(db_session): """Same for ApprovalService — constructor must be pure DB binding.""" from app.approval.service import ApprovalService svc = ApprovalService(db_session) # must NOT raise # template_config reads from integration_approval_flows, not Feishu # over HTTP. On an empty DB it returns {} cleanly. templates = await svc.template_config() assert templates == {} @pytest.mark.asyncio async def test_hr_lifecycle_service_constructs_without_sf(db_session): """SuccessFactorsLifecycleService must not eager-construct the SF client.""" from app.domains.hr.service import SuccessFactorsLifecycleService svc = SuccessFactorsLifecycleService(db_session) # must NOT raise # Reading batch list touches only the local DB, never SF. batches, total = await svc.list_batches(page=1, page_size=10) assert total == 0 assert batches == [] @pytest.mark.asyncio async def test_apikey_endpoints_respond_200_on_empty_db(client, admin_user, db_session): """End-to-end smoke of every admin-facing apikey GET. The original bug produced 404 on all of these. If any of them starts throwing again, this fails fast with a clean stack trace instead of the user reporting mysterious empty pages. """ # Seed one provider so the empty-list path and the populated-list # path are both exercised. provider = ApiKeyProvider( id=uuid.uuid4(), name="Test Provider", description="smoke", is_active=True, sort_order=0, ) db_session.add(provider) await db_session.commit() h = _admin_headers(admin_user) for path in [ "/api/v1/apikeys/providers", "/api/v1/apikeys/providers?include_inactive=true", "/api/v1/apikeys/assignments", "/api/v1/apikeys/my", ]: r = await client.get(path, headers=h) assert r.status_code == 200, ( f"{path} returned {r.status_code}: {r.text[:300]}" ) @pytest.mark.asyncio async def test_admin_surface_endpoints_all_reachable(client, admin_user): """Fast sanity: every admin-facing GET endpoint touched by the integration refactor must reach its handler (return 200 or a domain-appropriate non-500 on empty data). Catches the class of bug where a service's __init__ or a decorator fails, masking as 404/500 for the UI. """ h = _admin_headers(admin_user) for path, max_ok in [ ("/api/v1/apikeys/providers", 200), ("/api/v1/apikeys/assignments", 200), ("/api/v1/apikeys/my", 200), ("/api/v1/approval/asset/templates", 200), ("/api/v1/approval/asset/requests", 200), ("/api/v1/integrations", 200), ("/api/v1/integrations/kinds", 200), ("/api/v1/platform-settings", 200), ("/api/v1/provisioning/connectors", 200), ("/api/v1/provisioning/connector-types", 200), ("/api/v1/hr/sync/batches", 200), ]: r = await client.get(path, headers=h) # 200 = ok; 403 shouldn't happen for admin; 500 is always a bug. assert r.status_code == max_ok, ( f"{path} returned {r.status_code}: {r.text[:300]}" )