diff --git a/CHANGES.txt b/CHANGES.txt index 3bbdeb6..f4cc1af 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,20 @@ CHANGELOG This file tracks the version history and changes made to the Split/Harness FME Python API Client library. +3.5.8 (December 10, 2025) +------------------------- +Features: +- Added optional org_identifier and project_identifier support for Harness microclients + - These parameters can be set at client initialization or passed to individual method calls + - Parameters are only included in API request URLs when they are set (not None) + - When not provided, they are completely omitted from URLs rather than being passed as empty strings + - Applies to all Harness microclients: harness_project, harness_user, harness_group, + harness_apikey, service_account, token, role, resource_group, and role_assignment + +- Added filterType support to the group endpoint in order to allow fitlering by resourcegroups +- Update the client instantiation to warn when using the legacy bearer auth along with the x-api-key auth + + 3.5.7 (November 26, 2025) ------------------------- Bug Fixes: diff --git a/README.md b/README.md index f556ecd..3d30b6f 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,17 @@ Full documentation on this Python wrapper is available in [this link](https://he ## Using in Harness Mode -Starting with version 3.5.0, the Split API client supports operating in "harness mode" to interact with both Split and Harness APIs. This is required for usage in environments that have been migrated to Harness and want to use the new features. Existing API keys will continue to work with the non-deprecated endpoints after migration, but new Harness Tokens will be required for Harness mode. +Starting with version 3.5.0, the Split API client supports operating in "harness mode" to interact with both Split and Harness APIs. This is required for usage in environments that have been migrated to Harness **and** want to use the new features. Operating not in harness mode will work as it did before with existing Split bearer tokens, but the deprecated endpoints mentioned below will not function. For detailed information about Harness API endpoints, please refer to the [official Harness API documentation](https://apidocs.harness.io/). ### Authentication in Harness Mode -The client supports multiple authentication scenarios: +**Important:** Harness mode requires a `harness_token` (Harness API key). Split API keys (`apikey`) are **not supported** in harness mode. -1. Harness-specific endpoints always use the 'x-api-key' header format -2. Split endpoints will use the 'x-api-key' header when using the harness_token -3. Split endpoints will use the normal 'Authorization' header when using the apikey -4. If both harness_token and apikey are provided, the client will use the harness_token for Harness endpoints and the apikey for Split endpoints +If your account has been migrated to Harness, you should create a new Harness API key to use with this client. The `harness_token` is used for both Split API endpoints and Harness-specific endpoints via the `x-api-key` header. + +For non-migrated accounts that need to use Split API keys with Bearer authentication, use the standard (non-harness) mode instead. ### Base URLs and Endpoints @@ -44,19 +43,20 @@ To use the client in harness mode: ```python from splitapiclient.main import get_client -# Option 1: Use harness_token for Harness endpoints and apikey for Split endpoints +# Basic harness mode setup with required harness_token client = get_client({ 'harness_mode': True, - 'harness_token': 'YOUR_HARNESS_TOKEN', # Used for Harness-specific endpoints - 'apikey': 'YOUR_SPLIT_API_KEY', # Used for existing Split endpoints + 'harness_token': 'YOUR_HARNESS_TOKEN', # Required: Harness API key 'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID' # Required for Harness operations }) -# Option 2: Use harness_token for all operations (if apikey is not provided) +# Include optional org_identifier and project_identifier client = get_client({ 'harness_mode': True, - 'harness_token': 'YOUR_HARNESS_TOKEN', # Used for both Harness and Split endpoints - 'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID' + 'harness_token': 'YOUR_HARNESS_TOKEN', + 'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID', + 'org_identifier': 'YOUR_ORG_ID', # Optional: organization identifier + 'project_identifier': 'YOUR_PROJECT_ID' # Optional: project identifier }) ``` @@ -97,15 +97,30 @@ Basic example: # Account identifier is required for all Harness operations account_id = 'YOUR_ACCOUNT_IDENTIFIER' -# List all tokens +# List all tokens (org_identifier and project_identifier are optional) tokens = client.token.list(account_id) for token in tokens: print(f"Token: {token.name}, ID: {token.id}") -# List service accounts -service_accounts = client.service_account.list(account_id) +# List service accounts with org and project identifiers +org_id = 'YOUR_ORG_IDENTIFIER' +project_id = 'YOUR_PROJECT_IDENTIFIER' +service_accounts = client.service_account.list(account_id, org_identifier=org_id, project_identifier=project_id) for sa in service_accounts: print(f"Service Account: {sa.name}, ID: {sa.id}") + +# If org_identifier and project_identifier are set at client initialization, you can omit them +client = get_client({ + 'harness_mode': True, + 'harness_token': 'YOUR_HARNESS_TOKEN', + 'account_identifier': account_id, + 'org_identifier': org_id, + 'project_identifier': project_id +}) + +# Now you can call methods without specifying identifiers +service_accounts = client.service_account.list() # Uses default identifiers +projects = client.harness_project.list() # Uses default identifiers ``` For most creation, update, and delete endpoints for harness specific resources, you will need to pass through the JSON body directly. @@ -130,24 +145,68 @@ new_sa = client.service_account.create(sa_data, account_id) client.harness_user.add_user_to_groups(user.id, [group.id], account_id) ``` +### Harness Groups + +The `harness_group.list()` method supports an optional `filterType` parameter to filter groups: + +```python +# List all groups (default behavior) +all_groups = client.harness_group.list() + +# List groups with filterType to exclude inherited groups +groups = client.harness_group.list(filterType='EXCLUDE_INHERITED_GROUPS') + +# List groups with filterType and other optional parameters +groups = client.harness_group.list( + account_identifier='YOUR_ACCOUNT_ID', + org_identifier='YOUR_ORG_ID', + project_identifier='YOUR_PROJECT_ID', + filterType='INCLUDE_INHERITED_GROUPS' +) +``` + +**Note:** The `filterType` parameter is optional. When not provided (or set to `None`), it will be omitted from the API request. Valid values depend on the Harness API specification. + For detailed examples and API specifications for each resource, please refer to the [Harness API documentation](https://apidocs.harness.io/). -### Setting Default Account Identifier +### Setting Default Identifiers -To avoid specifying the account identifier with every request: +To avoid specifying identifiers with every request, you can set default values when creating the client: ```python -# Set default account identifier when creating the client +# Set default identifiers when creating the client client = get_client({ 'harness_mode': True, 'harness_token': 'YOUR_HARNESS_TOKEN', - 'account_identifier': 'YOUR_ACCOUNT_IDENTIFIER' + 'account_identifier': 'YOUR_ACCOUNT_IDENTIFIER', # Required + 'org_identifier': 'YOUR_ORG_IDENTIFIER', # Optional + 'project_identifier': 'YOUR_PROJECT_IDENTIFIER' # Optional }) -# Now you can make calls without specifying account_identifier in each request +# Now you can make calls without specifying identifiers in each request tokens = client.token.list() # account_identifier is automatically included -projects = client.harness_project.list() # account_identifier is automatically included +projects = client.harness_project.list() # account_identifier and org_identifier are automatically included (project_identifier is not used for projects endpoint) +``` + +**Note on Optional Identifiers:** +- `account_identifier` is **required** for all Harness operations +- `org_identifier` and `project_identifier` are **optional** and will be omitted from API requests if not provided +- If `org_identifier` or `project_identifier` are not set, they will not appear in the URL at all (not even as empty parameters) +- **Important:** The `harness_project` microclient does **not** support `project_identifier` as a query parameter. The projects endpoint only uses `org_identifier` (and `account_identifier`). Other microclients (service_account, token, role, etc.) do support `project_identifier`. +- You can override default identifiers by passing them as parameters to individual method calls: + +```python +# Override default identifiers for a specific request +# Note: project_identifier is not used for harness_project endpoints +projects = client.harness_project.list( + account_identifier='DIFFERENT_ACCOUNT_ID', + org_identifier='DIFFERENT_ORG_ID' +) + +# Use default identifiers but override only org_identifier +# Note: project_identifier is not used for harness_project endpoints +projects = client.harness_project.list(org_identifier='DIFFERENT_ORG_ID') ``` ## Quick Setup diff --git a/pyproject.toml b/pyproject.toml index 3e716fb..1de603e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "splitapiclient" -version = "3.5.7" +version = "3.5.8" license = "Apache-2.0" description = "This Python Library provides full support for Split REST Admin API, allow creating, deleting and editing Environments, Splits, Split Definitions, Segments, Segment Keys, Users, Groups, API Keys, Change Requests, Attributes and Identities" classifiers = [ diff --git a/splitapiclient/main/harness_apiclient.py b/splitapiclient/main/harness_apiclient.py index 24250f6..0f5adab 100644 --- a/splitapiclient/main/harness_apiclient.py +++ b/splitapiclient/main/harness_apiclient.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import warnings from splitapiclient.main.apiclient import BaseApiClient from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.util.exceptions import InsufficientConfigArgumentsException @@ -45,14 +46,18 @@ def __init__(self, config): Class constructor. :param config: Dictionary containing options required to instantiate - the API client. Should have AT LEAST one of the following keys: - - 'apikey': Split API key for authentication with Split endpoints - - 'harness_token': Harness authentication token used for x-api-key header with Harness endpoints - If harness_token is not provided, apikey will be used for all operations + the API client. Required keys: + - 'harness_token': Harness authentication token used for x-api-key header + This token is used for both Split API and Harness API endpoints. + Note: Split API keys (apikey) are not supported in harness mode. + Migrated accounts should create new Harness API keys. + Optional keys: - 'base_url': Base url where the Split API is hosted (optional, defaults to Split URL) - 'base_url_v3': Base url where the Split API v3 is hosted (optional, defaults to Split URL) - 'harness_base_url': Base url where the Harness API is hosted (optional, defaults to Harness URL) - 'account_identifier': Harness account identifier to use for all Harness operations (optional) + - 'org_identifier': Harness organization identifier to use for all Harness operations (optional) + - 'project_identifier': Harness project identifier to use for all Harness operations (optional) ''' # Set up Split API base URLs for existing endpoints if 'base_url' in config: @@ -72,30 +77,28 @@ def __init__(self, config): else: self._harness_base_url = self.BASE_HARNESS_URL - # Check if at least one authentication method is provided - if 'apikey' not in config and 'harness_token' not in config: + # Require harness_token in harness mode + if 'harness_token' not in config: raise InsufficientConfigArgumentsException( - 'At least one of the following keys must be present in the config dict for harness mode: apikey, harness_token' + 'harness_token is required in harness mode. Split API keys (apikey) are not supported by this client. ' + 'Please create a Harness API key for your migrated account and use the harness_token for authentication.' ) - # Set up authentication tokens - self._apikey = config.get('apikey') - self._harness_token = config.get('harness_token') + # Use harness_token for all operations + self._harness_token = config['harness_token'] + auth_token = self._harness_token - # If harness_token is not provided, use apikey for all operations - # If apikey is not provided, use harness_token for all operations - split_auth_token = self._apikey if self._apikey else self._harness_token - harness_auth_token = self._harness_token if self._harness_token else self._apikey - - # Store the account identifier + # Store the account identifier, org identifier, and project identifier self._account_identifier = config.get('account_identifier') + self._org_identifier = config.get('org_identifier') + self._project_identifier = config.get('project_identifier') - # Create HTTP clients for Split endpoints - split_http_client = HarnessHttpClient(self._base_url, split_auth_token) - split_http_clientv3 = HarnessHttpClient(self._base_url_v3, split_auth_token) + # Create HTTP clients - use same token for both Split and Harness endpoints + split_http_client = HarnessHttpClient(self._base_url, auth_token) + split_http_clientv3 = HarnessHttpClient(self._base_url_v3, auth_token) # Create HTTP client for Harness endpoints - harness_http_client = HarnessHttpClient(self._harness_base_url, harness_auth_token) + harness_http_client = HarnessHttpClient(self._harness_base_url, auth_token) # Standard microclients using Split endpoints self._environment_client = EnvironmentMicroClient(split_http_client) @@ -114,15 +117,15 @@ def __init__(self, config): self._flag_set_client = FlagSetMicroClient(split_http_clientv3) # Harness-specific microclients using Harness endpoints - self._token_client = TokenMicroClient(harness_http_client, self._account_identifier) - self._harness_apikey_client = HarnessApiKeyMicroClient(harness_http_client, self._account_identifier) - self._service_account_client = ServiceAccountMicroClient(harness_http_client, self._account_identifier) - self._harness_user_client = HarnessUserMicroClient(harness_http_client, self._account_identifier) - self._harness_group_client = HarnessGroupMicroClient(harness_http_client, self._account_identifier) - self._role_client = RoleMicroClient(harness_http_client, self._account_identifier) - self._resource_group_client = ResourceGroupMicroClient(harness_http_client, self._account_identifier) - self._role_assignment_client = RoleAssignmentMicroClient(harness_http_client, self._account_identifier) - self._harness_project_client = HarnessProjectMicroClient(harness_http_client, self._account_identifier) + self._token_client = TokenMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._harness_apikey_client = HarnessApiKeyMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._service_account_client = ServiceAccountMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._harness_user_client = HarnessUserMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._harness_group_client = HarnessGroupMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._role_client = RoleMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._resource_group_client = ResourceGroupMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._role_assignment_client = RoleAssignmentMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) + self._harness_project_client = HarnessProjectMicroClient(harness_http_client, self._account_identifier, self._org_identifier, self._project_identifier) @property def traffic_types(self): diff --git a/splitapiclient/microclients/harness/harness_apikey_microclient.py b/splitapiclient/microclients/harness/harness_apikey_microclient.py index db5edfe..a29e770 100644 --- a/splitapiclient/microclients/harness/harness_apikey_microclient.py +++ b/splitapiclient/microclients/harness/harness_apikey_microclient.py @@ -14,7 +14,7 @@ class HarnessApiKeyMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/ng/api/apikey?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', + 'url_template': '/ng/api/apikey?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class HarnessApiKeyMicroClient: }, 'get_apikey': { 'method': 'GET', - 'url_template': '/ng/api/apikey/aggregate/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', + 'url_template': '/ng/api/apikey/aggregate/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class HarnessApiKeyMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/ng/api/apikey?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/apikey?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class HarnessApiKeyMicroClient: }, 'add_permissions': { 'method': 'POST', - 'url_template': '/ng/api/roleassignments?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/roleassignments?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class HarnessApiKeyMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/ng/api/apikey/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', + 'url_template': '/ng/api/apikey/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,34 +69,57 @@ class HarnessApiKeyMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, parent_identifier=None, account_identifier=None): + def list(self, parent_identifier=None, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of HarnessApiKey objects. :param parent_identifier: Parent identifier for the API keys :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of HarnessApiKey objects :rtype: list(HarnessApiKey) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + try: + request_kwargs = { + 'accountIdentifier': account_id, + 'parentIdentifier': parent_identifier or "" + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - accountIdentifier=account_id, - parentIdentifier=parent_identifier or "" + endpoint, + **request_kwargs ) LOGGER.debug('Response: %s', response) return [HarnessApiKey(item, self._http_client) for item in response.get('data', [])] @@ -104,13 +127,15 @@ def list(self, parent_identifier=None, account_identifier=None): LOGGER.error(f"HTTP error fetching API keys: {str(e)}") return [] # Return empty list on HTTP error - def get(self, apikey_id, parent_identifier=None, account_identifier=None): + def get(self, apikey_id, parent_identifier=None, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific API key by ID :param apikey_id: ID of the API key to retrieve :param parent_identifier: Parent identifier for the API key :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: HarnessApiKey object :rtype: HarnessApiKey ''' @@ -118,24 +143,43 @@ def get(self, apikey_id, parent_identifier=None, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get_apikey'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'apiKeyIdentifier': apikey_id, + 'accountIdentifier': account_id, + 'parentIdentifier': parent_identifier or "" + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['get_apikey'], - apiKeyIdentifier=apikey_id, - accountIdentifier=account_id, - parentIdentifier=parent_identifier or "" + endpoint, + **request_kwargs ) LOGGER.debug('Response: %s', response) if(response.get('data').get('apiKey')): return HarnessApiKey(response.get('data').get('apiKey'), self._http_client) return None - def create(self, apikey_data, account_identifier=None): + def create(self, apikey_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new API key :param apikey_data: Dictionary containing API key data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created API key :rtype: HarnessApiKey ''' @@ -143,11 +187,28 @@ def create(self, apikey_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': apikey_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=apikey_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) LOGGER.debug('Response: %s', response) if(response.get('data')): @@ -155,13 +216,15 @@ def create(self, apikey_data, account_identifier=None): return None - def add_permissions(self, apikey_id, permissions, account_identifier=None): + def add_permissions(self, apikey_id, permissions, account_identifier=None, org_identifier=None, project_identifier=None): ''' Add permissions to an API key :param apikey_id: ID of the API key to add permissions to :param permissions: List of permissions to add as a role assignment :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -169,23 +232,42 @@ def add_permissions(self, apikey_id, permissions, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['add_permissions'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': permissions, + 'apiKeyIdentifier': apikey_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['add_permissions'], - body=permissions, - apiKeyIdentifier=apikey_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) LOGGER.debug('Response: %s', response) return True - def delete(self, apikey_id, parent_identifier=None, account_identifier=None): + def delete(self, apikey_id, parent_identifier=None, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete an API key :param apikey_id: ID of the API key to delete :param parent_identifier: Parent identifier for the API key :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -193,11 +275,28 @@ def delete(self, apikey_id, parent_identifier=None, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'apiKeyIdentifier': apikey_id, + 'accountIdentifier': account_id, + 'parentIdentifier': parent_identifier or "" + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete'], - apiKeyIdentifier=apikey_id, - accountIdentifier=account_id, - parentIdentifier=parent_identifier or "" + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/microclients/harness/harness_group_microclient.py b/splitapiclient/microclients/harness/harness_group_microclient.py index 29a7e4a..582f4ea 100644 --- a/splitapiclient/microclients/harness/harness_group_microclient.py +++ b/splitapiclient/microclients/harness/harness_group_microclient.py @@ -14,7 +14,7 @@ class HarnessGroupMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&pageIndex={pageIndex}&pageSize=100&filterType={filterType}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class HarnessGroupMicroClient: }, 'get_group': { 'method': 'GET', - 'url_template': '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class HarnessGroupMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class HarnessGroupMicroClient: }, 'update': { 'method': 'PATCH', - 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class HarnessGroupMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,36 +69,64 @@ class HarnessGroupMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None, filterType=None): ''' Returns a list of HarnessGroup objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default + :param filterType: Filter type to use for filtering groups :returns: list of HarnessGroup objects :rtype: list(HarnessGroup) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: try: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + if filterType is None: + endpoint['url_template'] = endpoint['url_template'].replace('&filterType={filterType}', '') + + request_kwargs = { + 'pageIndex': page_index, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + if filterType is not None: + request_kwargs['filterType'] = filterType + response = self._http_client.make_request( - self._endpoint['all_items'], - pageIndex=page_index, - accountIdentifier=account_id + endpoint, + **request_kwargs ) content = response.get('data', {}).get('content', []) if not content: @@ -112,82 +140,158 @@ def list(self, account_identifier=None): return [HarnessGroup(item, self._http_client) for item in final_list] - def get(self, group_identifier, account_identifier=None): + def get(self, group_identifier, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific group by ID :param group_identifier: ID of the group to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: HarnessGroup object :rtype: HarnessGroup ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get_group'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'groupIdentifier': group_identifier, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['get_group'], - groupIdentifier=group_identifier, - accountIdentifier=account_id + endpoint, + **request_kwargs ) - return HarnessGroup(response, self._http_client) + return HarnessGroup(response['data'], self._http_client) - def create(self, group_data, account_identifier=None): + def create(self, group_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new group :param group_data: Dictionary containing group data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created group :rtype: HarnessGroup ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': group_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=group_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return HarnessGroup(response['data'], self._http_client) - def update(self, update_data, account_identifier=None): + def update(self, update_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Update a group :param update_data: Dictionary containing update data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: updated group :rtype: HarnessGroup ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': update_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['update'], - body=update_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return HarnessGroup(response, self._http_client) - def delete(self, group_identifier, account_identifier=None): + def delete(self, group_identifier, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a group :param group_identifier: ID of the group to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'groupIdentifier': group_identifier, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete'], - groupIdentifier=group_identifier, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/microclients/harness/harness_project_microclient.py b/splitapiclient/microclients/harness/harness_project_microclient.py index 04fe326..865c633 100644 --- a/splitapiclient/microclients/harness/harness_project_microclient.py +++ b/splitapiclient/microclients/harness/harness_project_microclient.py @@ -14,7 +14,7 @@ class HarnessProjectMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/ng/api/projects?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=50', + 'url_template': '/ng/api/projects?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&pageIndex={pageIndex}&pageSize=50', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class HarnessProjectMicroClient: }, 'get': { 'method': 'GET', - 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class HarnessProjectMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/ng/api/projects?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/projects?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class HarnessProjectMicroClient: }, 'update': { 'method': 'PUT', - 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class HarnessProjectMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,27 +69,33 @@ class HarnessProjectMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None): ''' Returns a list of HarnessProject objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default :returns: list of HarnessProject objects :rtype: list(HarnessProject) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier page_index = 0 final_list = [] @@ -97,11 +103,21 @@ def list(self, account_identifier=None): total_projects_seen = 0 while True: try: - + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + + request_kwargs = { + 'pageIndex': page_index, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + response = self._http_client.make_request( - self._endpoint['all_items'], - pageIndex=page_index, - accountIdentifier=account_id + endpoint, + **request_kwargs ) data = response.get('data', {}) @@ -149,84 +165,136 @@ def list(self, account_identifier=None): return [HarnessProject(item, self._http_client) for item in final_list] - def get(self, project_identifier, account_identifier=None): + def get(self, project_identifier, account_identifier=None, org_identifier=None): ''' Get a specific project by ID - :param project_identifier: ID of the project to retrieve + :param project_identifier: ID of the project to retrieve (path parameter) :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default :returns: HarnessProject object :rtype: HarnessProject ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + + request_kwargs = { + 'projectIdentifier': project_identifier, # Path parameter - replaces {projectIdentifier} in path + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id response = self._http_client.make_request( - self._endpoint['get'], - projectIdentifier=project_identifier, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return HarnessProject(response.get('data', {}).get('project', {}), self._http_client) - def create(self, project_data, account_identifier=None): + def create(self, project_data, account_identifier=None, org_identifier=None): ''' Create a new project :param project_data: Dictionary containing project data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default :returns: newly created project :rtype: HarnessProject ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + + request_kwargs = { + 'body': project_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id response = self._http_client.make_request( - self._endpoint['create'], - body=project_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return HarnessProject(response.get('data', {}).get('project', {}), self._http_client) - def update(self, project_identifier, project_data, account_identifier=None): + def update(self, project_identifier, project_data, account_identifier=None, org_identifier=None): ''' Update an existing project - :param project_identifier: ID of the project to update + :param project_identifier: ID of the project to update (path parameter) :param project_data: Dictionary containing updated project data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default :returns: updated project :rtype: HarnessProject ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + + request_kwargs = { + 'projectIdentifier': project_identifier, # Path parameter - replaces {projectIdentifier} in path + 'accountIdentifier': account_id, + 'body': project_data + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id response = self._http_client.make_request( - self._endpoint['update'], - projectIdentifier=project_identifier, - accountIdentifier=account_id, - body=project_data + endpoint, + **request_kwargs ) return HarnessProject(response.get('data', {}).get('project', {}), self._http_client) - def delete(self, project_identifier, account_identifier=None): + def delete(self, project_identifier, account_identifier=None, org_identifier=None): ''' Delete a project - :param project_identifier: ID of the project to delete + :param project_identifier: ID of the project to delete (path parameter) :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + + request_kwargs = { + 'projectIdentifier': project_identifier, # Path parameter - replaces {projectIdentifier} in path + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id self._http_client.make_request( - self._endpoint['delete'], - projectIdentifier=project_identifier, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/microclients/harness/harness_user_microclient.py b/splitapiclient/microclients/harness/harness_user_microclient.py index 8be6dbf..2d2190f 100644 --- a/splitapiclient/microclients/harness/harness_user_microclient.py +++ b/splitapiclient/microclients/harness/harness_user_microclient.py @@ -14,7 +14,7 @@ class HarnessUserMicroClient: _endpoint = { 'all_items': { 'method': 'POST', # yes this is really a post for getting users - 'url_template': '/ng/api/user/aggregate?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}', + 'url_template': '/ng/api/user/aggregate?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&pageIndex={pageIndex}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class HarnessUserMicroClient: }, 'get_user': { 'method': 'GET', - 'url_template': '/ng/api/user/aggregate/{userId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user/aggregate/{userId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class HarnessUserMicroClient: }, 'invite': { 'method': 'POST', - 'url_template': '/ng/api/user/users?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user/users?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class HarnessUserMicroClient: }, 'update': { 'method': 'PUT', - 'url_template': '/ng/api/user/{userId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user/{userId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class HarnessUserMicroClient: }, 'add_user_to_groups': { 'method': 'PUT', - 'url_template': '/ng/api/user/add-user-to-groups/{userId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/user/add-user-to-groups/{userId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,7 +69,7 @@ class HarnessUserMicroClient: }, 'delete_pending': { 'method': 'DELETE', - 'url_template': '/ng/api/invites/{inviteId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/invites/{inviteId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -80,7 +80,7 @@ class HarnessUserMicroClient: }, 'list_pending': { 'method': 'POST', # yes this is also really a POST - 'url_template': '/ng/api/invites/aggregate?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/invites/aggregate?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -91,36 +91,59 @@ class HarnessUserMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of HarnessUser objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of HarnessUser objects :rtype: list(HarnessUser) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: try: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'pageIndex': page_index, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - pageIndex=page_index, - accountIdentifier=account_id + endpoint, + **request_kwargs ) content = response.get('data', {}).get('content', []) if not content: @@ -134,12 +157,14 @@ def list(self, account_identifier=None): return final_list - def get(self, user_id, account_identifier=None): + def get(self, user_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific user by ID :param user_id: ID of the user to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: HarnessUser object :rtype: HarnessUser ''' @@ -147,20 +172,39 @@ def get(self, user_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get_user'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'userId': user_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['get_user'], - userId=user_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) - return HarnessUser(response, self._http_client) + return HarnessUser(response['data']['user'], self._http_client) - def invite(self, user_data, account_identifier=None): + def invite(self, user_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Invite a new user :param user_data: Dictionary containing user data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly invited user :rtype: HarnessUser ''' @@ -168,21 +212,40 @@ def invite(self, user_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['invite'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': user_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['invite'], - body=user_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True - def update(self, user_id, update_data, account_identifier=None): + def update(self, user_id, update_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Update a user :param user_id: ID of the user to update :param update_data: Dictionary containing update data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: updated user :rtype: HarnessUser ''' @@ -190,22 +253,41 @@ def update(self, user_id, update_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': update_data, + 'userId': user_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['update'], - body=update_data, - userId=user_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return HarnessUser(response.get('data', {}), self._http_client) - def add_user_to_groups(self, user_id, group_ids, account_identifier=None): + def add_user_to_groups(self, user_id, group_ids, account_identifier=None, org_identifier=None, project_identifier=None): ''' Add a user to groups :param user_id: ID of the user to add to groups :param group_ids: List of group IDs to add the user to :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -213,21 +295,40 @@ def add_user_to_groups(self, user_id, group_ids, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['add_user_to_groups'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': {"userGroupIdsToAdd": group_ids}, + 'userId': user_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['add_user_to_groups'], - body={"userGroupIdsToAdd": group_ids}, - userId=user_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True - def delete_pending(self, invite_id, account_identifier=None): + def delete_pending(self, invite_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a pending invite :param invite_id: ID of the invite to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -235,19 +336,38 @@ def delete_pending(self, invite_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete_pending'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'inviteId': invite_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete_pending'], - inviteId=invite_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True - def list_pending(self, account_identifier=None): + def list_pending(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of pending users. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of pending users :rtype: list(HarnessUser) ''' @@ -255,14 +375,31 @@ def list_pending(self, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['list_pending'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'pageIndex': page_index, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['list_pending'], - pageIndex=page_index, - accountIdentifier=account_id + endpoint, + **request_kwargs ) content = response.get('data', {}).get('content', []) if not content: diff --git a/splitapiclient/microclients/harness/resource_group_microclient.py b/splitapiclient/microclients/harness/resource_group_microclient.py index ce7be23..25e0d01 100644 --- a/splitapiclient/microclients/harness/resource_group_microclient.py +++ b/splitapiclient/microclients/harness/resource_group_microclient.py @@ -14,7 +14,7 @@ class ResourceGroupMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/resourcegroup/api/v2/resourceGroup?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'url_template': '/resourcegroup/api/v2/resourcegroup?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&pageIndex={pageIndex}&pageSize=100', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class ResourceGroupMicroClient: }, 'get_resource_group': { 'method': 'GET', - 'url_template': '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}?accountIdentifier={accountIdentifier}', + 'url_template': '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class ResourceGroupMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/resourcegroup/api/v2/resourceGroup?accountIdentifier={accountIdentifier}', + 'url_template': '/resourcegroup/api/v2/resourcegroup?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class ResourceGroupMicroClient: }, 'update': { 'method': 'PUT', - 'url_template': '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}?accountIdentifier={accountIdentifier}', + 'url_template': '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class ResourceGroupMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}?accountIdentifier={accountIdentifier}', + 'url_template': '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,36 +69,59 @@ class ResourceGroupMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of ResourceGroup objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of ResourceGroup objects :rtype: list(ResourceGroup) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: try: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'accountIdentifier': account_id, + 'pageIndex': page_index + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - accountIdentifier=account_id, - pageIndex=page_index + endpoint, + **request_kwargs ) data = response.get('data', {}) content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] @@ -117,12 +140,14 @@ def list(self, account_identifier=None): return [ResourceGroup(item, self._http_client) for item in final_list] - def get(self, resource_group_id, account_identifier=None): + def get(self, resource_group_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific resource group by ID :param resource_group_id: ID of the resource group to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: ResourceGroup object :rtype: ResourceGroup ''' @@ -130,20 +155,39 @@ def get(self, resource_group_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get_resource_group'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'resourceGroupId': resource_group_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['get_resource_group'], - resourceGroupId=resource_group_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return ResourceGroup(response.get('data', {}).get('resourceGroup', {}), self._http_client) - def create(self, resource_group_data, account_identifier=None): + def create(self, resource_group_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new resource group :param resource_group_data: Dictionary containing resource group data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created resource group :rtype: ResourceGroup ''' @@ -151,22 +195,41 @@ def create(self, resource_group_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': resource_group_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=resource_group_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) resourceGroup = response.get('data', {}).get('resourceGroup', {}) return ResourceGroup(resourceGroup, self._http_client) - def update(self, resource_group_id, update_data, account_identifier=None): + def update(self, resource_group_id, update_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Update a resource group :param resource_group_id: ID of the resource group to update :param update_data: Dictionary containing update data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: updated resource group :rtype: ResourceGroup ''' @@ -174,22 +237,41 @@ def update(self, resource_group_id, update_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': update_data, + 'resourceGroupId': resource_group_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['update'], - body=update_data, - resourceGroupId=resource_group_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) resourceGroup = response.get('data', {}).get('resourceGroup', {}) return ResourceGroup(resourceGroup, self._http_client) - def delete(self, resource_group_id, account_identifier=None): + def delete(self, resource_group_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a resource group :param resource_group_id: ID of the resource group to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -197,10 +279,27 @@ def delete(self, resource_group_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'resourceGroupId': resource_group_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete'], - resourceGroupId=resource_group_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/microclients/harness/role_assignment_microclient.py b/splitapiclient/microclients/harness/role_assignment_microclient.py index 2e6bee9..8ed7f5c 100644 --- a/splitapiclient/microclients/harness/role_assignment_microclient.py +++ b/splitapiclient/microclients/harness/role_assignment_microclient.py @@ -14,7 +14,7 @@ class RoleAssignmentMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/authz/api/roleAssignments?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'url_template': '/authz/api/roleassignments?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&pageIndex={pageIndex}&pageSize=100', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class RoleAssignmentMicroClient: }, 'get_role_assignment': { 'method': 'GET', - 'url_template': '/authz/api/roleAssignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}', + 'url_template': '/authz/api/roleassignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class RoleAssignmentMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/authz/api/roleAssignments?accountIdentifier={accountIdentifier}', + 'url_template': '/authz/api/roleassignments?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class RoleAssignmentMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/authz/api/roleAssignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}', + 'url_template': '/authz/api/roleassignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,35 +58,58 @@ class RoleAssignmentMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of RoleAssignment objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of RoleAssignment objects :rtype: list(RoleAssignment) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: try: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'accountIdentifier': account_id, + 'pageIndex': page_index + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - accountIdentifier=account_id, - pageIndex=page_index + endpoint, + **request_kwargs ) data = response.get('data', {}) content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] @@ -105,12 +128,14 @@ def list(self, account_identifier=None): return [RoleAssignment(item, self._http_client) for item in final_list] - def get(self, role_assignment_id, account_identifier=None): + def get(self, role_assignment_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific role assignment by ID :param role_assignment_id: ID of the role assignment to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: RoleAssignment object :rtype: RoleAssignment ''' @@ -118,20 +143,39 @@ def get(self, role_assignment_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get_role_assignment'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'roleAssignmentId': role_assignment_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['get_role_assignment'], - roleAssignmentId=role_assignment_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return RoleAssignment(response.get('data', {}).get('roleAssignment', {}), self._http_client) - def create(self, role_assignment_data, account_identifier=None): + def create(self, role_assignment_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new role assignment :param role_assignment_data: Dictionary containing role assignment data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created role assignment :rtype: RoleAssignment ''' @@ -139,21 +183,40 @@ def create(self, role_assignment_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': role_assignment_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=role_assignment_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return RoleAssignment(response.get('data', {}).get('roleAssignment', {}), self._http_client) - def delete(self, role_assignment_id, account_identifier=None): + def delete(self, role_assignment_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a role assignment :param role_assignment_id: ID of the role assignment to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -161,10 +224,27 @@ def delete(self, role_assignment_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'roleAssignmentId': role_assignment_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete'], - roleAssignmentId=role_assignment_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/microclients/harness/role_microclient.py b/splitapiclient/microclients/harness/role_microclient.py index 92a11a9..7825014 100644 --- a/splitapiclient/microclients/harness/role_microclient.py +++ b/splitapiclient/microclients/harness/role_microclient.py @@ -14,7 +14,7 @@ class RoleMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/authz/api/roles?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'url_template': '/authz/api/roles?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&pageIndex={pageIndex}&pageSize=100', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class RoleMicroClient: }, 'get_role': { 'method': 'GET', - 'url_template': '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}', + 'url_template': '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class RoleMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/authz/api/roles?accountIdentifier={accountIdentifier}', + 'url_template': '/authz/api/roles?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class RoleMicroClient: }, 'update': { 'method': 'PUT', - 'url_template': '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}', + 'url_template': '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class RoleMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': 'roles/{roleId}?accountIdentifier={accountIdentifier}', + 'url_template': 'roles/{roleId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,36 +69,59 @@ class RoleMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of Role objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of Role objects :rtype: list(Role) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: try: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'accountIdentifier': account_id, + 'pageIndex': page_index + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - accountIdentifier=account_id, - pageIndex=page_index + endpoint, + **request_kwargs ) data = response.get('data', {}) content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] @@ -117,12 +140,14 @@ def list(self, account_identifier=None): return [Role(item, self._http_client) for item in final_list] - def get(self, role_id, account_identifier=None): + def get(self, role_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific role by ID :param role_id: ID of the role to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: Role object :rtype: Role ''' @@ -130,20 +155,39 @@ def get(self, role_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['get_role'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'roleId': role_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['get_role'], - roleId=role_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return Role(response.get('data', {}).get('role', {}), self._http_client) - def create(self, role_data, account_identifier=None): + def create(self, role_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new role :param role_data: Dictionary containing role data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created role :rtype: Role ''' @@ -151,21 +195,40 @@ def create(self, role_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': role_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=role_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return Role(response.get('data', {}).get('role', {}), self._http_client) - def update(self, role_id, update_data, account_identifier=None): + def update(self, role_id, update_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Update a role :param role_id: ID of the role to update :param update_data: Dictionary containing update data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: updated role :rtype: Role ''' @@ -173,21 +236,40 @@ def update(self, role_id, update_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': update_data, + 'roleId': role_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['update'], - body=update_data, - roleId=role_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return Role(response.get('data', {}).get('role', {}), self._http_client) - def delete(self, role_id, account_identifier=None): + def delete(self, role_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a role :param role_id: ID of the role to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -195,10 +277,27 @@ def delete(self, role_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'roleId': role_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete'], - roleId=role_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/microclients/harness/service_account_microclient.py b/splitapiclient/microclients/harness/service_account_microclient.py index f5b07b9..b4e3490 100644 --- a/splitapiclient/microclients/harness/service_account_microclient.py +++ b/splitapiclient/microclients/harness/service_account_microclient.py @@ -14,7 +14,7 @@ class ServiceAccountMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class ServiceAccountMicroClient: }, 'item': { 'method': 'GET', - 'url_template': '/ng/api/serviceaccount/aggregate/{serviceAccountId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/serviceaccount/aggregate/{serviceAccountId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class ServiceAccountMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class ServiceAccountMicroClient: }, 'update': { 'method': 'PUT', - 'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class ServiceAccountMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,32 +69,55 @@ class ServiceAccountMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of ServiceAccount objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of ServiceAccount objects :rtype: list(ServiceAccount) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + try: + request_kwargs = { + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - accountIdentifier=account_id, + endpoint, + **request_kwargs ) data = response.get('data', []) return [ServiceAccount(item, self._http_client) for item in data] @@ -102,12 +125,14 @@ def list(self, account_identifier=None): LOGGER.error(f"HTTP error fetching service accounts: {str(e)}") return [] # Return empty list on HTTP error - def get(self, service_account_id, account_identifier=None): + def get(self, service_account_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific service account by ID :param service_account_id: ID of the service account to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: ServiceAccount object :rtype: ServiceAccount ''' @@ -115,11 +140,28 @@ def get(self, service_account_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['item'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'serviceAccountId': service_account_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['item'], - serviceAccountId=service_account_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) # Handle different response formats @@ -128,12 +170,14 @@ def get(self, service_account_id, account_identifier=None): return ServiceAccount(data['serviceAccount'], self._http_client) - def create(self, service_account_data, account_identifier=None): + def create(self, service_account_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new service account :param service_account_data: Dictionary containing service account data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created service account :rtype: ServiceAccount ''' @@ -141,22 +185,41 @@ def create(self, service_account_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': service_account_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=service_account_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return ServiceAccount(response.get('data', {}), self._http_client) - def update(self, service_account_id, update_data, account_identifier=None): + def update(self, service_account_id, update_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Update a service account :param service_account_id: ID of the service account to update :param update_data: Dictionary containing update data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: updated service account :rtype: ServiceAccount ''' @@ -164,22 +227,41 @@ def update(self, service_account_id, update_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': update_data, + 'serviceAccountId': service_account_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['update'], - body=update_data, - serviceAccountId=service_account_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return ServiceAccount(response.get('data', {}), self._http_client) - def delete(self, service_account_id, account_identifier=None): + def delete(self, service_account_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a service account :param service_account_id: ID of the service account to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -187,11 +269,28 @@ def delete(self, service_account_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'serviceAccountId': service_account_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['delete'], - serviceAccountId=service_account_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) # For test compatibility, return the raw response diff --git a/splitapiclient/microclients/harness/token_microclient.py b/splitapiclient/microclients/harness/token_microclient.py index 3332598..f996ff8 100644 --- a/splitapiclient/microclients/harness/token_microclient.py +++ b/splitapiclient/microclients/harness/token_microclient.py @@ -14,7 +14,7 @@ class TokenMicroClient: _endpoint = { 'all_items': { 'method': 'GET', - 'url_template': '/ng/api/token/aggregate?apiKeyType=SERVICE_ACCOUNT&accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'url_template': '/ng/api/token/aggregate?apiKeyType=SERVICE_ACCOUNT&accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&pageIndex={pageIndex}&pageSize=100', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -25,7 +25,7 @@ class TokenMicroClient: }, 'create': { 'method': 'POST', - 'url_template': '/ng/api/token?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/token?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -36,7 +36,7 @@ class TokenMicroClient: }, 'rotate_token': { 'method': 'POST', - 'url_template': '/ng/api/token/rotate/{tokenId}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}&apiKeyIdentifier={apiKeyIdentifier}', + 'url_template': '/ng/api/token/rotate/{tokenId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}&apiKeyIdentifier={apiKeyIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -47,7 +47,7 @@ class TokenMicroClient: }, 'update_token': { 'method': 'PUT', - 'url_template': '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -58,7 +58,7 @@ class TokenMicroClient: }, 'delete': { 'method': 'DELETE', - 'url_template': '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}', + 'url_template': '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&projectIdentifier={projectIdentifier}', 'headers': [{ 'name': 'x-api-key', 'template': '{value}', @@ -69,36 +69,59 @@ class TokenMicroClient: }, } - def __init__(self, http_client, account_identifier=None): + def __init__(self, http_client, account_identifier=None, org_identifier=None, project_identifier=None): ''' Constructor :param http_client: HTTP client to use for requests :param account_identifier: Default account identifier to use for all requests + :param org_identifier: Default organization identifier to use for all requests + :param project_identifier: Default project identifier to use for all requests ''' self._http_client = http_client self._account_identifier = account_identifier + self._org_identifier = org_identifier + self._project_identifier = project_identifier - def list(self, account_identifier=None): + def list(self, account_identifier=None, org_identifier=None, project_identifier=None): ''' Returns a list of Token objects. :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: list of Token objects :rtype: list(Token) ''' account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier page_index = 0 final_list = [] while True: try: + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['all_items'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'accountIdentifier': account_id, + 'pageIndex': page_index + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id + response = self._http_client.make_request( - self._endpoint['all_items'], - accountIdentifier=account_id, - pageIndex=page_index + endpoint, + **request_kwargs ) data = response.get('data', {}) @@ -118,12 +141,14 @@ def list(self, account_identifier=None): return [Token(item, self._http_client) for item in final_list] - def get(self, token_id, account_identifier=None): + def get(self, token_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Get a specific token by ID :param token_id: ID of the token to retrieve :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: Token object :rtype: Token ''' @@ -131,17 +156,21 @@ def get(self, token_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier - tokens = self.list(account_identifier=account_id) + tokens = self.list(account_identifier=account_id, org_identifier=org_id, project_identifier=project_id) # Since tokens is already a list of Token objects, we need to check the _identifier attribute return next((token for token in tokens if token._identifier == token_id), None) - def create(self, token_data, account_identifier=None): + def create(self, token_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Create a new token :param token_data: Dictionary containing token data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: newly created token :rtype: Token ''' @@ -149,21 +178,40 @@ def create(self, token_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['create'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': token_data, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['create'], - body=token_data, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return response.get('data', "") - def update(self, token_id, update_data, account_identifier=None): + def update(self, token_id, update_data, account_identifier=None, org_identifier=None, project_identifier=None): ''' Update a token :param token_id: ID of the token to update :param update_data: Dictionary containing update data :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: updated token :rtype: Token ''' @@ -171,17 +219,34 @@ def update(self, token_id, update_data, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['update_token'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'body': update_data, + 'tokenId': token_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['update_token'], - body=update_data, - tokenId=token_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return Token(response.get('data', {}), self._http_client) - def rotate(self, token_id, parent_identifier, api_key_identifier, account_identifier=None): + def rotate(self, token_id, parent_identifier, api_key_identifier, account_identifier=None, org_identifier=None, project_identifier=None): ''' Rotate a token @@ -189,6 +254,8 @@ def rotate(self, token_id, parent_identifier, api_key_identifier, account_identi :param parent_identifier: Parent identifier for the token :param api_key_identifier: API key identifier for the token :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: rotated token :rtype: string ''' @@ -196,22 +263,41 @@ def rotate(self, token_id, parent_identifier, api_key_identifier, account_identi account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['rotate_token'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'tokenId': token_id, + 'parentIdentifier': parent_identifier, + 'apiKeyIdentifier': api_key_identifier, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id response = self._http_client.make_request( - self._endpoint['rotate_token'], - tokenId=token_id, - parentIdentifier=parent_identifier, - apiKeyIdentifier=api_key_identifier, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return response.get('data', "") - def delete(self, token_id, account_identifier=None): + def delete(self, token_id, account_identifier=None, org_identifier=None, project_identifier=None): ''' Delete a token :param token_id: ID of the token to delete :param account_identifier: Account identifier to use for this request, overrides the default + :param org_identifier: Organization identifier to use for this request, overrides the default + :param project_identifier: Project identifier to use for this request, overrides the default :returns: True if successful :rtype: bool ''' @@ -219,10 +305,27 @@ def delete(self, token_id, account_identifier=None): account_id = account_identifier if account_identifier is not None else self._account_identifier if account_id is None: raise ValueError("account_identifier must be provided either at client initialization or method call") + org_id = org_identifier if org_identifier is not None else self._org_identifier + project_id = project_identifier if project_identifier is not None else self._project_identifier + + # Conditionally modify endpoint URL template to omit optional parameters if not provided + endpoint = self._endpoint['delete'].copy() + if org_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&orgIdentifier={orgIdentifier}', '') + if project_id is None: + endpoint['url_template'] = endpoint['url_template'].replace('&projectIdentifier={projectIdentifier}', '') + + request_kwargs = { + 'tokenId': token_id, + 'accountIdentifier': account_id + } + if org_id is not None: + request_kwargs['orgIdentifier'] = org_id + if project_id is not None: + request_kwargs['projectIdentifier'] = project_id self._http_client.make_request( - self._endpoint['delete'], - tokenId=token_id, - accountIdentifier=account_id + endpoint, + **request_kwargs ) return True diff --git a/splitapiclient/tests/main/test_harness_apiclient_resources.py b/splitapiclient/tests/main/test_harness_apiclient_resources.py index dbc623c..74fa5f9 100644 --- a/splitapiclient/tests/main/test_harness_apiclient_resources.py +++ b/splitapiclient/tests/main/test_harness_apiclient_resources.py @@ -24,7 +24,6 @@ def test_harness_resource_properties(self, mocker): # Create a HarnessApiClient with minimal config client = HarnessApiClient({ - 'apikey': 'test-apikey', 'harness_token': 'test-harness-token' }) @@ -49,7 +48,6 @@ def test_harness_resource_operations(self, mocker): # Create a HarnessApiClient with minimal config client = HarnessApiClient({ - 'apikey': 'test-apikey', 'harness_token': 'test-harness-token', 'account_identifier': 'test-account-identifier' }) @@ -326,7 +324,7 @@ def mock_make_request(endpoint, body=None, **kwargs): return user_detail_response elif '/ng/api/user-groups' in url_template and '{groupIdentifier}' not in url_template and method == 'GET': return group_response - elif '/ng/api/user-groups/{groupIdentifier}' in url_template and method == 'GET': + elif '/ng/api/user-groups' in url_template and '{groupIdentifier}' in url_template and method == 'GET': return group_detail_response elif '/authz/api/roles' in url_template and '{roleId}' not in url_template and method == 'GET': return role_response @@ -334,15 +332,15 @@ def mock_make_request(endpoint, body=None, **kwargs): return role_detail_response elif '/authz/api/resourceGroups' in url_template and '{resourceGroupId}' not in url_template and method == 'GET': return resource_group_response - elif '/resourcegroup/api/v2/resourceGroup' in url_template and '{resourceGroupId}' not in url_template and method == 'GET': + elif '/resourcegroup/api/v2/resourcegroup' in url_template and '{resourceGroupId}' not in url_template and method == 'GET': return resource_group_response elif '/authz/api/resourceGroups/{resourceGroupId}' in url_template and method == 'GET': return resource_group_detail_response - elif '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}' in url_template and method == 'GET': + elif '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}' in url_template and method == 'GET': return resource_group_detail_response - elif '/authz/api/roleAssignments' in url_template and '{roleAssignmentId}' not in url_template and method == 'GET': + elif '/authz/api/roleassignments' in url_template and '{roleAssignmentId}' not in url_template and method == 'GET': return role_assignment_response - elif '/authz/api/roleAssignments/{roleAssignmentId}' in url_template and method == 'GET': + elif '/authz/api/roleassignments/{roleAssignmentId}' in url_template and method == 'GET': return role_assignment_detail_response elif '/ng/api/projects/aggregate' in url_template and method == 'GET': return project_response @@ -446,7 +444,6 @@ def test_harness_pagination(self, mocker): # Create a HarnessApiClient with mocked HTTP client client = HarnessApiClient({ 'harness_token': 'test-harness-token', - 'apikey': 'test-apikey', 'base_url': 'test-host', 'harness_base_url': 'test-harness-host', 'account_identifier': 'test-account-identifier' @@ -534,50 +531,51 @@ def test_harness_pagination(self, mocker): def test_harness_authentication_modes(self, mocker): ''' - Test that the HarnessApiClient properly handles different authentication modes + Test that the HarnessApiClient properly handles authentication requirements ''' # Mock the HTTP client initialization to avoid actual HTTP requests mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', return_value=None) - # Test with both apikey and harness_token - client1 = HarnessApiClient({ - 'apikey': 'test-apikey', + # Test with harness_token - should work + client = HarnessApiClient({ 'harness_token': 'test-harness-token' }) - # Verify that the HTTP client was initialized correctly for client1 + # Verify that the HTTP client was initialized correctly from splitapiclient.http_clients.harness_client import HarnessHttpClient - # For client1, harness_token should be used for Harness endpoints - harness_client1_calls = [ + # harness_token should be used for all endpoints (both Harness and Split) + harness_client_calls = [ call for call in HarnessHttpClient.__init__.call_args_list - if call[0][0] == 'https://app.harness.io/' and call[0][1] == 'test-harness-token' + if call[0][1] == 'test-harness-token' ] - assert len(harness_client1_calls) > 0 - - # Reset the mock before creating client2 - HarnessHttpClient.__init__.reset_mock() + # Should be called 3 times: Split v2, Split v3, and Harness endpoints + assert len(harness_client_calls) == 3 + + # Verify that the client has all the Harness resource properties + assert hasattr(client, 'token') + assert hasattr(client, 'harness_apikey') + assert hasattr(client, 'service_account') + assert hasattr(client, 'harness_user') + assert hasattr(client, 'harness_group') + assert hasattr(client, 'role') + assert hasattr(client, 'resource_group') + assert hasattr(client, 'role_assignment') + assert hasattr(client, 'harness_project') - # Test with only apikey - client2 = HarnessApiClient({ - 'apikey': 'test-apikey' - }) + def test_harness_mode_requires_harness_token(self, mocker): + ''' + Test that harness_token is required and apikey alone raises an error + ''' + from splitapiclient.util.exceptions import InsufficientConfigArgumentsException - # Verify that both clients have all the Harness resource properties - for client in [client1, client2]: - assert hasattr(client, 'token') - assert hasattr(client, 'harness_apikey') - assert hasattr(client, 'service_account') - assert hasattr(client, 'harness_user') - assert hasattr(client, 'harness_group') - assert hasattr(client, 'role') - assert hasattr(client, 'resource_group') - assert hasattr(client, 'role_assignment') - assert hasattr(client, 'harness_project') - - # For client2, apikey should be used for Harness endpoints - harness_client2_calls = [ - call for call in HarnessHttpClient.__init__.call_args_list - if call[0][0] == 'https://app.harness.io/' and call[0][1] == 'test-apikey' - ] - assert len(harness_client2_calls) > 0 + # Mock the HTTP client initialization to avoid actual HTTP requests + mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', return_value=None) + + # Test with only apikey - should raise an error + with pytest.raises(InsufficientConfigArgumentsException) as excinfo: + HarnessApiClient({ + 'apikey': 'test-apikey' + }) + + assert 'harness_token is required' in str(excinfo.value) diff --git a/splitapiclient/tests/microclients/harness/conftest.py b/splitapiclient/tests/microclients/harness/conftest.py new file mode 100644 index 0000000..7597e67 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/conftest.py @@ -0,0 +1,57 @@ +""" +Shared pytest fixtures and helpers for Harness microclient tests. +""" +from __future__ import absolute_import, division, print_function, unicode_literals + +import pytest + + +class FakeResponse: + """ + Simple class to mock Response objects from the requests module. + Used for testing URL generation without making actual HTTP calls. + """ + def __init__(self, status, text): + self.status_code = status + self.text = text + + +@pytest.fixture +def fake_response(): + """Factory fixture for creating FakeResponse objects.""" + def _create_response(status=200, text='{}'): + return FakeResponse(status, text) + return _create_response + + +@pytest.fixture +def mock_requests_get(mocker): + """Mock requests.get and return the mock for assertions.""" + mock = mocker.patch('splitapiclient.http_clients.sync_client.requests.get') + mock.return_value = FakeResponse(200, '{"data": []}') + return mock + + +@pytest.fixture +def mock_requests_post(mocker): + """Mock requests.post and return the mock for assertions.""" + mock = mocker.patch('splitapiclient.http_clients.sync_client.requests.post') + mock.return_value = FakeResponse(200, '{"data": {}}') + return mock + + +@pytest.fixture +def mock_requests_put(mocker): + """Mock requests.put and return the mock for assertions.""" + mock = mocker.patch('splitapiclient.http_clients.sync_client.requests.put') + mock.return_value = FakeResponse(200, '{"data": {}}') + return mock + + +@pytest.fixture +def mock_requests_delete(mocker): + """Mock requests.delete and return the mock for assertions.""" + mock = mocker.patch('splitapiclient.http_clients.sync_client.requests.delete') + mock.return_value = FakeResponse(200, '{}') + return mock + diff --git a/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py b/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py index 9a371c7..3693682 100644 --- a/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py +++ b/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py @@ -24,7 +24,6 @@ def test_workspace_endpoints_deprecated_in_harness_mode(self, mocker): # Create a HarnessApiClient client = HarnessApiClient({ - 'apikey': 'abc', 'harness_token': 'abc' }) @@ -138,7 +137,7 @@ class TestAuthenticationInHarnessMode: def test_harness_token_used_for_harness_endpoints(self, mocker): """ - Test that harness_token is used for Harness endpoints and apikey for Split endpoints + Test that harness_token is used for all endpoints (both Harness and Split) """ # Create a custom HTTP client class for testing class TestHttpClient(HarnessHttpClient): @@ -147,13 +146,7 @@ def __init__(self, baseurl, auth_token): self.auth_token = auth_token # Initialize with empty config self.config = {'base_args': {}} - - # For Harness HTTP client, set x-api-key in base_args - if 'harness' in baseurl: - self.config['base_args'] = {'x-api-key': auth_token} - else: - # For Split HTTP client, we'll check Authorization header in make_request - pass + self.config['base_args'] = {'x-api-key': auth_token} def make_request(self, endpoint, body=None, **kwargs): # Just return a successful response without making actual requests @@ -168,57 +161,28 @@ def make_request(self, endpoint, body=None, **kwargs): mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.make_request', TestHttpClient.make_request) - # Create client with both harness_token and apikey + # Create client with harness_token only client = HarnessApiClient({ - 'harness_token': 'harness_token_value', - 'apikey': 'api_key_value' + 'harness_token': 'harness_token_value' }) - # Check that the Harness HTTP client was initialized with harness_token + # Check that all HTTP clients were initialized with harness_token assert client._token_client._http_client.auth_token == 'harness_token_value' - - # Check that the Split HTTP client was initialized with apikey - assert client._split_client._http_client.auth_token == 'api_key_value' + assert client._split_client._http_client.auth_token == 'harness_token_value' def test_apikey_fallback_when_no_harness_token(self, mocker): """ - Test that apikey is used for all operations when harness_token is not provided + Test that harness_token is required and apikey alone raises an error """ - # Create a custom HTTP client class for testing - class TestHttpClient(HarnessHttpClient): - def __init__(self, baseurl, auth_token): - self.baseurl = baseurl - self.auth_token = auth_token - # Initialize with empty config - self.config = {'base_args': {}} - - # For Harness HTTP client, set x-api-key in base_args - if 'harness' in baseurl: - self.config['base_args'] = {'x-api-key': auth_token} - else: - # For Split HTTP client, we'll check Authorization header in make_request - pass - - def make_request(self, endpoint, body=None, **kwargs): - # Just return a successful response without making actual requests - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.text = '{}' - return {} + from splitapiclient.util.exceptions import InsufficientConfigArgumentsException - # Patch the HarnessHttpClient constructor to use our test class - mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', - TestHttpClient.__init__) - mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.make_request', - TestHttpClient.make_request) - - # Create client with only apikey - client = HarnessApiClient({ - 'apikey': 'api_key_value' - }) + # Patch the HarnessHttpClient constructor to avoid actual HTTP requests + mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', return_value=None) - # Check that the Harness HTTP client was initialized with apikey as fallback - assert client._token_client._http_client.auth_token == 'api_key_value' + # Create client with only apikey should raise an error + with pytest.raises(InsufficientConfigArgumentsException) as excinfo: + client = HarnessApiClient({ + 'apikey': 'api_key_value' + }) - # Check that the Split HTTP client was initialized with apikey - assert client._split_client._http_client.auth_token == 'api_key_value' + assert 'harness_token is required' in str(excinfo.value) diff --git a/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py index b3c5c5e..49293cc 100644 --- a/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import HarnessApiKeyMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import HarnessApiKey +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestHarnessApiKeyMicroClient: @@ -45,8 +48,12 @@ def test_list(self, mocker): result = akmc.list('parent1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/apikey?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', parentIdentifier='parent1' ) @@ -78,8 +85,12 @@ def test_list_empty_parent(self, mocker): result = akmc.list() # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/apikey?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', parentIdentifier="" ) @@ -116,8 +127,12 @@ def test_get(self, mocker): result = akmc.get('apikey1', 'parent1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['get_apikey'].copy() + expected_endpoint['url_template'] = '/ng/api/apikey/aggregate/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['get_apikey'], + expected_endpoint, apiKeyIdentifier='apikey1', accountIdentifier='test_account', parentIdentifier='parent1' @@ -149,8 +164,12 @@ def test_get_not_found(self, mocker): result = akmc.get('nonexistent', 'parent1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['get_apikey'].copy() + expected_endpoint['url_template'] = '/ng/api/apikey/aggregate/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['get_apikey'], + expected_endpoint, apiKeyIdentifier='nonexistent', accountIdentifier='test_account', parentIdentifier='parent1' @@ -196,8 +215,12 @@ def test_create(self, mocker): result = akmc.create(apikey_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/ng/api/apikey?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['create'], + expected_endpoint, body=apikey_data, accountIdentifier='test_account' ) @@ -242,8 +265,12 @@ def test_add_permissions(self, mocker): result = akmc.add_permissions('apikey1', permissions) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['add_permissions'].copy() + expected_endpoint['url_template'] = '/ng/api/roleassignments?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['add_permissions'], + expected_endpoint, body=permissions, apiKeyIdentifier='apikey1', accountIdentifier='test_account' @@ -267,8 +294,12 @@ def test_delete(self, mocker): result = akmc.delete('apikey1', 'parent1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessApiKeyMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/ng/api/apikey/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessApiKeyMicroClient._endpoint['delete'], + expected_endpoint, apiKeyIdentifier='apikey1', accountIdentifier='test_account', parentIdentifier='parent1' @@ -276,3 +307,149 @@ def test_delete(self, mocker): # Verify the result assert result is True + + +class TestHarnessApiKeyURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account') + client.list('parent1') + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account', org_identifier='org1') + client.list('parent1') + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_project_identifier_only(self, mocker): + """Verify list URL contains projectIdentifier when set, but not orgIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account', project_identifier='proj1') + client.list('parent1') + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list('parent1') + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list('parent1', org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'apiKey': {'identifier': 'ak1', 'name': 'AK1', 'description': '', 'parentIdentifier': 'parent1', 'apiKeyType': 'SERVICE_ACCOUNT'}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account') + client.get('ak1', 'parent1') + + called_url = mock_get.call_args[0][0] + assert '/apikey/aggregate/ak1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'apiKey': {'identifier': 'ak1', 'name': 'AK1', 'description': '', 'parentIdentifier': 'parent1', 'apiKeyType': 'SERVICE_ACCOUNT'}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account', org_identifier='org1') + client.get('ak1', 'parent1') + + called_url = mock_get.call_args[0][0] + assert '/apikey/aggregate/ak1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'apiKey': {'identifier': 'ak1', 'name': 'AK1', 'description': '', 'parentIdentifier': 'parent1', 'apiKeyType': 'SERVICE_ACCOUNT'}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessApiKeyMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('ak1', 'parent1') + + called_url = mock_get.call_args[0][0] + assert '/apikey/aggregate/ak1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'parentIdentifier=parent1' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url diff --git a/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py index 3b543f3..39b4184 100644 --- a/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import HarnessGroupMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import HarnessGroup +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestHarnessGroupMicroClient: @@ -51,13 +54,18 @@ def test_list(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier, projectIdentifier, and filterType removed) + expected_endpoint = HarnessGroupMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100' + SyncHttpClient.make_request.assert_any_call( - HarnessGroupMicroClient._endpoint['all_items'], + expected_endpoint, pageIndex=0, accountIdentifier='test_account' ) SyncHttpClient.make_request.assert_any_call( - HarnessGroupMicroClient._endpoint['all_items'], + expected_endpoint, pageIndex=1, accountIdentifier='test_account' ) @@ -79,10 +87,12 @@ def test_get(self, mocker): # Mock the API response response_data = { - 'identifier': 'group1', - 'name': 'Group 1', - 'accountIdentifier': 'test_account', - 'users': [] + 'data': { + 'identifier': 'group1', + 'name': 'Group 1', + 'accountIdentifier': 'test_account', + 'users': [] + } } # Set up the mock to return the response @@ -92,8 +102,12 @@ def test_get(self, mocker): result = gmc.get('group1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessGroupMicroClient._endpoint['get_group'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessGroupMicroClient._endpoint['get_group'], + expected_endpoint, groupIdentifier='group1', accountIdentifier='test_account' ) @@ -141,8 +155,12 @@ def test_create(self, mocker): result = gmc.create(group_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessGroupMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessGroupMicroClient._endpoint['create'], + expected_endpoint, body=group_data, accountIdentifier='test_account' ) @@ -187,8 +205,12 @@ def test_update(self, mocker): result = gmc.update(update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessGroupMicroClient._endpoint['update'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessGroupMicroClient._endpoint['update'], + expected_endpoint, body=update_data, accountIdentifier='test_account' ) @@ -214,11 +236,366 @@ def test_delete(self, mocker): result = gmc.delete('group1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessGroupMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessGroupMicroClient._endpoint['delete'], + expected_endpoint, groupIdentifier='group1', accountIdentifier='test_account' ) # Verify the result assert result is True + + def test_list_with_filter_type(self, mocker): + ''' + Test listing groups with filterType parameter + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'identifier': 'group1', + 'name': 'Group 1', + 'accountIdentifier': 'test_account', + 'users': [] + } + ] + } + } + + # Mock the API response for the second page (empty to end pagination) + second_page_data = { + 'data': { + 'content': [] + } + } + + # Set up the mock to return different responses for different calls + SyncHttpClient.make_request.side_effect = [first_page_data, second_page_data] + + # Call the method being tested with filterType + result = gmc.list(filterType='EXCLUDE_INHERITED_GROUPS') + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with filterType included, but orgIdentifier and projectIdentifier removed + expected_endpoint = HarnessGroupMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100&filterType={filterType}' + + SyncHttpClient.make_request.assert_any_call( + expected_endpoint, + pageIndex=0, + accountIdentifier='test_account', + filterType='EXCLUDE_INHERITED_GROUPS' + ) + SyncHttpClient.make_request.assert_any_call( + expected_endpoint, + pageIndex=1, + accountIdentifier='test_account', + filterType='EXCLUDE_INHERITED_GROUPS' + ) + + # Verify the result + assert len(result) == 1 + assert isinstance(result[0], HarnessGroup) + assert result[0]._identifier == 'group1' + + def test_list_with_filter_type_and_org_identifier(self, mocker): + ''' + Test listing groups with filterType and org_identifier parameters + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account', org_identifier='test_org') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'identifier': 'group1', + 'name': 'Group 1', + 'accountIdentifier': 'test_account', + 'users': [] + } + ] + } + } + + # Mock the API response for the second page (empty to end pagination) + second_page_data = { + 'data': { + 'content': [] + } + } + + # Set up the mock to return different responses for different calls + SyncHttpClient.make_request.side_effect = [first_page_data, second_page_data] + + # Call the method being tested with filterType + result = gmc.list(filterType='INCLUDE_INHERITED_GROUPS') + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with filterType and orgIdentifier included, but projectIdentifier removed + expected_endpoint = HarnessGroupMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups?accountIdentifier={accountIdentifier}&orgIdentifier={orgIdentifier}&pageIndex={pageIndex}&pageSize=100&filterType={filterType}' + + SyncHttpClient.make_request.assert_any_call( + expected_endpoint, + pageIndex=0, + accountIdentifier='test_account', + orgIdentifier='test_org', + filterType='INCLUDE_INHERITED_GROUPS' + ) + + # Verify the result + assert len(result) == 1 + assert isinstance(result[0], HarnessGroup) + + def test_list_with_filter_type_none(self, mocker): + ''' + Test listing groups with filterType=None (should be omitted from URL) + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'identifier': 'group1', + 'name': 'Group 1', + 'accountIdentifier': 'test_account', + 'users': [] + } + ] + } + } + + # Mock the API response for the second page (empty to end pagination) + second_page_data = { + 'data': { + 'content': [] + } + } + + # Set up the mock to return different responses for different calls + SyncHttpClient.make_request.side_effect = [first_page_data, second_page_data] + + # Call the method being tested with filterType=None (explicit) + result = gmc.list(filterType=None) + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with filterType removed (since it's None) + expected_endpoint = HarnessGroupMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/user-groups?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100' + + SyncHttpClient.make_request.assert_any_call( + expected_endpoint, + pageIndex=0, + accountIdentifier='test_account' + ) + + # Verify the result + assert len(result) == 1 + assert isinstance(result[0], HarnessGroup) + + +class TestHarnessGroupURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier, filterType) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier/filterType when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + # Return empty content on second call to stop pagination + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + assert 'filterType' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + assert 'filterType' not in called_url + + def test_list_url_with_project_identifier_only(self, mocker): + """Verify list URL contains projectIdentifier when set, but not orgIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier=proj1' in called_url + assert 'filterType' not in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_filter_type(self, mocker): + """Verify list URL contains filterType when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account') + client.list(filterType='EXCLUDE_INHERITED_GROUPS') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'filterType=EXCLUDE_INHERITED_GROUPS' in called_url + + def test_list_url_with_all_parameters(self, mocker): + """Verify list URL contains all parameters when all are set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list(filterType='INCLUDE_INHERITED_GROUPS') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + assert 'filterType=INCLUDE_INHERITED_GROUPS' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'identifier': 'group1', 'name': 'Group 1', 'accountIdentifier': 'test_account', 'users': []} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account') + client.get('group1') + + called_url = mock_get.call_args[0][0] + assert '/user-groups/group1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'identifier': 'group1', 'name': 'Group 1', 'accountIdentifier': 'test_account', 'users': []} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', org_identifier='org1') + client.get('group1') + + called_url = mock_get.call_args[0][0] + assert '/user-groups/group1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'identifier': 'group1', 'name': 'Group 1', 'accountIdentifier': 'test_account', 'users': []} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessGroupMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('group1') + + called_url = mock_get.call_args[0][0] + assert '/user-groups/group1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url diff --git a/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py index d2295d0..6d0a736 100644 --- a/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import HarnessProjectMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import HarnessProject +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestHarnessProjectMicroClient: @@ -79,13 +82,18 @@ def test_list(self, mocker): # Verify the make_request calls - should only be 2 calls now since we're respecting totalPages assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessProjectMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/projects?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=50' + SyncHttpClient.make_request.assert_any_call( - HarnessProjectMicroClient._endpoint['all_items'], + expected_endpoint, pageIndex=0, accountIdentifier='test_account' ) SyncHttpClient.make_request.assert_any_call( - HarnessProjectMicroClient._endpoint['all_items'], + expected_endpoint, pageIndex=1, accountIdentifier='test_account' ) @@ -129,8 +137,12 @@ def test_get(self, mocker): result = pmc.get('project1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier query params removed) + expected_endpoint = HarnessProjectMicroClient._endpoint['get'].copy() + expected_endpoint['url_template'] = '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessProjectMicroClient._endpoint['get'], + expected_endpoint, projectIdentifier='project1', accountIdentifier='test_account' ) @@ -181,8 +193,12 @@ def test_create(self, mocker): result = pmc.create(project_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier removed) + expected_endpoint = HarnessProjectMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/ng/api/projects?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessProjectMicroClient._endpoint['create'], + expected_endpoint, body=project_data, accountIdentifier='test_account' ) @@ -230,8 +246,12 @@ def test_update(self, mocker): result = pmc.update('project1', update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier query params removed) + expected_endpoint = HarnessProjectMicroClient._endpoint['update'].copy() + expected_endpoint['url_template'] = '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessProjectMicroClient._endpoint['update'], + expected_endpoint, projectIdentifier='project1', accountIdentifier='test_account', body=update_data @@ -258,11 +278,139 @@ def test_delete(self, mocker): result = pmc.delete('project1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier query params removed) + expected_endpoint = HarnessProjectMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessProjectMicroClient._endpoint['delete'], + expected_endpoint, projectIdentifier='project1', accountIdentifier='test_account' ) # Verify the result assert result is True + + +class TestHarnessProjectURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional orgIdentifier is correctly included or + excluded from the final URL. + + NOTE: The projects endpoint does NOT support projectIdentifier as a query parameter. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_org_identifier(self, mocker): + """Verify list URL doesn't contain orgIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + # First call returns data, second call returns data to trigger second page, third returns empty + mock_get.side_effect = [ + FakeResponse(200, json.dumps({ + 'data': {'content': [], 'totalPages': 1} + })), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessProjectMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + # Verify projectIdentifier is not in URL (projects endpoint doesn't support it) + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier(self, mocker): + """Verify list URL contains orgIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({ + 'data': {'content': [], 'totalPages': 1} + })), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessProjectMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + # Verify projectIdentifier is not in URL + assert 'projectIdentifier' not in called_url + + def test_list_url_with_method_override_org_identifier(self, mocker): + """Verify list URL uses method parameters to override instance org_identifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({ + 'data': {'content': [], 'totalPages': 1} + })), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessProjectMicroClient(hc, 'test_account', org_identifier='default_org') + client.list(org_identifier='override_org') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'default_org' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_org_identifier(self, mocker): + """Verify get URL doesn't contain orgIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'project': {'identifier': 'proj1', 'name': 'Project 1', 'description': '', 'orgIdentifier': 'org1', 'color': '', 'modules': []}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessProjectMicroClient(hc, 'test_account') + client.get('proj1') + + called_url = mock_get.call_args[0][0] + assert '/projects/proj1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + + def test_get_url_with_org_identifier(self, mocker): + """Verify get URL contains orgIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'project': {'identifier': 'proj1', 'name': 'Project 1', 'description': '', 'orgIdentifier': 'org1', 'color': '', 'modules': []}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessProjectMicroClient(hc, 'test_account', org_identifier='org1') + client.get('proj1') + + called_url = mock_get.call_args[0][0] + assert '/projects/proj1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + + def test_get_url_with_method_override_org_identifier(self, mocker): + """Verify get URL uses method parameters to override instance org_identifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'project': {'identifier': 'proj1', 'name': 'Project 1', 'description': '', 'orgIdentifier': 'override_org', 'color': '', 'modules': []}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessProjectMicroClient(hc, 'test_account', org_identifier='default_org') + client.get('proj1', org_identifier='override_org') + + called_url = mock_get.call_args[0][0] + assert '/projects/proj1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'default_org' not in called_url diff --git a/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py index 08c23d0..313ecbc 100644 --- a/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import HarnessUserMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import HarnessUser, HarnessInvite +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestHarnessUserMicroClient: @@ -57,13 +60,18 @@ def test_list(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/user/aggregate?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}' + SyncHttpClient.make_request.assert_any_call( - HarnessUserMicroClient._endpoint['all_items'], + expected_endpoint, pageIndex=0, accountIdentifier='test_account' ) SyncHttpClient.make_request.assert_any_call( - HarnessUserMicroClient._endpoint['all_items'], + expected_endpoint, pageIndex=1, accountIdentifier='test_account' ) @@ -85,11 +93,15 @@ def test_get(self, mocker): # Mock the API response response_data = { - 'uuid': 'user1', - 'name': 'User 1', - 'email': 'user1@example.com', - 'accountIdentifier': 'test_account', - 'status': 'ACTIVE' + 'data': { + 'user': { + 'uuid': 'user1', + 'name': 'User 1', + 'email': 'user1@example.com', + 'accountIdentifier': 'test_account', + 'status': 'ACTIVE' + } + } } # Set up the mock to return the response @@ -99,8 +111,12 @@ def test_get(self, mocker): result = umc.get('user1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['get_user'].copy() + expected_endpoint['url_template'] = '/ng/api/user/aggregate/{userId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessUserMicroClient._endpoint['get_user'], + expected_endpoint, userId='user1', accountIdentifier='test_account' ) @@ -140,8 +156,12 @@ def test_invite(self, mocker): result = umc.invite(user_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['invite'].copy() + expected_endpoint['url_template'] = '/ng/api/user/users?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessUserMicroClient._endpoint['invite'], + expected_endpoint, body=user_data, accountIdentifier='test_account' ) @@ -181,8 +201,12 @@ def test_update(self, mocker): result = umc.update('user1', update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['update'].copy() + expected_endpoint['url_template'] = '/ng/api/user/{userId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessUserMicroClient._endpoint['update'], + expected_endpoint, body=update_data, userId='user1', accountIdentifier='test_account' @@ -215,8 +239,12 @@ def test_add_user_to_groups(self, mocker): result = umc.add_user_to_groups('user1', group_ids) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['add_user_to_groups'].copy() + expected_endpoint['url_template'] = '/ng/api/user/add-user-to-groups/{userId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessUserMicroClient._endpoint['add_user_to_groups'], + expected_endpoint, body={"userGroupIdsToAdd": group_ids}, userId='user1', accountIdentifier='test_account' @@ -243,8 +271,12 @@ def test_delete_pending(self, mocker): result = umc.delete_pending('invite1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['delete_pending'].copy() + expected_endpoint['url_template'] = '/ng/api/invites/{inviteId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - HarnessUserMicroClient._endpoint['delete_pending'], + expected_endpoint, inviteId='invite1', accountIdentifier='test_account' ) @@ -295,13 +327,18 @@ def test_list_pending(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = HarnessUserMicroClient._endpoint['list_pending'].copy() + expected_endpoint['url_template'] = '/ng/api/invites/aggregate?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_any_call( - HarnessUserMicroClient._endpoint['list_pending'], + expected_endpoint, pageIndex=0, accountIdentifier='test_account' ) SyncHttpClient.make_request.assert_any_call( - HarnessUserMicroClient._endpoint['list_pending'], + expected_endpoint, pageIndex=1, accountIdentifier='test_account' ) @@ -312,3 +349,136 @@ def test_list_pending(self, mocker): assert isinstance(result[1], HarnessInvite) assert result[0]._id == 'invite1' assert result[1]._id == 'invite2' + + +class TestHarnessUserURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests (Note: user list uses POST, not GET) + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_post = mocker.patch('splitapiclient.http_clients.harness_client.requests.post') + mock_post.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account') + client.list() + + called_url = mock_post.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_post = mocker.patch('splitapiclient.http_clients.harness_client.requests.post') + mock_post.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_post.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_post = mocker.patch('splitapiclient.http_clients.harness_client.requests.post') + mock_post.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_post.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_post = mocker.patch('splitapiclient.http_clients.harness_client.requests.post') + mock_post.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_post.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'user': {'uuid': 'user1', 'name': 'User 1', 'email': 'user1@test.com', 'status': 'ACTIVE'}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account') + client.get('user1') + + called_url = mock_get.call_args[0][0] + assert '/user/aggregate/user1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'user': {'uuid': 'user1', 'name': 'User 1', 'email': 'user1@test.com', 'status': 'ACTIVE'}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account', org_identifier='org1') + client.get('user1') + + called_url = mock_get.call_args[0][0] + assert '/user/aggregate/user1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'user': {'uuid': 'user1', 'name': 'User 1', 'email': 'user1@test.com', 'status': 'ACTIVE'}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = HarnessUserMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('user1') + + called_url = mock_get.call_args[0][0] + assert '/user/aggregate/user1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url diff --git a/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py b/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py index e7766fc..2313dc4 100644 --- a/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import ResourceGroupMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import ResourceGroup +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestResourceGroupMicroClient: @@ -65,13 +68,18 @@ def test_list(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ResourceGroupMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/resourcegroup/api/v2/resourcegroup?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100' + SyncHttpClient.make_request.assert_any_call( - ResourceGroupMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=0 ) SyncHttpClient.make_request.assert_any_call( - ResourceGroupMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=1 ) @@ -116,8 +124,12 @@ def test_get(self, mocker): result = rgmc.get('rg1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ResourceGroupMicroClient._endpoint['get_resource_group'].copy() + expected_endpoint['url_template'] = '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ResourceGroupMicroClient._endpoint['get_resource_group'], + expected_endpoint, resourceGroupId='rg1', accountIdentifier='test_account' ) @@ -174,8 +186,12 @@ def test_create(self, mocker): result = rgmc.create(rg_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ResourceGroupMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/resourcegroup/api/v2/resourcegroup?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ResourceGroupMicroClient._endpoint['create'], + expected_endpoint, body=rg_data, accountIdentifier='test_account' ) @@ -227,8 +243,12 @@ def test_update(self, mocker): result = rgmc.update('rg1', update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ResourceGroupMicroClient._endpoint['update'].copy() + expected_endpoint['url_template'] = '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ResourceGroupMicroClient._endpoint['update'], + expected_endpoint, body=update_data, resourceGroupId='rg1', accountIdentifier='test_account' @@ -255,11 +275,149 @@ def test_delete(self, mocker): result = rgmc.delete('rg1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ResourceGroupMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/resourcegroup/api/v2/resourcegroup/{resourceGroupId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ResourceGroupMicroClient._endpoint['delete'], + expected_endpoint, resourceGroupId='rg1', accountIdentifier='test_account' ) # Verify the result assert result is True + + +class TestResourceGroupURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'resourceGroup': {'identifier': 'rg1', 'name': 'RG 1', 'description': '', 'resourceFilter': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account') + client.get('rg1') + + called_url = mock_get.call_args[0][0] + assert '/resourcegroup/' in called_url + assert 'rg1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'resourceGroup': {'identifier': 'rg1', 'name': 'RG 1', 'description': '', 'resourceFilter': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account', org_identifier='org1') + client.get('rg1') + + called_url = mock_get.call_args[0][0] + assert '/resourcegroup/' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'resourceGroup': {'identifier': 'rg1', 'name': 'RG 1', 'description': '', 'resourceFilter': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ResourceGroupMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('rg1') + + called_url = mock_get.call_args[0][0] + assert '/resourcegroup/' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url diff --git a/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py b/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py index 103f8f1..7554abe 100644 --- a/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import RoleAssignmentMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import RoleAssignment +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestRoleAssignmentMicroClient: @@ -63,13 +66,18 @@ def test_list(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleAssignmentMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/authz/api/roleassignments?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100' + SyncHttpClient.make_request.assert_any_call( - RoleAssignmentMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=0 ) SyncHttpClient.make_request.assert_any_call( - RoleAssignmentMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=1 ) @@ -112,8 +120,12 @@ def test_get(self, mocker): result = ramc.get('ra1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleAssignmentMicroClient._endpoint['get_role_assignment'].copy() + expected_endpoint['url_template'] = '/authz/api/roleassignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleAssignmentMicroClient._endpoint['get_role_assignment'], + expected_endpoint, roleAssignmentId='ra1', accountIdentifier='test_account' ) @@ -166,8 +178,12 @@ def test_create(self, mocker): result = ramc.create(ra_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleAssignmentMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/authz/api/roleassignments?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleAssignmentMicroClient._endpoint['create'], + expected_endpoint, body=ra_data, accountIdentifier='test_account' ) @@ -193,11 +209,148 @@ def test_delete(self, mocker): result = ramc.delete('ra1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleAssignmentMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/authz/api/roleassignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleAssignmentMicroClient._endpoint['delete'], + expected_endpoint, roleAssignmentId='ra1', accountIdentifier='test_account' ) # Verify the result assert result is True + + +class TestRoleAssignmentURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'roleAssignment': {'identifier': 'ra1', 'roleIdentifier': 'role1', 'resourceGroupIdentifier': 'rg1', 'principal': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account') + client.get('ra1') + + called_url = mock_get.call_args[0][0] + assert '/roleassignments/ra1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'roleAssignment': {'identifier': 'ra1', 'roleIdentifier': 'role1', 'resourceGroupIdentifier': 'rg1', 'principal': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account', org_identifier='org1') + client.get('ra1') + + called_url = mock_get.call_args[0][0] + assert '/roleassignments/ra1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'roleAssignment': {'identifier': 'ra1', 'roleIdentifier': 'role1', 'resourceGroupIdentifier': 'rg1', 'principal': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleAssignmentMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('ra1') + + called_url = mock_get.call_args[0][0] + assert '/roleassignments/ra1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url diff --git a/splitapiclient/tests/microclients/harness/role_microclient_test.py b/splitapiclient/tests/microclients/harness/role_microclient_test.py index 307c9af..5466dbf 100644 --- a/splitapiclient/tests/microclients/harness/role_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/role_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import RoleMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import Role +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestRoleMicroClient: @@ -57,13 +60,18 @@ def test_list(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/authz/api/roles?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100' + SyncHttpClient.make_request.assert_any_call( - RoleMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=0 ) SyncHttpClient.make_request.assert_any_call( - RoleMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=1 ) @@ -103,8 +111,12 @@ def test_get(self, mocker): result = rmc.get('role1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleMicroClient._endpoint['get_role'].copy() + expected_endpoint['url_template'] = '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleMicroClient._endpoint['get_role'], + expected_endpoint, roleId='role1', accountIdentifier='test_account' ) @@ -151,8 +163,12 @@ def test_create(self, mocker): result = rmc.create(role_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/authz/api/roles?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleMicroClient._endpoint['create'], + expected_endpoint, body=role_data, accountIdentifier='test_account' ) @@ -198,8 +214,12 @@ def test_update(self, mocker): result = rmc.update('role1', update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleMicroClient._endpoint['update'].copy() + expected_endpoint['url_template'] = '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleMicroClient._endpoint['update'], + expected_endpoint, body=update_data, roleId='role1', accountIdentifier='test_account' @@ -226,11 +246,148 @@ def test_delete(self, mocker): result = rmc.delete('role1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = RoleMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = 'roles/{roleId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - RoleMicroClient._endpoint['delete'], + expected_endpoint, roleId='role1', accountIdentifier='test_account' ) # Verify the result assert result is True + + +class TestRoleURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'role': {'identifier': 'role1', 'name': 'Role 1', 'description': '', 'permissions': []}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account') + client.get('role1') + + called_url = mock_get.call_args[0][0] + assert '/roles/role1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'role': {'identifier': 'role1', 'name': 'Role 1', 'description': '', 'permissions': []}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account', org_identifier='org1') + client.get('role1') + + called_url = mock_get.call_args[0][0] + assert '/roles/role1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'role': {'identifier': 'role1', 'name': 'Role 1', 'description': '', 'permissions': []}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = RoleMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('role1') + + called_url = mock_get.call_args[0][0] + assert '/roles/role1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url diff --git a/splitapiclient/tests/microclients/harness/service_account_microclient_test.py b/splitapiclient/tests/microclients/harness/service_account_microclient_test.py index a0c0107..6e5c260 100644 --- a/splitapiclient/tests/microclients/harness/service_account_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/service_account_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import ServiceAccountMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import ServiceAccount +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestServiceAccountMicroClient: @@ -45,8 +48,12 @@ def test_list(self, mocker): result = samc.list() # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ServiceAccountMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ServiceAccountMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account' ) @@ -86,8 +93,12 @@ def test_get(self, mocker): result = samc.get('sa1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier removed) + expected_endpoint = ServiceAccountMicroClient._endpoint['item'].copy() + expected_endpoint['url_template'] = '/ng/api/serviceaccount/aggregate/{serviceAccountId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ServiceAccountMicroClient._endpoint['item'], + expected_endpoint, serviceAccountId='sa1', accountIdentifier='test_account' ) @@ -133,8 +144,12 @@ def test_create(self, mocker): result = samc.create(sa_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ServiceAccountMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ServiceAccountMicroClient._endpoint['create'], + expected_endpoint, body=sa_data, accountIdentifier='test_account' ) @@ -178,8 +193,12 @@ def test_update(self, mocker): result = samc.update('sa1', update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ServiceAccountMicroClient._endpoint['update'].copy() + expected_endpoint['url_template'] = '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ServiceAccountMicroClient._endpoint['update'], + expected_endpoint, body=update_data, serviceAccountId='sa1', accountIdentifier='test_account' @@ -206,11 +225,190 @@ def test_delete(self, mocker): result = samc.delete('sa1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = ServiceAccountMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - ServiceAccountMicroClient._endpoint['delete'], + expected_endpoint, serviceAccountId='sa1', accountIdentifier='test_account' ) # Verify the result assert result is True + + +class TestServiceAccountURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_project_identifier_only(self, mocker): + """Verify list URL contains projectIdentifier when set, but not orgIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({'data': []})) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args[0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url + + # ========================================================================= + # GET method URL tests + # ========================================================================= + + def test_get_url_without_optional_identifiers(self, mocker): + """Verify get URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'serviceAccount': {'identifier': 'sa1', 'name': 'SA1', 'description': '', 'email': '', 'tags': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account') + client.get('sa1') + + called_url = mock_get.call_args[0][0] + assert '/serviceaccount/aggregate/sa1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_org_identifier_only(self, mocker): + """Verify get URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'serviceAccount': {'identifier': 'sa1', 'name': 'SA1', 'description': '', 'email': '', 'tags': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', org_identifier='org1') + client.get('sa1') + + called_url = mock_get.call_args[0][0] + assert '/serviceaccount/aggregate/sa1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_get_url_with_project_identifier_only(self, mocker): + """Verify get URL contains projectIdentifier when set, but not orgIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'serviceAccount': {'identifier': 'sa1', 'name': 'SA1', 'description': '', 'email': '', 'tags': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', project_identifier='proj1') + client.get('sa1') + + called_url = mock_get.call_args[0][0] + assert '/serviceaccount/aggregate/sa1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_get_url_with_both_identifiers(self, mocker): + """Verify get URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'serviceAccount': {'identifier': 'sa1', 'name': 'SA1', 'description': '', 'email': '', 'tags': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.get('sa1') + + called_url = mock_get.call_args[0][0] + assert '/serviceaccount/aggregate/sa1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_get_url_with_method_override_identifiers(self, mocker): + """Verify get URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.return_value = FakeResponse(200, json.dumps({ + 'data': {'serviceAccount': {'identifier': 'sa1', 'name': 'SA1', 'description': '', 'email': '', 'tags': {}}} + })) + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = ServiceAccountMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.get('sa1', org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args[0][0] + assert '/serviceaccount/aggregate/sa1' in called_url + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url diff --git a/splitapiclient/tests/microclients/harness/test_optional_identifiers.py b/splitapiclient/tests/microclients/harness/test_optional_identifiers.py new file mode 100644 index 0000000..c3140a1 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/test_optional_identifiers.py @@ -0,0 +1,627 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.microclients.harness import ( + HarnessProjectMicroClient, + ServiceAccountMicroClient, + TokenMicroClient, + HarnessApiKeyMicroClient, + HarnessUserMicroClient, + HarnessGroupMicroClient, + RoleMicroClient, + ResourceGroupMicroClient, + RoleAssignmentMicroClient +) +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestOptionalIdentifiers: + """ + Test that orgIdentifier and projectIdentifier are only included in URLs when they are set. + """ + + def test_list_without_org_and_project_identifiers(self, mocker): + """ + Test that when org_identifier and project_identifier are not set, + they are not included in the URL. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Mock empty response + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + + # Call list without org or project identifiers + pmc.list() + + # Verify the URL template does not contain orgIdentifier or projectIdentifier + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + # Verify request_kwargs does not include orgIdentifier or projectIdentifier + request_kwargs = call_args[1] + assert 'orgIdentifier' not in request_kwargs + assert 'projectIdentifier' not in request_kwargs + assert 'accountIdentifier' in request_kwargs + + def test_list_with_org_identifier_only(self, mocker): + """ + Test that when only org_identifier is set, only orgIdentifier is in the URL. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account', org_identifier='test_org') + + # Mock empty response + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + + # Call list + pmc.list() + + # Verify the URL template contains orgIdentifier but not projectIdentifier + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + # Verify request_kwargs includes orgIdentifier but not projectIdentifier + request_kwargs = call_args[1] + assert 'orgIdentifier' in request_kwargs + assert request_kwargs['orgIdentifier'] == 'test_org' + assert 'projectIdentifier' not in request_kwargs + assert 'accountIdentifier' in request_kwargs + + def test_list_with_project_identifier_only(self, mocker): + """ + Test that project_identifier is NOT used for projects list endpoint. + Note: The projects endpoint does not support projectIdentifier as a query parameter. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account', project_identifier='test_project') + + # Mock empty response + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + + # Call list + pmc.list() + + # Verify the URL template does NOT contain projectIdentifier (projects endpoint doesn't support it) + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'projectIdentifier' not in url_template + assert 'orgIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + # Verify request_kwargs does NOT include projectIdentifier + request_kwargs = call_args[1] + assert 'projectIdentifier' not in request_kwargs + assert 'orgIdentifier' not in request_kwargs + assert 'accountIdentifier' in request_kwargs + + def test_list_with_both_identifiers(self, mocker): + """ + Test that when both org_identifier and project_identifier are set, + only orgIdentifier is in the URL (projects endpoint doesn't support projectIdentifier). + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + # Mock empty response + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + + # Call list + pmc.list() + + # Verify the URL template contains orgIdentifier but NOT projectIdentifier + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' not in url_template # Projects endpoint doesn't support this + assert 'accountIdentifier' in url_template + + # Verify request_kwargs includes orgIdentifier but NOT projectIdentifier + request_kwargs = call_args[1] + assert 'orgIdentifier' in request_kwargs + assert request_kwargs['orgIdentifier'] == 'test_org' + assert 'projectIdentifier' not in request_kwargs # Projects endpoint doesn't support this + assert 'accountIdentifier' in request_kwargs + + def test_list_with_method_override(self, mocker): + """ + Test that method parameters override instance variables when set to non-None values. + Note: Passing None as a method parameter does NOT override instance variables + (this is current behavior - if you want to unset, don't pass the parameter). + Note: project_identifier is NOT used for projects list endpoint. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + # Initialize with org_identifier but not project_identifier + pmc = HarnessProjectMicroClient(sc, 'test_account', org_identifier='default_org') + + # Mock empty response + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + + # Call list with method-level org_identifier override + pmc.list(org_identifier='method_org') + + # Verify the URL template contains orgIdentifier (from method override) but NOT projectIdentifier + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'projectIdentifier' not in url_template # Projects endpoint doesn't support this + assert 'orgIdentifier' in url_template # Uses method override + + # Verify request_kwargs + request_kwargs = call_args[1] + assert 'projectIdentifier' not in request_kwargs # Projects endpoint doesn't support this + assert 'orgIdentifier' in request_kwargs + assert request_kwargs['orgIdentifier'] == 'method_org' # From method override + + def test_get_without_org_and_project_identifiers(self, mocker): + """ + Test get method without org and project identifiers. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Mock response + SyncHttpClient.make_request.return_value = { + 'data': {'project': {'identifier': 'proj1', 'name': 'Project 1'}} + } + + # Call get + pmc.get('proj1') + + # Verify the URL template does not contain orgIdentifier or projectIdentifier query params + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + # Note: projectIdentifier is in the path, but not as a query param when not set + assert '&orgIdentifier' not in url_template + assert '&projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + # Verify request_kwargs + request_kwargs = call_args[1] + assert 'orgIdentifier' not in request_kwargs + # projectIdentifier is in request_kwargs as path parameter, but not as query param + assert 'accountIdentifier' in request_kwargs + + def test_create_with_org_identifier_only(self, mocker): + """ + Test create method with only org_identifier set. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account', org_identifier='test_org') + + # Mock response + SyncHttpClient.make_request.return_value = { + 'data': {'project': {'identifier': 'new_proj', 'name': 'New Project'}} + } + + # Call create + pmc.create({'name': 'New Project'}) + + # Verify the URL template contains orgIdentifier but not projectIdentifier + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' not in url_template + + # Verify request_kwargs + request_kwargs = call_args[1] + assert 'orgIdentifier' in request_kwargs + assert request_kwargs['orgIdentifier'] == 'test_org' + assert 'projectIdentifier' not in request_kwargs + + def test_service_account_list_with_both_identifiers(self, mocker): + """ + Test service account list with both identifiers to verify pattern works across microclients. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + # Mock response + SyncHttpClient.make_request.return_value = {'data': []} + + # Call list + samc.list() + + # Verify the URL template contains both + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + # Verify request_kwargs includes both + request_kwargs = call_args[1] + assert 'orgIdentifier' in request_kwargs + assert request_kwargs['orgIdentifier'] == 'test_org' + assert 'projectIdentifier' in request_kwargs + assert request_kwargs['projectIdentifier'] == 'test_project' + + def test_service_account_list_without_identifiers(self, mocker): + """ + Test service account list without identifiers. + """ + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + # Mock response + SyncHttpClient.make_request.return_value = {'data': []} + + # Call list + samc.list() + + # Verify the URL template does not contain orgIdentifier or projectIdentifier + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + # Verify request_kwargs + request_kwargs = call_args[1] + assert 'orgIdentifier' not in request_kwargs + assert 'projectIdentifier' not in request_kwargs + assert 'accountIdentifier' in request_kwargs + + def test_token_list_without_identifiers(self, mocker): + """Test token list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + tmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_token_list_with_both_identifiers(self, mocker): + """Test token list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + tmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_harness_apikey_list_without_identifiers(self, mocker): + """Test harness apikey list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': []} + akmc.list('parent1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_harness_apikey_list_with_both_identifiers(self, mocker): + """Test harness apikey list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': []} + akmc.list('parent1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_harness_user_list_without_identifiers(self, mocker): + """Test harness user list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + umc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_harness_user_list_with_both_identifiers(self, mocker): + """Test harness user list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + umc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_harness_user_get_without_identifiers(self, mocker): + """Test harness user get without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'user': {'uuid': 'user1', 'name': 'User 1'}}} + umc.get('user1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert '&orgIdentifier' not in url_template + assert '&projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_harness_group_list_without_identifiers(self, mocker): + """Test harness group list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + gmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_harness_group_list_with_both_identifiers(self, mocker): + """Test harness group list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + gmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_harness_group_get_without_identifiers(self, mocker): + """Test harness group get without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'identifier': 'group1', 'name': 'Group 1'}} + gmc.get('group1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert '&orgIdentifier' not in url_template + assert '&projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_role_list_without_identifiers(self, mocker): + """Test role list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + rmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_role_list_with_both_identifiers(self, mocker): + """Test role list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + rmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_role_get_without_identifiers(self, mocker): + """Test role get without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'role': {'identifier': 'role1', 'name': 'Role 1'}}} + rmc.get('role1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + # Role get endpoint doesn't have org/project in URL template + assert 'accountIdentifier' in url_template + + def test_resource_group_list_without_identifiers(self, mocker): + """Test resource group list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + rgmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_resource_group_list_with_both_identifiers(self, mocker): + """Test resource group list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + rgmc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_resource_group_get_without_identifiers(self, mocker): + """Test resource group get without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'resourceGroup': {'identifier': 'rg1', 'name': 'RG 1'}}} + rgmc.get('rg1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert '&orgIdentifier' not in url_template + assert '&projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_role_assignment_list_without_identifiers(self, mocker): + """Test role assignment list without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + ramc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' not in url_template + assert 'projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_role_assignment_list_with_both_identifiers(self, mocker): + """Test role assignment list with both identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account', org_identifier='test_org', project_identifier='test_project') + + SyncHttpClient.make_request.return_value = {'data': {'content': []}} + ramc.list() + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert 'orgIdentifier' in url_template + assert 'projectIdentifier' in url_template + + def test_role_assignment_get_without_identifiers(self, mocker): + """Test role assignment get without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'roleAssignment': {'identifier': 'ra1'}}} + ramc.get('ra1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert '&orgIdentifier' not in url_template + assert '&projectIdentifier' not in url_template + assert 'accountIdentifier' in url_template + + def test_service_account_get_without_identifiers(self, mocker): + """Test service account get without identifiers.""" + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + SyncHttpClient.make_request.return_value = {'data': {'serviceAccount': {'identifier': 'sa1', 'name': 'SA 1'}}} + samc.get('sa1') + + call_args = SyncHttpClient.make_request.call_args + endpoint = call_args[0][0] + url_template = endpoint['url_template'] + + assert '&orgIdentifier' not in url_template + assert 'accountIdentifier' in url_template + diff --git a/splitapiclient/tests/microclients/harness/token_microclient_test.py b/splitapiclient/tests/microclients/harness/token_microclient_test.py index 3ee6d33..b5be940 100644 --- a/splitapiclient/tests/microclients/harness/token_microclient_test.py +++ b/splitapiclient/tests/microclients/harness/token_microclient_test.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from splitapiclient.microclients.harness import TokenMicroClient from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient from splitapiclient.resources.harness import Token +from splitapiclient.tests.microclients.harness.conftest import FakeResponse class TestTokenMicroClient: @@ -71,13 +74,18 @@ def test_list(self, mocker): # Verify the make_request calls assert SyncHttpClient.make_request.call_count == 2 + + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = TokenMicroClient._endpoint['all_items'].copy() + expected_endpoint['url_template'] = '/ng/api/token/aggregate?apiKeyType=SERVICE_ACCOUNT&accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100' + SyncHttpClient.make_request.assert_any_call( - TokenMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=0 ) SyncHttpClient.make_request.assert_any_call( - TokenMicroClient._endpoint['all_items'], + expected_endpoint, accountIdentifier='test_account', pageIndex=1 ) @@ -135,7 +143,7 @@ def test_get(self, mocker): result = tmc.get('token2') # Verify the list method was called with the correct parameters - tmc.list.assert_called_once_with(account_identifier='test_account') + tmc.list.assert_called_once_with(account_identifier='test_account', org_identifier=None, project_identifier=None) # Verify the result assert isinstance(result, Token) @@ -171,8 +179,12 @@ def test_create(self, mocker): result = tmc.create(token_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = TokenMicroClient._endpoint['create'].copy() + expected_endpoint['url_template'] = '/ng/api/token?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - TokenMicroClient._endpoint['create'], + expected_endpoint, body=token_data, accountIdentifier='test_account' ) @@ -214,8 +226,12 @@ def test_update(self, mocker): result = tmc.update('token1', update_data) # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = TokenMicroClient._endpoint['update_token'].copy() + expected_endpoint['url_template'] = '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - TokenMicroClient._endpoint['update_token'], + expected_endpoint, body=update_data, tokenId='token1', accountIdentifier='test_account' @@ -247,8 +263,12 @@ def test_rotate(self, mocker): result = tmc.rotate('token1', 'parent1', 'api_key1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = TokenMicroClient._endpoint['rotate_token'].copy() + expected_endpoint['url_template'] = '/ng/api/token/rotate/{tokenId}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}&apiKeyIdentifier={apiKeyIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - TokenMicroClient._endpoint['rotate_token'], + expected_endpoint, tokenId='token1', parentIdentifier='parent1', apiKeyIdentifier='api_key1', @@ -273,11 +293,93 @@ def test_delete(self, mocker): result = tmc.delete('token1') # Verify the make_request call + # Create expected endpoint with modified URL template (orgIdentifier and projectIdentifier removed) + expected_endpoint = TokenMicroClient._endpoint['delete'].copy() + expected_endpoint['url_template'] = '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}' + SyncHttpClient.make_request.assert_called_once_with( - TokenMicroClient._endpoint['delete'], + expected_endpoint, tokenId='token1', accountIdentifier='test_account' ) # Verify the result assert result is True + + +class TestTokenURLGeneration: + """ + Tests that verify actual URL generation by mocking at the requests level. + These tests ensure that optional parameters (orgIdentifier, projectIdentifier) + are correctly included or excluded from the final URL. + """ + + # ========================================================================= + # LIST method URL tests + # ========================================================================= + + def test_list_url_without_optional_identifiers(self, mocker): + """Verify list URL doesn't contain orgIdentifier/projectIdentifier when not set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = TokenMicroClient(hc, 'test_account') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier' not in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_org_identifier_only(self, mocker): + """Verify list URL contains orgIdentifier when set, but not projectIdentifier""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = TokenMicroClient(hc, 'test_account', org_identifier='org1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier' not in called_url + + def test_list_url_with_both_identifiers(self, mocker): + """Verify list URL contains both orgIdentifier and projectIdentifier when set""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = TokenMicroClient(hc, 'test_account', org_identifier='org1', project_identifier='proj1') + client.list() + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=org1' in called_url + assert 'projectIdentifier=proj1' in called_url + + def test_list_url_with_method_override_identifiers(self, mocker): + """Verify list URL uses method parameters to override instance defaults""" + mock_get = mocker.patch('splitapiclient.http_clients.harness_client.requests.get') + mock_get.side_effect = [ + FakeResponse(200, json.dumps({'data': {'content': []}})), + ] + + hc = HarnessHttpClient('https://app.harness.io', 'test_token') + client = TokenMicroClient(hc, 'test_account', org_identifier='default_org', project_identifier='default_proj') + client.list(org_identifier='override_org', project_identifier='override_proj') + + called_url = mock_get.call_args_list[0][0][0] + assert 'accountIdentifier=test_account' in called_url + assert 'orgIdentifier=override_org' in called_url + assert 'projectIdentifier=override_proj' in called_url + assert 'default_org' not in called_url + assert 'default_proj' not in called_url diff --git a/splitapiclient/version.py b/splitapiclient/version.py index aa89269..3886da8 100644 --- a/splitapiclient/version.py +++ b/splitapiclient/version.py @@ -1 +1 @@ -__version__ = '3.5.7' +__version__ = '3.5.8'