From b6d7af07fa9b0b678225b52f55c7485562ede0bd Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Wed, 15 Nov 2017 18:29:50 -0600 Subject: [PATCH] Add CLI formatted responses to Shipyard CLI Rather than always returning JSON or YAML, add functionality to return table responses when applicable for ease of reading. Adds some marker exceptions to the Shipyard API Client to handle cases where the user of the client would generally want to take a specific and repeatable course of action instead of handling the response in a case-by-case basis. Moved cli action testing to use the responses library instead of as many internal mocks. Added test response generators for standard api responses. Change-Id: I3a593fb29b6e76d971adc7f3bb3a4b7f378ed091 --- requirements.txt | 10 +- shipyard_client/api_client/base_client.py | 91 +++- shipyard_client/api_client/client_error.py | 8 + .../api_client/shipyard_api_client.py | 53 ++- .../api_client/shipyardclient_context.py | 53 +-- shipyard_client/cli/action.py | 168 ++++++-- shipyard_client/cli/cli_format_common.py | 211 ++++++++++ shipyard_client/cli/commit/actions.py | 27 +- shipyard_client/cli/control/actions.py | 28 +- shipyard_client/cli/create/actions.py | 63 ++- shipyard_client/cli/describe/actions.py | 117 +++++- shipyard_client/cli/format_utils.py | 139 ++++++ shipyard_client/cli/get/actions.py | 97 ++++- shipyard_client/cli/input_checks.py | 20 +- shipyard_client/cli/output_formatting.py | 47 --- .../test_shipyard_api_client.py | 104 +++-- .../unit/cli/commit/test_commit_actions.py | 97 +++-- .../unit/cli/control/test_control_actions.py | 142 +++++-- .../unit/cli/create/test_create_actions.py | 203 ++++++--- .../cli/describe/test_describe_actions.py | 397 ++++++++++++++---- .../tests/unit/cli/get/test_get_actions.py | 292 ++++++++----- .../tests/unit/cli/replace_api_client.py | 19 +- shipyard_client/tests/unit/cli/stubs.py | 164 ++++++++ .../tests/unit/cli/test_auth_validations.py | 69 +++ .../tests/unit/cli/test_format_utils.py | 152 +++++++ .../tests/unit/cli/test_input_checks.py | 56 --- .../tests/unit/cli/test_output_formatting.py | 56 --- test-requirements.txt | 1 + 28 files changed, 2120 insertions(+), 764 deletions(-) create mode 100644 shipyard_client/cli/cli_format_common.py create mode 100644 shipyard_client/cli/format_utils.py delete mode 100644 shipyard_client/cli/output_formatting.py create mode 100644 shipyard_client/tests/unit/cli/stubs.py create mode 100644 shipyard_client/tests/unit/cli/test_auth_validations.py create mode 100644 shipyard_client/tests/unit/cli/test_format_utils.py delete mode 100644 shipyard_client/tests/unit/cli/test_output_formatting.py diff --git a/requirements.txt b/requirements.txt index 8388a4b4..7783a70d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,11 +13,11 @@ # limitations under the License. alembic==0.9.5 -arrow==0.10.0 +arrow==0.10.0 # API and Client configparser==3.5.0 falcon==1.2.0 jsonschema==2.6.0 -keystoneauth1==2.13.0 +keystoneauth1==2.13.0 # API and Client keystonemiddleware==4.17.0 oslo.config==4.11.0 oslo.policy==1.25.1 @@ -26,11 +26,13 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 psycopg2==2.7.3.1 python-dateutil==2.6.1 python-memcached==1.58 -python-openstackclient==3.11.0 -requests==2.18.4 +requests==2.18.4 # API and Client SQLAlchemy==1.1.13 ulid==1.1 uwsgi==2.0.15 + +# Client click==6.7 click-default-group==1.2 +PTable==0.9.2 pyyaml==3.12 diff --git a/shipyard_client/api_client/base_client.py b/shipyard_client/api_client/base_client.py index f17e42c4..43538d20 100644 --- a/shipyard_client/api_client/base_client.py +++ b/shipyard_client/api_client/base_client.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,17 +11,44 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import abc import logging + +from keystoneauth1.exceptions.auth import AuthorizationFailure +from keystoneauth1.exceptions.catalog import EndpointNotFound +from keystoneauth1.identity import v3 +from keystoneauth1 import session import requests -from .client_error import ClientError +from shipyard_client.api_client.client_error import ClientError +from shipyard_client.api_client.client_error import UnauthenticatedClientError +from shipyard_client.api_client.client_error import UnauthorizedClientError -class BaseClient: +class BaseClient(metaclass=abc.ABCMeta): + """Abstract base client class + + Requrires the definition of service_type and interface by child classes + """ + @property + @abc.abstractmethod + def service_type(self): + """Specify the name/type used to lookup the service""" + pass + + @property + @abc.abstractmethod + def interface(self): + """The interface to choose from during service lookup + + Specify the interface to look up the service: public, internal admin + """ + pass + def __init__(self, context): self.logger = logging.Logger('api_client') self.context = context + self.endpoint = None def log_message(self, level, msg): """ Logs a message with context, and extra populated. """ @@ -57,14 +84,21 @@ class BaseClient: headers = { 'X-Context-Marker': self.context.context_marker, 'content-type': content_type, - 'X-Auth-Token': self.context.get_token() + 'X-Auth-Token': self.get_token() } self.debug('Post request url: ' + url) self.debug('Query Params: ' + str(query_params)) # This could use keystoneauth1 session, but that library handles # responses strangely (wraps all 400/500 in a keystone exception) - return requests.post( + response = requests.post( url, data=data, params=query_params, headers=headers) + # handle some cases where the response code is sufficient to know + # what needs to be done + if response.status_code == 401: + raise UnauthenticatedClientError() + if response.status_code == 403: + raise UnauthorizedClientError() + return response except requests.exceptions.RequestException as e: self.error(str(e)) raise ClientError(str(e)) @@ -76,11 +110,52 @@ class BaseClient: try: headers = { 'X-Context-Marker': self.context.context_marker, - 'X-Auth-Token': self.context.get_token() + 'X-Auth-Token': self.get_token() } self.debug('url: ' + url) self.debug('Query Params: ' + str(query_params)) - return requests.get(url, params=query_params, headers=headers) + response = requests.get(url, params=query_params, headers=headers) + # handle some cases where the response code is sufficient to know + # what needs to be done + if response.status_code == 401: + raise UnauthenticatedClientError() + if response.status_code == 403: + raise UnauthorizedClientError() + return response except requests.exceptions.RequestException as e: self.error(str(e)) raise ClientError(str(e)) + + def get_token(self): + """ + Returns the simple token string for a token acquired from keystone + """ + return self._get_ks_session().get_auth_headers().get('X-Auth-Token') + + def _get_ks_session(self): + self.logger.debug('Accessing keystone for keystone session') + try: + auth = v3.Password(**self.context.keystone_auth) + return session.Session(auth=auth) + except AuthorizationFailure as e: + self.logger.error('Could not authorize against keystone: %s', + str(e)) + raise ClientError(str(e)) + + def get_endpoint(self): + """Lookup the endpoint for the client. Cache it. + + Uses a keystone session to find an endpoint for the specified + service_type at the specified interface (public, internal, admin) + """ + if self.endpoint is None: + self.logger.debug('Accessing keystone for %s endpoint', + self.service_type) + try: + self.endpoint = self._get_ks_session().get_endpoint( + interface=self.interface, service_type=self.service_type) + except EndpointNotFound as e: + self.logger.error('Could not find %s interface for %s', + self.interface, self.service_type) + raise ClientError(str(e)) + return self.endpoint diff --git a/shipyard_client/api_client/client_error.py b/shipyard_client/api_client/client_error.py index b46076f0..f6af7423 100644 --- a/shipyard_client/api_client/client_error.py +++ b/shipyard_client/api_client/client_error.py @@ -15,3 +15,11 @@ class ClientError(Exception): pass + + +class UnauthorizedClientError(ClientError): + pass + + +class UnauthenticatedClientError(ClientError): + pass diff --git a/shipyard_client/api_client/shipyard_api_client.py b/shipyard_client/api_client/shipyard_api_client.py index 856b892e..a56596c5 100644 --- a/shipyard_client/api_client/shipyard_api_client.py +++ b/shipyard_client/api_client/shipyard_api_client.py @@ -42,10 +42,9 @@ class ShipyardClient(BaseClient): A client for shipyard API :param context: shipyardclient_context, context object """ - - def __init__(self, context): - super().__init__(context) - self.shipyard_url = context.shipyard_endpoint + # Set up the values used to look up the service endpoint. + interface = 'public' + service_type = 'shipyard' def post_configdocs(self, collection_id=None, @@ -60,8 +59,10 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"buffermode": buffer_mode} - url = ApiPaths.POST_GET_CONFIG.value.format(self.shipyard_url, - collection_id) + url = ApiPaths.POST_GET_CONFIG.value.format( + self.get_endpoint(), + collection_id + ) return self.post_resp(url, query_params, document_data) def get_configdocs(self, collection_id=None, version='buffer'): @@ -73,8 +74,9 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"version": version} - url = ApiPaths.POST_GET_CONFIG.value.format(self.shipyard_url, - collection_id) + url = ApiPaths.POST_GET_CONFIG.value.format( + self.get_endpoint(), + collection_id) return self.get_resp(url, query_params) def get_rendereddocs(self, version='buffer'): @@ -84,7 +86,9 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"version": version} - url = ApiPaths.GET_RENDERED.value.format(self.shipyard_url) + url = ApiPaths.GET_RENDERED.value.format( + self.get_endpoint() + ) return self.get_resp(url, query_params) def commit_configdocs(self, force=False): @@ -94,7 +98,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"force": force} - url = ApiPaths.COMMIT_CONFIG.value.format(self.shipyard_url) + url = ApiPaths.COMMIT_CONFIG.value.format(self.get_endpoint()) return self.post_resp(url, query_params) def get_actions(self): @@ -103,7 +107,9 @@ class ShipyardClient(BaseClient): :returns: lists all actions :rtype: Response object """ - url = ApiPaths.POST_GET_ACTIONS.value.format(self.shipyard_url) + url = ApiPaths.POST_GET_ACTIONS.value.format( + self.get_endpoint() + ) return self.get_resp(url) def post_actions(self, name=None, parameters=None): @@ -115,7 +121,9 @@ class ShipyardClient(BaseClient): :rtype: Response object """ action_data = {"name": name, "parameters": parameters} - url = ApiPaths.POST_GET_ACTIONS.value.format(self.shipyard_url) + url = ApiPaths.POST_GET_ACTIONS.value.format( + self.get_endpoint() + ) return self.post_resp( url, data=json.dumps(action_data), content_type='application/json') @@ -126,8 +134,10 @@ class ShipyardClient(BaseClient): :returns: information describing the action :rtype: Response object """ - url = ApiPaths.GET_ACTION_DETAIL.value.format(self.shipyard_url, - action_id) + url = ApiPaths.GET_ACTION_DETAIL.value.format( + self.get_endpoint(), + action_id + ) return self.get_resp(url) def get_validation_detail(self, action_id=None, validation_id=None): @@ -139,7 +149,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ url = ApiPaths.GET_VALIDATION_DETAIL.value.format( - self.shipyard_url, action_id, validation_id) + self.get_endpoint(), action_id, validation_id) return self.get_resp(url) def get_step_detail(self, action_id=None, step_id=None): @@ -150,8 +160,11 @@ class ShipyardClient(BaseClient): :returns: details for a step by id for the given action by Id :rtype: Response object """ - url = ApiPaths.GET_STEP_DETAIL.value.format(self.shipyard_url, - action_id, step_id) + url = ApiPaths.GET_STEP_DETAIL.value.format( + self.get_endpoint(), + action_id, + step_id + ) return self.get_resp(url) def post_control_action(self, action_id=None, control_verb=None): @@ -163,7 +176,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ url = ApiPaths.POST_CONTROL_ACTION.value.format( - self.shipyard_url, action_id, control_verb) + self.get_endpoint(), action_id, control_verb) return self.post_resp(url) def get_workflows(self, since=None): @@ -175,7 +188,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {'since': since} - url = ApiPaths.GET_WORKFLOWS.value.format(self.shipyard_url) + url = ApiPaths.GET_WORKFLOWS.value.format(self.get_endpoint()) return self.get_resp(url, query_params) def get_dag_detail(self, workflow_id=None): @@ -185,6 +198,6 @@ class ShipyardClient(BaseClient): :returns: details of a DAGs output :rtype: Response object """ - url = ApiPaths.GET_DAG_DETAIL.value.format(self.shipyard_url, + url = ApiPaths.GET_DAG_DETAIL.value.format(self.get_endpoint(), workflow_id) return self.get_resp(url) diff --git a/shipyard_client/api_client/shipyardclient_context.py b/shipyard_client/api_client/shipyardclient_context.py index fafb5cf2..d80a977a 100644 --- a/shipyard_client/api_client/shipyardclient_context.py +++ b/shipyard_client/api_client/shipyardclient_context.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,64 +11,25 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import logging -from keystoneauth1 import session -from keystoneauth1.identity import v3 -from keystoneauth1.exceptions.auth import AuthorizationFailure -from keystoneauth1.exceptions.catalog import EndpointNotFound -from .client_error import ClientError - LOG = logging.getLogger(__name__) class ShipyardClientContext: + """A context object for ShipyardClient instances.""" + def __init__(self, keystone_auth, context_marker, debug=False): - """ - shipyard context object + """Shipyard context object + :param bool debug: true, or false :param str context_marker: :param dict keystone_auth: auth_url, password, project_domain_name, project_name, username, user_domain_name """ - self.debug = debug - self.keystone_auth = keystone_auth - # the service type will for now just be shipyard will change later - self.service_type = 'shipyard' - self.shipyard_endpoint = self.get_endpoint() - self.set_debug() - self.context_marker = context_marker - - def set_debug(self): if self.debug: LOG.setLevel(logging.DEBUG) - def get_token(self): - """ - Returns the simple token string for a token acquired from keystone - """ - return self._get_ks_session().get_auth_headers().get('X-Auth-Token') - - def _get_ks_session(self): - LOG.debug('Accessing keystone for keystone session') - try: - auth = v3.Password(**self.keystone_auth) - return session.Session(auth=auth) - except AuthorizationFailure as e: - LOG.error('Could not authorize against keystone: %s', str(e)) - raise ClientError(str(e)) - - def get_endpoint(self): - """ - Wraps calls to keystone for lookup with overrides from configuration - """ - LOG.debug('Accessing keystone for %s endpoint', self.service_type) - try: - return self._get_ks_session().get_endpoint( - interface='public', service_type=self.service_type) - except EndpointNotFound as e: - LOG.error('Could not find a public interface for %s', - self.service_type) - raise ClientError(str(e)) + self.keystone_auth = keystone_auth + self.context_marker = context_marker diff --git a/shipyard_client/cli/action.py b/shipyard_client/cli/action.py index 9836c361..4b24860e 100644 --- a/shipyard_client/cli/action.py +++ b/shipyard_client/cli/action.py @@ -13,60 +13,164 @@ # limitations under the License. # Base classes for cli actions intended to invoke the api - +import abc import logging +from shipyard_client.api_client.client_error import ClientError +from shipyard_client.api_client.client_error import UnauthenticatedClientError +from shipyard_client.api_client.client_error import UnauthorizedClientError from shipyard_client.api_client.shipyard_api_client import ShipyardClient from shipyard_client.api_client.shipyardclient_context import \ ShipyardClientContext -from shipyard_client.api_client.client_error import ClientError -from shipyard_client.cli.input_checks import validate_auth_vars +from shipyard_client.cli import format_utils -class CliAction(object): +class AuthValuesError(Exception): + """Signals a failure in the authentication values provided to an action + + Daignostic parameter is forced since newlines in exception text apparently + do not print with the exception. + """ + def __init__(self, *, diagnostic): + self.diagnostic = diagnostic + + +class AbstractCliAction(metaclass=abc.ABCMeta): + """Abstract base class for CLI actions + + Base class to encapsulate the items that must be implemented by + concrete actions + """ + + @abc.abstractmethod + def invoke(self): + """Default invoke for CLI actions + + Descendent classes must override this method to perform the actual + needed invocation. The expected response from this method is a response + object or raise an exception. + """ + pass + + @property + @abc.abstractmethod + def cli_handled_err_resp_codes(self): + """Error response codes + + Descendent classes shadow this for those response codes from invocation + that should be handled using the format_utils.cli_format_error_handler + Note that 401, 403 responses are handled prior to this via exception, + and should not be represented here. e.g.: [400, 409]. + """ + pass + + @property + @abc.abstractmethod + def cli_handled_succ_resp_codes(self): + """Success response codes + + Concrete actions must implement cli_handled_succ_resp_codes to indicate + the response code that should utilize the overridden + cli_format_response_handler of the sepecific action + """ + pass + + @abc.abstractmethod + def cli_format_response_handler(self, response): + """Abstract format handler for cli output "good" responses + + Overridden by descendent classes to indicate the specific output format + when the ation is invoked with a output format of "cli". + + Expected to return the string of the output. + + For those actions that do not have a valid "cli" output, the following + would be generally appropriate for an implementation of this method to + return the api client's response: + + return format_utils.formatted_response_handler(response) + """ + pass + + +class CliAction(AbstractCliAction): """Action base for CliActions""" def __init__(self, ctx): - """Sets api_client""" + """Initialize CliAction""" self.logger = logging.getLogger('shipyard_cli') self.api_parameters = ctx.obj['API_PARAMETERS'] self.resp_txt = "" self.needs_credentials = False + self.output_format = ctx.obj['FORMAT'] - auth_vars = self.api_parameters['auth_vars'] - context_marker = self.api_parameters['context_marker'] - debug = self.api_parameters['debug'] + self.auth_vars = self.api_parameters.get('auth_vars') + self.context_marker = self.api_parameters.get('context_marker') + self.debug = self.api_parameters.get('debug') - validate_auth_vars(ctx, self.api_parameters.get('auth_vars')) + self.client_context = ShipyardClientContext( + self.auth_vars, self.context_marker, self.debug) - self.logger.debug("Passing environment varibles to the API client") - try: - shipyard_client_context = ShipyardClientContext( - auth_vars, context_marker, debug) - self.api_client = ShipyardClient(shipyard_client_context) - except ClientError as e: - self.logger.debug("The shipyard Client Context could not be set.") - ctx.fail('Client Error: %s.' % str(e)) + def get_api_client(self): + """Returns the api client for this action""" + return ShipyardClient(self.client_context) def invoke_and_return_resp(self): - """ - calls the invoke method in the approiate actions.py and returns the - formatted response - """ + """Lifecycle method to invoke and return a response - self.logger.debug("Inoking action.") - env_vars = self.api_parameters['auth_vars'] + Calls the invoke method in the child action and returns the formatted + response. + """ + self.logger.debug("Invoking: %s", self.__class__.__name__) try: - self.invoke() - except ClientError as e: - self.resp_txt = "Client Error: %s." % str(e) - except Exception as e: - self.resp_txt = "Error: Unable to invoke action because %s." % str( - e) + self.validate_auth_vars() + self.resp_txt = self.output_formatter(self.invoke()) + except AuthValuesError as ave: + self.resp_txt = "Error: {}".format(ave.diagnostic) + except UnauthenticatedClientError: + self.resp_txt = ("Error: Command requires authentication. " + "Check credential values") + except UnauthorizedClientError: + self.resp_txt = "Error: Unauthorized to perform this action." + except ClientError as ex: + self.resp_txt = "Error: Client Error: {}".format(str(ex)) + except Exception as ex: + self.resp_txt = ( + "Error: Unable to invoke action due to: {}".format(str(ex))) return self.resp_txt - def invoke(self): - """Default invoke""" - self.resp_txt = "Error: Invoke method is not defined for this action." + def output_formatter(self, response): + """Formats response (Requests library) from api_client + + Dispatches to the appropriate response format handler. + """ + if self.output_format == 'raw': + return format_utils.raw_format_response_handler(response) + elif self.output_format == 'cli': + if response.status_code in self.cli_handled_err_resp_codes: + return format_utils.cli_format_error_handler(response) + elif response.status_code in self.cli_handled_succ_resp_codes: + return self.cli_format_response_handler(response) + else: + self.logger.debug("Unexpected response received") + return format_utils.cli_format_error_handler(response) + else: # assume formatted + return format_utils.formatted_response_handler(response) + + def validate_auth_vars(self): + """Checks that the required authorization varible have been entered""" + required_auth_vars = ['auth_url'] + err_txt = [] + for var in required_auth_vars: + if self.auth_vars[var] is None: + err_txt.append( + 'Missing the required authorization variable: ' + '--os_{}'.format(var)) + if err_txt: + for var in self.auth_vars: + if (self.auth_vars.get(var) is None and + var not in required_auth_vars): + err_txt.append('- Also not set: --os_{}'.format(var)) + raise AuthValuesError(diagnostic='\n'.join(err_txt)) diff --git a/shipyard_client/cli/cli_format_common.py b/shipyard_client/cli/cli_format_common.py new file mode 100644 index 00000000..e28db1f4 --- /dev/null +++ b/shipyard_client/cli/cli_format_common.py @@ -0,0 +1,211 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Reusable parts for outputting Shipyard results in CLI format""" + +from shipyard_client.cli import format_utils + + +def gen_action_steps(step_list, action_id): + """Generate a table from the list of steps. + + Assumes that the input list contains dictionaries with 'id', 'index', and + 'state' fields. + Returns a string representation of the table. + """ + # Generate the steps table. + steps = format_utils.table_factory( + field_names=['Steps', 'Index', 'State'] + ) + if step_list: + for step in step_list: + steps.add_row( + ['step/{}/{}'.format(action_id, step.get('id')), + step.get('index'), + step.get('state')] + ) + else: + steps.add_row(['None', '', '']) + + return format_utils.table_get_string(steps) + + +def gen_action_commands(command_list): + """Generate a table from the list of commands + + Assumes command_list is a list of dictionaries with 'command', 'user', and + 'datetime'. + """ + cmds = format_utils.table_factory( + field_names=['Commands', 'User', 'Datetime'] + ) + if command_list: + for cmd in command_list: + cmds.add_row( + [cmd.get('command'), cmd.get('user'), cmd.get('datetime')] + ) + else: + cmds.add_row(['None', '', '']) + + return format_utils.table_get_string(cmds) + + +def gen_action_validations(validation_list): + """Generates a CLI formatted listing of validations + + Assumes validation_list is a list of dictionaries with 'validation_name', + 'action_id', 'id', and 'details'. + """ + if validation_list: + validations = [] + for val in validation_list: + validations.append('{} : validation/{}/{}\n'.format( + val.get('validation_name'), + val.get('action_id'), + val.get('id') + )) + validations.append(val.get('details')) + validations.append('\n\n') + return 'Validations: {}'.format('\n'.join(validations)) + else: + return 'Validations: {}'.format('None') + + +def gen_action_details(action_dict): + """Generates the detailed information for an action + + Assumes action_dict is a dictionary with 'name', 'id', 'action_lifecycle', + 'parameters', 'datetime', 'dag_status', 'context_marker', and 'user' + """ + details = format_utils.table_factory() + details.add_row(['Name:', action_dict.get('name')]) + details.add_row(['Action:', 'action/{}'.format(action_dict.get('id'))]) + details.add_row(['Lifecycle:', action_dict.get('action_lifecycle')]) + details.add_row(['Parameters:', str(action_dict.get('parameters'))]) + details.add_row(['Datetime:', action_dict.get('datetime')]) + details.add_row(['Dag Status:', action_dict.get('dag_status')]) + details.add_row(['Context Marker:', action_dict.get('context_marker')]) + details.add_row(['User:', action_dict.get('user')]) + return format_utils.table_get_string(details) + + +def gen_action_step_details(step_dict, action_id): + """Generates the detailed information for an action step + + Assumes action_dict is a dictionary with 'index', 'state', 'start_date', + 'end_date', 'duration', 'try_number', and 'operator' + """ + details = format_utils.table_factory() + details.add_row(['Name:', step_dict.get('task_id')]) + details.add_row(['Task ID:', 'step/{}/{}'.format( + action_id, + step_dict.get('task_id') + )]) + details.add_row(['Index:', step_dict.get('index')]) + details.add_row(['State:', step_dict.get('state')]) + details.add_row(['Start Date:', step_dict.get('start_date')]) + details.add_row(['End Date:', step_dict.get('end_date')]) + details.add_row(['Duration:', step_dict.get('duration')]) + details.add_row(['Try Number:', step_dict.get('try_number')]) + details.add_row(['Operator:', step_dict.get('operator')]) + return format_utils.table_get_string(details) + + +def gen_action_table(action_list): + """Generates a list of actions + + Assumes action_list is a list of dictionaries with 'name', 'id', and + 'action_lifecycle' + """ + actions = format_utils.table_factory( + field_names=['Name', 'Action', 'Lifecycle'] + ) + if action_list: + for action in action_list: + actions.add_row( + [action.get('name'), + 'action/{}'.format(action.get('id')), + action.get('action_lifecycle')] + ) + else: + actions.add_row(['None', '', '']) + + return format_utils.table_get_string(actions) + + +def gen_workflow_table(workflow_list): + """Generates a list of workflows + + Assumes workflow_list is a list of dictionaries with 'workflow_id' and + 'state' + """ + workflows = format_utils.table_factory( + field_names=['Workflows', 'State'] + ) + if workflow_list: + for workflow in workflow_list: + workflows.add_row( + [workflow.get('workflow_id'), workflow.get('state')]) + else: + workflows.add_row(['None', '']) + + return format_utils.table_get_string(workflows) + + +def gen_workflow_details(workflow_dict): + """Generates a workflow detail + + Assumes workflow_dict has 'execution_date', 'end_date', 'workflow_id', + 'start_date', 'external_trigger', 'steps', 'dag_id', 'state', 'run_id', + and 'sub_dags' + """ + details = format_utils.table_factory() + details.add_row(['Workflow:', workflow_dict.get('workflow_id')]) + + details.add_row(['State:', workflow_dict.get('state')]) + details.add_row(['Dag ID:', workflow_dict.get('dag_id')]) + details.add_row(['Execution Date:', workflow_dict.get('execution_date')]) + details.add_row(['Start Date:', workflow_dict.get('start_date')]) + details.add_row(['End Date:', workflow_dict.get('end_date')]) + details.add_row(['External Trigger:', + workflow_dict.get('external_trigger')]) + return format_utils.table_get_string(details) + + +def gen_workflow_steps(step_list): + """Generates a table of steps for a workflow + + Assumes step_list is a list of dictionaries with 'task_id' and 'state' + """ + steps = format_utils.table_factory( + field_names=['Steps', 'State'] + ) + if step_list: + for step in step_list: + steps.add_row([step.get('task_id'), step.get('state')]) + else: + steps.add_row(['None', '']) + + return format_utils.table_get_string(steps) + + +def gen_sub_workflows(wf_list): + """Generates the list of Sub Workflows + + Assumes wf_list is a list of dictionaries with the same contents as a + standard workflow + """ + wfs = [] + for wf in wf_list: + wfs.append(gen_workflow_details(wf)) + return '\n\n'.join(wfs) diff --git a/shipyard_client/cli/commit/actions.py b/shipyard_client/cli/commit/actions.py index 970ff6e6..169245cf 100644 --- a/shipyard_client/cli/commit/actions.py +++ b/shipyard_client/cli/commit/actions.py @@ -13,23 +13,38 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import format_utils class CommitConfigdocs(CliAction): """Actions to Commit Configdocs""" def __init__(self, ctx, force): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.force = force - self.output_format = ctx.obj['FORMAT'] self.logger.debug("CommitConfigdocs action initialized with force=%s", force) def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client commit_configdocs.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.commit_configdocs(force=self.force)) + return self.get_api_client().commit_configdocs(force=self.force) + + # Handle 400, 409 with default error handler for cli. + cli_handled_err_resp_codes = [400, 409] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + outfmt_string = "Configuration documents committed.\n{}" + return outfmt_string.format( + format_utils.cli_format_status_handler(response) + ) diff --git a/shipyard_client/cli/control/actions.py b/shipyard_client/cli/control/actions.py index 04f68025..8667b560 100644 --- a/shipyard_client/cli/control/actions.py +++ b/shipyard_client/cli/control/actions.py @@ -11,24 +11,38 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting class Control(CliAction): """Action to Pause Process""" def __init__(self, ctx, control_verb, action_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.action_id = action_id self.control_verb = control_verb - self.output_format = ctx.obj['FORMAT'] self.logger.debug("ControlPause action initialized") def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client post_control_action.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.post_control_action( - action_id=self.action_id, - control_verb=self.control_verb)) + return self.get_api_client().post_control_action( + action_id=self.action_id, + control_verb=self.control_verb + ) + + # Handle 400, 409 with default error handler for cli. + cli_handled_err_resp_codes = [400, 409] + + # Handle 202 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [202] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 202 responses + """ + return "{} successfully submitted for action {}".format( + self.control_verb, self.action_id) diff --git a/shipyard_client/cli/create/actions.py b/shipyard_client/cli/create/actions.py index 990fe78e..030d49ba 100644 --- a/shipyard_client/cli/create/actions.py +++ b/shipyard_client/cli/create/actions.py @@ -9,37 +9,51 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import cli_format_common +from shipyard_client.cli import format_utils class CreateAction(CliAction): """Action to Create Action""" def __init__(self, ctx, action_name, param): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("CreateAction action initialized with action command" "%s and parameters %s", action_name, param) self.action_name = action_name self.param = param - self.output_format = ctx.obj['FORMAT'] def invoke(self): - """Calls API Client and formats response from API Client""" + """Returns the response from API Client""" self.logger.debug("Calling API Client post_actions.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.post_actions( - name=self.action_name, - parameters=self.param)) + return self.get_api_client().post_actions(name=self.action_name, + parameters=self.param) + + # Handle 400, 409 with default error handler for cli. + cli_handled_err_resp_codes = [400, 409] + + # Handle 201 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [201] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 201 responses + """ + resp_j = response.json() + action_list = [resp_j] if resp_j else [] + return cli_format_common.gen_action_table(action_list) class CreateConfigdocs(CliAction): """Action to Create Configdocs""" def __init__(self, ctx, collection, buffer, data): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("CreateConfigdocs action initialized with" + " collection=%s,buffer=%s and data=%s", collection, @@ -47,13 +61,30 @@ class CreateConfigdocs(CliAction): self.collection = collection self.buffer = buffer self.data = data - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client post_configdocs.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.post_configdocs( - collection_id=self.collection, - buffer_mode=self.buffer, - document_data=self.data)) + return self.get_api_client().post_configdocs( + collection_id=self.collection, + buffer_mode=self.buffer, + document_data=self.data + ) + + # Handle 409 with default error handler for cli. + cli_handled_err_resp_codes = [409] + + # Handle 201 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [201] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 201 responses + """ + outfmt_string = "Configuration documents added.\n{}" + return outfmt_string.format( + format_utils.cli_format_status_handler(response) + ) diff --git a/shipyard_client/cli/describe/actions.py b/shipyard_client/cli/describe/actions.py index 8e47ab1a..ae94c842 100644 --- a/shipyard_client/cli/describe/actions.py +++ b/shipyard_client/cli/describe/actions.py @@ -11,87 +11,158 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import cli_format_common class DescribeAction(CliAction): """Action to Describe Action""" def __init__(self, ctx, action_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "DescribeAction action initialized with action_id=%s", action_id) self.action_id = action_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_action_detail.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_action_detail(action_id=self.action_id)) + return self.get_api_client().get_action_detail( + action_id=self.action_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + # Assemble the sections of the action details + return '{}\n\n{}\n\n{}\n\n{}\n'.format( + cli_format_common.gen_action_details(resp_j), + cli_format_common.gen_action_steps(resp_j.get('steps'), + resp_j.get('id')), + cli_format_common.gen_action_commands(resp_j.get('commands')), + cli_format_common.gen_action_validations( + resp_j.get('validations') + ) + ) class DescribeStep(CliAction): """Action to Describe Step""" def __init__(self, ctx, action_id, step_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "DescribeStep action initialized with action_id=%s and step_id=%s", action_id, step_id) self.action_id = action_id self.step_id = step_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_step_detail.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.get_step_detail( - action_id=self.action_id, - step_id=self.step_id)) + return self.get_api_client().get_step_detail(action_id=self.action_id, + step_id=self.step_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + return cli_format_common.gen_action_step_details(resp_j, + self.action_id) class DescribeValidation(CliAction): """Action to Describe Validation""" def __init__(self, ctx, action_id, validation_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( 'DescribeValidation action initialized with action_id=%s' 'and validation_id=%s', action_id, validation_id) self.validation_id = validation_id self.action_id = action_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_validation_detail.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_validation_detail( - action_id=self.action_id, validation_id=self.validation_id)) + return self.get_api_client().get_validation_detail( + action_id=self.action_id, validation_id=self.validation_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + val_list = [resp_j] if resp_j else [] + return cli_format_common.gen_action_validations(val_list) class DescribeWorkflow(CliAction): """Action to describe a workflow""" def __init__(self, ctx, workflow_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "DescribeWorkflow action initialized with workflow_id=%s", workflow_id) self.workflow_id = workflow_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_action_detail.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_dag_detail(workflow_id=self.workflow_id)) + return self.get_api_client().get_dag_detail( + workflow_id=self.workflow_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + # Assemble the workflow details + + return '{}\n\n{}\n\nSubworkflows:\n{}\n'.format( + cli_format_common.gen_workflow_details(resp_j), + cli_format_common.gen_workflow_steps(resp_j.get('steps', [])), + cli_format_common.gen_sub_workflows(resp_j.get('sub_dags', [])) + ) diff --git a/shipyard_client/cli/format_utils.py b/shipyard_client/cli/format_utils.py new file mode 100644 index 00000000..a6cc0acf --- /dev/null +++ b/shipyard_client/cli/format_utils.py @@ -0,0 +1,139 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import yaml + +from prettytable import PrettyTable +from prettytable.prettytable import PLAIN_COLUMNS + + +def cli_format_error_handler(response): + """Generic handler for standard Shipyard error responses + + Method is intended for use when there is no special handling needed + for the response. + :param client_response: a requests response object assuming the + standard error format + :returns: a generically formatted error response formulated from the + client_repsonse. The response will be in the format: + + Error: {{message}} + Reason: {{Reason}} + Additional: {{details message list messages}} + ... + """ + return cli_format_status_handler(response, is_error=True) + + +def cli_format_status_handler(response, is_error=False): + """Handler for standard Shipyard status and status-based error responses + + Method is intended for use when there is no special handling needed + for the response. If the response is empty, returns empty string + :param client_response: a requests response object assuming the + standard error format + :is_error: toggles the use of status or error verbiage + :returns: a generically formatted error response formulated from the + client_repsonse. The response will be in the format: + + [Error|Status]: {{message}} + Reason: {{Reason}} + Additional: {{details message list messages}} + ... + """ + formatted = "Error: {}\nReason: {}" if is_error \ + else "Status: {}\nReason: {}" + try: + if response.text: + resp_j = response.json() + resp = formatted.format(resp_j.get('message', 'Not specified'), + resp_j.get('reason', 'Not specified')) + if resp_j.get('details'): + for message in resp_j.get('details').get('messageList', []): + if message.get('error', False): + resp = resp + '\n- Error: {}'.format( + message.get('message')) + else: + resp = resp + '\n- Info: {}'.format( + message.get('message')) + return resp + else: + return '' + except ValueError: + return "Error: Unable to decode response. Value: {}".format( + response.text) + + +def raw_format_response_handler(response): + """Basic format handler to return raw response text""" + return response.text + + +def formatted_response_handler(response): + """Base format handler for either json or yaml depending on call""" + call = response.headers['Content-Type'] + if 'json' in call: + try: + return json.dumps(response.json(), sort_keys=True, indent=4) + except ValueError: + return ( + "This is not json and could not be printed as such. \n" + + response.text + ) + + else: # all others should be yaml + try: + return (yaml.dump_all( + yaml.safe_load_all(response.content), + width=79, + indent=4, + default_flow_style=False)) + except ValueError: + return ( + "This is not yaml and could not be printed as such.\n" + + response.text + ) + + +def table_factory(field_names=None, rows=None, style=None): + """Generate a table using prettytable + + Factory method for a prettytable using the PLAIN_COLUMN style unless + ovrridden by the style parameter. + If a field_names list of strings is passed in, the column names + will be initialized. + If rows are supplied (list of lists), the will be added as rows in + order. + """ + p = PrettyTable() + if style is None: + p.set_style(PLAIN_COLUMNS) + else: + p.set_style(style) + if field_names: + p.field_names = field_names + else: + p.header = False + if rows: + for row in rows: + p.add_row(row) + # This alignment only works if columns and rows are set up. + p.align = 'l' + return p + + +def table_get_string(table, align='l'): + """Wrapper to return a prettytable string with the supplied alignment""" + table.align = 'l' + return table.get_string() diff --git a/shipyard_client/cli/get/actions.py b/shipyard_client/cli/get/actions.py index 527b4c14..a2a3925d 100644 --- a/shipyard_client/cli/get/actions.py +++ b/shipyard_client/cli/get/actions.py @@ -11,78 +11,133 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import cli_format_common +from shipyard_client.cli import format_utils class GetActions(CliAction): """Action to Get Actions""" def __init__(self, ctx): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("GetActions action initialized.") - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_actions.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.get_actions()) + return self.get_api_client().get_actions() + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + return cli_format_common.gen_action_table(resp_j) class GetConfigdocs(CliAction): """Action to Get Configdocs""" def __init__(self, ctx, collection, version): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "GetConfigdocs action initialized with collection=%s and " "version=%s" % (collection, version)) self.collection = collection self.version = version - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_configdocs.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.get_configdocs( - collection_id=self.collection, - version=self.version)) + return self.get_api_client().get_configdocs( + collection_id=self.collection, version=self.version) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + Effectively passes through the YAML received. + :param response: a requests response object + :returns: a string representing a CLI appropriate response + Handles 200 responses + """ + return format_utils.raw_format_response_handler(response) class GetRenderedConfigdocs(CliAction): """Action to Get Rendered Configdocs""" def __init__(self, ctx, version): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("GetRenderedConfigdocs action initialized") self.version = version - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_rendereddocs.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_rendereddocs(version=self.version)) + return self.get_api_client().get_rendereddocs(version=self.version) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + Effectively passes through the YAML received. + :param response: a requests response object + :returns: a string representing a CLI appropriate response + Handles 200 responses + """ + return format_utils.raw_format_response_handler(response) class GetWorkflows(CliAction): """Action to get workflows""" def __init__(self, ctx, since=None): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("GetWorkflows action initialized.") self.since = since - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_actions.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_workflows(self.since)) + return self.get_api_client().get_workflows(self.since) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + wf_list = resp_j if resp_j else [] + return cli_format_common.gen_workflow_table(wf_list) diff --git a/shipyard_client/cli/input_checks.py b/shipyard_client/cli/input_checks.py index 08907eb7..32f289e2 100644 --- a/shipyard_client/cli/input_checks.py +++ b/shipyard_client/cli/input_checks.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""CLI value checks invoked from commands""" import arrow from arrow.parser import ParserError @@ -59,25 +60,6 @@ def check_workflow_id(ctx, workflow_id): 'YYYY-MM-DDTHH:mm:ss.SSSSSS') -def validate_auth_vars(ctx, auth_vars): - """Checks that the required authurization varible have been entered""" - - required_auth_vars = ['auth_url'] - err_txt = "" - for var in required_auth_vars: - if auth_vars[var] is None: - err_txt += ( - 'Missing the required authorization variable: ' - '--os_{}\n'.format(var)) - if err_txt != "": - err_txt += ('\nMissing the following additional authorization ' - 'options: ') - for var in auth_vars: - if auth_vars[var] is None and var not in required_auth_vars: - err_txt += '\n--os_{}'.format(var) - ctx.fail(err_txt) - - def check_reformat_parameter(ctx, param): """Checks for = format""" param_dictionary = {} diff --git a/shipyard_client/cli/output_formatting.py b/shipyard_client/cli/output_formatting.py deleted file mode 100644 index 30026c33..00000000 --- a/shipyard_client/cli/output_formatting.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-1.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import yaml - - -def output_formatting(output_format, response): - """formats response from api_client""" - if output_format == 'raw': - return response.text - else: # assume formatted - return formatted(response) - - -def formatted(response): - """Formats either json or yaml depending on call""" - call = response.headers['Content-Type'] - if 'json' in call: - try: - input = response.json() - return (json.dumps(input, sort_keys=True, indent=4)) - except ValueError: - return ("This is not json and could not be printed as such. \n " + - response.text) - - else: # all others should be yaml - try: - return (yaml.dump_all( - yaml.safe_load_all(response.content), - width=79, - indent=4, - default_flow_style=False)) - except ValueError: - return ("This is not yaml and could not be printed as such.\n " + - response.text) diff --git a/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py b/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py index a93b3f3c..7e596d2d 100644 --- a/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py +++ b/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,192 +11,204 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -import mock import json +import mock -from shipyard_client.api_client.shipyard_api_client import ShipyardClient from shipyard_client.api_client.base_client import BaseClient +from shipyard_client.api_client.shipyard_api_client import ShipyardClient +from shipyard_client.api_client.shipyardclient_context import \ + ShipyardClientContext -class TemporaryContext: - def __init__(self): - self.debug = True - self.keystone_Auth = {} - self.token = 'abcdefgh' - self.service_type = 'http://shipyard' - self.shipyard_endpoint = 'http://shipyard/api/v1.0' - self.context_marker = '123456' +def replace_get_endpoint(self): + """Fake get endpoint method to isolate testing""" + return 'http://shipyard/api/v1.0' def replace_post_rep(self, url, query_params={}, data={}, content_type=''): - """ - replaces call to shipyard client + """Replaces call to shipyard client + :returns: dict with url and parameters """ return {'url': url, 'params': query_params, 'data': data} def replace_get_resp(self, url, query_params={}, json=False): - """ - replaces call to shipyard client + """Replaces call to shipyard client. + :returns: dict with url and parameters """ return {'url': url, 'params': query_params} -def replace_base_constructor(self, context): - pass - - def get_api_client(): """ get a instance of shipyard client :returns: shipyard client with no context object """ - context = TemporaryContext() + keystone_auth = { + 'project_domain_name': 'projDomainTest', + 'user_domain_name': 'userDomainTest', + 'project_name': 'projectTest', + 'username': 'usernameTest', + 'password': 'passwordTest', + 'auth_url': 'urlTest' + }, + + context = ShipyardClientContext( + debug=True, + keystone_auth=keystone_auth, + context_marker='88888888-4444-4444-4444-121212121212' + ) return ShipyardClient(context) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_post_config_docs(*args): shipyard_client = get_api_client() buffermode = 'rejectoncontents' result = shipyard_client.post_configdocs('ABC', buffer_mode=buffermode) params = result['params'] assert result['url'] == '{}/configdocs/ABC'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['buffermode'] == buffermode -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_config_docs(*args): shipyard_client = get_api_client() version = 'buffer' result = shipyard_client.get_configdocs('ABC', version=version) params = result['params'] assert result['url'] == '{}/configdocs/ABC'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['version'] == version -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_rendered_config_docs(*args): shipyard_client = get_api_client() version = 'buffer' result = shipyard_client.get_rendereddocs(version=version) params = result['params'] assert result['url'] == '{}/renderedconfigdocs'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['version'] == version -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_commit_configs(*args): shipyard_client = get_api_client() force_mode = True result = shipyard_client.commit_configdocs(force_mode) params = result['params'] assert result['url'] == '{}/commitconfigdocs'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['force'] == force_mode -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_actions(*args): shipyard_client = get_api_client() result = shipyard_client.get_actions() - assert result['url'] == '{}/actions'.format(shipyard_client.shipyard_url) + assert result['url'] == '{}/actions'.format( + shipyard_client.get_endpoint() + ) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_post_actions(*args): shipyard_client = get_api_client() name = 'good action' parameters = {'hello': 'world'} result = shipyard_client.post_actions(name, parameters) data = json.loads(result['data']) - assert result['url'] == '{}/actions'.format(shipyard_client.shipyard_url) + assert result['url'] == '{}/actions'.format( + shipyard_client.get_endpoint() + ) assert data['name'] == name assert data['parameters']['hello'] == 'world' -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_action_details(*args): shipyard_client = get_api_client() action_id = 'GoodAction' result = shipyard_client.get_action_detail(action_id) assert result['url'] == '{}/actions/{}'.format( - shipyard_client.shipyard_url, action_id) + shipyard_client.get_endpoint(), action_id) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_val_details(*args): shipyard_client = get_api_client() action_id = 'GoodAction' validation_id = 'Validation' result = shipyard_client.get_validation_detail(action_id, validation_id) assert result['url'] == '{}/actions/{}/validationdetails/{}'.format( - shipyard_client.shipyard_url, action_id, validation_id) + shipyard_client.get_endpoint(), action_id, validation_id) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_step_details(*args): shipyard_client = get_api_client() action_id = 'GoodAction' step_id = 'TestStep' result = shipyard_client.get_step_detail(action_id, step_id) assert result['url'] == '{}/actions/{}/steps/{}'.format( - shipyard_client.shipyard_url, action_id, step_id) + shipyard_client.get_endpoint(), action_id, step_id) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_post_control(*args): shipyard_client = get_api_client() action_id = 'GoodAction' control_verb = 'Control' result = shipyard_client.post_control_action(action_id, control_verb) assert result['url'] == '{}/actions/{}/control/{}'.format( - shipyard_client.shipyard_url, action_id, control_verb) + shipyard_client.get_endpoint(), action_id, control_verb) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_workflows(*args): shipyard_client = get_api_client() since_mode = 'TestSince' result = shipyard_client.get_workflows(since_mode) - assert result['url'] == '{}/workflows'.format(shipyard_client.shipyard_url, - since_mode) + assert result['url'] == '{}/workflows'.format( + shipyard_client.get_endpoint()) + + params = result['params'] + assert 'since' in params -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_dag_details(*args): shipyard_client = get_api_client() workflow_id = 'TestWorkflow' result = shipyard_client.get_dag_detail(workflow_id) assert result['url'] == '{}/workflows/{}'.format( - shipyard_client.shipyard_url, workflow_id) + shipyard_client.get_endpoint(), workflow_id) diff --git a/shipyard_client/tests/unit/cli/commit/test_commit_actions.py b/shipyard_client/tests/unit/cli/commit/test_commit_actions.py index 8962eb2f..cee036f2 100644 --- a/shipyard_client/tests/unit/cli/commit/test_commit_actions.py +++ b/shipyard_client/tests/unit/cli/commit/test_commit_actions.py @@ -11,56 +11,65 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import mock -from shipyard_client.cli.commit.actions import CommitConfigdocs +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext - -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False -} +from shipyard_client.cli.commit.actions import CommitConfigdocs +from shipyard_client.tests.unit.cli import stubs -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_commit_configdocs(*args): + responses.add(responses.POST, + 'http://shiptest/commitconfigdocs?force=false', + body=None, + status=200) + response = CommitConfigdocs(stubs.StubCliContext(), + False).invoke_and_return_resp() + assert response == 'Configuration documents committed.\n' -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_commit_configdocs_409(*args): + api_resp = stubs.gen_err_resp(message="Conflicts message", + sub_message='Another bucket message', + sub_error_count=1, + sub_info_count=0, + reason='Conflicts reason', + code=409) + responses.add(responses.POST, + 'http://shiptest/commitconfigdocs?force=false', + body=api_resp, + status=409) + response = CommitConfigdocs(stubs.StubCliContext(), + False).invoke_and_return_resp() + assert 'Error: Conflicts message' in response + assert 'Configuration documents committed' not in response + assert 'Reason: Conflicts reason' in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.commit.actions.output_formatting', - side_effect=replace_output_formatting) -def test_CommitConfigdocs(*args): - response = CommitConfigdocs(ctx, True).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'commitconfigdocs' in url - # test function was called with correct parameters - params = response.get('params') - assert params.get('force') is True +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_commit_configdocs_forced(*args): + api_resp = stubs.gen_err_resp(message="Conflicts message forced", + sub_message='Another bucket message', + sub_error_count=1, + sub_info_count=0, + reason='Conflicts reason', + code=200) + responses.add(responses.POST, + 'http://shiptest/commitconfigdocs?force=true', + body=api_resp, + status=200) + response = CommitConfigdocs(stubs.StubCliContext(), + True).invoke_and_return_resp() + assert 'Status: Conflicts message forced' in response + assert 'Configuration documents committed' in response + assert 'Reason: Conflicts reason' in response diff --git a/shipyard_client/tests/unit/cli/control/test_control_actions.py b/shipyard_client/tests/unit/cli/control/test_control_actions.py index 0078cba4..e11d41da 100644 --- a/shipyard_client/tests/unit/cli/control/test_control_actions.py +++ b/shipyard_client/tests/unit/cli/control/test_control_actions.py @@ -11,59 +11,109 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import mock -from shipyard_client.cli.control.actions import Control +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext - -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False -} +from shipyard_client.cli.control.actions import Control +from shipyard_client.tests.unit.cli import stubs -class MockCTX(): - pass - - -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.control.actions.output_formatting', - side_effect=replace_output_formatting) +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_Control(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/pause', + body=None, + status=202 + ) control_verb = 'pause' id = '01BTG32JW87G0YKA1K29TKNAFX' - response = Control(ctx, control_verb, id).invoke_and_return_resp() + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() # test correct function was called - url = response.get('url') - assert 'control' in url + assert response == ('pause successfully submitted for action' + ' 01BTG32JW87G0YKA1K29TKNAFX') - # test function was called with correct parameters - assert control_verb in url - assert id in url + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_control_unpause(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/unpause', + body=None, + status=202 + ) + control_verb = 'unpause' + id = '01BTG32JW87G0YKA1K29TKNAFX' + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() + # test correct function was called + assert response == ('unpause successfully submitted for action' + ' 01BTG32JW87G0YKA1K29TKNAFX') + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_control_stop(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/stop', + body=None, + status=202 + ) + control_verb = 'stop' + id = '01BTG32JW87G0YKA1K29TKNAFX' + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() + # test correct function was called + assert response == ('stop successfully submitted for action' + ' 01BTG32JW87G0YKA1K29TKNAFX') + + +resp_body = """ +{ + "message": "Unable to pause action", + "details": { + "messageList": [ + { + "message": "Conflicting things", + "error": true + }, + { + "message": "Try soup", + "error": false + } + ] + }, + "reason": "Conflicts" +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_control_409(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/pause', + body=resp_body, + status=409 + ) + control_verb = 'pause' + id = '01BTG32JW87G0YKA1K29TKNAFX' + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() + # test correct function was called + assert 'Unable to pause action' in response diff --git a/shipyard_client/tests/unit/cli/create/test_create_actions.py b/shipyard_client/tests/unit/cli/create/test_create_actions.py index 7f4c0bb0..f478fa0c 100644 --- a/shipyard_client/tests/unit/cli/create/test_create_actions.py +++ b/shipyard_client/tests/unit/cli/create/test_create_actions.py @@ -15,81 +15,156 @@ import mock import yaml -from shipyard_client.cli.create.actions import CreateAction, CreateConfigdocs +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext +from shipyard_client.cli.create.actions import CreateAction +from shipyard_client.cli.create.actions import CreateConfigdocs +from shipyard_client.tests.unit.cli import stubs -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False +resp_body = """ +{ + "dag_status": "SCHEDULED", + "parameters": {}, + "dag_execution_date": "2017-09-24T19:05:49", + "id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "dag_id": "deploy_site", + "name": "deploy_site", + "user": "shipyard", + "context_marker": "629f2ea2-c59d-46b9-8641-7367a91a7016", + "timestamp": "2017-09-24 19:05:43.603591" } +""" -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_action(*args): + responses.add(responses.POST, + 'http://shiptest/actions', + body=resp_body, + status=201) + response = CreateAction(stubs.StubCliContext(), + action_name='deploy_site', + param=None).invoke_and_return_resp() + assert 'Name' in response + assert 'Action' in response + assert 'Lifecycle' in response + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' in response + assert 'Error:' not in response -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_action_400(*args): + responses.add(responses.POST, + 'http://shiptest/actions', + body=stubs.gen_err_resp(message='Error_400', + reason='bad action'), + status=400) + response = CreateAction(stubs.StubCliContext(), + action_name='deploy_dogs', + param=None).invoke_and_return_resp() + assert 'Error_400' in response + assert 'bad action' in response + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' not in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.create.actions.output_formatting', - side_effect=replace_output_formatting) -def test_CreateAction(*args): - action_name = 'redeploy_server' - param = {'server-name': 'mcp'} - response = CreateAction(ctx, action_name, param).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions' in url - # test function was called with correct parameters - data = response.get('data') - assert '"name": "redeploy_server"' in data - assert '"parameters": {"server-name": "mcp"}' in data +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_action_409(*args): + responses.add(responses.POST, + 'http://shiptest/actions', + body=stubs.gen_err_resp(message='Error_409', + reason='bad validations'), + status=409) + response = CreateAction(stubs.StubCliContext(), + action_name='deploy_site', + param=None).invoke_and_return_resp() + assert 'Error_409' in response + assert 'bad validations' in response + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' not in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.create.actions.output_formatting', - side_effect=replace_output_formatting) -def test_CreateConfigdocs(*args): - collection = 'design' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_configdocs(*args): + succ_resp = stubs.gen_err_resp(message='Validations succeeded', + sub_error_count=0, + sub_info_count=0, + reason='Validation', + code=200) + responses.add(responses.POST, + 'http://shiptest/configdocs/design', + body=succ_resp, + status=201) + filename = 'shipyard_client/tests/unit/cli/create/sample_yaml/sample.yaml' document_data = yaml.dump_all(filename) - buffer = 'append' - response = CreateConfigdocs(ctx, collection, buffer, + + response = CreateConfigdocs(stubs.StubCliContext(), + 'design', + 'append', document_data).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'configdocs' in url - # test function was called with correct parameters - assert collection in url - data = response.get('data') - assert document_data in data - params = response.get('params') - assert params.get('buffermode') == buffer + assert 'Configuration documents added.' + assert 'Status: Validations succeeded' in response + assert 'Reason: Validation' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_configdocs_201_with_val_fails(*args): + succ_resp = stubs.gen_err_resp(message='Validations failed', + sub_message='Some reason', + sub_error_count=2, + sub_info_count=1, + reason='Validation', + code=400) + responses.add(responses.POST, + 'http://shiptest/configdocs/design', + body=succ_resp, + status=201) + + filename = 'shipyard_client/tests/unit/cli/create/sample_yaml/sample.yaml' + document_data = yaml.dump_all(filename) + + response = CreateConfigdocs(stubs.StubCliContext(), + 'design', + 'append', + document_data).invoke_and_return_resp() + assert 'Configuration documents added.' in response + assert 'Status: Validations failed' in response + assert 'Reason: Validation' in response + assert 'Some reason-1' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_configdocs_409(*args): + err_resp = stubs.gen_err_resp(message='Invalid collection', + sub_message='Buffer is either not...', + sub_error_count=1, + sub_info_count=0, + reason='Buffermode : append', + code=409) + responses.add(responses.POST, + 'http://shiptest/configdocs/design', + body=err_resp, + status=409) + + filename = 'shipyard_client/tests/unit/cli/create/sample_yaml/sample.yaml' + document_data = yaml.dump_all(filename) + + response = CreateConfigdocs(stubs.StubCliContext(), + 'design', + 'append', + document_data).invoke_and_return_resp() + assert 'Error: Invalid collection' in response + assert 'Reason: Buffermode : append' in response + assert 'Buffer is either not...' in response diff --git a/shipyard_client/tests/unit/cli/describe/test_describe_actions.py b/shipyard_client/tests/unit/cli/describe/test_describe_actions.py index 8606b992..cf9d7800 100644 --- a/shipyard_client/tests/unit/cli/describe/test_describe_actions.py +++ b/shipyard_client/tests/unit/cli/describe/test_describe_actions.py @@ -11,106 +11,317 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import mock -from shipyard_client.cli.describe.actions import \ - DescribeAction, DescribeStep, DescribeValidation, DescribeWorkflow +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext +from shipyard_client.cli.describe.actions import DescribeAction +from shipyard_client.cli.describe.actions import DescribeStep +from shipyard_client.cli.describe.actions import DescribeValidation +from shipyard_client.cli.describe.actions import DescribeWorkflow +from shipyard_client.tests.unit.cli import stubs -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False + +GET_ACTION_API_RESP = """ +{ + "name": "deploy_site", + "dag_execution_date": "2017-09-24T19:05:49", + "validations": [], + "id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "dag_id": "deploy_site", + "command_audit": [ + { + "id": "01BTTMG16R9H3Z4JVQNBMRV1MZ", + "action_id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "datetime": "2017-09-24 19:05:49.530223+00:00", + "user": "shipyard", + "command": "invoke" + } + ], + "user": "shipyard", + "context_marker": "629f2ea2-c59d-46b9-8641-7367a91a7016", + "datetime": "2017-09-24 19:05:43.603591+00:00", + "dag_status": "failed", + "parameters": {}, + "steps": [ + { + "id": "action_xcom", + "url": "/actions/01BTTMFVDKZFRJM80FGD7J1AKN/steps/action_xcom", + "index": 1, + "state": "success" + } + ], + "action_lifecycle": "Failed" } +""" -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_action(*args): + responses.add(responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN', + body=GET_ACTION_API_RESP, + status=200) - -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeAction(*args): response = DescribeAction( - ctx, '01BTG32JW87G0YKA1K29TKNAFX').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions/01BTG32JW87G0YKA1K29TKNAFX' in url - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeStep(*args): - response = DescribeStep(ctx, '01BTG32JW87G0YKA1K29TKNAFX', - 'preflight').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions/01BTG32JW87G0YKA1K29TKNAFX/steps/preflight' in url - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeValidation(*args): - response = DescribeValidation( - ctx, '01BTG32JW87G0YKA1K29TKNAFX', - '01BTG3PKBS15KCKFZ56XXXBGF2').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions' in url - assert '01BTG32JW87G0YKA1K29TKNAFX' in url - assert 'validationdetails' in url - assert '01BTG3PKBS15KCKFZ56XXXBGF2' in url - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeWorkflow(*args): - response = DescribeWorkflow( - ctx, 'deploy_site__2017-01-01T12:34:56.123456' + stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN' ).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'workflows' in url - assert 'deploy_site__2017-01-01T12:34:56.123456' in url + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' in response + assert 'step/01BTTMFVDKZFRJM80FGD7J1AKN/action_xcom' in response + assert 'Steps' in response + assert 'Commands' in response + assert 'Validations:' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_action_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add(responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN', + body=api_resp, + status=404) + + response = DescribeAction( + stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN' + ).invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +GET_STEP_API_RESP = """ +{ + "end_date": "2017-09-24 19:05:59.446213", + "duration": 0.165181, + "queued_dttm": "2017-09-24 19:05:52.993983", + "operator": "PythonOperator", + "try_number": 1, + "task_id": "preflight", + "state": "success", + "execution_date": "2017-09-24 19:05:49", + "dag_id": "deploy_site", + "index": 1, + "start_date": "2017-09-24 19:05:59.281032" +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_step(*args): + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/steps/preflight', + body=GET_STEP_API_RESP, + status=200) + + response = DescribeStep(stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN', + 'preflight').invoke_and_return_resp() + assert 'step/01BTTMFVDKZFRJM80FGD7J1AKN/preflight' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_step_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/steps/preflight', + body=api_resp, + status=404) + + response = DescribeStep(stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN', + 'preflight').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +GET_VALIDATION_API_RESP = """ +{ + "validation_name": "validation_1", + "action_id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "id": "02AURNEWAAAESKN99EBF8J2BHD", + "details": "Validations failed for field 'abc'" +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_validation(*args): + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/' + 'validationdetails/02AURNEWAAAESKN99EBF8J2BHD', + body=GET_VALIDATION_API_RESP, + status=200) + + response = DescribeValidation( + stubs.StubCliContext(), + action_id='01BTTMFVDKZFRJM80FGD7J1AKN', + validation_id='02AURNEWAAAESKN99EBF8J2BHD').invoke_and_return_resp() + + v_str = "validation/01BTTMFVDKZFRJM80FGD7J1AKN/02AURNEWAAAESKN99EBF8J2BHD" + assert v_str in response + assert "Validations failed for field 'abc'" in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_validation_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/' + 'validationdetails/02AURNEWAAAESKN99EBF8J2BHD', + body=api_resp, + status=404) + + response = DescribeValidation( + stubs.StubCliContext(), + action_id='01BTTMFVDKZFRJM80FGD7J1AKN', + validation_id='02AURNEWAAAESKN99EBF8J2BHD').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +WF_API_RESP = """ +{ + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:03.361522", + "external_trigger": true, + "steps": [ + { + "end_date": "2017-10-09 21:19:14.916220", + "task_id": "action_xcom", + "start_date": "2017-10-09 21:19:14.798053", + "duration": 0.118167, + "queued_dttm": "2017-10-09 21:19:08.432582", + "try_number": 1, + "state": "success", + "operator": "PythonOperator", + "dag_id": "deploy_site", + "execution_date": "2017-10-09 21:19:03" + }, + { + "end_date": "2017-10-09 21:19:25.283785", + "task_id": "dag_concurrency_check", + "start_date": "2017-10-09 21:19:25.181492", + "duration": 0.102293, + "queued_dttm": "2017-10-09 21:19:19.283132", + "try_number": 1, + "state": "success", + "operator": "ConcurrencyCheckOperator", + "dag_id": "deploy_site", + "execution_date": "2017-10-09 21:19:03" + }, + { + "end_date": "2017-10-09 21:20:05.394677", + "task_id": "preflight", + "start_date": "2017-10-09 21:19:34.994775", + "duration": 30.399902, + "queued_dttm": "2017-10-09 21:19:28.449848", + "try_number": 1, + "state": "failed", + "operator": "SubDagOperator", + "dag_id": "deploy_site", + "execution_date": "2017-10-09 21:19:03" + } + ], + "dag_id": "deploy_site", + "state": "failed", + "run_id": "manual__2017-10-09T21:19:03", + "sub_dags": [ + { + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site.preflight__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:35.082479", + "external_trigger": false, + "dag_id": "deploy_site.preflight", + "state": "failed", + "run_id": "backfill_2017-10-09T21:19:03" + }, + { + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site.postflight__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:35.082479", + "external_trigger": false, + "dag_id": "deploy_site.postflight", + "state": "failed", + "run_id": "backfill_2017-10-09T21:19:03" + } + ] +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_workflow(*args): + responses.add( + responses.GET, + 'http://shiptest/workflows/deploy_site__2017-10-09T21:19:03.000000', + body=WF_API_RESP, + status=200) + + response = DescribeWorkflow( + stubs.StubCliContext(), + 'deploy_site__2017-10-09T21:19:03.000000' + ).invoke_and_return_resp() + assert 'deploy_site__2017-10-09T21:19:03.000000' in response + assert 'deploy_site.preflight__2017-10-09T21:19:03.000000' in response + assert 'deploy_site.postflight__2017-10-09T21:19:03.000000' in response + assert 'dag_concurrency_check' in response + assert 'Subworkflows:' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_workflow_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add( + responses.GET, + 'http://shiptest/workflows/deploy_site__2017-10-09T21:19:03.000000', + body=api_resp, + status=404) + + response = DescribeWorkflow( + stubs.StubCliContext(), + 'deploy_site__2017-10-09T21:19:03.000000' + ).invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response diff --git a/shipyard_client/tests/unit/cli/get/test_get_actions.py b/shipyard_client/tests/unit/cli/get/test_get_actions.py index c5ca333d..44ba5a6f 100644 --- a/shipyard_client/tests/unit/cli/get/test_get_actions.py +++ b/shipyard_client/tests/unit/cli/get/test_get_actions.py @@ -1,5 +1,4 @@ -# Copyright 2017 AT&T Intellectual Property. replace_shipyard All other rights -# reserved. +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,113 +11,218 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import mock -from shipyard_client.cli.get.actions import GetActions, GetConfigdocs, \ - GetRenderedConfigdocs, GetWorkflows +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext +from shipyard_client.cli.get.actions import GetActions +from shipyard_client.cli.get.actions import GetConfigdocs +from shipyard_client.cli.get.actions import GetRenderedConfigdocs +from shipyard_client.cli.get.actions import GetWorkflows +from shipyard_client.tests.unit.cli import stubs -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': '88888888-4444-4444-4444-121212121212', - 'debug': False -} +GET_ACTIONS_API_RESP = """ +[ + { + "dag_status": "failed", + "parameters": {}, + "steps": [ + { + "id": "action_xcom", + "url": "/actions/01BTP9T2WCE1PAJR2DWYXG805V/steps/action_xcom", + "index": 1, + "state": "success" + }, + { + "id": "concurrency_check", + "url": "/actions/01BTP9T2WCE1PAJR2DWYXG805V/steps/concurrency_check", + "index": 2, + "state": "success" + }, + { + "id": "preflight", + "url": "/actions/01BTP9T2WCE1PAJR2DWYXG805V/steps/preflight", + "index": 3, + "state": "failed" + } + ], + "action_lifecycle": "Failed", + "dag_execution_date": "2017-09-23T02:42:12", + "id": "01BTP9T2WCE1PAJR2DWYXG805V", + "dag_id": "deploy_site", + "datetime": "2017-09-23 02:42:06.860597+00:00", + "user": "shipyard", + "context_marker": "416dec4b-82f9-4339-8886-3a0c4982aec3", + "name": "deploy_site" + } +] +""" -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_actions(*args): + responses.add(responses.GET, + 'http://shiptest/actions', + body=GET_ACTIONS_API_RESP, + status=200) + response = GetActions(stubs.StubCliContext()).invoke_and_return_resp() + assert 'deploy_site' in response + assert 'action/01BTP9T2WCE1PAJR2DWYXG805V' in response + assert 'Lifecycle' in response -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_actions_empty(*args): + responses.add(responses.GET, + 'http://shiptest/actions', + body="[]", + status=200) + response = GetActions(stubs.StubCliContext()).invoke_and_return_resp() + assert 'None' in response + assert 'Lifecycle' in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetActions(*args): - response = GetActions(ctx).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions' in url - assert response.get('params') == {} +GET_CONFIGDOCS_API_RESP = """ +--- +yaml: yaml +--- +yaml2: yaml2 +... +""" -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetConfigdocs(*args): - response = GetConfigdocs(ctx, 'design', 'buffer').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'configdocs/design' in url - params = response.get('params') - assert params.get('version') == 'buffer' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_configdocs(*args): + responses.add(responses.GET, + 'http://shiptest/configdocs/design?version=buffer', + body=GET_CONFIGDOCS_API_RESP, + status=200) + response = GetConfigdocs(stubs.StubCliContext(), + collection='design', + version='buffer').invoke_and_return_resp() + assert response == GET_CONFIGDOCS_API_RESP -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetRenderedConfigdocs(*args): - response = GetRenderedConfigdocs(ctx, 'buffer').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'renderedconfigdocs' in url - params = response.get('params') - assert params.get('version') == 'buffer' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_configdocs_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + + responses.add(responses.GET, + 'http://shiptest/configdocs/design?version=buffer', + body=api_resp, + status=404) + response = GetConfigdocs(stubs.StubCliContext(), + collection='design', + version='buffer').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetWorkflows(*args): - response = GetWorkflows(ctx, since=None).invoke_and_return_resp() - url = response.get('url') - assert 'workflows' in url - assert 'since' not in url +GET_RENDEREDCONFIGDOCS_API_RESP = """ +--- +yaml: yaml +--- +yaml2: yaml2 +... +""" - response = GetWorkflows(ctx).invoke_and_return_resp() - url = response.get('url') - assert 'workflows' in url - assert 'since' not in url - since_val = '2017-01-01T12:34:56Z' - response = GetWorkflows(ctx, - since=since_val).invoke_and_return_resp() - url = response.get('url') - assert 'workflows' in url - params = response.get('params') - assert params.get('since') == since_val +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_renderedconfigdocs(*args): + responses.add(responses.GET, + 'http://shiptest/renderedconfigdocs?version=buffer', + body=GET_RENDEREDCONFIGDOCS_API_RESP, + status=200) + response = GetRenderedConfigdocs( + stubs.StubCliContext(), + version='buffer').invoke_and_return_resp() + assert response == GET_RENDEREDCONFIGDOCS_API_RESP + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_renderedconfigdocs_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + + responses.add(responses.GET, + 'http://shiptest/renderedconfigdocs?version=buffer', + body=api_resp, + status=404) + response = GetRenderedConfigdocs(stubs.StubCliContext(), + version='buffer').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +GET_WORKFLOWS_API_RESP = """ +[ + { + "execution_date": "2017-10-09 21:18:56", + "end_date": null, + "workflow_id": "deploy_site__2017-10-09T21:18:56.000000", + "start_date": "2017-10-09 21:18:56.685999", + "external_trigger": true, + "dag_id": "deploy_site", + "state": "failed", + "run_id": "manual__2017-10-09T21:18:56" + }, + { + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:03.361522", + "external_trigger": true, + "dag_id": "deploy_site", + "state": "failed", + "run_id": "manual__2017-10-09T21:19:03" + } +] +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_workflows(*args): + responses.add(responses.GET, + 'http://shiptest/workflows', + body=GET_WORKFLOWS_API_RESP, + status=200) + response = GetWorkflows(stubs.StubCliContext()).invoke_and_return_resp() + assert 'deploy_site__2017-10-09T21:19:03.000000' in response + assert 'deploy_site__2017-10-09T21:18:56.000000' in response + assert 'State' in response + assert 'Workflow' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_workflows_empty(*args): + responses.add(responses.GET, + 'http://shiptest/workflows', + body="[]", + status=200) + response = GetWorkflows(stubs.StubCliContext()).invoke_and_return_resp() + assert 'None' in response + assert 'State' in response diff --git a/shipyard_client/tests/unit/cli/replace_api_client.py b/shipyard_client/tests/unit/cli/replace_api_client.py index 38b32eff..0d72a82c 100644 --- a/shipyard_client/tests/unit/cli/replace_api_client.py +++ b/shipyard_client/tests/unit/cli/replace_api_client.py @@ -15,14 +15,9 @@ # For testing purposes only -class TemporaryContext(object): - def __init__(self): - self.debug = True - self.keystone_Auth = {} - self.token = 'abcdefgh' - self.service_type = 'http://shipyard' - self.shipyard_endpoint = 'http://shipyard/api/v1.0' - self.context_marker = '123456' +def replace_get_endpoint(): + """Replaces the get endpoint call to isolate tests""" + return 'http://shipyard-test' def replace_post_rep(self, url, query_params={}, data={}, content_type=''): @@ -39,11 +34,3 @@ def replace_get_resp(self, url, query_params={}, json=False): :returns: dict with url and parameters """ return {'url': url, 'params': query_params} - - -def replace_base_constructor(self, context): - pass - - -def replace_output_formatting(format, response): - return response diff --git a/shipyard_client/tests/unit/cli/stubs.py b/shipyard_client/tests/unit/cli/stubs.py new file mode 100644 index 00000000..c34ccad3 --- /dev/null +++ b/shipyard_client/tests/unit/cli/stubs.py @@ -0,0 +1,164 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import responses + +from shipyard_client.cli.action import CliAction +from shipyard_client.cli import format_utils + +DEFAULT_AUTH_VARS = { + 'project_domain_name': 'projDomainTest', + 'user_domain_name': 'userDomainTest', + 'project_name': 'projectTest', + 'username': 'usernameTest', + 'password': 'passwordTest', + 'auth_url': 'urlTest' +} + +DEFAULT_API_PARAMS = { + 'auth_vars': DEFAULT_AUTH_VARS, + 'context_marker': '88888888-4444-4444-4444-121212121212', + 'debug': False +} + +DEFAULT_BODY = """ +{ + "message": "Sample status response", + "details": { + "messageList": [ + { + "message": "Message1", + "error": false + }, + { + "message": "Message2", + "error": false + } + ] + }, + "reason": "Testing" +} +""" + +STATUS_TEMPL = """ +{{ + "kind": "Status", + "apiVersion": "v1", + "metadata": {{}}, + "status": "Valid", + "message": "{}", + "reason": "{}", + "details": {{ + "errorCount": {}, + "messageList": {} + }}, + "code": {} +}} +""" + +STATUS_MSG_TEMPL = """ +{{ + "message": "{}-{}", + "error": {} +}} +""" + + +def gen_err_resp(message='Err Message', + sub_message='Submessage', + sub_error_count=1, + sub_info_count=0, + reason='Reason Text', + code=400): + """Generates a fake status/error response for testing purposes""" + sub_messages = [] + for i in range(0, sub_error_count): + sub_messages.append(STATUS_MSG_TEMPL.format(sub_message, i, 'true')) + for i in range(0, sub_info_count): + sub_messages.append(STATUS_MSG_TEMPL.format(sub_message, i, 'false')) + msg_list = '[{}]'.format(','.join(sub_messages)) + resp_str = STATUS_TEMPL.format(message, + reason, + sub_error_count, + msg_list, + code) + return resp_str + + +def gen_api_param(auth_vars=None, + context_marker='88888888-4444-4444-4444-121212121212', + debug=False): + """Generates an object that is useful as input to a StubCliContext""" + if auth_vars is None: + auth_vars = DEFAULT_AUTH_VARS + return { + 'auth_vars': auth_vars, + 'context_marker': context_marker, + 'debug': debug + } + + +class StubCliContext(): + """A stub CLI context that can be configured for tests""" + def __init__(self, + fmt='cli', + api_parameters=None): + if api_parameters is None: + api_parameters = gen_api_param() + self.obj = {} + self.obj['API_PARAMETERS'] = api_parameters + self.obj['FORMAT'] = fmt + + +class StubAction(CliAction): + """A modifiable action that can be used to drive specific behaviors""" + def __init__(self, + ctx, + body=DEFAULT_BODY, + status_code=200, + method='GET'): + super().__init__(ctx) + self.body = body + self.status_code = status_code + self.method = method + + def invoke(self): + """Uses responses package to return a fake response""" + return responses.Response( + method=self.method, + url='http://shiptest/stub', + body=self.body, + status=self.status_code + ) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [400] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + return format_utils.table_factory( + field_names=['Col1', 'Col2'], + rows=[ + ['message', resp_j.get('message')], + ['reason', resp_j.get('reason')] + ] + ) diff --git a/shipyard_client/tests/unit/cli/test_auth_validations.py b/shipyard_client/tests/unit/cli/test_auth_validations.py new file mode 100644 index 00000000..5a489ddc --- /dev/null +++ b/shipyard_client/tests/unit/cli/test_auth_validations.py @@ -0,0 +1,69 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from shipyard_client.cli.action import AuthValuesError +from shipyard_client.tests.unit.cli import stubs + + +def test_validate_auth_vars_valid(): + action = stubs.StubAction(stubs.StubCliContext()) + try: + action.validate_auth_vars() + except AuthValuesError: + # Valid parameters should not raise an AuthValuesError + assert False + + +def test_validate_auth_vars_missing_required(): + auth_vars = { + 'project_domain_name': 'default', + 'user_domain_name': 'default', + 'project_name': 'service', + 'username': 'shipyard', + 'password': 'password', + 'auth_url': None + } + + param = stubs.gen_api_param(auth_vars=auth_vars) + action = stubs.StubAction(stubs.StubCliContext(api_parameters=param)) + with pytest.raises(AuthValuesError): + try: + action.validate_auth_vars() + except AuthValuesError as ex: + assert 'os_auth_url' in ex.diagnostic + assert 'os_username' not in ex.diagnostic + assert 'os_password' not in ex.diagnostic + raise + + +def test_validate_auth_vars_missing_required_and_others(): + auth_vars = { + 'project_domain_name': 'default', + 'user_domain_name': 'default', + 'project_name': 'service', + 'username': None, + 'password': 'password', + 'auth_url': None + } + param = stubs.gen_api_param(auth_vars=auth_vars) + action = stubs.StubAction(stubs.StubCliContext(api_parameters=param)) + with pytest.raises(AuthValuesError): + try: + action.validate_auth_vars() + except AuthValuesError as ex: + assert 'os_auth_url' in ex.diagnostic + assert 'os_username' in ex.diagnostic + assert 'os_password' not in ex.diagnostic + raise diff --git a/shipyard_client/tests/unit/cli/test_format_utils.py b/shipyard_client/tests/unit/cli/test_format_utils.py new file mode 100644 index 00000000..676c572a --- /dev/null +++ b/shipyard_client/tests/unit/cli/test_format_utils.py @@ -0,0 +1,152 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from unittest.mock import MagicMock + +from prettytable.prettytable import DEFAULT + +import shipyard_client.cli.format_utils as format_utils + + +def test_cli_format_error_handler_bogus_json(): + """Tests the generic handler for shipyard error response if passed + unrecognized json + """ + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads('{"key": "value"}')) + output = format_utils.cli_format_error_handler(resp) + assert 'Error: Not specified' in output + assert 'Reason: Not specified' in output + + +def test_cli_format_error_handler_broken_json(): + """Tests the generic handler for shipyard error response if passed + unrecognized json + """ + resp = MagicMock() + resp.json.side_effect = ValueError("") + resp.text = "Not JSON" + output = format_utils.cli_format_error_handler(resp) + assert 'Error: Unable to decode response. Value: Not JSON' in output + + +def test_cli_format_error_handler_no_messages(): + """Tests the generic handler for shipyard error response if passed + json in the right format, but with no messages + """ + resp_val = """ +{ + "apiVersion": "v1.0", + "status": "Failure", + "metadata": {}, + "message": "Unauthenticated", + "code": "401 Unauthorized", + "details": {}, + "kind": "status", + "reason": "Credentials are not established" +} +""" + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads(resp_val)) + output = format_utils.cli_format_error_handler(resp) + print(output) + assert "Error: Unauthenticated" in output + assert "Reason: Credentials are not established" in output + + +def test_cli_format_error_handler_messages(): + """Tests the generic handler for shipyard error response if passed + a response with messages in the detail + """ + resp_val = """ +{ + "apiVersion": "v1.0", + "status": "Failure", + "metadata": {}, + "message": "Unauthenticated", + "code": "401 Unauthorized", + "details": { + "messageList": [ + { "message":"Hello1", "error": false }, + { "message":"Hello2", "error": false }, + { "message":"Hello3", "error": true } + ] + }, + "kind": "status", + "reason": "Credentials are not established" +} +""" + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads(resp_val)) + output = format_utils.cli_format_error_handler(resp) + assert "Error: Unauthenticated" in output + assert "Reason: Credentials are not established" in output + assert "- Error: Hello3" in output + assert "- Info: Hello2" in output + + +def test_cli_format_error_handler_messages_broken(): + """Tests the generic handler for shipyard error response if passed + a response with messages in the detail, but missing error or message + elements + """ + resp_val = """ +{ + "apiVersion": "v1.0", + "status": "Failure", + "metadata": {}, + "message": "Unauthenticated", + "code": "401 Unauthorized", + "details": { + "messageList": [ + { "message":"Hello1", "error": false }, + { "error": true }, + { "message":"Hello3" } + ] + }, + "kind": "status", + "reason": "Credentials are not established" +} +""" + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads(resp_val)) + output = format_utils.cli_format_error_handler(resp) + assert "Error: Unauthenticated" in output + assert "Reason: Credentials are not established" in output + assert "- Error: None" in output + assert "- Info: Hello3" in output + + +def test_table_factory(): + t = format_utils.table_factory() + assert t.get_string() == '' + + +def test_table_factory_fields(): + t = format_utils.table_factory(field_names=['a', 'b', 'c']) + t.add_row(['1', '2', '3']) + assert 'a' in t.get_string() + assert 'b' in t.get_string() + assert 'c' in t.get_string() + + +def test_table_factory_fields_data(): + t = format_utils.table_factory(style=DEFAULT, + field_names=['a', 'b', 'c'], + rows=[['1', '2', '3'], ['4', '5', '6']]) + assert 'a' in t.get_string() + assert 'b' in t.get_string() + assert 'c' in t.get_string() + assert '1' in t.get_string() + assert '6' in t.get_string() diff --git a/shipyard_client/tests/unit/cli/test_input_checks.py b/shipyard_client/tests/unit/cli/test_input_checks.py index 2a716e7b..290ee2fc 100644 --- a/shipyard_client/tests/unit/cli/test_input_checks.py +++ b/shipyard_client/tests/unit/cli/test_input_checks.py @@ -200,62 +200,6 @@ def test_check_action_commands_none(): assert 'call.fail(' in str(ctx.mock_calls[0]) -def test_validate_auth_vars_valid(): - ctx = Mock(side_effect=Exception("failed")) - auth_vars = { - 'project_domain_name': 'default', - 'user_domain_name': 'default', - 'project_name': 'service', - 'username': 'shipyard', - 'password': 'password', - 'auth_url': 'abcdefg' - } - input_checks.validate_auth_vars(ctx, auth_vars) - ctx.fail.assert_not_called() - - -def test_validate_auth_vars_missing_required(): - ctx = Mock(side_effect=Exception("failed")) - auth_vars = { - 'project_domain_name': 'default', - 'user_domain_name': 'default', - 'project_name': 'service', - 'username': 'shipyard', - 'password': 'password', - 'auth_url': None - } - try: - input_checks.validate_auth_vars(ctx, auth_vars) - except Exception: - pass - # py 3.6: ctx.fail.assert_called() - assert 'call.fail(' in str(ctx.mock_calls[0]) - assert 'os_auth_url' in str(ctx.mock_calls[0]) - assert 'os_username' not in str(ctx.mock_calls[0]) - assert 'os_password' not in str(ctx.mock_calls[0]) - - -def test_validate_auth_vars_missing_required_and_others(): - ctx = Mock(side_effect=Exception("failed")) - auth_vars = { - 'project_domain_name': 'default', - 'user_domain_name': 'default', - 'project_name': 'service', - 'username': None, - 'password': 'password', - 'auth_url': None - } - try: - input_checks.validate_auth_vars(ctx, auth_vars) - except Exception: - pass - # py 3.6: ctx.fail.assert_called() - assert 'call.fail(' in str(ctx.mock_calls[0]) - assert 'os_auth_url' in str(ctx.mock_calls[0]) - assert 'os_username' in str(ctx.mock_calls[0]) - assert 'os_password' not in str(ctx.mock_calls[0]) - - def test_check_reformat_parameter_valid(): ctx = Mock(side_effect=Exception("failed")) param = ['this=that'] diff --git a/shipyard_client/tests/unit/cli/test_output_formatting.py b/shipyard_client/tests/unit/cli/test_output_formatting.py deleted file mode 100644 index 8e613df1..00000000 --- a/shipyard_client/tests/unit/cli/test_output_formatting.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import yaml -from mock import patch, ANY -from requests.models import Response - -from shipyard_client.cli.output_formatting import output_formatting - -json_response = Response() -json_response._content = b'{ "key" : "a" }' -json_response.status_code = 200 -json_response.headers['content-type'] = 'application/json' - -yaml_response = Response() -yaml_response._content = b'''Projects: - C/C++ Libraries: - - libyaml # "C" Fast YAML 1.1 - - Syck # (dated) "C" YAML 1.0 - - yaml-cpp # C++ YAML 1.2 implementation - Ruby: - - psych # libyaml wrapper (in Ruby core for 1.9.2) - - RbYaml # YAML 1.1 (PyYAML Port) - - yaml4r # YAML 1.0, standard library syck binding - Python: - - PyYAML # YAML 1.1, pure python and libyaml binding - - ruamel.yaml # YAML 1.2, update of PyYAML with round-tripping of comments - - PySyck # YAML 1.0, syck binding''' - -yaml_response.headers['content-type'] = 'application/yaml' - - -def test_output_formatting(): - """call output formatting and check correct one was given""" - - with patch.object(json, 'dumps') as mock_method: - output_formatting('format', json_response) - mock_method.assert_called_once_with( - json_response.json(), sort_keys=True, indent=4) - - with patch.object(yaml, 'dump_all') as mock_method: - output_formatting('format', yaml_response) - mock_method.assert_called_once_with( - ANY, width=79, indent=4, default_flow_style=False) diff --git a/test-requirements.txt b/test-requirements.txt index 799b505a..f1c8685b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ pytest==3.2.1 pytest-cov==2.5.1 mock==2.0.0 +responses==0.8.1 testfixtures==5.1.1 apache-airflow[crypto,celery,postgres,hive,hdfs,jdbc]==1.8.1