feat(apim-apps): add external credentials support for new applications
Applications can now declare a `credentials:` list in the YAML manifest.
Each entry holds a static `client_id` (and optional `secret`) that is
provisioned via POST /applications/{id}/extcredentials when the application
is first created. Existing applications are never modified.
Also adds CHANGELOG.md and ROADMAP.md for the v0.1.0 initial release.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import textwrap
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from apim_apps import load_manifest, AppCredential
|
||||
from apim_apps import normalize_env, EnvConfig, Manifest, OrgSpec, AppSpec
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Loader — parsing credentials from YAML
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WITH_CREDENTIAL = textwrap.dedent("""\
|
||||
organizations: []
|
||||
applications:
|
||||
- name: "MyApp"
|
||||
organization: "MyOrg"
|
||||
credentials:
|
||||
- client_id: "ext-client-id"
|
||||
""")
|
||||
|
||||
WITH_SECRET = textwrap.dedent("""\
|
||||
organizations: []
|
||||
applications:
|
||||
- name: "MyApp"
|
||||
organization: "MyOrg"
|
||||
credentials:
|
||||
- client_id: "ext-client-id"
|
||||
secret: "s3cr3t"
|
||||
""")
|
||||
|
||||
MULTIPLE_CREDENTIALS = textwrap.dedent("""\
|
||||
organizations: []
|
||||
applications:
|
||||
- name: "MyApp"
|
||||
organization: "MyOrg"
|
||||
credentials:
|
||||
- client_id: "client-one"
|
||||
- client_id: "client-two"
|
||||
""")
|
||||
|
||||
WITHOUT_CREDENTIAL = textwrap.dedent("""\
|
||||
organizations: []
|
||||
applications:
|
||||
- name: "MyApp"
|
||||
organization: "MyOrg"
|
||||
""")
|
||||
|
||||
|
||||
def test_credential_is_parsed(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(WITH_CREDENTIAL)
|
||||
m = load_manifest(str(f))
|
||||
assert m.applications[0].credentials == [AppCredential(client_id="ext-client-id")]
|
||||
|
||||
|
||||
def test_credential_with_secret_parsed(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(WITH_SECRET)
|
||||
m = load_manifest(str(f))
|
||||
assert m.applications[0].credentials == [AppCredential(client_id="ext-client-id", secret="s3cr3t")]
|
||||
|
||||
|
||||
def test_multiple_credentials_parsed(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(MULTIPLE_CREDENTIALS)
|
||||
m = load_manifest(str(f))
|
||||
assert m.applications[0].credentials == [
|
||||
AppCredential(client_id="client-one"),
|
||||
AppCredential(client_id="client-two"),
|
||||
]
|
||||
|
||||
|
||||
def test_credentials_default_to_empty(tmp_path):
|
||||
f = tmp_path / "apps.yaml"
|
||||
f.write_text(WITHOUT_CREDENTIAL)
|
||||
m = load_manifest(str(f))
|
||||
assert m.applications[0].credentials == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Engine — credentials set on new apps only, never on existing or dry-run
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@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 = {"MyOrg": {"id": "org-1", "name": "MyOrg"}}
|
||||
instance.list_apps.return_value = {}
|
||||
instance.list_app_api_access.return_value = []
|
||||
instance.create_app.return_value = {"id": "app-1", "name": "MyApp"}
|
||||
yield instance
|
||||
|
||||
|
||||
def _app(**kwargs):
|
||||
return AppSpec(name="MyApp", org_name="MyOrg", **kwargs)
|
||||
|
||||
|
||||
def test_ext_credential_created_for_new_app(env, mock_client):
|
||||
manifest = Manifest(
|
||||
applications=[_app(credentials=[AppCredential(client_id="ext-id")])]
|
||||
)
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_app_ext_credential.assert_called_once_with("app-1", "ext-id", "")
|
||||
assert len(result.errors) == 0
|
||||
|
||||
|
||||
def test_ext_credential_with_secret(env, mock_client):
|
||||
manifest = Manifest(
|
||||
applications=[_app(credentials=[AppCredential(client_id="ext-id", secret="s3cr3t")])]
|
||||
)
|
||||
normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_app_ext_credential.assert_called_once_with("app-1", "ext-id", "s3cr3t")
|
||||
|
||||
|
||||
def test_multiple_ext_credentials_created(env, mock_client):
|
||||
manifest = Manifest(
|
||||
applications=[_app(credentials=[
|
||||
AppCredential(client_id="client-one"),
|
||||
AppCredential(client_id="client-two"),
|
||||
])]
|
||||
)
|
||||
normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
assert mock_client.create_app_ext_credential.call_count == 2
|
||||
mock_client.create_app_ext_credential.assert_any_call("app-1", "client-one", "")
|
||||
mock_client.create_app_ext_credential.assert_any_call("app-1", "client-two", "")
|
||||
|
||||
|
||||
def test_no_ext_credential_when_credentials_absent(env, mock_client):
|
||||
manifest = Manifest(applications=[_app()])
|
||||
normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_app_ext_credential.assert_not_called()
|
||||
|
||||
|
||||
def test_ext_credential_not_set_on_existing_app(env, mock_client):
|
||||
mock_client.list_apps.return_value = {("MyOrg", "MyApp"): {"id": "app-1", "name": "MyApp"}}
|
||||
manifest = Manifest(
|
||||
applications=[_app(credentials=[AppCredential(client_id="ext-id")])]
|
||||
)
|
||||
normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
mock_client.create_app.assert_not_called()
|
||||
mock_client.create_app_ext_credential.assert_not_called()
|
||||
|
||||
|
||||
def test_ext_credential_not_set_in_dry_run(env, mock_client):
|
||||
manifest = Manifest(
|
||||
applications=[_app(credentials=[AppCredential(client_id="ext-id")])]
|
||||
)
|
||||
normalize_env(env, manifest, dry_run=True)
|
||||
|
||||
mock_client.create_app_ext_credential.assert_not_called()
|
||||
|
||||
|
||||
def test_credential_error_recorded_in_result(env, mock_client):
|
||||
mock_client.create_app_ext_credential.side_effect = RuntimeError("API failure")
|
||||
manifest = Manifest(
|
||||
applications=[_app(credentials=[AppCredential(client_id="bad-id")])]
|
||||
)
|
||||
result = normalize_env(env, manifest, dry_run=False)
|
||||
|
||||
assert any("MyApp" in e for e in result.errors)
|
||||
Reference in New Issue
Block a user