"""External API meta surface — /api/v1/open/me + hosted reference. Pins: * /me echoes client + scopes for a valid api token; rejects pat/svc/none * /openapi.json is credential-gated (api key via header or ?key=); the curated spec contains the cloud routes but NOT the directory routes (those are the separate identity-pull feature) """ from __future__ import annotations import uuid import pytest from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api_clients.models import ApiClient, ApiClientToken from app.api_clients.scopes import SCOPE_CLOUD_SUBSCRIPTIONS_READ from app.api_clients.tokens import ApiClientTokenService from app.core.config import settings from app.core.service_token import hash_token from app.identity.person_models import IdentityPerson pytestmark = pytest.mark.asyncio def _h(token: str) -> dict: return {"Authorization": f"Bearer {token}"} async def _mk_client( db: AsyncSession, *, scopes: list[str], person_visibility: dict | None = None, ) -> str: owner = IdentityPerson( id=uuid.uuid4(), work_email=f"co-{uuid.uuid4().hex[:8]}@pingcap.com", display_name="Owner", employment_status="active", source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:10]}", ) db.add(owner) await db.flush() client = ApiClient( id=uuid.uuid4(), name="BI", slug=f"bi-{uuid.uuid4().hex[:6]}", owner_person_id=owner.id, scopes=scopes, person_visibility=person_visibility, ) db.add(client) await db.flush() await db.refresh(client) _row, plaintext = await ApiClientTokenService(db).mint( client=client, name="t", expires_in_days=None, created_by=None, ) await db.commit() return plaintext async def test_me_verify(client, db_session): token = await _mk_client(db_session, scopes=[SCOPE_CLOUD_SUBSCRIPTIONS_READ]) r = await client.get("/api/v1/open/me", headers=_h(token)) assert r.status_code == 200, r.text data = r.json()["data"] assert data["scopes"] == [SCOPE_CLOUD_SUBSCRIPTIONS_READ] assert data["client"]["slug"].startswith("bi-") assert data["token_prefix"].startswith("helix_api_") # Unrestricted is stated, not implied by omission. assert data["person_visibility"] == {"restricted": False} async def test_me_reports_person_visibility_ceiling(client, db_session): """The people endpoints make an out-of-ceiling person indistinguishable from a non-existent one — deliberately. That makes /me the ONLY place a client can discover its own reach is narrowed, so it must say so.""" token = await _mk_client( db_session, scopes=[SCOPE_CLOUD_SUBSCRIPTIONS_READ], person_visibility={"countries": ["CN"], "employment_statuses": ["active"]}, ) r = await client.get("/api/v1/open/me", headers=_h(token)) assert r.status_code == 200, r.text pv = r.json()["data"]["person_visibility"] assert pv["restricted"] is True assert pv["countries"] == ["CN"] assert pv["employment_statuses"] == ["active"] async def test_me_does_not_call_an_unreadable_ceiling_unrestricted( client, db_session, ): """A ceiling the data plane cannot parse makes that plane return nobody. Reporting `restricted: false` here would be a lie in the one place built to explain an unexpectedly empty result set.""" from sqlalchemy import text token = await _mk_client(db_session, scopes=[SCOPE_CLOUD_SUBSCRIPTIONS_READ]) await db_session.execute( text("UPDATE api_clients SET person_visibility = :v WHERE slug LIKE 'bi-%'"), {"v": '"not-an-object"'}, ) await db_session.commit() r = await client.get("/api/v1/open/me", headers=_h(token)) assert r.status_code == 200, r.text pv = r.json()["data"]["person_visibility"] assert pv["restricted"] is True assert pv["misconfigured"] is True async def test_legacy_token_hash_is_accepted_then_upgraded(db_session): token = await _mk_client( db_session, scopes=[SCOPE_CLOUD_SUBSCRIPTIONS_READ], ) row = await db_session.scalar(select(ApiClientToken)) assert row is not None keyed_hash = hash_token(token) legacy_hash = await db_session.scalar(select(func.sha2(token, 256))) assert legacy_hash != keyed_hash row.token_hash = legacy_hash await db_session.commit() resolved = await ApiClientTokenService(db_session).authenticate(token) assert resolved is not None assert resolved[1].id == row.id assert row.token_hash == keyed_hash async def test_token_fingerprint_is_keyed_and_deterministic(monkeypatch): plaintext = "helix_api_test-token-material" monkeypatch.setattr(settings, "CREDENTIAL_ENCRYPTION_KEY", "key-A") first = hash_token(plaintext) assert hash_token(plaintext) == first assert len(first) == 64 monkeypatch.setattr(settings, "CREDENTIAL_ENCRYPTION_KEY", "key-B") assert hash_token(plaintext) != first async def test_me_rejects_other_credentials(client, db_session): for bad in ("helix_pat_x", "helix_svc_x", "garbage"): assert (await client.get("/api/v1/open/me", headers=_h(bad))).status_code == 401 assert (await client.get("/api/v1/open/me")).status_code == 401 async def test_openapi_credential_gated_and_curated(client, db_session): token = await _mk_client(db_session, scopes=[SCOPE_CLOUD_SUBSCRIPTIONS_READ]) # no credential → 401 assert (await client.get("/api/v1/open/openapi.json")).status_code == 401 # via header r = await client.get("/api/v1/open/openapi.json", headers=_h(token)) assert r.status_code == 200, r.text paths = r.json()["paths"] assert "/api/v1/open/cloud/subscriptions" in paths assert "/api/v1/open/me" in paths # directory routes belong to the separate identity-pull feature assert not any(p.startswith("/api/v1/open/directory") for p in paths) # via ?key= r2 = await client.get(f"/api/v1/open/openapi.json?key={token}") assert r2.status_code == 200