feat(apim-apps): first commit
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from apim_apps import AxwayClient, _AppendOnlySession, EnvConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
env = EnvConfig(name="TEST", url="https://test.example.com:8075", username="u", password="p")
|
||||
return AxwayClient(env)
|
||||
|
||||
|
||||
def test_session_is_append_only(client):
|
||||
assert isinstance(client.session, _AppendOnlySession)
|
||||
|
||||
|
||||
def test_delete_raises_runtime_error(client):
|
||||
with pytest.raises(RuntimeError, match="DELETE is not allowed"):
|
||||
client.session.delete("https://test.example.com:8075/api/portal/v1.4/applications/123")
|
||||
|
||||
|
||||
def test_get_is_allowed(client):
|
||||
with patch("requests.Session.request") as mock_req:
|
||||
mock_req.return_value = MagicMock(status_code=200)
|
||||
client.session.get("https://test.example.com")
|
||||
mock_req.assert_called_once()
|
||||
assert mock_req.call_args[0][0].upper() == "GET"
|
||||
|
||||
|
||||
def test_post_is_allowed(client):
|
||||
with patch("requests.Session.request") as mock_req:
|
||||
mock_req.return_value = MagicMock(status_code=201)
|
||||
client.session.post("https://test.example.com", json={})
|
||||
mock_req.assert_called_once()
|
||||
assert mock_req.call_args[0][0].upper() == "POST"
|
||||
|
||||
|
||||
def test_no_delete_method_on_client():
|
||||
"""AxwayClient must not expose any delete_* method."""
|
||||
delete_methods = [m for m in dir(AxwayClient) if m.startswith("delete")]
|
||||
assert delete_methods == [], f"Unexpected delete methods found: {delete_methods}"
|
||||
@@ -0,0 +1,116 @@
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from apim_apps.cli import main
|
||||
|
||||
|
||||
SAMPLE_CONFIG = json.dumps({
|
||||
"environments": [{"name": "DEV_LAN", "url": "https://dev.example.com:8075"}]
|
||||
})
|
||||
|
||||
SAMPLE_MANIFEST = textwrap.dedent("""\
|
||||
organizations: []
|
||||
applications: []
|
||||
""")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_files(tmp_path):
|
||||
(tmp_path / "config.json").write_text(SAMPLE_CONFIG)
|
||||
(tmp_path / "apps.yaml").write_text(SAMPLE_MANIFEST)
|
||||
return tmp_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Credential validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_exits_when_apim_user_missing(project_files):
|
||||
argv = ["apim_apps.py", "--config", str(project_files / "config.json"),
|
||||
"--manifest", str(project_files / "apps.yaml"), "--dry-run", "--env", "DEV_LAN"]
|
||||
env = {"APIM_PASSWORD": "secret"}
|
||||
with patch("sys.argv", argv), patch.dict(os.environ, env, clear=True):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main()
|
||||
assert exc.value.code == 1
|
||||
|
||||
|
||||
def test_exits_when_apim_password_missing(project_files):
|
||||
argv = ["apim_apps.py", "--config", str(project_files / "config.json"),
|
||||
"--manifest", str(project_files / "apps.yaml"), "--dry-run", "--env", "DEV_LAN"]
|
||||
env = {"APIM_USER": "admin"}
|
||||
with patch("sys.argv", argv), patch.dict(os.environ, env, clear=True):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main()
|
||||
assert exc.value.code == 1
|
||||
|
||||
|
||||
def test_exits_when_both_credentials_missing(project_files):
|
||||
argv = ["apim_apps.py", "--config", str(project_files / "config.json"),
|
||||
"--manifest", str(project_files / "apps.yaml"), "--dry-run", "--env", "DEV_LAN"]
|
||||
with patch("sys.argv", argv), patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main()
|
||||
assert exc.value.code == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --env filtering
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_exits_when_env_not_found(project_files):
|
||||
argv = ["apim_apps.py", "--config", str(project_files / "config.json"),
|
||||
"--manifest", str(project_files / "apps.yaml"), "--env", "UNKNOWN"]
|
||||
env = {"APIM_USER": "admin", "APIM_PASSWORD": "secret"}
|
||||
with patch("sys.argv", argv), patch.dict(os.environ, env, clear=True):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main()
|
||||
assert exc.value.code == 1
|
||||
|
||||
|
||||
def test_dry_run_succeeds_with_valid_env(project_files):
|
||||
argv = ["apim_apps.py", "--config", str(project_files / "config.json"),
|
||||
"--manifest", str(project_files / "apps.yaml"), "--dry-run", "--env", "DEV_LAN"]
|
||||
env = {"APIM_USER": "admin", "APIM_PASSWORD": "secret"}
|
||||
with patch("sys.argv", argv), patch.dict(os.environ, env, clear=True):
|
||||
with patch("apim_apps.engine.AxwayClient") as MockClient:
|
||||
instance = MockClient.return_value
|
||||
instance.list_orgs.return_value = {}
|
||||
instance.list_apps.return_value = {}
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main()
|
||||
assert exc.value.code == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --init-* helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_init_manifest_creates_file(tmp_path):
|
||||
out = tmp_path / "apps.yaml"
|
||||
with patch("sys.argv", ["apim_apps.py", "--init-manifest", "--manifest", str(out)]):
|
||||
main()
|
||||
assert out.exists()
|
||||
assert out.stat().st_size > 0
|
||||
|
||||
|
||||
def test_init_config_creates_file(tmp_path):
|
||||
out = tmp_path / "config.json"
|
||||
with patch("sys.argv", ["apim_apps.py", "--init-config", "--config", str(out)]):
|
||||
main()
|
||||
assert out.exists()
|
||||
data = json.loads(out.read_text())
|
||||
assert "environments" in data
|
||||
|
||||
|
||||
def test_init_config_no_credentials_in_template(tmp_path):
|
||||
"""Generated config.json must not contain username or password."""
|
||||
out = tmp_path / "config.json"
|
||||
with patch("sys.argv", ["apim_apps.py", "--init-config", "--config", str(out)]):
|
||||
main()
|
||||
data = json.loads(out.read_text())
|
||||
for entry in data["environments"]:
|
||||
assert "username" not in entry
|
||||
assert "password" not in entry
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
import pytest
|
||||
from apim_apps import load_config
|
||||
|
||||
SAMPLE = {
|
||||
"environments": [
|
||||
{"name": "DEV_LAN", "url": "https://dev.example.com:8075", "verify_ssl": False},
|
||||
{"name": "PROD", "url": "https://prod.example.com:8075", "verify_ssl": True},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_load_config_names_and_urls(tmp_path):
|
||||
f = tmp_path / "config.json"
|
||||
f.write_text(json.dumps(SAMPLE))
|
||||
envs = load_config(str(f))
|
||||
|
||||
assert len(envs) == 2
|
||||
assert envs[0].name == "DEV_LAN"
|
||||
assert envs[0].url == "https://dev.example.com:8075"
|
||||
assert envs[0].verify_ssl is False
|
||||
assert envs[1].name == "PROD"
|
||||
assert envs[1].verify_ssl is True
|
||||
|
||||
|
||||
def test_credentials_not_loaded_from_config(tmp_path):
|
||||
"""Credentials come from env vars, not from config.json."""
|
||||
f = tmp_path / "config.json"
|
||||
f.write_text(json.dumps(SAMPLE))
|
||||
envs = load_config(str(f))
|
||||
|
||||
for env in envs:
|
||||
assert env.username == ""
|
||||
assert env.password == ""
|
||||
|
||||
|
||||
def test_verify_ssl_defaults_to_false(tmp_path):
|
||||
config = {"environments": [{"name": "DEV", "url": "https://dev.example.com:8075"}]}
|
||||
f = tmp_path / "config.json"
|
||||
f.write_text(json.dumps(config))
|
||||
envs = load_config(str(f))
|
||||
|
||||
assert envs[0].verify_ssl is False
|
||||
@@ -0,0 +1,96 @@
|
||||
import textwrap
|
||||
import pytest
|
||||
from apim_apps import load_manifest, ApiAccess
|
||||
|
||||
MINIMAL = textwrap.dedent("""\
|
||||
organizations: []
|
||||
applications: []
|
||||
""")
|
||||
|
||||
FULL = textwrap.dedent("""\
|
||||
organizations:
|
||||
- name: "MyOrg"
|
||||
description: "Test org"
|
||||
email: "org@example.com"
|
||||
enabled: true
|
||||
development: false
|
||||
environments:
|
||||
- DEV_LAN
|
||||
|
||||
applications:
|
||||
- name: "MyApp"
|
||||
organization: "MyOrg"
|
||||
description: "Test app"
|
||||
email: "app@example.com"
|
||||
enabled: true
|
||||
environments:
|
||||
- DEV_LAN
|
||||
apis:
|
||||
- name: "MyAPI"
|
||||
version: "v1"
|
||||
- "OtherAPI"
|
||||
""")
|
||||
|
||||
NO_ENVS = textwrap.dedent("""\
|
||||
organizations:
|
||||
- name: "GlobalOrg"
|
||||
applications:
|
||||
- name: "GlobalApp"
|
||||
organization: "GlobalOrg"
|
||||
""")
|
||||
|
||||
|
||||
def test_minimal(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(MINIMAL)
|
||||
m = load_manifest(str(f))
|
||||
assert m.organizations == []
|
||||
assert m.applications == []
|
||||
|
||||
|
||||
def test_full_org_fields(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(FULL)
|
||||
m = load_manifest(str(f))
|
||||
|
||||
assert len(m.organizations) == 1
|
||||
org = m.organizations[0]
|
||||
assert org.name == "MyOrg"
|
||||
assert org.description == "Test org"
|
||||
assert org.email == "org@example.com"
|
||||
assert org.enabled is True
|
||||
assert org.development is False
|
||||
assert org.environments == ["DEV_LAN"]
|
||||
|
||||
|
||||
def test_full_app_fields(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(FULL)
|
||||
m = load_manifest(str(f))
|
||||
|
||||
assert len(m.applications) == 1
|
||||
app = m.applications[0]
|
||||
assert app.name == "MyApp"
|
||||
assert app.org_name == "MyOrg"
|
||||
assert app.environments == ["DEV_LAN"]
|
||||
|
||||
|
||||
def test_api_access_named(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(FULL)
|
||||
m = load_manifest(str(f))
|
||||
apis = m.applications[0].apis
|
||||
|
||||
assert len(apis) == 2
|
||||
assert apis[0] == ApiAccess(api_name="MyAPI", api_version="v1")
|
||||
assert apis[1] == ApiAccess(api_name="OtherAPI", api_version="")
|
||||
|
||||
|
||||
def test_defaults_when_envs_omitted(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(NO_ENVS)
|
||||
m = load_manifest(str(f))
|
||||
|
||||
assert m.organizations[0].environments == []
|
||||
assert m.applications[0].environments == []
|
||||
assert m.applications[0].apis == []
|
||||
@@ -0,0 +1,198 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from apim_apps import normalize_env, EnvConfig, Manifest, OrgSpec, AppSpec, ApiAccess
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
return EnvConfig(name="TEST", url="https://test.example.com:8075", username="u", password="p")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
with patch("apim_apps.engine.AxwayClient") as MockClient:
|
||||
instance = MockClient.return_value
|
||||
instance.list_orgs.return_value = {}
|
||||
instance.list_apps.return_value = {}
|
||||
instance.list_app_api_access.return_value = []
|
||||
yield instance
|
||||
|
||||
|
||||
def _manifest(orgs=None, apps=None):
|
||||
return Manifest(organizations=orgs or [], applications=apps or [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dry-run
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_dry_run_makes_no_api_calls(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg", apis=[ApiAccess(api_name="MyAPI")])],
|
||||
)
|
||||
mock_client.list_orgs.return_value = {}
|
||||
mock_client.list_apps.return_value = {}
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=True)
|
||||
|
||||
mock_client.create_org.assert_not_called()
|
||||
mock_client.create_app.assert_not_called()
|
||||
mock_client.grant_api_access.assert_not_called()
|
||||
assert len(result.errors) == 0
|
||||
|
||||
|
||||
def test_dry_run_reports_planned_actions(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg")],
|
||||
)
|
||||
result = normalize_env(env, manifest, dry_run=True)
|
||||
|
||||
kinds = {a.kind for a in result.actions_done}
|
||||
assert "create_org" in kinds
|
||||
assert "create_app" in kinds
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apply mode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_apply_creates_org_and_app(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg")],
|
||||
)
|
||||
mock_client.create_org.return_value = {"id": "org-1", "name": "MyOrg"}
|
||||
mock_client.create_app.return_value = {"id": "app-1", "name": "MyApp"}
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_org.assert_called_once()
|
||||
mock_client.create_app.assert_called_once()
|
||||
assert len(result.errors) == 0
|
||||
|
||||
|
||||
def test_apply_grants_api_subscription(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg", apis=[ApiAccess(api_name="MyAPI")])],
|
||||
)
|
||||
mock_client.create_org.return_value = {"id": "org-1", "name": "MyOrg"}
|
||||
mock_client.create_app.return_value = {"id": "app-1", "name": "MyApp"}
|
||||
mock_client.find_frontend_api.return_value = {"id": "api-1", "name": "MyAPI"}
|
||||
|
||||
normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.grant_api_access.assert_called_once_with("app-1", "api-1")
|
||||
|
||||
|
||||
def test_apply_skips_missing_frontend_api(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg", apis=[ApiAccess(api_name="Ghost")])],
|
||||
)
|
||||
mock_client.create_org.return_value = {"id": "org-1", "name": "MyOrg"}
|
||||
mock_client.create_app.return_value = {"id": "app-1", "name": "MyApp"}
|
||||
mock_client.find_frontend_api.return_value = None
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.grant_api_access.assert_not_called()
|
||||
assert any("Ghost" in e for e in result.errors)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Skip when already present
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_skips_existing_org(env, mock_client):
|
||||
manifest = _manifest(orgs=[OrgSpec(name="MyOrg")])
|
||||
mock_client.list_orgs.return_value = {"MyOrg": {"id": "org-1", "name": "MyOrg"}}
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_org.assert_not_called()
|
||||
assert any(a.kind == "create_org" for a in result.actions_skipped)
|
||||
|
||||
|
||||
def test_skips_existing_app(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg")],
|
||||
)
|
||||
mock_client.list_orgs.return_value = {"MyOrg": {"id": "org-1", "name": "MyOrg"}}
|
||||
mock_client.list_apps.return_value = {("MyOrg", "MyApp"): {"id": "app-1", "name": "MyApp"}}
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_app.assert_not_called()
|
||||
assert any(a.kind == "create_app" for a in result.actions_skipped)
|
||||
|
||||
|
||||
def test_skips_already_granted_api(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="MyOrg")],
|
||||
apps=[AppSpec(name="MyApp", org_name="MyOrg", apis=[ApiAccess(api_name="MyAPI")])],
|
||||
)
|
||||
mock_client.list_orgs.return_value = {"MyOrg": {"id": "org-1", "name": "MyOrg"}}
|
||||
mock_client.list_apps.return_value = {("MyOrg", "MyApp"): {"id": "app-1", "name": "MyApp"}}
|
||||
mock_client.list_app_api_access.return_value = [{"apiName": "MyAPI", "id": "api-1"}]
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.grant_api_access.assert_not_called()
|
||||
assert any(a.kind == "grant_api" for a in result.actions_skipped)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Environment filtering
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_env_filter_excludes_other_env(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[
|
||||
OrgSpec(name="GlobalOrg"),
|
||||
OrgSpec(name="ProdOnly", environments=["PROD"]),
|
||||
]
|
||||
)
|
||||
result = normalize_env(env, manifest, dry_run=True)
|
||||
|
||||
org_names = [a.detail["org"] for a in result.actions_done + result.actions_skipped]
|
||||
assert "GlobalOrg" in org_names
|
||||
assert "ProdOnly" not in org_names
|
||||
|
||||
|
||||
def test_env_filter_matches_current_env(env, mock_client):
|
||||
manifest = _manifest(
|
||||
orgs=[OrgSpec(name="TestOnly", environments=["TEST"])]
|
||||
)
|
||||
result = normalize_env(env, manifest, dry_run=True)
|
||||
|
||||
org_names = [a.detail["org"] for a in result.actions_done + result.actions_skipped]
|
||||
assert "TestOnly" in org_names
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error handling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_error_when_org_missing_for_app(env, mock_client):
|
||||
manifest = _manifest(
|
||||
apps=[AppSpec(name="Orphan", org_name="MissingOrg")]
|
||||
)
|
||||
mock_client.list_orgs.return_value = {}
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
assert any("MissingOrg" in e for e in result.errors)
|
||||
|
||||
|
||||
def test_list_orgs_failure_returns_error(env, mock_client):
|
||||
import requests
|
||||
mock_client.list_orgs.side_effect = requests.ConnectionError("timeout")
|
||||
manifest = _manifest(orgs=[OrgSpec(name="MyOrg")])
|
||||
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
Reference in New Issue
Block a user