"""Portable ticloud profile bootstrap (``ensure_profile``). ``ticloud`` reads API keys only from a local config file, never the environment, so the extract engine must materialize the named profile from per-source secrets at runtime — otherwise auth is bound to a hand-created ``~/.ticloud/config.toml`` on one machine and breaks on a cloud runner. These tests pin the three decision paths without invoking the real CLI (subprocess is mocked). Runnable without a test harness:: .venv/bin/python tests/test_ensure_profile.py """ from __future__ import annotations import os import pathlib import sys from unittest import mock sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from ingestion.extract.tidbcloud.engine import export from ingestion.extract.tidbcloud.engine.config import SourceConfig def _config() -> SourceConfig: return SourceConfig( path=pathlib.Path("source.yaml"), data={"source": {"name": "x", "database": "x"}, "tidb_cloud_cli_profile": "prof"}, ) def test_noop_when_profile_already_exists() -> None: """An existing local profile is never clobbered — no `config create` is run.""" with mock.patch.object(export, "_existing_profiles", return_value={"prof"}), \ mock.patch.object(export.subprocess, "run") as run: result = export.ensure_profile(_config()) assert result["bootstrapped"] is False assert result["reason"] == "already present" run.assert_not_called() def test_skip_when_no_api_keys() -> None: """Profile missing and no keys to bootstrap from: skip (don't fail) — the engine's explicit --profile will surface a clear error downstream, same as before.""" with mock.patch.dict(os.environ, {}, clear=True), \ mock.patch.object(export, "_existing_profiles", return_value=set()), \ mock.patch.object(export.subprocess, "run") as run: result = export.ensure_profile(_config()) assert result["bootstrapped"] is False assert result["reason"] == "no api keys in secrets" run.assert_not_called() def test_creates_from_secrets_when_missing() -> None: """Profile missing but keys present: bootstrap it via `ticloud config create`, passing the per-source key pair (so the credential travels with the source, not the machine).""" env = {"TIDB_CLOUD_PUBLIC_KEY": "PUBKEY", "TIDB_CLOUD_PRIVATE_KEY": "PRIVKEY"} with mock.patch.dict(os.environ, env, clear=True), \ mock.patch.object(export, "find_ticloud_bin", return_value="ticloud"), \ mock.patch.object(export, "_existing_profiles", return_value=set()), \ mock.patch.object(export.subprocess, "run") as run: result = export.ensure_profile(_config()) assert result["bootstrapped"] is True args = run.call_args.args[0] assert args[:3] == ["ticloud", "config", "create"] assert "--profile-name" in args and args[args.index("--profile-name") + 1] == "prof" assert args[args.index("--public-key") + 1] == "PUBKEY" assert args[args.index("--private-key") + 1] == "PRIVKEY" if __name__ == "__main__": cases = [value for name, value in sorted(globals().items()) if name.startswith("test_") and callable(value)] for case in cases: case() print(f"ok {case.__name__}") print(f"\n{len(cases)} passed")