diff --git a/docs/env_variable/README.md b/docs/env_variable/README.md new file mode 100644 index 00000000..df8e6575 --- /dev/null +++ b/docs/env_variable/README.md @@ -0,0 +1,44 @@ +# Environment Variable Management + +## Env Variable Client + +### Initialization +```python +from conductor.client.configuration.configuration import Configuration +from conductor.client.configuration.settings.authentication_settings import AuthenticationSettings +from conductor.client.orkes.orkes_env_variable_client import OrkesEnvVariableClient + +configuration = Configuration( + server_api_url=SERVER_API_URL, + debug=False, + authentication_settings=AuthenticationSettings(key_id=KEY_ID, key_secret=KEY_SECRET) +) + +env_variable_client = OrkesEnvVariableClient(configuration) +``` + +### Saving Environment Variable + +```python +env_variable_client.save_env_variable("VAR_NAME", "VAR_VALUE") +``` + +### Get Environment Variable + +#### Get a specific variable + +```python +value = env_variable_client.get_env_variable("VAR_NAME") +``` + +#### Get all variable + +```python +var_names = env_variable_client.get_all_env_variables() +``` + +### Delete Environment Variable + +```python +env_variable_client.delete_env_variable("VAR_NAME") +``` diff --git a/src/conductor/client/env_variable_client.py b/src/conductor/client/env_variable_client.py new file mode 100644 index 00000000..03e8b643 --- /dev/null +++ b/src/conductor/client/env_variable_client.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from typing import Dict + + +class EnvVariableClient(ABC): + @abstractmethod + def save_env_variable(self, name: str, value: str): + pass + + @abstractmethod + def get_env_variable(self, name: str) -> str: + pass + + @abstractmethod + def get_all_env_variables(self) -> Dict: + pass + + @abstractmethod + def delete_env_variable(self, name: str): + pass diff --git a/src/conductor/client/http/api/environment_resource_api.py b/src/conductor/client/http/api/environment_resource_api.py new file mode 100644 index 00000000..46c02e53 --- /dev/null +++ b/src/conductor/client/http/api/environment_resource_api.py @@ -0,0 +1,394 @@ +from __future__ import absolute_import + +import re # noqa: F401 + +# python 2 and python 3 compatibility library +import six +import json + +from conductor.client.http.api_client import ApiClient + + +class EnvironmentResourceApi(object): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + Ref: https://github.com/swagger-api/swagger-codegen + """ + + def __init__(self, api_client=None): + if api_client is None: + api_client = ApiClient() + self.api_client = api_client + + def create_or_update_env_variable(self, body, key, **kwargs): # noqa: E501 + """Create or update an environment variable (requires metadata or admin role) # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.create_or_update_env_variable(body, key, async_req=True) + >>> result = thread.get() + + :param async_req bool + :param str body: (required) + :param str key: (required) + :return: None + If the method is called asynchronously, + returns the request thread. + """ + kwargs['_return_http_data_only'] = True + if kwargs.get('async_req'): + return self.create_or_update_env_variable_with_http_info(body, key, **kwargs) # noqa: E501 + else: + (data) = self.create_or_update_env_variable_with_http_info(body, key, **kwargs) # noqa: E501 + return data + + def create_or_update_env_variable_with_http_info(self, body, key, **kwargs): # noqa: E501 + """Create or update an environment variable (requires metadata or admin role) # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.create_or_update_env_variable_with_http_info(body, key, async_req=True) + >>> result = thread.get() + + :param async_req bool + :param str body: (required) + :param str key: (required) + :return: None + If the method is called asynchronously, + returns the request thread. + """ + + all_params = ['body', 'key'] # noqa: E501 + all_params.append('async_req') + all_params.append('_return_http_data_only') + all_params.append('_preload_content') + all_params.append('_request_timeout') + + params = locals() + for key, val in six.iteritems(params['kwargs']): + if key not in all_params: + raise TypeError( + "Got an unexpected keyword argument '%s'" + " to method create_or_update_env_variable" % key + ) + params[key] = val + del params['kwargs'] + # verify the required parameter 'body' is set + if ('body' not in params or + params['body'] is None): + raise ValueError("Missing the required parameter `body` when calling `create_or_update_env_variable`") # noqa: E501 + # verify the required parameter 'key' is set + if ('key' not in params or + params['key'] is None): + raise ValueError("Missing the required parameter `key` when calling `create_or_update_env_variable`") # noqa: E501 + + collection_formats = {} + + path_params = {} + if 'key' in params: + path_params['key'] = params['key'] # noqa: E501 + + query_params = [] + + header_params = {} + + form_params = [] + local_var_files = {} + + body_params = None + if 'body' in params: + body_params = params['body'] + # HTTP header `Content-Type` + header_params['Content-Type'] = self.api_client.select_header_content_type( # noqa: E501 + ['text/plain']) # noqa: E501 + + # Authentication setting + auth_settings = ['api_key'] # noqa: E501 + + return self.api_client.call_api( + '/environment/{key}', 'PUT', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_type=None, # noqa: E501 + auth_settings=auth_settings, + async_req=params.get('async_req'), + _return_http_data_only=params.get('_return_http_data_only'), + _preload_content=params.get('_preload_content', True), + _request_timeout=params.get('_request_timeout'), + collection_formats=collection_formats) + + def delete_env_variable(self, key, **kwargs): # noqa: E501 + """Delete an environment variable (requires metadata or admin role) # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.delete_env_variable(key, async_req=True) + >>> result = thread.get() + + :param async_req bool + :param str key: (required) + :return: str + If the method is called asynchronously, + returns the request thread. + """ + kwargs['_return_http_data_only'] = True + if kwargs.get('async_req'): + return self.delete_env_variable_with_http_info(key, **kwargs) # noqa: E501 + else: + (data) = self.delete_env_variable_with_http_info(key, **kwargs) # noqa: E501 + return data + + def delete_env_variable_with_http_info(self, key, **kwargs): # noqa: E501 + """Delete an environment variable (requires metadata or admin role) # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.delete_env_variable_with_http_info(key, async_req=True) + >>> result = thread.get() + + :param async_req bool + :param str key: (required) + :return: str + If the method is called asynchronously, + returns the request thread. + """ + + all_params = ['key'] # noqa: E501 + all_params.append('async_req') + all_params.append('_return_http_data_only') + all_params.append('_preload_content') + all_params.append('_request_timeout') + + params = locals() + for key, val in six.iteritems(params['kwargs']): + if key not in all_params: + raise TypeError( + "Got an unexpected keyword argument '%s'" + " to method delete_env_variable" % key + ) + params[key] = val + del params['kwargs'] + # verify the required parameter 'key' is set + if ('key' not in params or + params['key'] is None): + raise ValueError("Missing the required parameter `key` when calling `delete_env_variable`") # noqa: E501 + + collection_formats = {} + + path_params = {} + if 'key' in params: + path_params['key'] = params['key'] # noqa: E501 + + query_params = [] + + header_params = {} + + form_params = [] + local_var_files = {} + + body_params = None + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + ['application/json']) # noqa: E501 + + # Authentication setting + auth_settings = ['api_key'] # noqa: E501 + + return self.api_client.call_api( + '/environment/{key}', 'DELETE', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_type='str', # noqa: E501 + auth_settings=auth_settings, + async_req=params.get('async_req'), + _return_http_data_only=params.get('_return_http_data_only'), + _preload_content=params.get('_preload_content', True), + _request_timeout=params.get('_request_timeout'), + collection_formats=collection_formats) + + def get1(self, key, **kwargs): # noqa: E501 + """Get the environment value by key # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.get1(key, async_req=True) + >>> result = thread.get() + + :param async_req bool + :param str key: (required) + :return: str + If the method is called asynchronously, + returns the request thread. + """ + kwargs['_return_http_data_only'] = True + if kwargs.get('async_req'): + return self.get1_with_http_info(key, **kwargs) # noqa: E501 + else: + (data) = self.get1_with_http_info(key, **kwargs) # noqa: E501 + return data + + def get1_with_http_info(self, key, **kwargs): # noqa: E501 + """Get the environment value by key # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.get1_with_http_info(key, async_req=True) + >>> result = thread.get() + + :param async_req bool + :param str key: (required) + :return: str + If the method is called asynchronously, + returns the request thread. + """ + + all_params = ['key'] # noqa: E501 + all_params.append('async_req') + all_params.append('_return_http_data_only') + all_params.append('_preload_content') + all_params.append('_request_timeout') + + params = locals() + for key, val in six.iteritems(params['kwargs']): + if key not in all_params: + raise TypeError( + "Got an unexpected keyword argument '%s'" + " to method get1" % key + ) + params[key] = val + del params['kwargs'] + # verify the required parameter 'key' is set + if ('key' not in params or + params['key'] is None): + raise ValueError("Missing the required parameter `key` when calling `get1`") # noqa: E501 + + collection_formats = {} + + path_params = {} + if 'key' in params: + path_params['key'] = params['key'] # noqa: E501 + + query_params = [] + + header_params = {} + + form_params = [] + local_var_files = {} + + body_params = None + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + ['text/plain']) # noqa: E501 + + # Authentication setting + auth_settings = ['api_key'] # noqa: E501 + + return self.api_client.call_api( + '/environment/{key}', 'GET', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_type='str', # noqa: E501 + auth_settings=auth_settings, + async_req=params.get('async_req'), + _return_http_data_only=params.get('_return_http_data_only'), + _preload_content=params.get('_preload_content', True), + _request_timeout=params.get('_request_timeout'), + collection_formats=collection_formats) + + def get_all(self, **kwargs): # noqa: E501 + """List all the environment variables # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.get_all(async_req=True) + >>> result = thread.get() + + :param async_req bool + :return: dict(str, str) + If the method is called asynchronously, + returns the request thread. + """ + kwargs['_return_http_data_only'] = True + if kwargs.get('async_req'): + return self.get_all_with_http_info(**kwargs) # noqa: E501 + else: + (data) = self.get_all_with_http_info(**kwargs) # noqa: E501 + return data + + def get_all_with_http_info(self, **kwargs): # noqa: E501 + """List all the environment variables # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + >>> thread = api.get_all_with_http_info(async_req=True) + >>> result = thread.get() + + :param async_req bool + :return: dict(str, str) + If the method is called asynchronously, + returns the request thread. + """ + + all_params = [] # noqa: E501 + all_params.append('async_req') + all_params.append('_return_http_data_only') + all_params.append('_preload_content') + all_params.append('_request_timeout') + + params = locals() + for key, val in six.iteritems(params['kwargs']): + if key not in all_params: + raise TypeError( + "Got an unexpected keyword argument '%s'" + " to method get_all" % key + ) + params[key] = val + del params['kwargs'] + + collection_formats = {} + + path_params = {} + + query_params = [] + + header_params = {} + + form_params = [] + local_var_files = {} + + body_params = None + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + ['application/json']) # noqa: E501 + + # Authentication setting + auth_settings = ['api_key'] # noqa: E501 + + return self.api_client.call_api( + '/environment', 'GET', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_type='dict(str, str)', # noqa: E501 + auth_settings=auth_settings, + async_req=params.get('async_req'), + _return_http_data_only=params.get('_return_http_data_only'), + _preload_content=params.get('_preload_content', True), + _request_timeout=params.get('_request_timeout'), + collection_formats=collection_formats) diff --git a/src/conductor/client/http/rest.py b/src/conductor/client/http/rest.py index 4133ad0c..531f2126 100644 --- a/src/conductor/client/http/rest.py +++ b/src/conductor/client/http/rest.py @@ -79,6 +79,9 @@ def request(self, method, url, query_params=None, headers=None, request_body = '{}' if body is not None: request_body = json.dumps(body) + if isinstance(body, str): + request_body = request_body.strip('"') + r = self.connection.request( method, url, data=request_body, diff --git a/src/conductor/client/orkes/orkes_base_client.py b/src/conductor/client/orkes/orkes_base_client.py index 0d567944..a3618e36 100644 --- a/src/conductor/client/orkes/orkes_base_client.py +++ b/src/conductor/client/orkes/orkes_base_client.py @@ -12,6 +12,7 @@ from conductor.client.http.api.task_resource_api import TaskResourceApi from conductor.client.http.api.user_resource_api import UserResourceApi from conductor.client.http.api.workflow_resource_api import WorkflowResourceApi +from conductor.client.http.api.environment_resource_api import EnvironmentResourceApi from conductor.client.http.api_client import ApiClient from conductor.client.orkes.api.tags_api import TagsApi @@ -34,3 +35,4 @@ def __init__(self, configuration: Configuration): self.tagsApi = TagsApi(self.api_client) self.integrationApi = IntegrationResourceApi(self.api_client) self.promptApi = PromptResourceApi(self.api_client) + self.environmentResourceApi = EnvironmentResourceApi(self.api_client) diff --git a/src/conductor/client/orkes/orkes_env_variable_client.py b/src/conductor/client/orkes/orkes_env_variable_client.py new file mode 100644 index 00000000..5f713d23 --- /dev/null +++ b/src/conductor/client/orkes/orkes_env_variable_client.py @@ -0,0 +1,22 @@ +from typing import Dict + +from conductor.client.configuration.configuration import Configuration +from conductor.client.orkes.orkes_base_client import OrkesBaseClient +from conductor.client.env_variable_client import EnvVariableClient + + +class OrkesEnvVariableClient(OrkesBaseClient, EnvVariableClient): + def __init__(self, configuration: Configuration): + super().__init__(configuration) + + def save_env_variable(self, name: str, value: str): + self.environmentResourceApi.create_or_update_env_variable(value, name) + + def get_env_variable(self, name: str) -> str: + return self.environmentResourceApi.get1(name) + + def get_all_env_variables(self) -> Dict: + return self.environmentResourceApi.get_all() + + def delete_env_variable(self, name: str): + self.environmentResourceApi.delete_env_variable(name) \ No newline at end of file diff --git a/src/conductor/client/orkes_clients.py b/src/conductor/client/orkes_clients.py index 57bfd1fd..511aa2a6 100644 --- a/src/conductor/client/orkes_clients.py +++ b/src/conductor/client/orkes_clients.py @@ -2,6 +2,7 @@ from conductor.client.configuration.configuration import Configuration from conductor.client.integration_client import IntegrationClient from conductor.client.metadata_client import MetadataClient +from conductor.client.env_variable_client import EnvVariableClient from conductor.client.orkes.orkes_integration_client import OrkesIntegrationClient from conductor.client.orkes.orkes_metadata_client import OrkesMetadataClient from conductor.client.orkes.orkes_prompt_client import OrkesPromptClient @@ -9,6 +10,7 @@ from conductor.client.orkes.orkes_task_client import OrkesTaskClient from conductor.client.orkes.orkes_scheduler_client import OrkesSchedulerClient from conductor.client.orkes.orkes_secret_client import OrkesSecretClient +from conductor.client.orkes.orkes_env_variable_client import OrkesEnvVariableClient from conductor.client.orkes.orkes_authorization_client import OrkesAuthorizationClient from conductor.client.prompt_client import PromptClient from conductor.client.scheduler_client import SchedulerClient @@ -39,6 +41,9 @@ def get_scheduler_client(self) -> SchedulerClient: def get_secret_client(self) -> SecretClient: return OrkesSecretClient(self.configuration) + def get_env_variable_client(self) -> EnvVariableClient: + return OrkesEnvVariableClient(self.configuration) + def get_task_client(self) -> TaskClient: return OrkesTaskClient(self.configuration) diff --git a/src/conductor/client/scheduler_client.py b/src/conductor/client/scheduler_client.py index f507d78d..23969dd0 100644 --- a/src/conductor/client/scheduler_client.py +++ b/src/conductor/client/scheduler_client.py @@ -13,7 +13,7 @@ def save_schedule(self, save_schedule_request: SaveScheduleRequest): pass @abstractmethod - def get_schedule(self, name: str) -> (Optional[WorkflowSchedule], str): + def get_schedule(self, name: str) -> WorkflowSchedule: pass @abstractmethod diff --git a/tests/integration/client/orkes/test_orkes_clients.py b/tests/integration/client/orkes/test_orkes_clients.py index e9b0666d..2580eb08 100644 --- a/tests/integration/client/orkes/test_orkes_clients.py +++ b/tests/integration/client/orkes/test_orkes_clients.py @@ -31,6 +31,7 @@ TASK_TYPE = 'IntegrationTestOrkesClientsTask_' + SUFFIX SCHEDULE_NAME = 'IntegrationTestSchedulerClientSch_' + SUFFIX SECRET_NAME = 'IntegrationTestSecretClientSec_' + SUFFIX +VARIABLE_NAME = 'IntegrationTestOrkesClientsVar_' + SUFFIX APPLICATION_NAME = 'IntegrationTestAuthClientApp_' + SUFFIX USER_ID = 'integrationtest_' + SUFFIX[0:5].lower() + "@orkes.io" GROUP_ID = 'integrationtest_group_' + SUFFIX[0:5].lower() @@ -50,6 +51,7 @@ def __init__(self, configuration: Configuration): self.scheduler_client = orkes_clients.get_scheduler_client() self.secret_client = orkes_clients.get_secret_client() self.authorization_client = orkes_clients.get_authorization_client() + self.env_variable_client = orkes_clients.get_env_variable_client() self.workflow_id = None def run(self) -> None: @@ -66,6 +68,7 @@ def run(self) -> None: self.test_workflow_lifecycle(workflowDef, workflow) self.test_task_lifecycle() self.test_secret_lifecycle() + self.test_environment_variable_lifecycle() self.test_scheduler_lifecycle(workflowDef) self.test_application_lifecycle() self.__test_unit_test_workflow() @@ -144,6 +147,34 @@ def test_secret_lifecycle(self): except ApiException as e: assert e.code == 404 + def test_environment_variable_lifecycle(self): + env_vars = self.env_variable_client.get_all_env_variables() + + num_env_vars = len(env_vars) + + self.env_variable_client.save_env_variable(VARIABLE_NAME, 'env_var_value') + + assert self.env_variable_client.get_env_variable(VARIABLE_NAME), 'env_var_value' + + self.env_variable_client.save_env_variable(VARIABLE_NAME + "_2", 'val_2') + + env_vars = self.env_variable_client.get_all_env_variables() + + assert len(env_vars) == num_env_vars + 2 + + self.env_variable_client.delete_env_variable(VARIABLE_NAME + '_2') + + try: + self.env_variable_client.get_env_variable(VARIABLE_NAME + '_2') + except ApiException as e: + assert e.code == 404 + + self.env_variable_client.delete_env_variable(VARIABLE_NAME) + + env_vars = self.env_variable_client.get_all_env_variables() + + assert len(env_vars.keys()) == num_env_vars + def test_scheduler_lifecycle(self, workflowDef): startWorkflowRequest = StartWorkflowRequest( name=WORKFLOW_NAME, workflow_def=workflowDef @@ -605,9 +636,11 @@ def __get_workflow_definition(self, path): f = open(path, "r") workflowJSON = json.loads(f.read()) workflowDef = self.api_client.deserialize_class(workflowJSON, "WorkflowDef") + f.close() return workflowDef def __get_test_inputs(self, path): f = open(path, "r") inputJSON = json.loads(f.read()) + f.close() return inputJSON diff --git a/tests/integration/test_workflow_client_intg.py b/tests/integration/test_workflow_client_intg.py index d3e5e315..8e550c24 100644 --- a/tests/integration/test_workflow_client_intg.py +++ b/tests/integration/test_workflow_client_intg.py @@ -40,14 +40,21 @@ def setUpClass(cls): cls.config = get_configuration() cls.workflow_client = OrkesWorkflowClient(cls.config) logger.info(f'setting up TestOrkesWorkflowClientIntg with config {cls.config}') - - def test_all(self): logger.info('START: integration tests') + + @classmethod + def tearDownClass(cls): + logger.info('END: integration tests') + + def test_workflows(self): configuration = self.config workflow_executor = WorkflowExecutor(configuration) # test_async.test_async_method(api_client) run_workflow_definition_tests(workflow_executor) run_workflow_execution_tests(configuration, workflow_executor) - TestOrkesClients(configuration=configuration).run() - logger.info('END: integration tests') + + def test_orkes_clients(self): + TestOrkesClients(configuration=self.config).run() + + diff --git a/tests/unit/orkes/test_env_variable_client.py b/tests/unit/orkes/test_env_variable_client.py new file mode 100644 index 00000000..b9b0510f --- /dev/null +++ b/tests/unit/orkes/test_env_variable_client.py @@ -0,0 +1,64 @@ +import json +import logging +import unittest +from unittest.mock import patch, MagicMock + +from conductor.client.http.rest import ApiException +from conductor.client.configuration.configuration import Configuration +from conductor.client.http.api.environment_resource_api import EnvironmentResourceApi +from conductor.client.orkes.orkes_env_variable_client import OrkesEnvVariableClient + +VARIABLE_KEY = 'ut_env_var_key' +VARIABLE_VALUE = 'ut_env_var_value' +ERROR_BODY = '{"message":"No such environment variable found by key"}' + + +class TestEnvVariableClient(unittest.TestCase): + + @classmethod + def setUpClass(cls): + configuration = Configuration("http://localhost:8080/api") + cls.env_variable_client = OrkesEnvVariableClient(configuration) + + def setUp(self): + logging.disable(logging.CRITICAL) + + def tearDown(self): + logging.disable(logging.NOTSET) + + def test_init(self): + message = "environmentResourceApi is not of type EnvironmentResourceApi" + self.assertIsInstance(self.env_variable_client.environmentResourceApi, EnvironmentResourceApi, message) + + @patch.object(EnvironmentResourceApi, 'create_or_update_env_variable') + def test_save_env_variable(self, mock): + self.env_variable_client.save_env_variable(VARIABLE_KEY, VARIABLE_VALUE) + mock.assert_called_with(VARIABLE_VALUE, VARIABLE_KEY) + + @patch.object(EnvironmentResourceApi, 'get1') + def test_get_env_variable(self, mock): + mock.return_value = VARIABLE_VALUE + env_var = self.env_variable_client.get_env_variable(VARIABLE_KEY) + mock.assert_called_with(VARIABLE_KEY) + self.assertEqual(env_var, VARIABLE_VALUE) + + @patch.object(EnvironmentResourceApi, 'get1') + def test_get_variable_non_existing(self, mock): + error_body = {'status': 404, 'message': 'Variable not found'} + mock.side_effect = MagicMock(side_effect=ApiException(status=404, body=json.dumps(error_body))) + with self.assertRaises(ApiException): + self.env_variable_client.get_env_variable("WRONG_ENV_VAR_KEY") + mock.assert_called_with("WRONG_ENV_VAR_KEY") + + @patch.object(EnvironmentResourceApi, 'get_all') + def test_get_all_env_variables(self, mock): + env_var_dict = { "TEST_VARIABLE_1": "v1", "TEST_VARIABLE_2": "v2" } + mock.return_value = env_var_dict + env_variables= self.env_variable_client.get_all_env_variables() + self.assertTrue(mock.called) + self.assertDictEqual(env_variables, env_var_dict) + + @patch.object(EnvironmentResourceApi, 'delete_env_variable') + def test_delete_env_variable(self, mock): + self.env_variable_client.delete_env_variable(VARIABLE_KEY) + mock.assert_called_with(VARIABLE_KEY)