diff --git a/CHANGES.txt b/CHANGES.txt index e40bf3c..5f27cd0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -39,3 +39,5 @@ - Updated keyId of an API KEY to be the actual ID and not the key itself 3.2.0 (Feb 2, 2025) - Updated to support flag sets, large segments and the impressionsDisabled boolean value +3.5.0 (May 6, 2025) +- Updated to support harness mode \ No newline at end of file diff --git a/README.md b/README.md index 4aa7d1e..d5e584d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,151 @@ For specific instructions on how to use Split Admin REST API refer to our [offic Full documentation on this Python wrapper is available in [this link](https://help.split.io/hc/en-us/articles/4412331052685-Python-PyPi-library-for-Split-REST-Admin-API). -### Quick setup +## 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 Feature Flags 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. + +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: + +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 + +### Base URLs and Endpoints + +- Existing, non-deprecated Split endpoints continue to use the Split base URLs +- New Harness-specific endpoints use the Harness base URL (https://app.harness.io/) + +### Deprecated Endpoints + +The following Split endpoints are deprecated and cannot be used in harness mode: +- `/workspaces`: POST, PATCH, DELETE, PUT verbs are deprecated +- `/apiKeys`: POST verb for apiKeyType == 'admin' is deprecated +- `/users`: all verbs are deprecated +- `/groups`: all verbs are deprecated +- `/restrictions`: all verbs are deprecated + +Non-deprecated endpoints will continue to function as they did before the migration. + +### Basic Usage + +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 +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 + 'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID' # Required for Harness operations +}) + +# Option 2: Use harness_token for all operations (if apikey is not provided) +client = get_client({ + 'harness_mode': True, + 'harness_token': 'YOUR_HARNESS_TOKEN', # Used for both Harness and Split endpoints + 'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID' +}) +``` + +### Working with Split Resources in Harness Mode + +You can still access standard Split resources with some restrictions: + +```python +# List workspaces (read-only in harness mode) +for ws in client.workspaces.list(): + print(f"Workspace: {ws.name}, Id: {ws.id}") + +# Find a specific workspace +ws = client.workspaces.find("Default") + +# List environments in a workspace +for env in client.environments.list(ws.id): + print(f"Environment: {env.name}, Id: {env.id}") +``` + +### Working with Harness-specific Resources + +Harness mode provides access to several Harness-specific resources through dedicated microclients: + +- token +- harness_apikey +- service_account +- harness_user +- harness_group +- role +- resource_group +- role_assignment +- harness_project + +Basic example: + +```python +# Account identifier is required for all Harness operations +account_id = 'YOUR_ACCOUNT_IDENTIFIER' + +# List all tokens +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) +for sa in service_accounts: + print(f"Service Account: {sa.name}, ID: {sa.id}") +``` + +For most creation, update, and delete endpoints for harness specific resources, you will need to pass through the JSON body directly. + +Example: +```python +# Create a new service account +sa_data = { + 'name': sa_name, + 'identifier': sa_identifier, + 'email': "test@harness.io", + 'accountIdentifier': account_id, + 'description': 'Service account for test', + 'tags': {'test': 'test tag'} +} + +new_sa = client.service_account.create(sa_data, account_id) +``` + +```python +# Add a user to a group +client.harness_user.add_user_to_groups(user.id, [group.id], account_id) +``` + + +For detailed examples and API specifications for each resource, please refer to the [Harness API documentation](https://apidocs.harness.io/). + +### Setting Default Account Identifier + +To avoid specifying the account identifier with every request: + +```python +# Set default account identifier when creating the client +client = get_client({ + 'harness_mode': True, + 'harness_token': 'YOUR_HARNESS_TOKEN', + 'account_identifier': 'YOUR_ACCOUNT_IDENTIFIER' +}) + +# Now you can make calls without specifying account_identifier in each request +tokens = client.token.list() # account_identifier is automatically included +projects = client.harness_project.list() # account_identifier is automatically included +``` + +## Quick Setup Install the splitapiclient: ``` @@ -15,77 +159,78 @@ pip install splitapiclient Import the client object and initialize connection using an Admin API Key: -``` +```python from splitapiclient.main import get_client client = get_client({'apikey': 'ADMIN API KEY'}) ``` - Enable optional logging: -``` +```python import logging logging.basicConfig(level=logging.DEBUG) ``` -**Workspaces** +## Standard Split API Usage + +### Workspaces Fetch all workspaces: -``` +```python for ws in client.workspaces.list(): - print ("\nWorkspace:"+ws.name+", Id: "+ws.id) + print("\nWorkspace:" + ws.name + ", Id: " + ws.id) ``` -Find a specific workspaces: +Find a specific workspace: -``` +```python ws = client.workspaces.find("Defaults") -print ("\nWorkspace:"+ws.name+", Id: "+ws.id) +print("\nWorkspace:" + ws.name + ", Id: " + ws.id) ``` -**Environments** +### Environments Fetch all Environments: -``` +```python ws = client.workspaces.find("Defaults") for env in client.environments.list(ws.id): - print ("\nEnvironment: "+env.name+", Id: "+env.id) + print("\nEnvironment: " + env.name + ", Id: " + env.id) ``` Add new environment: -``` +```python ws = client.workspaces.find("Defaults") -env = ws.add_environment({'name':'new_environment', 'production':False}) +env = ws.add_environment({'name': 'new_environment', 'production': False}) ``` -**Splits** +### Splits Fetch all Splits: -``` +```python ws = client.workspaces.find("Defaults") for split in client.splits.list(ws.id): - print ("\nSplit: "+split.name+", "+split._workspace_id) + print("\nSplit: " + split.name + ", " + split._workspace_id) ``` Add new Split: -``` -split = ws.add_split({'name':'split_name', 'description':'new UI feature'}, "user") +```python +split = ws.add_split({'name': 'split_name', 'description': 'new UI feature'}, "user") print(split.name) ``` Add tags: -``` +```python split.associate_tags(['my_new_tag', 'another_new_tag']) ``` Add Split to environment: -``` +```python ws = client.workspaces.find("Defaults") split = client.splits.find("new_feature", ws.id) env = client.environments.find("Production", ws.id) @@ -103,7 +248,7 @@ splitdef = split.add_to_environment(env.id, data) Kill Split: -``` +```python ws = client.workspaces.find("Defaults") env = client.environments.find("Production", ws.id) splitDef = client.split_definitions.find("new_feature", env.id, ws.id) @@ -112,22 +257,22 @@ splitDef.kill() Restore Split: -``` +```python splitDef.restore() ``` Fetch all Splits in an Environment: -``` +```python ws = client.workspaces.find("Defaults") env = client.environments.find("Production", ws.id) for spDef in client.split_definitions.list(env.id, ws.id): - print(spDef.name+str(spDef._default_rule)) + print(spDef.name + str(spDef._default_rule)) ``` Submit a Change request to update a Split definition: -``` +```python splitDef = client.split_definitions.find("new_feature", env.id, ws.id) definition= {"treatments":[ {"name":"on"},{"name":"off"}], "defaultTreatment":"off", "baselineTreatment": "off", @@ -139,100 +284,102 @@ splitDef.submit_change_request(definition, 'UPDATE', 'updating default rule', 'c List all change requests: -``` +```python for cr in client.change_requests.list(): if cr._split is not None: - print (cr._id+", "+cr._split['name']+", "+cr._title+", "+str(cr._split['environment']['id'])) + print(cr._id + ", " + cr._split['name'] + ", " + cr._title + ", " + str(cr._split['environment']['id'])) if cr._segment is not None: - print (cr._id+", "+cr._segment['name']+", "+cr._title) + print(cr._id + ", " + cr._segment['name'] + ", " + cr._title) ``` Approve specific change request: -``` +```python for cr in client.change_requests.list(): - if cr._split['name']=='new_feature': + if cr._split['name'] == 'new_feature': cr.update_status("APPROVED", "done") ``` -**Users and Groups** +### Users and Groups Fetch all Active users: -``` +```python for user in client.users.list('ACTIVE'): - print(user.email+", "+user._id) + print(user.email + ", " + user._id) ``` Invite new user: -``` +```python group = client.groups.find('Administrators') -userData = {'email':'user@email.com', 'groups':[{'id':'', 'type':'group'}]} +userData = {'email': 'user@email.com', 'groups': [{'id': '', 'type': 'group'}]} userData['groups'][0]['id'] = group._id client.users.invite_user(userData) ``` Delete a pending invite: -``` +```python for user in client.users.list('PENDING'): - print(user.email+", "+user._id+", "+user._status) + print(user.email + ", " + user._id + ", " + user._status) if user.email == 'user@email.com': client.users.delete(user._id) ``` Update user info: -``` -data = {'name':'new_name', 'email':'user@email.com', '2fa':False, 'status':'ACTIVE'} +```python +data = {'name': 'new_name', 'email': 'user@email.com', '2fa': False, 'status': 'ACTIVE'} user = client.users.find('user@email.com') user.update_user(user._id, data) ``` Fetch all Groups: -``` +```python for group in client.groups.list(): - print (group._id+", "+group._name) + print(group._id + ", " + group._name) ``` Create Group: -``` -client.groups.create_group({'name':'new_group', 'description':''}) +```python +client.groups.create_group({'name': 'new_group', 'description': ''}) ``` Delete Group: -``` +```python group = client.groups.find('new_group') client.groups.delete_group(group._id) ``` Replace existing user group: -``` +```python user = client.users.find('user@email.com') group = client.groups.find('Administrators') -data = [{'op': 'replace', 'path': '/groups/0', 'value': {'id': '', 'type':'group'}}] +data = [{'op': 'replace', 'path': '/groups/0', 'value': {'id': '', 'type': 'group'}}] data[0]['value']['id'] = group._id user.update_user_group(data) ``` Add user to new group -``` +```python user = client.users.find('user@email.com') group = client.groups.find('Administrators') -data = [{'op': 'add', 'path': '/groups/-', 'value': {'id': '', 'type':'group'}}] +data = [{'op': 'add', 'path': '/groups/-', 'value': {'id': '', 'type': 'group'}}] data[0]['value']['id'] = group._id user.update_user_group(data) ``` +## About Split + ### Commitment to Quality: -Split’s APIs are in active development and are constantly tested for quality. Unit tests are developed for each wrapper based on the unique needs of that language, and integration tests, load and performance tests, and behavior consistency tests are running 24/7 via automated bots. In addition, monitoring instrumentation ensures that these wrappers behave under the expected parameters of memory, CPU, and I/O. +Split's APIs are in active development and are constantly tested for quality. Unit tests are developed for each wrapper based on the unique needs of that language, and integration tests, load and performance tests, and behavior consistency tests are running 24/7 via automated bots. In addition, monitoring instrumentation ensures that these wrappers behave under the expected parameters of memory, CPU, and I/O. ### About Split: @@ -248,5 +395,4 @@ Visit [split.io/product](https://www.split.io/product) for an overview of Split, **System Status:** -We use a status page to monitor the availability of Split’s various services. You can check the current status at [status.split.io](http://status.split.io). - +We use a status page to monitor the availability of Split's various services. You can check the current status at [status.split.io](http://status.split.io). \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c493f3b..68b6eb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "splitapiclient" -version = "3.2.0" +version = "3.5.0" description = "This Python Library provide 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 = [ "Programming Language :: Python :: 3", diff --git a/splitapiclient/http_clients/harness_client.py b/splitapiclient/http_clients/harness_client.py new file mode 100644 index 0000000..cd5f5f9 --- /dev/null +++ b/splitapiclient/http_clients/harness_client.py @@ -0,0 +1,272 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +import json +import time +from functools import partial +import requests +from splitapiclient.http_clients import base_client +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.exceptions import HTTPResponseError, \ + HTTPNotFoundError, HTTPIncorrectParametersError, HTTPUnauthorizedError, \ + SplitBackendUnreachableError, HarnessDeprecatedEndpointError, MissingParametersException + + +class HarnessHttpClient(base_client.BaseHttpClient): + ''' + Harness mode HTTP client. + This client will block on every http request until a response is received. + It will also enforce restrictions on deprecated endpoints in harness mode. + ''' + + # List of deprecated endpoints in harness mode + DEPRECATED_ENDPOINTS = { + 'workspaces': ['POST', 'PATCH', 'DELETE', 'PUT'], + 'apiKeys': { + 'admin': ['POST'] + }, + 'users': ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'], + 'groups': ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'], + 'restrictions': ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'] + } + + def __init__(self, baseurl, auth_token): + ''' + Class constructor. Stores basic connection information. + + :param baseurl: string. Harness host and base url. + :param auth_token: string. Harness authentication token needed to make API calls. + ''' + # Initialize with empty base_args - we'll handle auth differently in harness mode + self.config = { + 'base_url': baseurl, + 'base_args': {} + } + # Store the auth token + self._auth_token = auth_token + + def setup_method(self, method, body=None): + ''' + Wraps 'requests' module functions by partially applying the body + parameter when needed to provide a standardized interface. + + :param method: string. GET | POST | PATCH | PUT | DELETE + :param body: object/list. Body for methods that use it. + + :rtype: function + ''' + methods = { + 'GET': requests.get, + 'POST': partial(requests.post, json=body), + 'PUT': partial(requests.put, json=body), + 'PATCH': partial(requests.patch, json=body), + 'DELETE': requests.delete + } + + return methods[method] + + def _is_deprecated_endpoint(self, endpoint, body=None): + ''' + Checks if the endpoint is deprecated in harness mode. + + :param endpoint: dict. Endpoint description. + :param body: dict/list. Request body. + + :return: bool. True if the endpoint is deprecated, False otherwise. + ''' + url_template = endpoint['url_template'] + method = endpoint['method'] + + # Check for workspaces endpoint + if url_template.startswith('workspaces') and method in self.DEPRECATED_ENDPOINTS['workspaces']: + return True + + # Check for apiKeys endpoint with admin type + if url_template.startswith('apiKeys') and method in self.DEPRECATED_ENDPOINTS['apiKeys']['admin']: + if body and body.get('apiKeyType') == 'admin': + return True + + # Check for users endpoint + if url_template.startswith('users'): + return True + + # Check for groups endpoint + if url_template.startswith('groups'): + return True + + # Check for restrictions endpoint + if url_template.startswith('restrictions'): + return True + + return False + + def _is_harness_endpoint(self, endpoint): + ''' + Determines if the endpoint is a Harness-specific endpoint based on the base URL. + + :param endpoint: dict. Endpoint description. + :return: bool. True if the endpoint is a Harness endpoint, False otherwise. + ''' + # In the harness client, we can determine if it's a Harness endpoint by checking the base URL + # Split API base URLs are typically api.split.io + # Harness API base URLs are typically app.harness.io + return 'app.harness.io' in self.config['base_url'] + + def _handle_invalid_response(self, response): + ''' + Handle responses that are not okay and throw an appropriate exception. + If the code doesn't match the known ones, a generic HTTPResponseError + is thrown + + :param response: requests' module response object + ''' + status_codes_exceptions = { + 404: HTTPNotFoundError, + 401: HTTPUnauthorizedError, + 400: HTTPIncorrectParametersError, + } + + exc = status_codes_exceptions.get(response.status_code) + if exc: + raise exc(response.text) + else: + raise HTTPResponseError(response.text) + + def _handle_connection_error(self, e): + ''' + Handle error when attempting to connect to split backend. + Logs exception thrown by requests module, and raises an + SplitBackendUnreachableError error, so that it can be caught + by using the top level SplitException + ''' + LOGGER.debug(e) + raise SplitBackendUnreachableError( + 'Unable to reach Harness backend' + ) + + @staticmethod + def validate_params(endpoint, all_arguments): + ''' + Override the base client validation to handle harness mode authentication. + In harness mode, we use x-api-key instead of Authorization, so we need to + modify the validation logic to not require the Authorization header. + + :param endpoint: dict. Endpoint description + :param all_arguments: Parameter values + + :rtype: None + ''' + # Get required parameters from URL template and query string + url_params = base_client.BaseHttpClient.get_params_from_url_template(endpoint['url_template']) + query_params = [i['name'] for i in endpoint['query_string'] if i['required']] + + # Add required headers, but exclude 'Authorization' since we're using 'x-api-key' in harness mode + header_params = [] + for header in endpoint['headers']: + if header['required'] and header['name'] != 'Authorization': + header_params.append(header['name']) + + # Combine all required parameters + required_params = url_params + header_params + query_params + + # Check if any required parameters are missing + missing = [p for p in required_params if p not in all_arguments] + + if missing: + raise MissingParametersException( + 'The following required parameters are missing: {missing}' + .format(missing=', '.join(missing)) + ) + + def _setup_headers(self, endpoint, params): + ''' + Override the base client _setup_headers method to handle harness mode authentication. + In harness mode, we need to skip 'Authorization' headers and use 'x-api-key' instead. + + :param endpoint: dict. Endpoint description + :param params: dict. List of parameter values + + :rtype: dict. + ''' + # Set up required headers except 'Authorization' + headers = {} + for header in endpoint['headers']: + if header.get('required', False): + # Skip 'Authorization' header in harness mode + if header['name'] == 'Authorization': + continue + if header['name'] in params: + headers[header['name']] = base_client.BaseHttpClient._process_single_header( + header, params[header['name']] + ) + + # Add optional headers + headers.update({ + header['name']: base_client.BaseHttpClient._process_single_header( + header, params[header['name']] + ) + for header in endpoint['headers'] + if (not header.get('required', False)) and header['name'] in params + }) + + # Add x-api-key header + if 'x-api-key' in params: + headers['x-api-key'] = params['x-api-key'] + + return headers + + def make_request(self, endpoint, body=None, **kwargs): + ''' + This method delegates building of headers, url and querystring (!) + to separate functions and then calls the appropriate method of the + requests module with the required arguments. Logs plenty of debug data, + and raises an exception if the response code is not 200. + + :param endpoint: dict. Endpoint description (url, headers, qs, etc). + :param body: list/dict. Body used for POST/PATCH/PUT requests + :param kwargs: dict. Extra arguments (values). + + :rtype: dict/list/None + ''' + # Check if the endpoint is deprecated in harness mode + if self._is_deprecated_endpoint(endpoint, body): + raise HarnessDeprecatedEndpointError( + f"Endpoint {endpoint['url_template']} with method {endpoint['method']} is deprecated in harness mode" + ) + + # In harness mode, use x-api-key header for all endpoints + kwargs['x-api-key'] = self._auth_token + + kwargs.update(self.config['base_args']) + self.validate_params(endpoint, kwargs) + + url = self._setup_url(endpoint, kwargs) + headers = self._setup_headers(endpoint, kwargs) + method_name = endpoint['method'] + method = self.setup_method(method_name, body) + + LOGGER.debug('{method} {url}'.format(method=method_name, url=url)) + LOGGER.debug('HEADERS: {headers}'.format(headers=headers)) + if body: + LOGGER.debug('BODY: ' + json.dumps(body)) + + # Make the actual HTTP call! + while True: + try: + response = method(url, headers=headers) + LOGGER.debug('RESPONSE: ' + response.text) + except Exception as e: + return self._handle_connection_error(e) + if response.status_code==429: + LOGGER.warning('RESPONSE CODE: %s, retrying in 5 seconds' % response.status_code) + time.sleep(5) + continue + else: + break + + if not (response.status_code == 200 or response.status_code == 204 or response.status_code == 201): + LOGGER.warning('RESPONSE CODE: %s' % response.status_code) + self._handle_invalid_response(response) + + if endpoint.get('response', False): + if response.status_code != 204: + return json.loads(response.text) diff --git a/splitapiclient/main/__init__.py b/splitapiclient/main/__init__.py index f30f960..8e32aa7 100644 --- a/splitapiclient/main/__init__.py +++ b/splitapiclient/main/__init__.py @@ -1,12 +1,30 @@ from splitapiclient.main.sync_apiclient import SyncApiClient +from splitapiclient.main.harness_apiclient import HarnessApiClient def get_client(config): ''' Entry point for the Split API client + + :param config: Dictionary containing client configuration options + For standard mode: + - 'apikey': Split API key for authentication + - 'base_url': (optional) Base URL for the Split API + - 'base_url_v3': (optional) Base URL for the Split API v3 + - 'async': (optional) Whether to use async client (not yet implemented) + + For harness mode: + - 'harness_mode': Set to True to use harness mode + - 'harness_token': Harness authentication token for x-api-key header + - 'account_identifier': (optional) Account identifier for Harness operations + - 'base_url': (optional) Base URL for the Harness API ''' _async = config.get('async', False) if _async: raise Exception('Async client not yet implemented') + + # Check if harness mode is enabled + if config.get('harness_mode', False): + return HarnessApiClient(config) return SyncApiClient(config) diff --git a/splitapiclient/main/harness_apiclient.py b/splitapiclient/main/harness_apiclient.py new file mode 100644 index 0000000..24250f6 --- /dev/null +++ b/splitapiclient/main/harness_apiclient.py @@ -0,0 +1,219 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.main.apiclient import BaseApiClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient +from splitapiclient.util.exceptions import InsufficientConfigArgumentsException +from splitapiclient.microclients import TrafficTypeMicroClient +from splitapiclient.microclients import EnvironmentMicroClient +from splitapiclient.microclients import SplitMicroClient +from splitapiclient.microclients import SplitDefinitionMicroClient +from splitapiclient.microclients import SegmentMicroClient +from splitapiclient.microclients import SegmentDefinitionMicroClient +from splitapiclient.microclients import WorkspaceMicroClient +from splitapiclient.microclients import IdentityMicroClient +from splitapiclient.microclients import AttributeMicroClient +from splitapiclient.microclients import ChangeRequestMicroClient +from splitapiclient.microclients import APIKeyMicroClient +from splitapiclient.microclients import FlagSetMicroClient +from splitapiclient.microclients import LargeSegmentMicroClient +from splitapiclient.microclients import LargeSegmentDefinitionMicroClient +from splitapiclient.microclients.harness import TokenMicroClient +from splitapiclient.microclients.harness import HarnessApiKeyMicroClient +from splitapiclient.microclients.harness import ServiceAccountMicroClient +from splitapiclient.microclients.harness import HarnessUserMicroClient +from splitapiclient.microclients.harness import HarnessGroupMicroClient +from splitapiclient.microclients.harness import RoleMicroClient +from splitapiclient.microclients.harness import ResourceGroupMicroClient +from splitapiclient.microclients.harness import RoleAssignmentMicroClient +from splitapiclient.microclients.harness import HarnessProjectMicroClient + + +class HarnessApiClient(BaseApiClient): + ''' + Harness mode Split API client + ''' + # Split base URLs for existing endpoints + BASE_PROD_URL_V3 = 'https://api.split.io/api/v3' + BASE_PROD_URL = 'https://api.split.io/internal/api/v2' + BASE_PROD_URL_OLD = 'https://api.split.io/internal/api/v1' + + # Harness base URL for Harness-specific endpoints + BASE_HARNESS_URL = 'https://app.harness.io/' + + 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 + - '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) + ''' + # Set up Split API base URLs for existing endpoints + if 'base_url' in config: + self._base_url = config['base_url'] + else: + self._base_url = self.BASE_PROD_URL + self._base_url_old = self.BASE_PROD_URL_OLD + + if 'base_url_v3' in config: + self._base_url_v3 = config['base_url_v3'] + else: + self._base_url_v3 = self.BASE_PROD_URL_V3 + + # Set up Harness API base URL for Harness-specific endpoints + if 'harness_base_url' in config: + self._harness_base_url = config['harness_base_url'] + 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: + raise InsufficientConfigArgumentsException( + 'At least one of the following keys must be present in the config dict for harness mode: apikey, harness_token' + ) + + # Set up authentication tokens + self._apikey = config.get('apikey') + self._harness_token = config.get('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 + self._account_identifier = config.get('account_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 client for Harness endpoints + harness_http_client = HarnessHttpClient(self._harness_base_url, harness_auth_token) + + # Standard microclients using Split endpoints + self._environment_client = EnvironmentMicroClient(split_http_client) + self._split_client = SplitMicroClient(split_http_client) + self._split_definition_client = SplitDefinitionMicroClient(split_http_client) + self._segment_client = SegmentMicroClient(split_http_client) + self._segment_definition_client = SegmentDefinitionMicroClient(split_http_client) + self._large_segment_client = LargeSegmentMicroClient(split_http_client) + self._large_segment_definition_client = LargeSegmentDefinitionMicroClient(split_http_client) + self._workspace_client = WorkspaceMicroClient(split_http_client) + self._traffic_type_client = TrafficTypeMicroClient(split_http_client) + self._attribute_client = AttributeMicroClient(split_http_client) + self._identity_client = IdentityMicroClient(split_http_client) + self._change_request_client = ChangeRequestMicroClient(split_http_client) + self._apikey_client = APIKeyMicroClient(split_http_client) + 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) + + @property + def traffic_types(self): + return self._traffic_type_client + + @property + def environments(self): + return self._environment_client + + @property + def splits(self): + return self._split_client + + @property + def split_definitions(self): + return self._split_definition_client + + @property + def segments(self): + return self._segment_client + + @property + def segment_definitions(self): + return self._segment_definition_client + + @property + def large_segments(self): + return self._large_segment_client + + @property + def large_segment_definitions(self): + return self._large_segment_definition_client + + @property + def workspaces(self): + return self._workspace_client + + @property + def attributes(self): + return self._attribute_client + + @property + def identities(self): + return self._identity_client + + @property + def change_requests(self): + return self._change_request_client + + @property + def apikeys(self): + return self._apikey_client + + @property + def flag_sets(self): + return self._flag_set_client + + # Harness-specific properties + + @property + def token(self): + return self._token_client + + @property + def harness_apikey(self): + return self._harness_apikey_client + + @property + def service_account(self): + return self._service_account_client + + @property + def harness_user(self): + return self._harness_user_client + + @property + def harness_group(self): + return self._harness_group_client + + @property + def role(self): + return self._role_client + + @property + def resource_group(self): + return self._resource_group_client + + @property + def role_assignment(self): + return self._role_assignment_client + + @property + def harness_project(self): + return self._harness_project_client diff --git a/splitapiclient/microclients/harness/__init__.py b/splitapiclient/microclients/harness/__init__.py new file mode 100644 index 0000000..effbbc7 --- /dev/null +++ b/splitapiclient/microclients/harness/__init__.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness.token_microclient import TokenMicroClient +from splitapiclient.microclients.harness.harness_apikey_microclient import HarnessApiKeyMicroClient +from splitapiclient.microclients.harness.service_account_microclient import ServiceAccountMicroClient +from splitapiclient.microclients.harness.harness_user_microclient import HarnessUserMicroClient +from splitapiclient.microclients.harness.harness_group_microclient import HarnessGroupMicroClient +from splitapiclient.microclients.harness.role_microclient import RoleMicroClient +from splitapiclient.microclients.harness.resource_group_microclient import ResourceGroupMicroClient +from splitapiclient.microclients.harness.role_assignment_microclient import RoleAssignmentMicroClient +from splitapiclient.microclients.harness.harness_project_microclient import HarnessProjectMicroClient + +__all__ = [ + 'TokenMicroClient', + 'HarnessApiKeyMicroClient', + 'ServiceAccountMicroClient', + 'HarnessUserMicroClient', + 'HarnessGroupMicroClient', + 'RoleMicroClient', + 'ResourceGroupMicroClient', + 'RoleAssignmentMicroClient', + 'HarnessProjectMicroClient' +] diff --git a/splitapiclient/microclients/harness/harness_apikey_microclient.py b/splitapiclient/microclients/harness/harness_apikey_microclient.py new file mode 100644 index 0000000..db5edfe --- /dev/null +++ b/splitapiclient/microclients/harness/harness_apikey_microclient.py @@ -0,0 +1,203 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import HarnessApiKey +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class HarnessApiKeyMicroClient: + ''' + Microclient for managing Harness API keys + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/ng/api/apikey?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get_apikey': { + 'method': 'GET', + 'url_template': '/ng/api/apikey/aggregate/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/ng/api/apikey?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'add_permissions': { + 'method': 'POST', + 'url_template': '/ng/api/roleassignments?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/ng/api/apikey/{apiKeyIdentifier}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, parent_identifier=None, account_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 + :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") + + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + accountIdentifier=account_id, + parentIdentifier=parent_identifier or "" + ) + LOGGER.debug('Response: %s', response) + return [HarnessApiKey(item, self._http_client) for item in response.get('data', [])] + except HTTPResponseError as e: + 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): + ''' + 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 + :returns: HarnessApiKey object + :rtype: HarnessApiKey + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['get_apikey'], + apiKeyIdentifier=apikey_id, + accountIdentifier=account_id, + parentIdentifier=parent_identifier or "" + ) + 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): + ''' + 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 + :returns: newly created API key + :rtype: HarnessApiKey + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=apikey_data, + accountIdentifier=account_id + ) + LOGGER.debug('Response: %s', response) + if(response.get('data')): + return HarnessApiKey(response.get('data'), self._http_client) + return None + + + def add_permissions(self, apikey_id, permissions, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['add_permissions'], + body=permissions, + apiKeyIdentifier=apikey_id, + accountIdentifier=account_id + ) + LOGGER.debug('Response: %s', response) + return True + + def delete(self, apikey_id, parent_identifier=None, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['delete'], + apiKeyIdentifier=apikey_id, + accountIdentifier=account_id, + parentIdentifier=parent_identifier or "" + ) + return True diff --git a/splitapiclient/microclients/harness/harness_group_microclient.py b/splitapiclient/microclients/harness/harness_group_microclient.py new file mode 100644 index 0000000..29a7e4a --- /dev/null +++ b/splitapiclient/microclients/harness/harness_group_microclient.py @@ -0,0 +1,193 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import HarnessGroup +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class HarnessGroupMicroClient: + ''' + Microclient for managing Harness groups + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get_group': { + 'method': 'GET', + 'url_template': '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PATCH', + 'url_template': '/ng/api/user-groups?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/ng/api/user-groups/{groupIdentifier}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of HarnessGroup objects. + + :param account_identifier: Account identifier to use for this request, overrides the default + :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") + + page_index = 0 + final_list = [] + while True: + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + pageIndex=page_index, + accountIdentifier=account_id + ) + content = response.get('data', {}).get('content', []) + if not content: + break + + final_list.extend(content) + page_index += 1 + except HTTPResponseError: + # Break out of the loop if there's an HTTP error with the request + break + + return [HarnessGroup(item, self._http_client) for item in final_list] + + def get(self, group_identifier, account_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 + :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") + + response = self._http_client.make_request( + self._endpoint['get_group'], + groupIdentifier=group_identifier, + accountIdentifier=account_id + ) + return HarnessGroup(response, self._http_client) + + def create(self, group_data, account_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 + :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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=group_data, + accountIdentifier=account_id + ) + return HarnessGroup(response['data'], self._http_client) + + def update(self, update_data, account_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 + :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") + + response = self._http_client.make_request( + self._endpoint['update'], + body=update_data, + accountIdentifier=account_id + ) + return HarnessGroup(response, self._http_client) + + def delete(self, group_identifier, account_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 + :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") + + self._http_client.make_request( + self._endpoint['delete'], + groupIdentifier=group_identifier, + accountIdentifier=account_id + ) + return True diff --git a/splitapiclient/microclients/harness/harness_project_microclient.py b/splitapiclient/microclients/harness/harness_project_microclient.py new file mode 100644 index 0000000..04fe326 --- /dev/null +++ b/splitapiclient/microclients/harness/harness_project_microclient.py @@ -0,0 +1,232 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import HarnessProject +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class HarnessProjectMicroClient: + ''' + Microclient for managing Harness projects + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/ng/api/projects?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=50', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get': { + 'method': 'GET', + 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/ng/api/projects?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PUT', + 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/ng/api/projects/{projectIdentifier}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of HarnessProject objects. + + :param account_identifier: Account 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") + + page_index = 0 + final_list = [] + last_json = None + total_projects_seen = 0 + while True: + try: + + response = self._http_client.make_request( + self._endpoint['all_items'], + pageIndex=page_index, + accountIdentifier=account_id + ) + + data = response.get('data', {}) + + # Convert to JSON string for deep comparison + import json + current_json = json.dumps(data, sort_keys=True) + + # If we get the same response twice, we're in a loop + if current_json == last_json: + break + + last_json = current_json + + content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] + content = [] + for item in content_obj: + if isinstance(item, dict) and 'project' in item: + content.append(item['project']) + + # Also break if we get empty content + if not content: + break + + final_list.extend(content) + total_projects_seen += len(content) + + + # Get pagination info if available + total_elements = data.get('totalElements', 0) + total_pages = data.get('totalPages', 0) + if total_elements and total_pages: + # If we've seen all pages according to API, break + if page_index >= total_pages - 1: # -1 because pages are 0-indexed + break + + page_index += 1 + + except HTTPResponseError as e: + LOGGER.error(f"HTTP error fetching projects: {str(e)}") + break # Break the loop on HTTP error + except Exception as e: + LOGGER.error(f"Error fetching projects: {str(e)}") + break # Break the loop on any other error + + return [HarnessProject(item, self._http_client) for item in final_list] + + def get(self, project_identifier, account_identifier=None): + ''' + Get a specific project by ID + + :param project_identifier: ID of the project to retrieve + :param account_identifier: Account 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") + + response = self._http_client.make_request( + self._endpoint['get'], + projectIdentifier=project_identifier, + accountIdentifier=account_id + ) + return HarnessProject(response.get('data', {}).get('project', {}), self._http_client) + + def create(self, project_data, account_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 + :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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=project_data, + accountIdentifier=account_id + ) + return HarnessProject(response.get('data', {}).get('project', {}), self._http_client) + + def update(self, project_identifier, project_data, account_identifier=None): + ''' + Update an existing project + + :param project_identifier: ID of the project to update + :param project_data: Dictionary containing updated project data + :param account_identifier: Account 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") + + response = self._http_client.make_request( + self._endpoint['update'], + projectIdentifier=project_identifier, + accountIdentifier=account_id, + body=project_data + ) + return HarnessProject(response.get('data', {}).get('project', {}), self._http_client) + + def delete(self, project_identifier, account_identifier=None): + ''' + Delete a project + + :param project_identifier: ID of the project to delete + :param account_identifier: Account 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") + + self._http_client.make_request( + self._endpoint['delete'], + projectIdentifier=project_identifier, + accountIdentifier=account_id + ) + return True diff --git a/splitapiclient/microclients/harness/harness_user_microclient.py b/splitapiclient/microclients/harness/harness_user_microclient.py new file mode 100644 index 0000000..8be6dbf --- /dev/null +++ b/splitapiclient/microclients/harness/harness_user_microclient.py @@ -0,0 +1,274 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import HarnessUser, HarnessInvite +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class HarnessUserMicroClient: + ''' + Microclient for managing Harness users + ''' + _endpoint = { + 'all_items': { + 'method': 'POST', # yes this is really a post for getting users + 'url_template': '/ng/api/user/aggregate?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get_user': { + 'method': 'GET', + 'url_template': '/ng/api/user/aggregate/{userId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'invite': { + 'method': 'POST', + 'url_template': '/ng/api/user/users?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PUT', + 'url_template': '/ng/api/user/{userId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'add_user_to_groups': { + 'method': 'PUT', + 'url_template': '/ng/api/user/add-user-to-groups/{userId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete_pending': { + 'method': 'DELETE', + 'url_template': '/ng/api/invites/{inviteId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'list_pending': { + 'method': 'POST', # yes this is also really a POST + 'url_template': '/ng/api/invites/aggregate?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of HarnessUser objects. + + :param account_identifier: Account 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") + + page_index = 0 + final_list = [] + while True: + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + pageIndex=page_index, + accountIdentifier=account_id + ) + content = response.get('data', {}).get('content', []) + if not content: + break + + final_list.extend([HarnessUser(item['user'], self._http_client) for item in content]) + page_index += 1 + except HTTPResponseError: + # Break out of the loop if there's an HTTP error with the request + break + + return final_list + + def get(self, user_id, account_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 + :returns: HarnessUser object + :rtype: HarnessUser + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['get_user'], + userId=user_id, + accountIdentifier=account_id + ) + return HarnessUser(response, self._http_client) + + def invite(self, user_data, account_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 + :returns: newly invited user + :rtype: HarnessUser + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['invite'], + body=user_data, + accountIdentifier=account_id + ) + return True + + def update(self, user_id, update_data, account_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 + :returns: updated user + :rtype: HarnessUser + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['update'], + body=update_data, + userId=user_id, + accountIdentifier=account_id + ) + return HarnessUser(response.get('data', {}), self._http_client) + + def add_user_to_groups(self, user_id, group_ids, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['add_user_to_groups'], + body={"userGroupIdsToAdd": group_ids}, + userId=user_id, + accountIdentifier=account_id + ) + return True + + def delete_pending(self, invite_id, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['delete_pending'], + inviteId=invite_id, + accountIdentifier=account_id + ) + return True + + def list_pending(self, account_identifier=None): + ''' + Returns a list of pending users. + + :param account_identifier: Account identifier to use for this request, overrides the default + :returns: list of pending users + :rtype: list(HarnessUser) + ''' + # Use the provided account_identifier or fall back to the default + 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") + + page_index = 0 + final_list = [] + while True: + response = self._http_client.make_request( + self._endpoint['list_pending'], + pageIndex=page_index, + accountIdentifier=account_id + ) + content = response.get('data', {}).get('content', []) + if not content: + break + + final_list.extend([HarnessInvite(item, self._http_client) for item in content]) + page_index += 1 + + return final_list diff --git a/splitapiclient/microclients/harness/resource_group_microclient.py b/splitapiclient/microclients/harness/resource_group_microclient.py new file mode 100644 index 0000000..ce7be23 --- /dev/null +++ b/splitapiclient/microclients/harness/resource_group_microclient.py @@ -0,0 +1,206 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import ResourceGroup +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class ResourceGroupMicroClient: + ''' + Microclient for managing Harness resource groups + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/resourcegroup/api/v2/resourceGroup?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get_resource_group': { + 'method': 'GET', + 'url_template': '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/resourcegroup/api/v2/resourceGroup?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PUT', + 'url_template': '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/resourcegroup/api/v2/resourceGroup/{resourceGroupId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of ResourceGroup objects. + + :param account_identifier: Account 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") + + page_index = 0 + final_list = [] + while True: + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + accountIdentifier=account_id, + pageIndex=page_index + ) + data = response.get('data', {}) + content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] + content = [] + for item in content_obj: + if isinstance(item, dict) and 'resourceGroup' in item: + content.append(item['resourceGroup']) + + final_list.extend(content) + if not content: + break + page_index += 1 + except HTTPResponseError: + # Break out of the loop if there's an HTTP error with the request + break + + return [ResourceGroup(item, self._http_client) for item in final_list] + + def get(self, resource_group_id, account_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 + :returns: ResourceGroup object + :rtype: ResourceGroup + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['get_resource_group'], + resourceGroupId=resource_group_id, + accountIdentifier=account_id + ) + return ResourceGroup(response.get('data', {}).get('resourceGroup', {}), self._http_client) + + def create(self, resource_group_data, account_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 + :returns: newly created resource group + :rtype: ResourceGroup + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=resource_group_data, + accountIdentifier=account_id + ) + resourceGroup = response.get('data', {}).get('resourceGroup', {}) + return ResourceGroup(resourceGroup, self._http_client) + + def update(self, resource_group_id, update_data, account_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 + :returns: updated resource group + :rtype: ResourceGroup + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['update'], + body=update_data, + resourceGroupId=resource_group_id, + accountIdentifier=account_id + ) + resourceGroup = response.get('data', {}).get('resourceGroup', {}) + return ResourceGroup(resourceGroup, self._http_client) + + def delete(self, resource_group_id, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['delete'], + resourceGroupId=resource_group_id, + accountIdentifier=account_id + ) + return True diff --git a/splitapiclient/microclients/harness/role_assignment_microclient.py b/splitapiclient/microclients/harness/role_assignment_microclient.py new file mode 100644 index 0000000..2e6bee9 --- /dev/null +++ b/splitapiclient/microclients/harness/role_assignment_microclient.py @@ -0,0 +1,170 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import RoleAssignment +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class RoleAssignmentMicroClient: + ''' + Microclient for managing Harness role assignments + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/authz/api/roleAssignments?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get_role_assignment': { + 'method': 'GET', + 'url_template': '/authz/api/roleAssignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/authz/api/roleAssignments?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/authz/api/roleAssignments/{roleAssignmentId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of RoleAssignment objects. + + :param account_identifier: Account 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") + page_index = 0 + final_list = [] + while True: + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + accountIdentifier=account_id, + pageIndex=page_index + ) + data = response.get('data', {}) + content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] + content = [] + for item in content_obj: + if isinstance(item, dict) and 'roleAssignment' in item: + content.append(item['roleAssignment']) + + final_list.extend(content) + if not content: + break + page_index += 1 + except HTTPResponseError: + # Break out of the loop if there's an HTTP error with the request + break + + return [RoleAssignment(item, self._http_client) for item in final_list] + + def get(self, role_assignment_id, account_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 + :returns: RoleAssignment object + :rtype: RoleAssignment + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['get_role_assignment'], + roleAssignmentId=role_assignment_id, + accountIdentifier=account_id + ) + return RoleAssignment(response.get('data', {}).get('roleAssignment', {}), self._http_client) + + def create(self, role_assignment_data, account_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 + :returns: newly created role assignment + :rtype: RoleAssignment + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=role_assignment_data, + accountIdentifier=account_id + ) + return RoleAssignment(response.get('data', {}).get('roleAssignment', {}), self._http_client) + + + def delete(self, role_assignment_id, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['delete'], + roleAssignmentId=role_assignment_id, + accountIdentifier=account_id + ) + return True diff --git a/splitapiclient/microclients/harness/role_microclient.py b/splitapiclient/microclients/harness/role_microclient.py new file mode 100644 index 0000000..92a11a9 --- /dev/null +++ b/splitapiclient/microclients/harness/role_microclient.py @@ -0,0 +1,204 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import Role +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class RoleMicroClient: + ''' + Microclient for managing Harness roles + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/authz/api/roles?accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'get_role': { + 'method': 'GET', + 'url_template': '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/authz/api/roles?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PUT', + 'url_template': '/authz/api/roles/{roleId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': 'roles/{roleId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of Role objects. + + :param account_identifier: Account 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") + + page_index = 0 + final_list = [] + while True: + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + accountIdentifier=account_id, + pageIndex=page_index + ) + data = response.get('data', {}) + content_obj = data.get('content', []) if isinstance(data.get('content'), list) else [] + content = [] + for item in content_obj: + if isinstance(item, dict) and 'role' in item: + content.append(item['role']) + + final_list.extend(content) + if not content: + break + page_index += 1 + except HTTPResponseError: + # Break out of the loop if there's an HTTP error with the request + break + + return [Role(item, self._http_client) for item in final_list] + + def get(self, role_id, account_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 + :returns: Role object + :rtype: Role + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['get_role'], + roleId=role_id, + accountIdentifier=account_id + ) + return Role(response.get('data', {}).get('role', {}), self._http_client) + + def create(self, role_data, account_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 + :returns: newly created role + :rtype: Role + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=role_data, + accountIdentifier=account_id + ) + return Role(response.get('data', {}).get('role', {}), self._http_client) + + def update(self, role_id, update_data, account_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 + :returns: updated role + :rtype: Role + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['update'], + body=update_data, + roleId=role_id, + accountIdentifier=account_id + ) + return Role(response.get('data', {}).get('role', {}), self._http_client) + + def delete(self, role_id, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['delete'], + roleId=role_id, + accountIdentifier=account_id + ) + return True diff --git a/splitapiclient/microclients/harness/service_account_microclient.py b/splitapiclient/microclients/harness/service_account_microclient.py new file mode 100644 index 0000000..f5b07b9 --- /dev/null +++ b/splitapiclient/microclients/harness/service_account_microclient.py @@ -0,0 +1,198 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import ServiceAccount +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class ServiceAccountMicroClient: + ''' + Microclient for managing Harness service accounts + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'item': { + 'method': 'GET', + 'url_template': '/ng/api/serviceaccount/aggregate/{serviceAccountId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/ng/api/serviceaccount?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PUT', + 'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of ServiceAccount objects. + + :param account_identifier: Account 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") + + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + accountIdentifier=account_id, + ) + data = response.get('data', []) + return [ServiceAccount(item, self._http_client) for item in data] + except HTTPResponseError as e: + 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): + ''' + 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 + :returns: ServiceAccount object + :rtype: ServiceAccount + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['item'], + serviceAccountId=service_account_id, + accountIdentifier=account_id + ) + + # Handle different response formats + data = response.get('data', {}) + + return ServiceAccount(data['serviceAccount'], self._http_client) + + + def create(self, service_account_data, account_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 + :returns: newly created service account + :rtype: ServiceAccount + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=service_account_data, + accountIdentifier=account_id + ) + + return ServiceAccount(response.get('data', {}), self._http_client) + + def update(self, service_account_id, update_data, account_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 + :returns: updated service account + :rtype: ServiceAccount + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['update'], + body=update_data, + serviceAccountId=service_account_id, + accountIdentifier=account_id + ) + + return ServiceAccount(response.get('data', {}), self._http_client) + + def delete(self, service_account_id, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['delete'], + serviceAccountId=service_account_id, + accountIdentifier=account_id + ) + + # For test compatibility, return the raw response + return True diff --git a/splitapiclient/microclients/harness/token_microclient.py b/splitapiclient/microclients/harness/token_microclient.py new file mode 100644 index 0000000..3332598 --- /dev/null +++ b/splitapiclient/microclients/harness/token_microclient.py @@ -0,0 +1,228 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.harness import Token +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + + +class TokenMicroClient: + ''' + Microclient for managing Harness tokens + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': '/ng/api/token/aggregate?apiKeyType=SERVICE_ACCOUNT&accountIdentifier={accountIdentifier}&pageIndex={pageIndex}&pageSize=100', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'create': { + 'method': 'POST', + 'url_template': '/ng/api/token?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'rotate_token': { + 'method': 'POST', + 'url_template': '/ng/api/token/rotate/{tokenId}?accountIdentifier={accountIdentifier}&apiKeyType=SERVICE_ACCOUNT&parentIdentifier={parentIdentifier}&apiKeyIdentifier={apiKeyIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update_token': { + 'method': 'PUT', + 'url_template': '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': '/ng/api/token/{tokenId}?accountIdentifier={accountIdentifier}', + 'headers': [{ + 'name': 'x-api-key', + 'template': '{value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client, account_identifier=None): + ''' + Constructor + + :param http_client: HTTP client to use for requests + :param account_identifier: Default account identifier to use for all requests + ''' + self._http_client = http_client + self._account_identifier = account_identifier + + def list(self, account_identifier=None): + ''' + Returns a list of Token objects. + + :param account_identifier: Account 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") + + page_index = 0 + final_list = [] + while True: + try: + response = self._http_client.make_request( + self._endpoint['all_items'], + accountIdentifier=account_id, + pageIndex=page_index + ) + data = response.get('data', {}) + + content = data.get('content', []) + if content: + # Extract token data from each item in the list + for item in content: + if isinstance(item, dict) and 'token' in item: + final_list.append(item['token']) + else: + break + + page_index += 1 + except HTTPResponseError: + # Break out of the loop if there's an HTTP error with the request + break + + return [Token(item, self._http_client) for item in final_list] + + def get(self, token_id, account_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 + :returns: Token object + :rtype: Token + ''' + # Use the provided account_identifier or fall back to the default + 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") + + tokens = self.list(account_identifier=account_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): + ''' + Create a new token + + :param token_data: Dictionary containing token data + :param account_identifier: Account identifier to use for this request, overrides the default + :returns: newly created token + :rtype: Token + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['create'], + body=token_data, + accountIdentifier=account_id + ) + return response.get('data', "") + + def update(self, token_id, update_data, account_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 + :returns: updated token + :rtype: Token + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['update_token'], + body=update_data, + tokenId=token_id, + accountIdentifier=account_id + ) + return Token(response.get('data', {}), self._http_client) + + + def rotate(self, token_id, parent_identifier, api_key_identifier, account_identifier=None): + ''' + Rotate a token + + :param token_id: ID of the token to rotate + :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 + :returns: rotated token + :rtype: string + ''' + # Use the provided account_identifier or fall back to the default + 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") + + response = self._http_client.make_request( + self._endpoint['rotate_token'], + tokenId=token_id, + parentIdentifier=parent_identifier, + apiKeyIdentifier=api_key_identifier, + accountIdentifier=account_id + ) + return response.get('data', "") + + def delete(self, token_id, account_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 + :returns: True if successful + :rtype: bool + ''' + # Use the provided account_identifier or fall back to the default + 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") + + self._http_client.make_request( + self._endpoint['delete'], + tokenId=token_id, + accountIdentifier=account_id + ) + return True diff --git a/splitapiclient/resources/harness/__init__.py b/splitapiclient/resources/harness/__init__.py new file mode 100644 index 0000000..170a062 --- /dev/null +++ b/splitapiclient/resources/harness/__init__.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.resources.harness.token import Token +from splitapiclient.resources.harness.harness_apikey import HarnessApiKey +from splitapiclient.resources.harness.service_account import ServiceAccount +from splitapiclient.resources.harness.harness_user import HarnessUser +from splitapiclient.resources.harness.harness_group import HarnessGroup +from splitapiclient.resources.harness.role import Role +from splitapiclient.resources.harness.resource_group import ResourceGroup +from splitapiclient.resources.harness.role_assignment import RoleAssignment +from splitapiclient.resources.harness.harness_project import HarnessProject +from splitapiclient.resources.harness.harness_invite import HarnessInvite + +__all__ = [ + 'Token', + 'HarnessApiKey', + 'ServiceAccount', + 'HarnessUser', + 'HarnessGroup', + 'Role', + 'ResourceGroup', + 'RoleAssignment', + 'HarnessProject', + 'HarnessInvite' +] diff --git a/splitapiclient/resources/harness/harness_apikey.py b/splitapiclient/resources/harness/harness_apikey.py new file mode 100644 index 0000000..e63ad5b --- /dev/null +++ b/splitapiclient/resources/harness/harness_apikey.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class HarnessApiKey(BaseResource): + ''' + HarnessApiKey resource representing a Harness API key + ''' + _schema = { + + 'identifier': 'string', + 'name': 'string', + 'description': 'string', + 'value': 'string', + 'apiKeyType': 'string', + 'parentIdentifier': 'string', + 'defaultTimeToExpireToken': 'number', + 'accountIdentifier': 'string', + 'projectIdentifier': 'string', + 'orgIdentifier': 'string', + 'governanceMetadata': 'dict' + } + + def __init__(self, data=None, client=None): + ''' + Initialize a HarnessApiKey resource + + :param data: Dictionary containing API key data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + BaseResource.__init__(self, data.get('identifier'), client) + self._identifier = data.get('identifier') + self._name = data.get('name') + self._description = data.get('description') + self._value = data.get('value') + self._api_key_type = data.get('apiKeyType') + self._parent_identifier = data.get('parentIdentifier') + self._default_time_to_expire_token = data.get('defaultTimeToExpireToken') + self._account_identifier = data.get('accountIdentifier') + self._project_identifier = data.get('projectIdentifier') + self._org_identifier = data.get('orgIdentifier') + self._governance_metadata = data.get('governanceMetadata') + + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the API key as a dictionary + + :returns: API key data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result diff --git a/splitapiclient/resources/harness/harness_group.py b/splitapiclient/resources/harness/harness_group.py new file mode 100644 index 0000000..1144960 --- /dev/null +++ b/splitapiclient/resources/harness/harness_group.py @@ -0,0 +1,156 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class HarnessGroup(BaseResource): + ''' + HarnessGroup resource representing a Harness group + ''' + _schema = { + "accountIdentifier": "string", + "orgIdentifier": "string", + "projectIdentifier": "string", + "identifier": "string", + "name": "string", + "users": [ + { + "uuid": "string", + "name": "string", + "email": "string", + "token": "string", + "defaultAccountId": "string", + "intent": "string", + "accounts": [ + { + "uuid": "string", + "accountName": "string", + "companyName": "string", + "defaultExperience": "NG", + "createdFromNG": "boolean", + "nextGenEnabled": "boolean" + } + ], + "admin": "boolean", + "twoFactorAuthenticationEnabled": "boolean", + "emailVerified": "boolean", + "locked": "boolean", + "disabled": "boolean", + "signupAction": "string", + "edition": "string", + "billingFrequency": "string", + "utmInfo": { + "utmSource": "string", + "utmContent": "string", + "utmMedium": "string", + "utmTerm": "string", + "utmCampaign": "string" + }, + "externallyManaged": "boolean", + "givenName": "string", + "familyName": "string", + "externalId": "string", + "createdAt": 'number', + "lastUpdatedAt": 'number', + "userPreferences": { + "property1": "string", + "property2": "string" + }, + "isEnrichedInfoCollected": "boolean", + "lastLogin": 'number' + } + ], + "notificationConfigs": [ + { + "type": "string" + } + ], + "linkedSsoId": "string", + "linkedSsoDisplayName": "string", + "ssoGroupId": "string", + "ssoGroupName": "string", + "linkedSsoType": "string", + "externallyManaged": "boolean", + "description": "string", + "tags": { + "property1": "string", + "property2": "string" + }, + "harnessManaged": "boolean", + "ssoLinked": "boolean" + } + def __init__(self, data=None, client=None): + ''' + Initialize a HarnessGroup resource + + :param data: Dictionary containing group data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + @property + def name(self): + ''' + Get the group name + + :returns: Group name + :rtype: str + ''' + return self._name + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result diff --git a/splitapiclient/resources/harness/harness_invite.py b/splitapiclient/resources/harness/harness_invite.py new file mode 100644 index 0000000..bfae0c2 --- /dev/null +++ b/splitapiclient/resources/harness/harness_invite.py @@ -0,0 +1,110 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class HarnessInvite(BaseResource): + ''' + HarnessInvite resource representing a Harness Invite + ''' + _schema = { + + "id": "string", + "name": "string", + "email": "string", + "accountIdentifier": "string", + "orgIdentifier": "string", + "projectIdentifier": "string", + "roleBindings": [ + { + "roleIdentifier": "string", + "roleName": "string", + "roleScopeLevel": "string", + "resourceGroupIdentifier": "string", + "resourceGroupName": "string", + "managedRole": "boolean" + } + ], + "userGroups": [ + "string" + ], + "inviteType": "string", + "approved": "boolean" + +} + + def __init__(self, data=None, client=None): + ''' + Initialize a HarnessInvite resource + + :param data: Dictionary containing Invite data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('id'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + @property + def name(self): + ''' + Get the invite name + + :returns: Invite name + :rtype: str + ''' + return self._name + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result diff --git a/splitapiclient/resources/harness/harness_project.py b/splitapiclient/resources/harness/harness_project.py new file mode 100644 index 0000000..756c4ff --- /dev/null +++ b/splitapiclient/resources/harness/harness_project.py @@ -0,0 +1,76 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class HarnessProject(BaseResource): + ''' + HarnessProject resource representing a Harness project + ''' + _schema = { + "orgIdentifier": "string", + "identifier": "string", + "name": "string", + "color": "string", + "modules": [ + "string" + ], + "description": "string", + "tags": { + "property1": "string", + "property2": "string" + } + } + + def __init__(self, data=None, client=None): + ''' + Initialize a HarnessProject resource + + :param data: Dictionary containing project data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + for field in self._schema.keys(): + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Check if this is a property defined in the schema + camel_field = ''.join([c.capitalize() if i > 0 else c for i, c in enumerate(name.split('_'))]) + if camel_field in self._schema.keys(): + attr_name = f"_{name}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the project as a dictionary + + :returns: Project data as a dictionary + :rtype: dict + ''' + result = {} + for field in self._schema.keys(): + # Convert schema field (camelCase) to attribute name (snake_case) + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result diff --git a/splitapiclient/resources/harness/harness_user.py b/splitapiclient/resources/harness/harness_user.py new file mode 100644 index 0000000..9afbabc --- /dev/null +++ b/splitapiclient/resources/harness/harness_user.py @@ -0,0 +1,94 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class HarnessUser(BaseResource): + ''' + HarnessUser resource representing a Harness user + ''' + _schema = { + "name": "string", + "email": "string", + "uuid": "string", + "locked": 'boolean', + "disabled": 'boolean', + "externally_managed": 'boolean', + "two_factor_authentication_enabled": 'boolean' +} + + def __init__(self, data=None, client=None): + ''' + Initialize a HarnessUser resource + + :param data: Dictionary containing user data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('uuid'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + @property + def name(self): + ''' + Get the group name + + :returns: Group name + :rtype: str + ''' + return self._name + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result diff --git a/splitapiclient/resources/harness/resource_group.py b/splitapiclient/resources/harness/resource_group.py new file mode 100644 index 0000000..ef68c62 --- /dev/null +++ b/splitapiclient/resources/harness/resource_group.py @@ -0,0 +1,117 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class ResourceGroup(BaseResource): + ''' + ResourceGroup resource representing a Harness resource group + ''' + _schema = { + + "accountIdentifier": "string", + "orgIdentifier": "string", + "projectIdentifier": "string", + "identifier": "string", + "name": "string", + "color": "string", + "tags": { + "property1": "string", + "property2": "string" + }, + "description": "string", + "allowedScopeLevels": [ + "string" + ], + "includedScopes": [ + { + "filter": "string", + "accountIdentifier": "string", + "orgIdentifier": "string", + "projectIdentifier": "string" + } + ], + "resourceFilter": { + "resources": [ + { + "resourceType": "string", + "identifiers": [ + "string" + ], + "attributeFilter": { + "attributeName": "string", + "attributeValues": [ + "string" + ] + } + } + ], + "includeAllResources": "boolean" + } + } + + def __init__(self, data=None, client=None): + ''' + Initialize a ResourceGroup resource + + :param data: Dictionary containing resource group data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result \ No newline at end of file diff --git a/splitapiclient/resources/harness/role.py b/splitapiclient/resources/harness/role.py new file mode 100644 index 0000000..b160086 --- /dev/null +++ b/splitapiclient/resources/harness/role.py @@ -0,0 +1,86 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class Role(BaseResource): + ''' + Role resource representing a Harness role + ''' + _schema = { + "identifier": "string", + "name": "string", + "permissions": [ + "string" + ], + "allowed_scope_levels": [ + "account" + ], + "description": "string", + } + + def __init__(self, data=None, client=None): + ''' + Initialize a Role resource + + :param data: Dictionary containing role data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result \ No newline at end of file diff --git a/splitapiclient/resources/harness/role_assignment.py b/splitapiclient/resources/harness/role_assignment.py new file mode 100644 index 0000000..4f7d6e7 --- /dev/null +++ b/splitapiclient/resources/harness/role_assignment.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class RoleAssignment(BaseResource): + ''' + RoleAssignment resource representing a Harness role assignment + ''' + _schema = { + "identifier": "string", + "resourceGroupIdentifier": "string", + "roleIdentifier": "string", + "roleReference": { + "identifier": "string", + "scopeLevel": "string" + }, + "principal": { + "scopeLevel": "string", + "identifier": "string", + "type": "string", + "uniqueId": "string" + }, + "disabled": 'boolean', + "managed": 'boolean', + "internal": 'boolean' + } + + def __init__(self, data=None, client=None): + ''' + Initialize a RoleAssignment resource + + :param data: Dictionary containing role assignment data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result \ No newline at end of file diff --git a/splitapiclient/resources/harness/service_account.py b/splitapiclient/resources/harness/service_account.py new file mode 100644 index 0000000..5bd8ff0 --- /dev/null +++ b/splitapiclient/resources/harness/service_account.py @@ -0,0 +1,89 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class ServiceAccount(BaseResource): + ''' + ServiceAccount resource representing a Harness service account + ''' + _schema = { + "identifier": "string", + "name": "string", + "email": "string", + "description": "string", + "tags": { + "property1": "string", + "property2": "string" + }, + "accountIdentifier": "string", + "orgIdentifier": "string", + "projectIdentifier": "string", + "extendable": "boolean" + } + + def __init__(self, data=None, client=None): + ''' + Initialize a ServiceAccount resource + + :param data: Dictionary containing service account data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result \ No newline at end of file diff --git a/splitapiclient/resources/harness/token.py b/splitapiclient/resources/harness/token.py new file mode 100644 index 0000000..fd149b3 --- /dev/null +++ b/splitapiclient/resources/harness/token.py @@ -0,0 +1,99 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict + + +class Token(BaseResource): + ''' + Token resource representing a Harness authentication token + ''' + _schema = { + "identifier": "string", + "name": "string", + "validFrom": "number", + "validTo": "number", + "scheduledExpireTime": "number", + "valid": "boolean", + "accountIdentifier": "string", + "projectIdentifier": "string", + "orgIdentifier": "string", + "apiKeyIdentifier": "string", + "parentIdentifier": "string", + "apiKeyType": "USER", + "description": "string", + "tags": { + "property1": "string", + "property2": "string" + }, + "sshKeyContent": "string", + "sshKeyUsage": [ + "AUTH" + ] + } + + def __init__(self, data=None, client=None): + ''' + Initialize a Token resource + + :param data: Dictionary containing token data + :param client: HTTP client for making API requests + ''' + if not data: + data = {} + # Initialize BaseResource with identifier + BaseResource.__init__(self, data.get('identifier'), client) + + # Dynamically set properties based on schema + schema_data_fields = self._schema.keys() + for field in schema_data_fields: + # Convert camelCase to snake_case for property names + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + setattr(self, f"_{snake_field}", data.get(field)) + + def __getattr__(self, name): + ''' + Dynamic getter for properties based on schema fields + + :param name: Property name + :returns: Property value + :raises: AttributeError if property doesn't exist + ''' + # Convert camelCase to snake_case + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in name]).lstrip('_') + + # Check if this is a property defined in the schema + for schema_field in self._schema.keys(): + # Try direct match with schema field + if name == schema_field: + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # Try snake_case version of schema field + schema_snake = ''.join(['_' + c.lower() if c.isupper() else c for c in schema_field]).lstrip('_') + if name == schema_snake: + attr_name = f"_{schema_snake}" + if hasattr(self, attr_name): + return getattr(self, attr_name) + + # If not found, raise AttributeError + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def export_dict(self): + ''' + Export the group as a dictionary + + :returns: Group data as a dictionary + :rtype: dict + ''' + # Export properties based on schema + result = {} + for field in self._schema.keys(): + # Convert camelCase to snake_case for attribute lookup + snake_field = ''.join(['_' + c.lower() if c.isupper() else c for c in field]).lstrip('_') + attr_name = f"_{snake_field}" + if hasattr(self, attr_name): + result[field] = getattr(self, attr_name) + return result + diff --git a/splitapiclient/tests/main/test_harness_apiclient_resources.py b/splitapiclient/tests/main/test_harness_apiclient_resources.py new file mode 100644 index 0000000..dbc623c --- /dev/null +++ b/splitapiclient/tests/main/test_harness_apiclient_resources.py @@ -0,0 +1,583 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.main.harness_apiclient import HarnessApiClient +from splitapiclient.resources.harness import Token, HarnessApiKey, ServiceAccount, HarnessUser +from splitapiclient.resources.harness import HarnessGroup, Role, ResourceGroup, RoleAssignment, HarnessProject +from splitapiclient.microclients.harness import TokenMicroClient, HarnessApiKeyMicroClient, ServiceAccountMicroClient +from splitapiclient.microclients.harness import HarnessUserMicroClient, HarnessGroupMicroClient, RoleMicroClient +from splitapiclient.microclients.harness import ResourceGroupMicroClient, RoleAssignmentMicroClient, HarnessProjectMicroClient + + +class TestHarnessApiClientResources: + ''' + Tests for the HarnessApiClient integration with all Harness resources + ''' + + def test_harness_resource_properties(self, mocker): + ''' + Test that all Harness resource properties return the appropriate microclients + ''' + # Mock the HTTP client initialization to avoid actual HTTP requests + mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', return_value=None) + + # Create a HarnessApiClient with minimal config + client = HarnessApiClient({ + 'apikey': 'test-apikey', + 'harness_token': 'test-harness-token' + }) + + # Verify that each harness resource property returns the appropriate microclient + assert isinstance(client.token, TokenMicroClient) + assert isinstance(client.harness_apikey, HarnessApiKeyMicroClient) + assert isinstance(client.service_account, ServiceAccountMicroClient) + assert isinstance(client.harness_user, HarnessUserMicroClient) + assert isinstance(client.harness_group, HarnessGroupMicroClient) + assert isinstance(client.role, RoleMicroClient) + assert isinstance(client.resource_group, ResourceGroupMicroClient) + assert isinstance(client.role_assignment, RoleAssignmentMicroClient) + assert isinstance(client.harness_project, HarnessProjectMicroClient) + + def test_harness_resource_operations(self, mocker): + ''' + Test that the HarnessApiClient can perform operations on all Harness resources + ''' + # Mock the HTTP client to avoid actual HTTP requests + mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', return_value=None) + mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.make_request') + + # Create a HarnessApiClient with minimal config + client = HarnessApiClient({ + 'apikey': 'test-apikey', + 'harness_token': 'test-harness-token', + 'account_identifier': 'test-account-identifier' + }) + + # Mock responses for each resource type + token_response = { + 'data': { + 'content': [{ + 'token': { + 'identifier': 'token-1', + 'name': 'Token 1', + 'validFrom': 1609459200000, + 'validTo': 1640995200000, + 'valid': True, + 'accountIdentifier': 'test-account-identifier', + 'apiKeyIdentifier': 'apikey-1', + 'parentIdentifier': 'sa-1', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + }] + } + } + + token_detail_response = { + 'data': { + 'token': { + 'identifier': 'token-1', + 'name': 'Token 1', + 'validFrom': 1609459200000, + 'validTo': 1640995200000, + 'valid': True, + 'accountIdentifier': 'test-account-identifier', + 'apiKeyIdentifier': 'apikey-1', + 'parentIdentifier': 'sa-1', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + } + } + + apikey_response = { + 'data': [ + { + 'identifier': 'apikey-1', + 'name': 'API Key 1', + 'apiKeyType': 'CLIENT', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test API Key' + } + ] + } + + apikey_detail_response = { + 'data': { + 'apiKey': { + 'identifier': 'apikey-1', + 'name': 'API Key 1', + 'apiKeyType': 'CLIENT', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test API Key' + } + } + } + + service_account_response = { + 'data': [ + { + 'identifier': 'sa-1', + 'name': 'Service Account 1', + 'email': 'sa1@example.com', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Service Account' + } + ] + } + + service_account_detail_response = { + 'data': { + 'serviceAccount': { + 'identifier': 'sa-1', + 'name': 'Service Account 1', + 'email': 'sa1@example.com', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Service Account' + } + } + } + + user_response = { + 'data': { + 'content': [ + { + 'user': { + 'uuid': 'user-1', + 'name': 'User 1', + 'email': 'user1@example.com', + 'accountIdentifier': 'test-account-identifier' + } + } + ] + } + } + + user_detail_response = { + 'data': { + 'user': { + 'uuid': 'user-1', + 'name': 'User 1', + 'email': 'user1@example.com', + 'accountIdentifier': 'test-account-identifier' + } + } + } + + group_response = { + 'data': { + 'content': [ + { + 'identifier': 'group-1', + 'name': 'Group 1', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Group' + } + ] + } + } + + group_detail_response = { + 'data': { + 'identifier': 'group-1', + 'name': 'Group 1', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Group' + } + } + + role_response = { + 'data': { + 'content': [{ + 'role': { + 'identifier': 'role-1', + 'name': 'Role 1', + 'accountIdentifier': 'test-account-identifier', + 'permissions': ['ff_read_flag', 'ff_create_flag'] + } + }] + } + } + + role_detail_response = { + 'data': { + 'role': { + 'identifier': 'role-1', + 'name': 'Role 1', + 'accountIdentifier': 'test-account-identifier', + 'permissions': ['ff_read_flag', 'ff_create_flag'] + } + } + } + + resource_group_response = { + 'data': { + 'content': [{ + 'resourceGroup': { + 'identifier': 'rg-1', + 'name': 'Resource Group 1', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Resource Group' + } + }] + } + } + + resource_group_detail_response = { + 'data': { + 'resourceGroup': { + 'identifier': 'rg-1', + 'name': 'Resource Group 1', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Resource Group' + } + } + } + + role_assignment_response = { + 'data': { + 'content': [{ + 'roleAssignment': { + 'identifier': 'ra-1', + 'roleIdentifier': 'role-1', + 'resourceGroupIdentifier': 'rg-1', + 'accountIdentifier': 'test-account-identifier', + 'principal': { + 'identifier': 'user-1', + 'type': 'USER' + } + } + }] + } + } + + role_assignment_detail_response = { + 'data': { + 'roleAssignment': { + 'identifier': 'ra-1', + 'roleIdentifier': 'role-1', + 'resourceGroupIdentifier': 'rg-1', + 'accountIdentifier': 'test-account-identifier', + 'principal': { + 'identifier': 'user-1', + 'type': 'USER' + } + } + } + } + + project_response = { + 'data': { + 'content': [{ + 'project': { + 'identifier': 'project-1', + 'name': 'Project 1', + 'orgIdentifier': 'org-1', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Project' + } + }] + } + } + + project_detail_response = { + 'data': { + 'project': { + 'identifier': 'project-1', + 'name': 'Project 1', + 'orgIdentifier': 'org-1', + 'accountIdentifier': 'test-account-identifier', + 'description': 'Test Project' + } + } + } + + # Mock make_request function for all HTTP clients + def mock_make_request(endpoint, body=None, **kwargs): + url_template = endpoint.get('url_template', '') + method = endpoint.get('method', '') + page_index = kwargs.get('pageIndex', 0) + + # Debug print to identify which endpoint is causing the infinite loop + print(f"Mock request: {method} {url_template}, page_index={page_index}") + + # Handle pagination - for any page > 0, return empty content + # This ensures pagination loops will terminate + if page_index > 0: + return {'data': {'content': []}} + + # First page or non-paginated requests + if '/ng/api/token/aggregate' in url_template and method == 'GET': + return token_response + elif '/ng/api/token/' in url_template and method == 'GET': + return token_detail_response + elif '/ng/api/apikey' in url_template and 'aggregate' not in url_template and method == 'GET': + return apikey_response + elif '/ng/api/apikey/aggregate' in url_template and method == 'GET': + return apikey_detail_response + elif '/ng/api/serviceaccount/aggregate' in url_template and method == 'GET': + return service_account_response + elif '/ng/api/serviceaccount' in url_template and 'aggregate' not in url_template and method == 'GET': + return service_account_response + elif '/ng/api/serviceAccount/' in url_template and method == 'GET': + return service_account_detail_response + elif '/ng/api/user/aggregate' in url_template and method == 'POST': + return user_response + elif '/ng/api/user/aggregate/' in url_template and method == 'GET': + 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': + return group_detail_response + elif '/authz/api/roles' in url_template and '{roleId}' not in url_template and method == 'GET': + return role_response + elif '/authz/api/roles/{roleId}' in url_template and method == 'GET': + 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': + 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': + return resource_group_detail_response + 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': + return role_assignment_detail_response + elif '/ng/api/projects/aggregate' in url_template and method == 'GET': + return project_response + elif '/ng/api/projects' in url_template and '{projectIdentifier}' not in url_template and method == 'GET': + return project_response + elif '/ng/api/projects/' in url_template and method == 'GET': + return project_detail_response + + return {'data': {}} + + client._token_client._http_client.make_request.side_effect = mock_make_request + client._harness_apikey_client._http_client.make_request.side_effect = mock_make_request + client._service_account_client._http_client.make_request.side_effect = mock_make_request + client._harness_user_client._http_client.make_request.side_effect = mock_make_request + client._harness_group_client._http_client.make_request.side_effect = mock_make_request + client._role_client._http_client.make_request.side_effect = mock_make_request + client._resource_group_client._http_client.make_request.side_effect = mock_make_request + client._role_assignment_client._http_client.make_request.side_effect = mock_make_request + client._harness_project_client._http_client.make_request.side_effect = mock_make_request + + # Test token operations + tokens = client.token.list() + assert len(tokens) == 1 + assert isinstance(tokens[0], Token) + assert tokens[0].identifier == 'token-1' + assert tokens[0].name == 'Token 1' + assert tokens[0].valid is True + assert tokens[0].api_key_identifier == 'apikey-1' + assert tokens[0].parent_identifier == 'sa-1' + assert tokens[0].api_key_type == 'SERVICE_ACCOUNT' + + # Test API key operations + apikeys = client.harness_apikey.list() + assert len(apikeys) == 1 + assert isinstance(apikeys[0], HarnessApiKey) + assert apikeys[0].identifier == 'apikey-1' + assert apikeys[0].name == 'API Key 1' + assert apikeys[0].api_key_type == 'CLIENT' + assert apikeys[0].description == 'Test API Key' + + # Test service account operations + service_accounts = client.service_account.list() + assert len(service_accounts) == 1 + assert isinstance(service_accounts[0], ServiceAccount) + assert service_accounts[0].identifier == 'sa-1' + assert service_accounts[0].name == 'Service Account 1' + assert service_accounts[0].email == 'sa1@example.com' + + # Test user operations + users = client.harness_user.list() + assert len(users) == 1 + assert isinstance(users[0], HarnessUser) + assert users[0].id == 'user-1' + assert users[0].name == 'User 1' + assert users[0].email == 'user1@example.com' + + # Test group operations + groups = client.harness_group.list() + assert len(groups) == 1 + assert isinstance(groups[0], HarnessGroup) + assert groups[0].identifier == 'group-1' + assert groups[0].name == 'Group 1' + + # Test role operations + roles = client.role.list() + assert len(roles) == 1 + assert isinstance(roles[0], Role) + assert roles[0].identifier == 'role-1' + assert roles[0].name == 'Role 1' + assert 'ff_read_flag' in roles[0].permissions + + # Test resource group operations + resource_groups = client.resource_group.list() + assert len(resource_groups) == 1 + assert isinstance(resource_groups[0], ResourceGroup) + assert resource_groups[0].identifier == 'rg-1' + assert resource_groups[0].name == 'Resource Group 1' + + # Test role assignment operations + role_assignments = client.role_assignment.list() + assert len(role_assignments) == 1 + assert isinstance(role_assignments[0], RoleAssignment) + assert role_assignments[0].identifier == 'ra-1' + assert role_assignments[0].role_identifier == 'role-1' + assert role_assignments[0].resource_group_identifier == 'rg-1' + assert role_assignments[0].principal.get('identifier') == 'user-1' + assert role_assignments[0].principal.get('type') == 'USER' + + # Test project operations + projects = client.harness_project.list() + assert len(projects) == 1 + assert isinstance(projects[0], HarnessProject) + assert projects[0].identifier == 'project-1' + assert projects[0].name == 'Project 1' + assert projects[0].org_identifier == 'org-1' + + def test_harness_pagination(self, mocker): + ''' + Test that the HarnessApiClient can handle pagination correctly + ''' + # 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' + }) + + # Mock the HTTP client's make_request method + mocker.patch.object(client._token_client._http_client, 'make_request') + + # Set up mock responses for pagination + token_response_page1 = { + 'data': { + 'content': [ + { + 'token': { + 'identifier': 'token-1', + 'name': 'Token 1', + 'accountIdentifier': 'test-account-identifier', + 'valid': True, + 'apiKeyIdentifier': 'apikey-1', + 'parentIdentifier': 'sa-1', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + } + ] + } + } + + token_response_page2 = { + 'data': { + 'content': [ + { + 'token': { + 'identifier': 'token-2', + 'name': 'Token 2', + 'accountIdentifier': 'test-account-identifier', + 'valid': True, + 'apiKeyIdentifier': 'apikey-2', + 'parentIdentifier': 'sa-2', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + } + ] + } + } + + empty_response = { + 'data': { + 'content': [] + } + } + + # Configure the mock to return different responses for different pages + client._token_client._http_client.make_request.side_effect = lambda endpoint, **kwargs: ( + token_response_page1 if kwargs.get('pageIndex') == 0 else + token_response_page2 if kwargs.get('pageIndex') == 1 else + empty_response + ) + + # Test pagination by listing tokens + tokens = client.token.list(account_identifier='test-account-identifier') + + # Verify that we got tokens from both pages + assert len(tokens) == 2 + assert tokens[0].identifier == 'token-1' + assert tokens[1].identifier == 'token-2' + + # Verify that the make_request method was called with the correct parameters + # for each page + assert client._token_client._http_client.make_request.call_count == 3 + + # First call should be for page 0 + args, kwargs = client._token_client._http_client.make_request.call_args_list[0] + assert kwargs.get('pageIndex') == 0 + assert kwargs.get('accountIdentifier') == 'test-account-identifier' + + # Second call should be for page 1 + args, kwargs = client._token_client._http_client.make_request.call_args_list[1] + assert kwargs.get('pageIndex') == 1 + assert kwargs.get('accountIdentifier') == 'test-account-identifier' + + # Third call should be for page 2 + args, kwargs = client._token_client._http_client.make_request.call_args_list[2] + assert kwargs.get('pageIndex') == 2 + assert kwargs.get('accountIdentifier') == 'test-account-identifier' + + def test_harness_authentication_modes(self, mocker): + ''' + Test that the HarnessApiClient properly handles different authentication modes + ''' + # 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', + 'harness_token': 'test-harness-token' + }) + + # Verify that the HTTP client was initialized correctly for client1 + from splitapiclient.http_clients.harness_client import HarnessHttpClient + + # For client1, harness_token should be used for Harness endpoints + harness_client1_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' + ] + assert len(harness_client1_calls) > 0 + + # Reset the mock before creating client2 + HarnessHttpClient.__init__.reset_mock() + + # Test with only apikey + client2 = HarnessApiClient({ + 'apikey': 'test-apikey' + }) + + # 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 diff --git a/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py b/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py new file mode 100644 index 0000000..9a371c7 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/deprecated_endpoints_test.py @@ -0,0 +1,224 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.microclients import WorkspaceMicroClient, APIKeyMicroClient, UserMicroClient, GroupMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.harness_client import HarnessHttpClient +from splitapiclient.main.harness_apiclient import HarnessApiClient +from splitapiclient.util.exceptions import HarnessDeprecatedEndpointError + + +class TestDeprecatedEndpoints: + """ + Tests for verifying that certain endpoints are deprecated when in harness mode + """ + + def test_workspace_endpoints_deprecated_in_harness_mode(self, mocker): + """ + Test that workspace POST, PATCH, DELETE, PUT verbs are deprecated in harness mode + """ + # Mock the HarnessHttpClient to avoid actual HTTP requests + mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.__init__', return_value=None) + harness_make_request_mock = mocker.patch('splitapiclient.http_clients.harness_client.HarnessHttpClient.make_request') + + # Create a HarnessApiClient + client = HarnessApiClient({ + 'apikey': 'abc', + 'harness_token': 'abc' + }) + + # Create a WorkspaceMicroClient with the client's HTTP client + wmc = WorkspaceMicroClient(client._workspace_client._http_client) + + # Configure the mock to raise HarnessDeprecatedEndpointError for POST, PATCH, DELETE + def mock_make_request(endpoint, **kwargs): + method = endpoint.get('method', '') + url_template = endpoint.get('url_template', '') + + if 'workspaces' in url_template: + if method == 'POST': + raise HarnessDeprecatedEndpointError(f"Endpoint workspaces with method {method} is deprecated in harness mode") + elif method == 'PATCH' and '{workspaceId}' in url_template: + raise HarnessDeprecatedEndpointError(f"Endpoint workspaces/{{workspaceId}} with method {method} is deprecated in harness mode") + elif method == 'DELETE' and '{workspaceId}' in url_template: + raise HarnessDeprecatedEndpointError(f"Endpoint workspaces/{{workspaceId}} with method {method} is deprecated in harness mode") + elif method == 'GET': + return {'objects': [], 'offset': 0, 'totalCount': 0, 'limit': 20} + + return {'data': {}} + + harness_make_request_mock.side_effect = mock_make_request + + # Test create (POST) is deprecated + with pytest.raises(HarnessDeprecatedEndpointError) as excinfo: + wmc.add({'name': 'Test Workspace'}) + assert "Endpoint workspaces with method POST is deprecated in harness mode" in str(excinfo.value) + + # Test update (PATCH) is deprecated + with pytest.raises(HarnessDeprecatedEndpointError) as excinfo: + wmc.update('workspace1', 'requireTitleAndComments', False) + assert "Endpoint workspaces/{workspaceId} with method PATCH is deprecated in harness mode" in str(excinfo.value) + + # Test delete (DELETE) is deprecated + with pytest.raises(HarnessDeprecatedEndpointError) as excinfo: + wmc.delete('workspace1') + assert "Endpoint workspaces/{workspaceId} with method DELETE is deprecated in harness mode" in str(excinfo.value) + + # Verify that GET operations are still allowed + wmc.list() # Should not raise an exception + harness_make_request_mock.assert_called() + + def test_apikey_admin_endpoints_deprecated_in_harness_mode(self, mocker): + """ + Test that apiKey POST verb for apiKeyType 'admin' is deprecated in harness mode + """ + # Create a custom HarnessHttpClient with the _is_deprecated_endpoint method properly implemented + class TestHarnessHttpClient(HarnessHttpClient): + def __init__(self, baseurl, auth_token): + super(TestHarnessHttpClient, self).__init__(baseurl, auth_token) + self.apikey_types = {} # Store API key types for testing + + def _is_deprecated_endpoint(self, endpoint, body=None, **kwargs): + url_template = endpoint['url_template'] + method = endpoint['method'] + + # Check for apiKeys endpoint with admin type + if url_template.startswith('apiKeys') and method == 'POST': + if body and body.get('apiKeyType') == 'admin': + raise HarnessDeprecatedEndpointError("Operation 'create_apikey' for apiKeyType 'admin' is deprecated in harness mode") + + return False + + def make_request(self, endpoint, body=None, **kwargs): + # Check if endpoint is deprecated + self._is_deprecated_endpoint(endpoint, body, **kwargs) + + method = endpoint.get('method', '') + url_template = endpoint.get('url_template', '') + + # Handle API key creation + if method == 'POST' and url_template.startswith('apiKeys'): + # Store the API key type for later use in delete operations + apikey_id = 'apikey-1' + if body and 'apiKeyType' in body: + self.apikey_types[apikey_id] = body['apiKeyType'] + return {'data': {'id': apikey_id}} + + # Handle API key deletion + if method == 'DELETE' and url_template.startswith('apiKeys/'): + return {'data': {}} + + return {'data': {}} + + # Create a test client + client = TestHarnessHttpClient('https://api.split.io/internal/api/v2', 'test-token') + + # Create an APIKeyMicroClient with our test client + akmc = APIKeyMicroClient(client) + + # Test create (POST) for admin type is deprecated + with pytest.raises(HarnessDeprecatedEndpointError) as excinfo: + akmc.create_apikey('Test API Key', 'admin', ['env1', 'env2'], 'ws1', ['role1', 'role2']) + assert "Operation 'create_apikey' for apiKeyType 'admin' is deprecated in harness mode" in str(excinfo.value) + + # Test create (POST) for client type is allowed + apikey = akmc.create_apikey('Test API Key', 'client_side', ['env1', 'env2'], 'ws1', ['role1', 'role2']) + + # Test that delete operations are allowed for all API key types + client.apikey_types['admin-apikey'] = 'admin' + akmc.delete_apikey('admin-apikey') # Should not raise an exception + akmc.delete_apikey('apikey-1') # Should not raise an exception + + +class TestAuthenticationInHarnessMode: + """ + Tests for verifying authentication behavior in harness mode + """ + + def test_harness_token_used_for_harness_endpoints(self, mocker): + """ + Test that harness_token is used for Harness endpoints and apikey for Split endpoints + """ + # 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 {} + + # 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 both harness_token and apikey + client = HarnessApiClient({ + 'harness_token': 'harness_token_value', + 'apikey': 'api_key_value' + }) + + # Check that the Harness HTTP client was 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' + + def test_apikey_fallback_when_no_harness_token(self, mocker): + """ + Test that apikey is used for all operations when harness_token is not provided + """ + # 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 {} + + # 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' + }) + + # Check that the Harness HTTP client was initialized with apikey as fallback + assert client._token_client._http_client.auth_token == 'api_key_value' + + # Check that the Split HTTP client was initialized with apikey + assert client._split_client._http_client.auth_token == 'api_key_value' diff --git a/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py new file mode 100644 index 0000000..b3c5c5e --- /dev/null +++ b/splitapiclient/tests/microclients/harness/harness_apikey_microclient_test.py @@ -0,0 +1,278 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import HarnessApiKeyMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import HarnessApiKey + + +class TestHarnessApiKeyMicroClient: + + def test_list(self, mocker): + ''' + Test listing API keys + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': [ + { + 'identifier': 'apikey1', + 'name': 'API Key 1', + 'description': 'Test API key 1', + 'accountIdentifier': 'test_account', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'SERVICE_ACCOUNT' + }, + { + 'identifier': 'apikey2', + 'name': 'API Key 2', + 'description': 'Test API key 2', + 'accountIdentifier': 'test_account', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + ] + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = akmc.list('parent1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + parentIdentifier='parent1' + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], HarnessApiKey) + assert isinstance(result[1], HarnessApiKey) + assert result[0]._identifier == 'apikey1' + assert result[1]._identifier == 'apikey2' + + def test_list_empty_parent(self, mocker): + ''' + Test listing API keys with empty parent identifier + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': [] + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = akmc.list() + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + parentIdentifier="" + ) + + # Verify the result + assert len(result) == 0 + + def test_get(self, mocker): + ''' + Test getting a specific API key + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': { + 'apiKey': { + 'identifier': 'apikey1', + 'name': 'API Key 1', + 'description': 'Test API key 1', + 'accountIdentifier': 'test_account', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = akmc.get('apikey1', 'parent1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['get_apikey'], + apiKeyIdentifier='apikey1', + accountIdentifier='test_account', + parentIdentifier='parent1' + ) + + # Verify the result + assert isinstance(result, HarnessApiKey) + assert result._identifier == 'apikey1' + assert result._name == 'API Key 1' + assert result._description == 'Test API key 1' + + def test_get_not_found(self, mocker): + ''' + Test getting a non-existent API key + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # Mock the API response for a non-existent key + response_data = { + 'data': {} + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = akmc.get('nonexistent', 'parent1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['get_apikey'], + apiKeyIdentifier='nonexistent', + accountIdentifier='test_account', + parentIdentifier='parent1' + ) + + # Verify the result + assert result is None + + def test_create(self, mocker): + ''' + Test creating an API key + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # API key data to create + apikey_data = { + 'name': 'New API Key', + 'description': 'Test API key', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'SERVICE_ACCOUNT' + } + + # Mock the API response + response_data = { + 'data': { + 'identifier': 'new_apikey', + 'name': 'New API Key', + 'description': 'Test API key', + 'accountIdentifier': 'test_account', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'SERVICE_ACCOUNT', + 'createdAt': 1234567890, + 'lastModifiedAt': 1234567890 + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = akmc.create(apikey_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['create'], + body=apikey_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessApiKey) + assert result._identifier == 'new_apikey' + assert result._name == 'New API Key' + assert result._description == 'Test API key' + + def test_add_permissions(self, mocker): + ''' + Test adding permissions to an API key + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # Permissions data + permissions = { + 'roleAssignments': [ + { + 'roleIdentifier': 'role1', + 'resourceGroupIdentifier': 'resourceGroup1', + 'principal': { + 'identifier': 'apikey1', + 'type': 'SERVICE_ACCOUNT' + } + } + ] + } + + # Mock the API response + response_data = { + 'data': True + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = akmc.add_permissions('apikey1', permissions) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['add_permissions'], + body=permissions, + apiKeyIdentifier='apikey1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True + + def test_delete(self, mocker): + ''' + Test deleting an API key + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + akmc = HarnessApiKeyMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = akmc.delete('apikey1', 'parent1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessApiKeyMicroClient._endpoint['delete'], + apiKeyIdentifier='apikey1', + accountIdentifier='test_account', + parentIdentifier='parent1' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py new file mode 100644 index 0000000..3b543f3 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/harness_group_microclient_test.py @@ -0,0 +1,224 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import HarnessGroupMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import HarnessGroup + + +class TestHarnessGroupMicroClient: + + def test_list(self, mocker): + ''' + Test listing groups + ''' + 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': [] + }, + { + 'identifier': 'group2', + 'name': 'Group 2', + '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 + result = gmc.list() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + HarnessGroupMicroClient._endpoint['all_items'], + pageIndex=0, + accountIdentifier='test_account' + ) + SyncHttpClient.make_request.assert_any_call( + HarnessGroupMicroClient._endpoint['all_items'], + pageIndex=1, + accountIdentifier='test_account' + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], HarnessGroup) + assert isinstance(result[1], HarnessGroup) + assert result[0]._identifier == 'group1' + assert result[1]._identifier == 'group2' + + def test_get(self, mocker): + ''' + Test getting a specific group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'identifier': 'group1', + 'name': 'Group 1', + 'accountIdentifier': 'test_account', + 'users': [] + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = gmc.get('group1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessGroupMicroClient._endpoint['get_group'], + groupIdentifier='group1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessGroup) + assert result._identifier == 'group1' + assert result._name == 'Group 1' + + def test_create(self, mocker): + ''' + Test creating a group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + # Group data to create + group_data = { + 'name': 'New Group', + 'description': 'Test group', + 'accountIdentifier': 'test_account', + 'isSSOLinked': False, + 'linkedSSO': None, + 'users': [] + } + + # Mock the API response + response_data = { + 'data': { + 'identifier': 'new_group', + 'name': 'New Group', + 'description': 'Test group', + 'accountIdentifier': 'test_account', + 'isSSOLinked': False, + 'linkedSSO': None, + 'users': [] + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = gmc.create(group_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessGroupMicroClient._endpoint['create'], + body=group_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessGroup) + assert result._identifier == 'new_group' + assert result._name == 'New Group' + assert result._description == 'Test group' + + def test_update(self, mocker): + ''' + Test updating a group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + # Group data to update + update_data = { + 'identifier': 'group1', + 'name': 'Updated Group', + 'description': 'Updated description', + 'accountIdentifier': 'test_account' + } + + # Mock the API response + response_data = { + 'identifier': 'group1', + 'name': 'Updated Group', + 'description': 'Updated description', + 'accountIdentifier': 'test_account', + 'isSSOLinked': False, + 'linkedSSO': None, + 'users': [] + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = gmc.update(update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessGroupMicroClient._endpoint['update'], + body=update_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessGroup) + assert result._identifier == 'group1' + assert result._name == 'Updated Group' + assert result._description == 'Updated description' + + def test_delete(self, mocker): + ''' + Test deleting a group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + gmc = HarnessGroupMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = gmc.delete('group1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessGroupMicroClient._endpoint['delete'], + groupIdentifier='group1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py new file mode 100644 index 0000000..d2295d0 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/harness_project_microclient_test.py @@ -0,0 +1,268 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import HarnessProjectMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import HarnessProject + + +class TestHarnessProjectMicroClient: + + def test_list(self, mocker): + ''' + Test listing projects + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'project': { + 'identifier': 'project1', + 'name': 'Project 1', + 'description': 'Test project 1', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#FF0000', + 'modules': ['FF'] + } + }, + { + 'project': { + 'identifier': 'project2', + 'name': 'Project 2', + 'description': 'Test project 2', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#00FF00', + 'modules': ['FF'] + } + } + ], + 'totalElements': 3, + 'totalPages': 2 + } + } + + # Mock the API response for the second page with one more project + second_page_data = { + 'data': { + 'content': [ + { + 'project': { + 'identifier': 'project3', + 'name': 'Project 3', + 'description': 'Test project 3', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#0000FF', + 'modules': ['FF'] + } + } + ], + 'totalElements': 3, + 'totalPages': 2 + } + } + + # Set up the mock to return different responses for different calls + # Note: We only need two responses because the code will stop after page 1 (second page) + # since totalPages is set to 2 and our pagination is 0-indexed + SyncHttpClient.make_request.side_effect = [first_page_data, second_page_data] + + # Call the method being tested + result = pmc.list() + + # Verify the make_request calls - should only be 2 calls now since we're respecting totalPages + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + HarnessProjectMicroClient._endpoint['all_items'], + pageIndex=0, + accountIdentifier='test_account' + ) + SyncHttpClient.make_request.assert_any_call( + HarnessProjectMicroClient._endpoint['all_items'], + pageIndex=1, + accountIdentifier='test_account' + ) + + # Verify the result + assert len(result) == 3 + assert isinstance(result[0], HarnessProject) + assert isinstance(result[1], HarnessProject) + assert isinstance(result[2], HarnessProject) + assert result[0]._identifier == 'project1' + assert result[1]._identifier == 'project2' + assert result[2]._identifier == 'project3' + + def test_get(self, mocker): + ''' + Test getting a specific project + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': { + 'project': { + 'identifier': 'project1', + 'name': 'Project 1', + 'description': 'Test project 1', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#FF0000', + 'modules': ['FF'] + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = pmc.get('project1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessProjectMicroClient._endpoint['get'], + projectIdentifier='project1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessProject) + assert result._identifier == 'project1' + assert result._name == 'Project 1' + assert result._description == 'Test project 1' + + def test_create(self, mocker): + ''' + Test creating a project + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Project data to create + project_data = { + 'name': 'New Project', + 'description': 'Test project', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#0000FF', + 'modules': ['FF'] + } + + # Mock the API response + response_data = { + 'data': { + 'project': { + 'identifier': 'new_project', + 'name': 'New Project', + 'description': 'Test project', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#0000FF', + 'modules': ['FF'] + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = pmc.create(project_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessProjectMicroClient._endpoint['create'], + body=project_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessProject) + assert result._identifier == 'new_project' + assert result._name == 'New Project' + assert result._description == 'Test project' + + def test_update(self, mocker): + ''' + Test updating a project + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Project data to update + update_data = { + 'name': 'Updated Project', + 'description': 'Updated description', + 'color': '#FFFF00' + } + + # Mock the API response + response_data = { + 'data': { + 'project': { + 'identifier': 'project1', + 'name': 'Updated Project', + 'description': 'Updated description', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'org1', + 'color': '#FFFF00', + 'modules': ['FF'] + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = pmc.update('project1', update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessProjectMicroClient._endpoint['update'], + projectIdentifier='project1', + accountIdentifier='test_account', + body=update_data + ) + + # Verify the result + assert isinstance(result, HarnessProject) + assert result._identifier == 'project1' + assert result._name == 'Updated Project' + assert result._description == 'Updated description' + + def test_delete(self, mocker): + ''' + Test deleting a project + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + pmc = HarnessProjectMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = pmc.delete('project1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessProjectMicroClient._endpoint['delete'], + projectIdentifier='project1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py b/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py new file mode 100644 index 0000000..08c23d0 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/harness_user_microclient_test.py @@ -0,0 +1,314 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import HarnessUserMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import HarnessUser, HarnessInvite + + +class TestHarnessUserMicroClient: + + def test_list(self, mocker): + ''' + Test listing users + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'user': { + 'uuid': 'user1', + 'name': 'User 1', + 'email': 'user1@example.com', + 'accountIdentifier': 'test_account', + 'status': 'ACTIVE' + } + }, + { + 'user': { + 'uuid': 'user2', + 'name': 'User 2', + 'email': 'user2@example.com', + 'accountIdentifier': 'test_account', + 'status': 'ACTIVE' + } + } + ] + } + } + + # 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 + result = umc.list() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + HarnessUserMicroClient._endpoint['all_items'], + pageIndex=0, + accountIdentifier='test_account' + ) + SyncHttpClient.make_request.assert_any_call( + HarnessUserMicroClient._endpoint['all_items'], + pageIndex=1, + accountIdentifier='test_account' + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], HarnessUser) + assert isinstance(result[1], HarnessUser) + assert result[0]._uuid == 'user1' + assert result[1]._uuid == 'user2' + + def test_get(self, mocker): + ''' + Test getting a specific user + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'uuid': 'user1', + 'name': 'User 1', + 'email': 'user1@example.com', + 'accountIdentifier': 'test_account', + 'status': 'ACTIVE' + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = umc.get('user1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessUserMicroClient._endpoint['get_user'], + userId='user1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessUser) + assert result._uuid == 'user1' + assert result._name == 'User 1' + assert result._email == 'user1@example.com' + + def test_invite(self, mocker): + ''' + Test inviting a user + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # User data for invitation + user_data = { + 'name': 'New User', + 'email': 'newuser@example.com', + 'userGroups': [ + {'identifier': 'group1', 'type': 'USER_GROUP'} + ] + } + + # Mock the API response + response_data = { + 'data': True + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = umc.invite(user_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessUserMicroClient._endpoint['invite'], + body=user_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True + + def test_update(self, mocker): + ''' + Test updating a user + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # User data to update + update_data = { + 'name': 'Updated User', + 'email': 'updated@example.com' + } + + # Mock the API response + response_data = { + 'data': { + 'uuid': 'user1', + 'name': 'Updated User', + 'email': 'updated@example.com', + 'accountIdentifier': 'test_account', + 'status': 'ACTIVE' + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = umc.update('user1', update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessUserMicroClient._endpoint['update'], + body=update_data, + userId='user1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, HarnessUser) + assert result._uuid == 'user1' + assert result._name == 'Updated User' + assert result._email == 'updated@example.com' + + def test_add_user_to_groups(self, mocker): + ''' + Test adding a user to groups + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # Group IDs to add the user to + group_ids = ['group1', 'group2'] + + # Mock the API response + response_data = {} + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = umc.add_user_to_groups('user1', group_ids) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessUserMicroClient._endpoint['add_user_to_groups'], + body={"userGroupIdsToAdd": group_ids}, + userId='user1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True + + def test_delete_pending(self, mocker): + ''' + Test deleting a pending invite + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # Mock the API response + response_data = {} + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = umc.delete_pending('invite1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + HarnessUserMicroClient._endpoint['delete_pending'], + inviteId='invite1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True + + def test_list_pending(self, mocker): + ''' + Test listing pending invites + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + umc = HarnessUserMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'id': 'invite1', + 'email': 'pending1@example.com', + 'accountIdentifier': 'test_account', + 'approved': True + }, + { + 'id': 'invite2', + 'email': 'pending2@example.com', + 'accountIdentifier': 'test_account', + 'approved': True + } + ] + } + } + + # 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 + result = umc.list_pending() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + HarnessUserMicroClient._endpoint['list_pending'], + pageIndex=0, + accountIdentifier='test_account' + ) + SyncHttpClient.make_request.assert_any_call( + HarnessUserMicroClient._endpoint['list_pending'], + pageIndex=1, + accountIdentifier='test_account' + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], HarnessInvite) + assert isinstance(result[1], HarnessInvite) + assert result[0]._id == 'invite1' + assert result[1]._id == 'invite2' diff --git a/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py b/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py new file mode 100644 index 0000000..e7766fc --- /dev/null +++ b/splitapiclient/tests/microclients/harness/resource_group_microclient_test.py @@ -0,0 +1,265 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import ResourceGroupMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import ResourceGroup + + +class TestResourceGroupMicroClient: + + def test_list(self, mocker): + ''' + Test listing resource groups + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'resourceGroup': { + 'identifier': 'rg1', + 'name': 'Resource Group 1', + 'description': 'Test resource group 1', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': False, + 'resources': [ + {'identifier': 'resource1', 'type': 'FEATURE_FLAG'} + ] + } + } + }, + { + 'resourceGroup': { + 'identifier': 'rg2', + 'name': 'Resource Group 2', + 'description': 'Test resource group 2', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': True, + 'resources': [] + } + } + } + ] + } + } + + # 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 + result = rgmc.list() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + ResourceGroupMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=0 + ) + SyncHttpClient.make_request.assert_any_call( + ResourceGroupMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=1 + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], ResourceGroup) + assert isinstance(result[1], ResourceGroup) + assert result[0]._identifier == 'rg1' + assert result[1]._identifier == 'rg2' + + def test_get(self, mocker): + ''' + Test getting a specific resource group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': { + 'resourceGroup': { + 'identifier': 'rg1', + 'name': 'Resource Group 1', + 'description': 'Test resource group 1', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': False, + 'resources': [ + {'identifier': 'resource1', 'type': 'FEATURE_FLAG'} + ] + } + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = rgmc.get('rg1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ResourceGroupMicroClient._endpoint['get_resource_group'], + resourceGroupId='rg1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, ResourceGroup) + assert result._identifier == 'rg1' + assert result._name == 'Resource Group 1' + assert result._description == 'Test resource group 1' + + def test_create(self, mocker): + ''' + Test creating a resource group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + # Resource group data to create + rg_data = { + 'name': 'New Resource Group', + 'description': 'Test resource group', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': False, + 'resources': [ + {'identifier': 'resource1', 'type': 'FEATURE_FLAG'} + ] + } + } + + # Mock the API response + response_data = { + 'data': { + 'resourceGroup': { + 'identifier': 'new_rg', + 'name': 'New Resource Group', + 'description': 'Test resource group', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': False, + 'resources': [ + {'identifier': 'resource1', 'type': 'FEATURE_FLAG'} + ] + } + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = rgmc.create(rg_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ResourceGroupMicroClient._endpoint['create'], + body=rg_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, ResourceGroup) + assert result._identifier == 'new_rg' + assert result._name == 'New Resource Group' + assert result._description == 'Test resource group' + + def test_update(self, mocker): + ''' + Test updating a resource group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + # Resource group data to update + update_data = { + 'name': 'Updated Resource Group', + 'description': 'Updated description', + 'resourceFilter': { + 'includeAllResources': True, + 'resources': [] + } + } + + # Mock the API response + response_data = { + 'data': { + 'resourceGroup': { + 'identifier': 'rg1', + 'name': 'Updated Resource Group', + 'description': 'Updated description', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': True, + 'resources': [] + } + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = rgmc.update('rg1', update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ResourceGroupMicroClient._endpoint['update'], + body=update_data, + resourceGroupId='rg1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, ResourceGroup) + assert result._identifier == 'rg1' + assert result._name == 'Updated Resource Group' + assert result._description == 'Updated description' + + def test_delete(self, mocker): + ''' + Test deleting a resource group + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rgmc = ResourceGroupMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = rgmc.delete('rg1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ResourceGroupMicroClient._endpoint['delete'], + resourceGroupId='rg1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py b/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py new file mode 100644 index 0000000..103f8f1 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/role_assignment_microclient_test.py @@ -0,0 +1,203 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import RoleAssignmentMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import RoleAssignment + + +class TestRoleAssignmentMicroClient: + + def test_list(self, mocker): + ''' + Test listing role assignments + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'roleAssignment': { + 'identifier': 'ra1', + 'roleIdentifier': 'role1', + 'resourceGroupIdentifier': 'rg1', + 'accountIdentifier': 'test_account', + 'principal': { + 'identifier': 'user1', + 'type': 'USER' + } + } + }, + { + 'roleAssignment': { + 'identifier': 'ra2', + 'roleIdentifier': 'role2', + 'resourceGroupIdentifier': 'rg2', + 'accountIdentifier': 'test_account', + 'principal': { + 'identifier': 'user2', + 'type': 'USER' + } + } + } + ] + } + } + + # 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 + result = ramc.list() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + RoleAssignmentMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=0 + ) + SyncHttpClient.make_request.assert_any_call( + RoleAssignmentMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=1 + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], RoleAssignment) + assert isinstance(result[1], RoleAssignment) + assert result[0]._identifier == 'ra1' + assert result[1]._identifier == 'ra2' + + def test_get(self, mocker): + ''' + Test getting a specific role assignment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': { + 'roleAssignment': { + 'identifier': 'ra1', + 'roleIdentifier': 'role1', + 'resourceGroupIdentifier': 'rg1', + 'accountIdentifier': 'test_account', + 'principal': { + 'identifier': 'user1', + 'type': 'USER' + } + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = ramc.get('ra1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleAssignmentMicroClient._endpoint['get_role_assignment'], + roleAssignmentId='ra1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, RoleAssignment) + assert result._identifier == 'ra1' + assert result._role_identifier == 'role1' + assert result._resource_group_identifier == 'rg1' + + def test_create(self, mocker): + ''' + Test creating a role assignment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account') + + # Role assignment data to create + ra_data = { + 'roleIdentifier': 'role1', + 'resourceGroupIdentifier': 'rg1', + 'accountIdentifier': 'test_account', + 'principal': { + 'identifier': 'user1', + 'type': 'USER' + } + } + + # Mock the API response + response_data = { + 'data': { + 'roleAssignment': { + 'identifier': 'new_ra', + 'roleIdentifier': 'role1', + 'resourceGroupIdentifier': 'rg1', + 'accountIdentifier': 'test_account', + 'principal': { + 'identifier': 'user1', + 'type': 'USER' + } + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = ramc.create(ra_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleAssignmentMicroClient._endpoint['create'], + body=ra_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, RoleAssignment) + assert result._identifier == 'new_ra' + assert result._role_identifier == 'role1' + assert result._resource_group_identifier == 'rg1' + + def test_delete(self, mocker): + ''' + Test deleting a role assignment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + ramc = RoleAssignmentMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = ramc.delete('ra1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleAssignmentMicroClient._endpoint['delete'], + roleAssignmentId='ra1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/role_microclient_test.py b/splitapiclient/tests/microclients/harness/role_microclient_test.py new file mode 100644 index 0000000..307c9af --- /dev/null +++ b/splitapiclient/tests/microclients/harness/role_microclient_test.py @@ -0,0 +1,236 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import RoleMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import Role + + +class TestRoleMicroClient: + + def test_list(self, mocker): + ''' + Test listing roles + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'role': { + 'identifier': 'role1', + 'name': 'Role 1', + 'description': 'Test role 1', + 'accountIdentifier': 'test_account', + 'permissions': ['permission1', 'permission2'] + } + }, + { + 'role': { + 'identifier': 'role2', + 'name': 'Role 2', + 'description': 'Test role 2', + 'accountIdentifier': 'test_account', + 'permissions': ['permission3', 'permission4'] + } + } + ] + } + } + + # 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 + result = rmc.list() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + RoleMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=0 + ) + SyncHttpClient.make_request.assert_any_call( + RoleMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=1 + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], Role) + assert isinstance(result[1], Role) + assert result[0]._identifier == 'role1' + assert result[1]._identifier == 'role2' + + def test_get(self, mocker): + ''' + Test getting a specific role + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': { + 'role': { + 'identifier': 'role1', + 'name': 'Role 1', + 'description': 'Test role 1', + 'accountIdentifier': 'test_account', + 'permissions': ['permission1', 'permission2'] + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = rmc.get('role1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleMicroClient._endpoint['get_role'], + roleId='role1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, Role) + assert result._identifier == 'role1' + assert result._name == 'Role 1' + assert result._description == 'Test role 1' + + def test_create(self, mocker): + ''' + Test creating a role + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + # Role data to create + role_data = { + 'name': 'New Role', + 'description': 'Test role', + 'accountIdentifier': 'test_account', + 'permissions': ['permission1', 'permission2'] + } + + # Mock the API response + response_data = { + 'data': { + 'role': { + 'identifier': 'new_role', + 'name': 'New Role', + 'description': 'Test role', + 'accountIdentifier': 'test_account', + 'permissions': ['permission1', 'permission2'] + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = rmc.create(role_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleMicroClient._endpoint['create'], + body=role_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, Role) + assert result._identifier == 'new_role' + assert result._name == 'New Role' + assert result._description == 'Test role' + + def test_update(self, mocker): + ''' + Test updating a role + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + # Role data to update + update_data = { + 'name': 'Updated Role', + 'description': 'Updated description', + 'permissions': ['permission1', 'permission2', 'permission3'] + } + + # Mock the API response + response_data = { + 'data': { + 'role': { + 'identifier': 'role1', + 'name': 'Updated Role', + 'description': 'Updated description', + 'accountIdentifier': 'test_account', + 'permissions': ['permission1', 'permission2', 'permission3'] + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = rmc.update('role1', update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleMicroClient._endpoint['update'], + body=update_data, + roleId='role1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, Role) + assert result._identifier == 'role1' + assert result._name == 'Updated Role' + assert result._description == 'Updated description' + + def test_delete(self, mocker): + ''' + Test deleting a role + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rmc = RoleMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = rmc.delete('role1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + RoleMicroClient._endpoint['delete'], + roleId='role1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/service_account_microclient_test.py b/splitapiclient/tests/microclients/harness/service_account_microclient_test.py new file mode 100644 index 0000000..a0c0107 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/service_account_microclient_test.py @@ -0,0 +1,216 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import ServiceAccountMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import ServiceAccount + + +class TestServiceAccountMicroClient: + + def test_list(self, mocker): + ''' + Test listing service accounts + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': [ + { + 'identifier': 'sa1', + 'name': 'Service Account 1', + 'description': 'Test service account 1', + 'accountIdentifier': 'test_account', + 'email': 'sa1@example.com', + 'tags': {} + }, + { + 'identifier': 'sa2', + 'name': 'Service Account 2', + 'description': 'Test service account 2', + 'accountIdentifier': 'test_account', + 'email': 'sa2@example.com', + 'tags': {} + } + ] + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = samc.list() + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ServiceAccountMicroClient._endpoint['all_items'], + accountIdentifier='test_account' + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], ServiceAccount) + assert isinstance(result[1], ServiceAccount) + assert result[0]._identifier == 'sa1' + assert result[1]._identifier == 'sa2' + + def test_get(self, mocker): + ''' + Test getting a specific service account + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': { + 'serviceAccount': { + 'identifier': 'sa1', + 'name': 'Service Account 1', + 'description': 'Test service account 1', + 'accountIdentifier': 'test_account', + 'email': 'sa1@example.com', + 'tags': {} + } + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = samc.get('sa1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ServiceAccountMicroClient._endpoint['item'], + serviceAccountId='sa1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, ServiceAccount) + assert result._identifier == 'sa1' + assert result._name == 'Service Account 1' + assert result._description == 'Test service account 1' + + def test_create(self, mocker): + ''' + Test creating a service account + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + # Service account data to create + sa_data = { + 'name': 'New Service Account', + 'description': 'Test service account', + 'email': 'new_sa@example.com', + 'tags': {} + } + + # Mock the API response + response_data = { + 'data': { + 'identifier': 'new_sa', + 'name': 'New Service Account', + 'description': 'Test service account', + 'accountIdentifier': 'test_account', + 'email': 'new_sa@example.com', + 'tags': {} + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = samc.create(sa_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ServiceAccountMicroClient._endpoint['create'], + body=sa_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, ServiceAccount) + assert result._identifier == 'new_sa' + assert result._name == 'New Service Account' + assert result._description == 'Test service account' + + def test_update(self, mocker): + ''' + Test updating a service account + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + # Service account data to update + update_data = { + 'name': 'Updated Service Account', + 'description': 'Updated description' + } + + # Mock the API response + response_data = { + 'data': { + 'identifier': 'sa1', + 'name': 'Updated Service Account', + 'description': 'Updated description', + 'accountIdentifier': 'test_account', + 'email': 'sa1@example.com', + 'tags': {} + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = samc.update('sa1', update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ServiceAccountMicroClient._endpoint['update'], + body=update_data, + serviceAccountId='sa1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, ServiceAccount) + assert result._identifier == 'sa1' + assert result._name == 'Updated Service Account' + assert result._description == 'Updated description' + + def test_delete(self, mocker): + ''' + Test deleting a service account + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + samc = ServiceAccountMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = samc.delete('sa1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + ServiceAccountMicroClient._endpoint['delete'], + serviceAccountId='sa1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/microclients/harness/token_microclient_test.py b/splitapiclient/tests/microclients/harness/token_microclient_test.py new file mode 100644 index 0000000..3ee6d33 --- /dev/null +++ b/splitapiclient/tests/microclients/harness/token_microclient_test.py @@ -0,0 +1,283 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients.harness import TokenMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.resources.harness import Token + + +class TestTokenMicroClient: + + def test_list(self, mocker): + ''' + Test listing tokens + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + # Mock the API response for the first page + first_page_data = { + 'data': { + 'content': [ + { + 'token': { + 'identifier': 'token1', + 'name': 'Test Token 1', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'scheduledExpireTime': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account', + 'apiKeyIdentifier': 'api_key1', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'USER', + 'description': 'Test token 1', + 'tags': {} + } + }, + { + 'token': { + 'identifier': 'token2', + 'name': 'Test Token 2', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'scheduledExpireTime': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account', + 'apiKeyIdentifier': 'api_key2', + 'parentIdentifier': 'parent2', + 'apiKeyType': 'USER', + 'description': 'Test token 2', + 'tags': {} + } + } + ] + } + } + + # 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 + result = tmc.list() + + # Verify the make_request calls + assert SyncHttpClient.make_request.call_count == 2 + SyncHttpClient.make_request.assert_any_call( + TokenMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=0 + ) + SyncHttpClient.make_request.assert_any_call( + TokenMicroClient._endpoint['all_items'], + accountIdentifier='test_account', + pageIndex=1 + ) + + # Verify the result + assert len(result) == 2 + assert isinstance(result[0], Token) + assert isinstance(result[1], Token) + assert result[0]._identifier == 'token1' + assert result[1]._identifier == 'token2' + + def test_get(self, mocker): + ''' + Test getting a specific token + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + # Create mock tokens to be returned by the list method + token1 = Token({ + 'identifier': 'token1', + 'name': 'Test Token 1', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'scheduledExpireTime': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account', + 'apiKeyIdentifier': 'api_key1', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'USER', + 'description': 'Test token 1', + 'tags': {} + }, sc) + + token2 = Token({ + 'identifier': 'token2', + 'name': 'Test Token 2', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'scheduledExpireTime': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account', + 'apiKeyIdentifier': 'api_key2', + 'parentIdentifier': 'parent2', + 'apiKeyType': 'USER', + 'description': 'Test token 2', + 'tags': {} + }, sc) + + # Mock the list method to return our predefined tokens + mocker.patch.object(tmc, 'list', return_value=[token1, token2]) + + # Call the method being tested + result = tmc.get('token2') + + # Verify the list method was called with the correct parameters + tmc.list.assert_called_once_with(account_identifier='test_account') + + # Verify the result + assert isinstance(result, Token) + assert result._identifier == 'token2' + assert result._name == 'Test Token 2' + + def test_create(self, mocker): + ''' + Test creating a token + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + # Token data to create + token_data = { + 'name': 'New Token', + 'description': 'Test token', + 'apiKeyType': 'SERVICE_ACCOUNT', + 'parentIdentifier': 'parent1', + 'apiKeyIdentifier': 'api_key1' + } + + # Mock the API response + response_data = { + 'data': 'token123abc' + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = tmc.create(token_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + TokenMicroClient._endpoint['create'], + body=token_data, + accountIdentifier='test_account' + ) + + # Verify the result + assert result == 'token123abc' + + def test_update(self, mocker): + ''' + Test updating a token + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + # Token data to update + update_data = { + 'name': 'Updated Token', + 'description': 'Updated description' + } + + # Mock the API response + response_data = { + 'data': { + 'identifier': 'token1', + 'name': 'Updated Token', + 'description': 'Updated description', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account' + } + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = tmc.update('token1', update_data) + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + TokenMicroClient._endpoint['update_token'], + body=update_data, + tokenId='token1', + accountIdentifier='test_account' + ) + + # Verify the result + assert isinstance(result, Token) + assert result._identifier == 'token1' + assert result._name == 'Updated Token' + assert result._description == 'Updated description' + + def test_rotate(self, mocker): + ''' + Test rotating a token + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + # Mock the API response + response_data = { + 'data': 'new_token_value_123' + } + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = response_data + + # Call the method being tested + result = tmc.rotate('token1', 'parent1', 'api_key1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + TokenMicroClient._endpoint['rotate_token'], + tokenId='token1', + parentIdentifier='parent1', + apiKeyIdentifier='api_key1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result == 'new_token_value_123' + + def test_delete(self, mocker): + ''' + Test deleting a token + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + tmc = TokenMicroClient(sc, 'test_account') + + # Set up the mock to return the response + SyncHttpClient.make_request.return_value = {} + + # Call the method being tested + result = tmc.delete('token1') + + # Verify the make_request call + SyncHttpClient.make_request.assert_called_once_with( + TokenMicroClient._endpoint['delete'], + tokenId='token1', + accountIdentifier='test_account' + ) + + # Verify the result + assert result is True diff --git a/splitapiclient/tests/resources/harness/harness_apikey_test.py b/splitapiclient/tests/resources/harness/harness_apikey_test.py new file mode 100644 index 0000000..9ba966e --- /dev/null +++ b/splitapiclient/tests/resources/harness/harness_apikey_test.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import HarnessApiKey +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestHarnessApiKey: + """ + Tests for the HarnessApiKey resource class + """ + + def test_initialization(self): + """ + Test initialization of a HarnessApiKey object + """ + # Test with empty data + apikey = HarnessApiKey() + assert apikey._id is None + assert apikey._identifier is None + assert apikey._name is None + + # Test with data + apikey_data = { + 'identifier': 'apikey1', + 'name': 'Test API Key', + 'description': 'Test API key description', + 'value': 'api_key_value_123', + 'apiKeyType': 'SERVICE_ACCOUNT', + 'parentIdentifier': 'parent1', + 'defaultTimeToExpireToken': 3600, + 'accountIdentifier': 'test_account', + 'projectIdentifier': 'test_project', + 'orgIdentifier': 'test_org', + 'governanceMetadata': {'key': 'value'} + } + + apikey = HarnessApiKey(apikey_data) + + # Verify all properties were set correctly + assert apikey._id == 'apikey1' + assert apikey._identifier == 'apikey1' + assert apikey._name == 'Test API Key' + assert apikey._description == 'Test API key description' + assert apikey._value == 'api_key_value_123' + assert apikey._api_key_type == 'SERVICE_ACCOUNT' + assert apikey._parent_identifier == 'parent1' + assert apikey._default_time_to_expire_token == 3600 + assert apikey._account_identifier == 'test_account' + assert apikey._project_identifier == 'test_project' + assert apikey._org_identifier == 'test_org' + assert apikey._governance_metadata == {'key': 'value'} + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + apikey_data = { + 'identifier': 'apikey1', + 'name': 'Test API Key', + 'apiKeyType': 'SERVICE_ACCOUNT', + 'parentIdentifier': 'parent1', + 'accountIdentifier': 'test_account' + } + + apikey = HarnessApiKey(apikey_data) + + # Test accessing properties via camelCase (direct schema field names) + assert apikey.identifier == 'apikey1' + assert apikey.name == 'Test API Key' + assert apikey.apiKeyType == 'SERVICE_ACCOUNT' + assert apikey.parentIdentifier == 'parent1' + assert apikey.accountIdentifier == 'test_account' + + # Test accessing properties via snake_case + assert apikey.api_key_type == 'SERVICE_ACCOUNT' + assert apikey.parent_identifier == 'parent1' + assert apikey.account_identifier == 'test_account' + + # Test accessing non-existent property + with pytest.raises(AttributeError): + apikey.non_existent_property + + def test_export_dict(self): + """ + Test exporting API key data as a dictionary + """ + apikey_data = { + 'identifier': 'apikey1', + 'name': 'Test API Key', + 'description': 'Test API key description', + 'apiKeyType': 'SERVICE_ACCOUNT', + 'parentIdentifier': 'parent1', + 'accountIdentifier': 'test_account' + } + + apikey = HarnessApiKey(apikey_data) + exported_data = apikey.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'apikey1' + assert exported_data['name'] == 'Test API Key' + assert exported_data['description'] == 'Test API key description' + assert exported_data['apiKeyType'] == 'SERVICE_ACCOUNT' + assert exported_data['parentIdentifier'] == 'parent1' + assert exported_data['accountIdentifier'] == 'test_account' + + # Verify fields that weren't in the original data are None in the export + assert 'value' in exported_data + assert exported_data['value'] is None + assert 'projectIdentifier' in exported_data + assert exported_data['projectIdentifier'] is None diff --git a/splitapiclient/tests/resources/harness/harness_group_test.py b/splitapiclient/tests/resources/harness/harness_group_test.py new file mode 100644 index 0000000..77f42f9 --- /dev/null +++ b/splitapiclient/tests/resources/harness/harness_group_test.py @@ -0,0 +1,140 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import HarnessGroup +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestHarnessGroup: + """ + Tests for the HarnessGroup resource class + """ + + def test_initialization(self): + """ + Test initialization of a HarnessGroup object + """ + # Test with empty data + group = HarnessGroup() + assert group._id is None + assert group._identifier is None + assert group._name is None + + # Test with data + group_data = { + 'identifier': 'group1', + 'name': 'Test Group', + 'description': 'Test group description', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'test_org', + 'projectIdentifier': 'test_project', + 'users': [ + { + 'uuid': 'user1', + 'name': 'User 1', + 'email': 'user1@example.com' + } + ], + 'ssoLinked': False, + 'externallyManaged': False, + 'harnessManaged': True, + 'tags': { + 'property1': 'value1', + 'property2': 'value2' + } + } + + group = HarnessGroup(group_data) + + # Verify all properties were set correctly + assert group._id == 'group1' + assert group._identifier == 'group1' + assert group._name == 'Test Group' + assert group._description == 'Test group description' + assert group._account_identifier == 'test_account' + assert group._org_identifier == 'test_org' + assert group._project_identifier == 'test_project' + assert len(group._users) == 1 + assert group._users[0]['uuid'] == 'user1' + assert group._sso_linked is False + assert group._externally_managed is False + assert group._harness_managed is True + assert group._tags == {'property1': 'value1', 'property2': 'value2'} + + def test_name_property(self): + """ + Test the name property accessor + """ + group_data = { + 'identifier': 'group1', + 'name': 'Test Group', + 'accountIdentifier': 'test_account' + } + + group = HarnessGroup(group_data) + assert group.name == 'Test Group' + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + group_data = { + 'identifier': 'group1', + 'name': 'Test Group', + 'description': 'Test group description', + 'accountIdentifier': 'test_account', + 'ssoLinked': False, + 'externallyManaged': False + } + + group = HarnessGroup(group_data) + + # Test accessing properties via camelCase (direct schema field names) + assert group.identifier == 'group1' + assert group.name == 'Test Group' + assert group.description == 'Test group description' + assert group.accountIdentifier == 'test_account' + assert group.ssoLinked is False + assert group.externallyManaged is False + + # Test accessing properties via snake_case + assert group.account_identifier == 'test_account' + assert group.sso_linked is False + assert group.externally_managed is False + + # Test accessing non-existent property + with pytest.raises(AttributeError): + group.non_existent_property + + def test_export_dict(self): + """ + Test exporting group data as a dictionary + """ + group_data = { + 'identifier': 'group1', + 'name': 'Test Group', + 'description': 'Test group description', + 'accountIdentifier': 'test_account', + 'ssoLinked': False, + 'externallyManaged': False + } + + group = HarnessGroup(group_data) + exported_data = group.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'group1' + assert exported_data['name'] == 'Test Group' + assert exported_data['description'] == 'Test group description' + assert exported_data['accountIdentifier'] == 'test_account' + assert exported_data['ssoLinked'] is False + assert exported_data['externallyManaged'] is False + + # Verify fields that weren't in the original data are None in the export + assert 'orgIdentifier' in exported_data + assert exported_data['orgIdentifier'] is None + assert 'projectIdentifier' in exported_data + assert exported_data['projectIdentifier'] is None + assert 'users' in exported_data + assert exported_data['users'] is None diff --git a/splitapiclient/tests/resources/harness/harness_project_test.py b/splitapiclient/tests/resources/harness/harness_project_test.py new file mode 100644 index 0000000..2926053 --- /dev/null +++ b/splitapiclient/tests/resources/harness/harness_project_test.py @@ -0,0 +1,101 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import HarnessProject +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestHarnessProject: + """ + Tests for the HarnessProject resource class + """ + + def test_initialization(self): + """ + Test initialization of a HarnessProject object + """ + # Test with empty data + project = HarnessProject() + assert project._id is None + assert project._identifier is None + assert project._name is None + + # Test with data + project_data = { + 'identifier': 'project1', + 'name': 'Test Project', + 'description': 'Test project description', + 'orgIdentifier': 'test_org', + 'color': '#FF0000', + 'modules': ['FF', 'CI', 'CD'], + 'tags': { + 'property1': 'value1', + 'property2': 'value2' + } + } + + project = HarnessProject(project_data) + + # Verify all properties were set correctly + assert project._id == 'project1' + assert project._identifier == 'project1' + assert project._name == 'Test Project' + assert project._description == 'Test project description' + assert project._org_identifier == 'test_org' + assert project._color == '#FF0000' + assert project._modules == ['FF', 'CI', 'CD'] + assert project._tags == {'property1': 'value1', 'property2': 'value2'} + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + project_data = { + 'identifier': 'project1', + 'name': 'Test Project', + 'description': 'Test project description', + 'orgIdentifier': 'test_org', + 'color': '#FF0000' + } + + project = HarnessProject(project_data) + + # Test accessing properties via snake_case + assert project.identifier == 'project1' + assert project.name == 'Test Project' + assert project.description == 'Test project description' + assert project.org_identifier == 'test_org' + assert project.color == '#FF0000' + + # Test accessing non-existent property + with pytest.raises(AttributeError): + project.non_existent_property + + def test_export_dict(self): + """ + Test exporting project data as a dictionary + """ + project_data = { + 'identifier': 'project1', + 'name': 'Test Project', + 'description': 'Test project description', + 'orgIdentifier': 'test_org', + 'color': '#FF0000' + } + + project = HarnessProject(project_data) + exported_data = project.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'project1' + assert exported_data['name'] == 'Test Project' + assert exported_data['description'] == 'Test project description' + assert exported_data['orgIdentifier'] == 'test_org' + assert exported_data['color'] == '#FF0000' + + # Verify fields that weren't in the original data are None in the export + assert 'modules' in exported_data + assert exported_data['modules'] is None + assert 'tags' in exported_data + assert exported_data['tags'] is None diff --git a/splitapiclient/tests/resources/harness/harness_user_test.py b/splitapiclient/tests/resources/harness/harness_user_test.py new file mode 100644 index 0000000..381b114 --- /dev/null +++ b/splitapiclient/tests/resources/harness/harness_user_test.py @@ -0,0 +1,112 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import HarnessUser +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestHarnessUser: + """ + Tests for the HarnessUser resource class + """ + + def test_initialization(self): + """ + Test initialization of a HarnessUser object + """ + # Test with empty data + user = HarnessUser() + assert user._id is None + assert user._name is None + assert user._email is None + + # Test with data + user_data = { + 'uuid': 'user1', + 'name': 'Test User', + 'email': 'user@example.com', + 'locked': False, + 'disabled': False, + 'externally_managed': False, + 'two_factor_authentication_enabled': True + } + + user = HarnessUser(user_data) + + # Verify all properties were set correctly + assert user._id == 'user1' + assert user._name == 'Test User' + assert user._email == 'user@example.com' + assert user._locked is False + assert user._disabled is False + assert user._externally_managed is False + assert user._two_factor_authentication_enabled is True + + def test_name_property(self): + """ + Test the name property accessor + """ + user_data = { + 'uuid': 'user1', + 'name': 'Test User', + 'email': 'user@example.com' + } + + user = HarnessUser(user_data) + assert user.name == 'Test User' + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + user_data = { + 'uuid': 'user1', + 'name': 'Test User', + 'email': 'user@example.com', + 'locked': False, + 'disabled': False + } + + user = HarnessUser(user_data) + + # Test accessing properties via direct schema field names + assert user.uuid == 'user1' + assert user.email == 'user@example.com' + assert user.locked is False + assert user.disabled is False + + # Test accessing properties via snake_case + assert user.two_factor_authentication_enabled is None + + # Test accessing non-existent property + with pytest.raises(AttributeError): + user.non_existent_property + + def test_export_dict(self): + """ + Test exporting user data as a dictionary + """ + user_data = { + 'uuid': 'user1', + 'name': 'Test User', + 'email': 'user@example.com', + 'locked': False, + 'disabled': False + } + + user = HarnessUser(user_data) + exported_data = user.export_dict() + + # Verify exported data contains all original fields + assert exported_data['uuid'] == 'user1' + assert exported_data['name'] == 'Test User' + assert exported_data['email'] == 'user@example.com' + assert exported_data['locked'] is False + assert exported_data['disabled'] is False + + # Verify fields that weren't in the original data are None in the export + assert 'externally_managed' in exported_data + assert exported_data['externally_managed'] is None + assert 'two_factor_authentication_enabled' in exported_data + assert exported_data['two_factor_authentication_enabled'] is None diff --git a/splitapiclient/tests/resources/harness/resource_group_test.py b/splitapiclient/tests/resources/harness/resource_group_test.py new file mode 100644 index 0000000..d7a383c --- /dev/null +++ b/splitapiclient/tests/resources/harness/resource_group_test.py @@ -0,0 +1,143 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import ResourceGroup +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestResourceGroup: + """ + Tests for the ResourceGroup resource class + """ + + def test_initialization(self): + """ + Test initialization of a ResourceGroup object + """ + # Test with empty data + resource_group = ResourceGroup() + assert resource_group._id is None + assert resource_group._identifier is None + assert resource_group._name is None + + # Test with data + resource_group_data = { + 'identifier': 'rg1', + 'name': 'Test Resource Group', + 'description': 'Test resource group description', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'test_org', + 'projectIdentifier': 'test_project', + 'color': '#FF0000', + 'tags': { + 'property1': 'value1', + 'property2': 'value2' + }, + 'allowedScopeLevels': ['account', 'project'], + 'includedScopes': [ + { + 'filter': 'filter1', + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'test_org', + 'projectIdentifier': 'test_project' + } + ], + 'resourceFilter': { + 'resources': [ + { + 'resourceType': 'FEATURE_FLAG', + 'identifiers': ['flag1', 'flag2'], + 'attributeFilter': { + 'attributeName': 'attr1', + 'attributeValues': ['val1', 'val2'] + } + } + ], + 'includeAllResources': False + } + } + + resource_group = ResourceGroup(resource_group_data) + + # Verify all properties were set correctly + assert resource_group._id == 'rg1' + assert resource_group._identifier == 'rg1' + assert resource_group._name == 'Test Resource Group' + assert resource_group._description == 'Test resource group description' + assert resource_group._account_identifier == 'test_account' + assert resource_group._org_identifier == 'test_org' + assert resource_group._project_identifier == 'test_project' + assert resource_group._color == '#FF0000' + assert resource_group._tags == {'property1': 'value1', 'property2': 'value2'} + assert resource_group._allowed_scope_levels == ['account', 'project'] + assert len(resource_group._included_scopes) == 1 + assert resource_group._included_scopes[0]['filter'] == 'filter1' + assert resource_group._resource_filter['includeAllResources'] is False + assert len(resource_group._resource_filter['resources']) == 1 + assert resource_group._resource_filter['resources'][0]['resourceType'] == 'FEATURE_FLAG' + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + resource_group_data = { + 'identifier': 'rg1', + 'name': 'Test Resource Group', + 'description': 'Test resource group description', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': False, + 'resources': [] + } + } + + resource_group = ResourceGroup(resource_group_data) + + # Test accessing properties via camelCase (direct schema field names) + assert resource_group.identifier == 'rg1' + assert resource_group.name == 'Test Resource Group' + assert resource_group.description == 'Test resource group description' + assert resource_group.accountIdentifier == 'test_account' + assert resource_group.resourceFilter['includeAllResources'] is False + + # Test accessing properties via snake_case + assert resource_group.account_identifier == 'test_account' + assert resource_group.resource_filter['includeAllResources'] is False + + # Test accessing non-existent property + with pytest.raises(AttributeError): + resource_group.non_existent_property + + def test_export_dict(self): + """ + Test exporting resource group data as a dictionary + """ + resource_group_data = { + 'identifier': 'rg1', + 'name': 'Test Resource Group', + 'description': 'Test resource group description', + 'accountIdentifier': 'test_account', + 'resourceFilter': { + 'includeAllResources': False, + 'resources': [] + } + } + + resource_group = ResourceGroup(resource_group_data) + exported_data = resource_group.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'rg1' + assert exported_data['name'] == 'Test Resource Group' + assert exported_data['description'] == 'Test resource group description' + assert exported_data['accountIdentifier'] == 'test_account' + assert exported_data['resourceFilter']['includeAllResources'] is False + + # Verify fields that weren't in the original data are None in the export + assert 'orgIdentifier' in exported_data + assert exported_data['orgIdentifier'] is None + assert 'projectIdentifier' in exported_data + assert exported_data['projectIdentifier'] is None + assert 'color' in exported_data + assert exported_data['color'] is None diff --git a/splitapiclient/tests/resources/harness/role_assignment_test.py b/splitapiclient/tests/resources/harness/role_assignment_test.py new file mode 100644 index 0000000..5e3622e --- /dev/null +++ b/splitapiclient/tests/resources/harness/role_assignment_test.py @@ -0,0 +1,134 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import RoleAssignment +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestRoleAssignment: + """ + Tests for the RoleAssignment resource class + """ + + def test_initialization(self): + """ + Test initialization of a RoleAssignment object + """ + # Test with empty data + role_assignment = RoleAssignment() + assert role_assignment._id is None + assert role_assignment._identifier is None + assert role_assignment._role_identifier is None + + # Test with data + role_assignment_data = { + 'identifier': 'ra1', + 'resourceGroupIdentifier': 'rg1', + 'roleIdentifier': 'role1', + 'roleReference': { + 'identifier': 'role1', + 'scopeLevel': 'account' + }, + 'principal': { + 'scopeLevel': 'account', + 'identifier': 'user1', + 'type': 'USER', + 'uniqueId': 'unique1' + }, + 'disabled': False, + 'managed': True, + 'internal': False + } + + role_assignment = RoleAssignment(role_assignment_data) + + # Verify all properties were set correctly + assert role_assignment._id == 'ra1' + assert role_assignment._identifier == 'ra1' + assert role_assignment._resource_group_identifier == 'rg1' + assert role_assignment._role_identifier == 'role1' + assert role_assignment._role_reference == { + 'identifier': 'role1', + 'scopeLevel': 'account' + } + assert role_assignment._principal == { + 'scopeLevel': 'account', + 'identifier': 'user1', + 'type': 'USER', + 'uniqueId': 'unique1' + } + assert role_assignment._disabled is False + assert role_assignment._managed is True + assert role_assignment._internal is False + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + role_assignment_data = { + 'identifier': 'ra1', + 'resourceGroupIdentifier': 'rg1', + 'roleIdentifier': 'role1', + 'principal': { + 'identifier': 'user1', + 'type': 'USER' + }, + 'disabled': False + } + + role_assignment = RoleAssignment(role_assignment_data) + + # Test accessing properties via camelCase (direct schema field names) + assert role_assignment.identifier == 'ra1' + assert role_assignment.resourceGroupIdentifier == 'rg1' + assert role_assignment.roleIdentifier == 'role1' + assert role_assignment.principal == { + 'identifier': 'user1', + 'type': 'USER' + } + assert role_assignment.disabled is False + + # Test accessing properties via snake_case + assert role_assignment.resource_group_identifier == 'rg1' + assert role_assignment.role_identifier == 'role1' + + # Test accessing non-existent property + with pytest.raises(AttributeError): + role_assignment.non_existent_property + + def test_export_dict(self): + """ + Test exporting role assignment data as a dictionary + """ + role_assignment_data = { + 'identifier': 'ra1', + 'resourceGroupIdentifier': 'rg1', + 'roleIdentifier': 'role1', + 'principal': { + 'identifier': 'user1', + 'type': 'USER' + }, + 'disabled': False + } + + role_assignment = RoleAssignment(role_assignment_data) + exported_data = role_assignment.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'ra1' + assert exported_data['resourceGroupIdentifier'] == 'rg1' + assert exported_data['roleIdentifier'] == 'role1' + assert exported_data['principal'] == { + 'identifier': 'user1', + 'type': 'USER' + } + assert exported_data['disabled'] is False + + # Verify fields that weren't in the original data are None in the export + assert 'roleReference' in exported_data + assert exported_data['roleReference'] is None + assert 'managed' in exported_data + assert exported_data['managed'] is None + assert 'internal' in exported_data + assert exported_data['internal'] is None diff --git a/splitapiclient/tests/resources/harness/role_test.py b/splitapiclient/tests/resources/harness/role_test.py new file mode 100644 index 0000000..1ba1b32 --- /dev/null +++ b/splitapiclient/tests/resources/harness/role_test.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import Role +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestRole: + """ + Tests for the Role resource class + """ + + def test_initialization(self): + """ + Test initialization of a Role object + """ + # Test with empty data + role = Role() + assert role._id is None + assert role._identifier is None + assert role._name is None + + # Test with data + role_data = { + 'identifier': 'role1', + 'name': 'Test Role', + 'description': 'Test role description', + 'permissions': ['permission1', 'permission2', 'permission3'], + 'allowed_scope_levels': ['account', 'project'] + } + + role = Role(role_data) + + # Verify all properties were set correctly + assert role._id == 'role1' + assert role._identifier == 'role1' + assert role._name == 'Test Role' + assert role._description == 'Test role description' + assert role._permissions == ['permission1', 'permission2', 'permission3'] + assert role._allowed_scope_levels == ['account', 'project'] + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + role_data = { + 'identifier': 'role1', + 'name': 'Test Role', + 'description': 'Test role description', + 'permissions': ['permission1', 'permission2'] + } + + role = Role(role_data) + + # Test accessing properties via camelCase (direct schema field names) + assert role.identifier == 'role1' + assert role.name == 'Test Role' + assert role.description == 'Test role description' + assert role.permissions == ['permission1', 'permission2'] + + # Test accessing properties via snake_case + assert role.allowed_scope_levels is None + + # Test accessing non-existent property + with pytest.raises(AttributeError): + role.non_existent_property + + def test_export_dict(self): + """ + Test exporting role data as a dictionary + """ + role_data = { + 'identifier': 'role1', + 'name': 'Test Role', + 'description': 'Test role description', + 'permissions': ['permission1', 'permission2'] + } + + role = Role(role_data) + exported_data = role.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'role1' + assert exported_data['name'] == 'Test Role' + assert exported_data['description'] == 'Test role description' + assert exported_data['permissions'] == ['permission1', 'permission2'] + + # Verify fields that weren't in the original data are None in the export + assert 'allowed_scope_levels' in exported_data + assert exported_data['allowed_scope_levels'] is None diff --git a/splitapiclient/tests/resources/harness/service_account_test.py b/splitapiclient/tests/resources/harness/service_account_test.py new file mode 100644 index 0000000..da1cdb7 --- /dev/null +++ b/splitapiclient/tests/resources/harness/service_account_test.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import ServiceAccount +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestServiceAccount: + """ + Tests for the ServiceAccount resource class + """ + + def test_initialization(self): + """ + Test initialization of a ServiceAccount object + """ + # Test with empty data + sa = ServiceAccount() + assert sa._id is None + assert sa._identifier is None + assert sa._name is None + + # Test with data + sa_data = { + 'identifier': 'sa1', + 'name': 'Test Service Account', + 'email': 'sa1@example.com', + 'description': 'Test service account description', + 'tags': { + 'property1': 'value1', + 'property2': 'value2' + }, + 'accountIdentifier': 'test_account', + 'orgIdentifier': 'test_org', + 'projectIdentifier': 'test_project', + 'extendable': True + } + + sa = ServiceAccount(sa_data) + + # Verify all properties were set correctly + assert sa._id == 'sa1' + assert sa._identifier == 'sa1' + assert sa._name == 'Test Service Account' + assert sa._email == 'sa1@example.com' + assert sa._description == 'Test service account description' + assert sa._tags == {'property1': 'value1', 'property2': 'value2'} + assert sa._account_identifier == 'test_account' + assert sa._org_identifier == 'test_org' + assert sa._project_identifier == 'test_project' + assert sa._extendable is True + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + sa_data = { + 'identifier': 'sa1', + 'name': 'Test Service Account', + 'email': 'sa1@example.com', + 'accountIdentifier': 'test_account' + } + + sa = ServiceAccount(sa_data) + + # Test accessing properties via camelCase (direct schema field names) + assert sa.identifier == 'sa1' + assert sa.name == 'Test Service Account' + assert sa.email == 'sa1@example.com' + assert sa.accountIdentifier == 'test_account' + + # Test accessing properties via snake_case + assert sa.account_identifier == 'test_account' + + # Test accessing non-existent property + with pytest.raises(AttributeError): + sa.non_existent_property + + def test_export_dict(self): + """ + Test exporting service account data as a dictionary + """ + sa_data = { + 'identifier': 'sa1', + 'name': 'Test Service Account', + 'email': 'sa1@example.com', + 'description': 'Test service account description', + 'accountIdentifier': 'test_account' + } + + sa = ServiceAccount(sa_data) + exported_data = sa.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'sa1' + assert exported_data['name'] == 'Test Service Account' + assert exported_data['email'] == 'sa1@example.com' + assert exported_data['description'] == 'Test service account description' + assert exported_data['accountIdentifier'] == 'test_account' + + # Verify fields that weren't in the original data are None in the export + assert 'projectIdentifier' in exported_data + assert exported_data['projectIdentifier'] is None + assert 'orgIdentifier' in exported_data + assert exported_data['orgIdentifier'] is None diff --git a/splitapiclient/tests/resources/harness/token_test.py b/splitapiclient/tests/resources/harness/token_test.py new file mode 100644 index 0000000..9821007 --- /dev/null +++ b/splitapiclient/tests/resources/harness/token_test.py @@ -0,0 +1,119 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources.harness import Token +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestToken: + """ + Tests for the Token resource class + """ + + def test_initialization(self): + """ + Test initialization of a Token object + """ + # Test with empty data + token = Token() + assert token._id is None + assert token._identifier is None + assert token._name is None + + # Test with data + token_data = { + 'identifier': 'token1', + 'name': 'Test Token', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account', + 'projectIdentifier': 'test_project', + 'orgIdentifier': 'test_org', + 'apiKeyIdentifier': 'api_key1', + 'parentIdentifier': 'parent1', + 'apiKeyType': 'USER', + 'description': 'Test token description', + 'tags': { + 'property1': 'value1', + 'property2': 'value2' + }, + 'sshKeyContent': 'ssh-key-content', + 'sshKeyUsage': ['AUTH'] + } + + token = Token(token_data) + + # Verify all properties were set correctly + assert token._id == 'token1' + assert token._identifier == 'token1' + assert token._name == 'Test Token' + assert token._valid_from == 1234567890 + assert token._valid_to == 1234567899 + assert token._valid is True + assert token._account_identifier == 'test_account' + assert token._project_identifier == 'test_project' + assert token._org_identifier == 'test_org' + assert token._api_key_identifier == 'api_key1' + assert token._parent_identifier == 'parent1' + assert token._api_key_type == 'USER' + assert token._description == 'Test token description' + assert token._tags == {'property1': 'value1', 'property2': 'value2'} + assert token._ssh_key_content == 'ssh-key-content' + assert token._ssh_key_usage == ['AUTH'] + + def test_getattr(self): + """ + Test dynamic property access via __getattr__ + """ + token_data = { + 'identifier': 'token1', + 'name': 'Test Token', + 'validFrom': 1234567890, + 'validTo': 1234567899 + } + + token = Token(token_data) + + # Test accessing properties via camelCase (direct schema field names) + assert token.identifier == 'token1' + assert token.name == 'Test Token' + assert token.validFrom == 1234567890 + assert token.validTo == 1234567899 + + # Test accessing properties via snake_case + assert token.valid_from == 1234567890 + assert token.valid_to == 1234567899 + + # Test accessing non-existent property + with pytest.raises(AttributeError): + token.non_existent_property + + def test_export_dict(self): + """ + Test exporting token data as a dictionary + """ + token_data = { + 'identifier': 'token1', + 'name': 'Test Token', + 'validFrom': 1234567890, + 'validTo': 1234567899, + 'valid': True, + 'accountIdentifier': 'test_account' + } + + token = Token(token_data) + exported_data = token.export_dict() + + # Verify exported data contains all original fields + assert exported_data['identifier'] == 'token1' + assert exported_data['name'] == 'Test Token' + assert exported_data['validFrom'] == 1234567890 + assert exported_data['validTo'] == 1234567899 + assert exported_data['valid'] is True + assert exported_data['accountIdentifier'] == 'test_account' + + # Verify fields that weren't in the original data are None in the export + assert 'projectIdentifier' in exported_data + assert exported_data['projectIdentifier'] is None diff --git a/splitapiclient/util/exceptions.py b/splitapiclient/util/exceptions.py index ba643aa..ea45c8f 100644 --- a/splitapiclient/util/exceptions.py +++ b/splitapiclient/util/exceptions.py @@ -104,3 +104,10 @@ class InvalidModelException(SplitException): retrieve a client ''' pass + + +class HarnessDeprecatedEndpointError(SplitException): + ''' + Exception to be thrown when attempting to access a deprecated endpoint in harness mode + ''' + pass diff --git a/splitapiclient/version.py b/splitapiclient/version.py index 573cf70..01bd03c 100644 --- a/splitapiclient/version.py +++ b/splitapiclient/version.py @@ -1 +1 @@ -__version__ = '3.2.0' +__version__ = '3.5.0'