diff --git a/doc/commands.md b/doc/commands.md index e7e4d5f2..13a6b742 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -20,6 +20,7 @@ 18. [flp](commands/flp.md) - Fiori Launchpad 19. [rap](commands/businessservice.md) - RAP Business Services 20. [strust](commands/strust.md) - SSL Certificates -21. [structure](commands/structure.md) - ABAP DDIC structures -22. [table](commands/table.md) - ABAP DDIC transparent tables -23. [badi](commands/badi.md) - New style (Enhancements) BAdI operations +21. [dataelement](commands/dataelement.md) - ABAP DDIC Data Elements +22. [structure](commands/structure.md) - ABAP DDIC structures +23. [table](commands/table.md) - ABAP DDIC transparent tables +24. [badi](commands/badi.md) - New style (Enhancements) BAdI operations \ No newline at end of file diff --git a/doc/commands/dataelement.md b/doc/commands/dataelement.md new file mode 100644 index 00000000..e0228025 --- /dev/null +++ b/doc/commands/dataelement.md @@ -0,0 +1,26 @@ +# Data Element + +- [Data Element](#data-element) + - [define](#define) + +## define + +Define an ABAP DDIC Data Element. + +```bash +sapcli dataelement define DATA_ELEMENT_NAME --type=domain|predefinedAbapType [--corrnr TRANSPORT] [--activate] [--no-error-existing] [--domain_name] [--data_type] [--data_type_length] [--data_type_decimals] [--label_short] [--label_medium] [--label_long] [--label_heading] +``` + +* _DATA\_ELEMENT\_NAME_ specifying the name of the data element +* _--type [domain|predefinedAbapType]_ type kind +* _--domain\_name_ domain name (e.g. BUKRS) [default = ''] - mandatory in case the _--type_=domain **(optional)** +* _--data\_type_ data type (e.g. CHAR) [default = ''] - mandatory in case the _--type_=predefinedAbapType **(optional)** +* _--data\_type\_length_ data type length (e.g. 5) [default = '0'] **(optional)** +* _--data\_type\_decimals_ data type decimals (e.g. 3) [default = '0'] **(optional)** +* _--label\_short_ short label [default = ''] **(optional)** +* _--label\_medium_ medium label [default = ''] **(optional)** +* _--label\_long_ long label [default = ''] **(optional)** +* _--label\_heading_ heading label [default = ''] **(optional)** +* _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** +* _--activate_ activate after finishing the data element modification **(optional)** +* _--no-error-existing_ do not fail if data element already exists **(optional)** diff --git a/sap/adt/__init__.py b/sap/adt/__init__.py index c173f07a..785e63e9 100644 --- a/sap/adt/__init__.py +++ b/sap/adt/__init__.py @@ -14,3 +14,4 @@ from sap.adt.table import Table # noqa: F401 from sap.adt.enhancement_implementation import EnhancementImplementation # noqa: F401 from sap.adt.structure import Structure # noqa: F401 +from sap.adt.dataelement import DataElement # noqa: F401 diff --git a/sap/adt/dataelement.py b/sap/adt/dataelement.py new file mode 100644 index 00000000..042a0896 --- /dev/null +++ b/sap/adt/dataelement.py @@ -0,0 +1,163 @@ +"""ABAP Data Element ADT functionality module""" + +from enum import Enum + +from sap.adt.objects import ADTObject, ADTObjectType, ADTObjectSourceEditor +# pylint: disable=unused-import +from sap.adt.annotations import ( + OrderedClassMembers, + xml_text_node_property, + XmlNodeProperty, +) +from sap.adt.objects import XMLNamespace, XMLNS_ADTCORE + +XMLNS_DTEL = XMLNamespace('dtel', 'http://www.sap.com/adt/dictionary/dataelements') + +LABELS_LENGTH = { + 'short': '10', + 'medium': '20', + 'long': '40', + 'heading': '55' +} + + +class DataElementValidationIssues(Enum): + """"DataElement validation issues""" + + NO_ISSUE = 0 # DataElement is correctly defined + MISSING_DOMAIN_NAME = 1 # DataElement is Domain based by Domain Name was not provided + MISSING_TYPE_NAME = 2 # DataElement is created from a predefined type by it was not provided + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-few-public-methods +class DataElementDefinition(metaclass=OrderedClassMembers): + """ADT Data Element data collector""" + + typ = xml_text_node_property('dtel:typeKind') + typ_name = xml_text_node_property('dtel:typeName') + data_type = xml_text_node_property('dtel:dataType') + data_type_length = xml_text_node_property('dtel:dataTypeLength') + data_type_decimals = xml_text_node_property('dtel:dataTypeDecimals') + label_short = xml_text_node_property('dtel:shortFieldLabel') + label_short_length = xml_text_node_property('dtel:shortFieldLength') + label_short_max_length = xml_text_node_property('dtel:shortFieldMaxLength') + label_medium = xml_text_node_property('dtel:mediumFieldLabel') + label_medium_length = xml_text_node_property('dtel:mediumFieldLength') + label_medium_max_length = xml_text_node_property('dtel:mediumFieldMaxLength') + label_long = xml_text_node_property('dtel:longFieldLabel') + label_long_length = xml_text_node_property('dtel:longFieldLength') + label_long_max_length = xml_text_node_property('dtel:longFieldMaxLength') + label_heading = xml_text_node_property('dtel:headingFieldLabel') + label_heading_length = xml_text_node_property('dtel:headingFieldLength') + label_heading_max_length = xml_text_node_property('dtel:headingFieldMaxLength') + search_help = xml_text_node_property('dtel:searchHelp') + search_help_parameter = xml_text_node_property('dtel:searchHelpParameter') + set_get_parameter = xml_text_node_property('dtel:setGetParameter') + default_component_name = xml_text_node_property('dtel:defaultComponentName') + deactivate_input_history = xml_text_node_property('dtel:deactivateInputHistory') + change_document = xml_text_node_property('dtel:changeDocument') + left_to_right_direction = xml_text_node_property('dtel:leftToRightDirection') + deactivate_bidi_filtering = xml_text_node_property('dtel:deactivateBIDIFiltering') + + +class DataElement(ADTObject): + """ABAP Data Element""" + + OBJTYPE = ADTObjectType( + 'DTEL/DE', + 'ddic/dataelements', + XMLNamespace('blue', 'http://www.sap.com/wbobj/dictionary/dtel', parents=[XMLNS_ADTCORE, XMLNS_DTEL]), + 'application/vnd.sap.adt.dataelements.v2+xml', + { + 'application/vnd.sap.adt.dataelements.v2+xml': '', + 'application/vnd.sap.adt.dataelements.v1+xml': '' + }, + 'wbobj', + editor_factory=ADTObjectSourceEditor + ) + + definition = XmlNodeProperty('dtel:dataElement') + + def __init__(self, connection, name, package=None, metadata=None): + super().__init__(connection, name, metadata, active_status='inactive') + + self._metadata.package_reference.name = package + self.definition = DataElementDefinition() + + def set_type(self, value): + """Setter for Type Kind element""" + + self.definition.typ = value + + def set_type_name(self, value): + """Setter for Type Name element""" + + self.definition.typ_name = value.upper() if value is not None else None + + def set_data_type(self, value): + """Setter for Data Type element""" + + self.definition.data_type = value.upper() if value is not None else None + + def set_data_type_length(self, value): + """Setter for Data Type Length element""" + + self.definition.data_type_length = value + + def set_data_type_decimals(self, value): + """Setter for Data Type Decimals element""" + + self.definition.data_type_decimals = value + + def set_label_short(self, value): + """Setter for Label Short element""" + + self.definition.label_short = value + + def set_label_medium(self, value): + """Setter for Label Medium element""" + + self.definition.label_medium = value + + def set_label_long(self, value): + """Setter for Label Long element""" + + self.definition.label_long = value + + def set_label_heading(self, value): + """Setter for Label Heading element""" + + self.definition.label_heading = value + + def normalize(self): + """Validate Data Element setup before save""" + de = self.definition + + if de.typ == 'domain': + de.data_type = '' + de.data_type_length = '0' + de.data_type_decimals = '0' + if de.typ == 'predefinedAbapType': + de.typ_name = '' + + de.label_short_length = de.label_short_length if not de.label_short_length else LABELS_LENGTH['short'] + de.label_medium_length = de.label_medium_length if not de.label_medium_length else LABELS_LENGTH['medium'] + de.label_long_length = de.label_long_length if not de.label_long_length else LABELS_LENGTH['long'] + de.label_heading_length = de.label_heading_length if not de.label_heading_length else LABELS_LENGTH['heading'] + + de.deactivate_input_history = de.deactivate_input_history if de.deactivate_input_history is not None else False + de.change_document = de.change_document if de.change_document is not None else False + de.left_to_right_direction = de.left_to_right_direction if de.left_to_right_direction is not None else False + # pylint: disable=line-too-long + de.deactivate_bidi_filtering = de.deactivate_bidi_filtering if de.deactivate_bidi_filtering is not None else False + + def validate(self): + """Validate Data Element setup""" + + if self.definition.typ == 'domain' and not self.definition.typ_name: + return DataElementValidationIssues.MISSING_DOMAIN_NAME + if self.definition.typ == 'predefinedAbapType' and not self.definition.data_type: + return DataElementValidationIssues.MISSING_TYPE_NAME + + return DataElementValidationIssues.NO_ISSUE diff --git a/sap/adt/objects.py b/sap/adt/objects.py index a192a046..e7701530 100644 --- a/sap/adt/objects.py +++ b/sap/adt/objects.py @@ -233,7 +233,9 @@ def get_uri_for_type(self, mimetype): """ try: - return '/' + self._typeuris[mimetype] + mimetype_uri = self._typeuris[mimetype] + + return '/' + mimetype_uri if mimetype_uri != '' else '' except KeyError: # pylint: disable=raise-missing-from raise SAPCliError('Object {type} does not support plain \'text\' format') diff --git a/sap/cli/__init__.py b/sap/cli/__init__.py index 09d71495..62c8a3ef 100644 --- a/sap/cli/__init__.py +++ b/sap/cli/__init__.py @@ -47,6 +47,7 @@ def commands(): import sap.cli.table import sap.cli.badi import sap.cli.structure + import sap.cli.dataelement if CommandsCache.adt is None: CommandsCache.adt = [ @@ -69,6 +70,7 @@ def commands(): (adt_connection_from_args, sap.cli.rap.CommandGroup()), (adt_connection_from_args, sap.cli.table.CommandGroup()), (adt_connection_from_args, sap.cli.structure.CommandGroup()), + (adt_connection_from_args, sap.cli.dataelement.CommandGroup()), (adt_connection_from_args, sap.cli.checkin.CommandGroup()), (adt_connection_from_args, sap.cli.badi.CommandGroup()), ] diff --git a/sap/cli/dataelement.py b/sap/cli/dataelement.py new file mode 100644 index 00000000..7a4d4841 --- /dev/null +++ b/sap/cli/dataelement.py @@ -0,0 +1,131 @@ +"""ADT proxy for ABAP DDIC Data Element""" + +from sap.errors import SAPCliError +import sap.adt +from sap.adt.dataelement import DataElementValidationIssues +from sap.adt.errors import ExceptionResourceAlreadyExists +import sap.cli.object +import sap.cli.wb +import sap.cli.core + + +class CommandGroup(sap.cli.object.CommandGroupObjectMaster): + """Adapter converting command line parameters to sap.adt.DataElement methods + calls. + """ + + def __init__(self): + super().__init__('dataelement') + + self.define() + + def instance(self, connection, name, args, metadata=None): + package = None + if hasattr(args, 'package'): + package = args.package + + return sap.adt.DataElement(connection, name.upper(), package=package, metadata=metadata) + + +@CommandGroup.argument_corrnr() +@CommandGroup.argument('--no-error-existing', action='store_true', default=False, + help='Do not fail if data element already exists') +@CommandGroup.argument('-a', '--activate', action='store_true', default=False, help='Activate after modification') +@CommandGroup.argument('-t', '--type', required=True, choices=['domain', 'predefinedAbapType'], + type=str, help='Type kind') +@CommandGroup.argument('-d', '--domain_name', default=None, type=str, help='Domain name') +@CommandGroup.argument('-dt', '--data_type', default=None, type=str, help='Data type') +@CommandGroup.argument('-dtl', '--data_type_length', default='0', type=str, help='Data type length') +@CommandGroup.argument('-dtd', '--data_type_decimals', default='0', type=str, help='Data type decimals') +@CommandGroup.argument('-ls', '--label_short', default='', type=str, help='Short label') +@CommandGroup.argument('-lm', '--label_medium', default='', type=str, help='Medium label') +@CommandGroup.argument('-ll', '--label_long', default='', type=str, help='Long label') +@CommandGroup.argument('-lh', '--label_heading', default='', type=str, help='Heading label') +@CommandGroup.argument('package', help='Package assignment') +@CommandGroup.argument('description', help='Data element description') +@CommandGroup.argument('name', help='Data element name') +@CommandGroup.command() +# pylint: disable=too-many-branches +def define(connection, args): + """Changes attributes of the given Data Element""" + + console = sap.cli.core.get_console() + + metadata = sap.adt.ADTCoreData(language='EN', master_language='EN', responsible=connection.user, + description=args.description) + + dataelement = sap.adt.DataElement(connection, args.name.upper(), args.package, metadata=metadata) + + # Create Data Element + console.printout(f'Creating data element {args.name}') + try: + dataelement.create(args.corrnr) + except ExceptionResourceAlreadyExists as error: + # Date Element already exists + console.printout(f'Data element {args.name} already exists') + if not args.no_error_existing: + raise error + + # Fetch data element's content + dataelement.fetch() + + if hasattr(args, 'type'): + dataelement.set_type(args.type) + + if hasattr(args, 'domain_name'): + dataelement.set_type_name(args.domain_name) + + if hasattr(args, 'data_type'): + dataelement.set_data_type(args.data_type) + + if hasattr(args, 'data_type_length'): + dataelement.set_data_type_length(args.data_type_length) + + if hasattr(args, 'data_type_decimals'): + dataelement.set_data_type_decimals(args.data_type_decimals) + + if hasattr(args, 'label_short'): + dataelement.set_label_short(args.label_short) + + if hasattr(args, 'label_medium'): + dataelement.set_label_medium(args.label_medium) + + if hasattr(args, 'label_long'): + dataelement.set_label_long(args.label_long) + + if hasattr(args, 'label_heading'): + dataelement.set_label_heading(args.label_heading) + + dataelement.normalize() + + validation_issue_key = dataelement.validate() + + match validation_issue_key: + case DataElementValidationIssues.MISSING_DOMAIN_NAME: + console.printerr( + 'Domain name must be provided (--domain_name) if the type (--type) is "domain"') + + case DataElementValidationIssues.MISSING_TYPE_NAME: + console.printerr( + 'Data type name must be provided (--data_type) if the type (--type) is "predefinedAbapType"') + + case DataElementValidationIssues.NO_ISSUE: + pass + + case _: + raise SAPCliError( + f'BUG: please report a forgotten case DataElementValidationIssues({validation_issue_key})') + + if validation_issue_key != DataElementValidationIssues.NO_ISSUE: + return 1 + + # Push Data Element changes + console.printout(f'Data element {args.name} setup performed') + with dataelement.open_editor(corrnr=args.corrnr) as editor: + editor.push() + + # Activate Data Element + if args.activate: + console.printout(f'Data element {args.name} activation performed') + activator = sap.cli.wb.ObjectActivationWorker() + sap.cli.object.activate_object_list(activator, ((args.name, dataelement),), count=1) diff --git a/test/unit/fixtures_adt_dataelement.py b/test/unit/fixtures_adt_dataelement.py new file mode 100644 index 00000000..ca92cf34 --- /dev/null +++ b/test/unit/fixtures_adt_dataelement.py @@ -0,0 +1,187 @@ +DATA_ELEMENT_NAME = 'TEST_DATA_ELEMENT' + +DATA_ELEMENT_DEFINITION_ADT_XML = f''' + + + + + + + + + predefinedAbapType + + STRING + 000000 + 000000 + + 10 + 10 + + 20 + 20 + + 40 + 40 + + 55 + 55 + + + + + false + false + false + false + +''' + +CREATE_DATA_ELEMENT_ADT_XML = f''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + +READ_DATA_ELEMENT_BODY = DATA_ELEMENT_DEFINITION_ADT_XML + +WRITE_DATA_ELEMENT_BODY = f''' + + + + + + + + + + predefinedAbapType + + STRING + 6 + 0 + Req. Add. + 10 + 10 + Request Address + 15 + 20 + Request Address + 20 + 40 + Request Address + 55 + 55 + + + + + false + false + false + false + +''' + +DEFINE_DATA_ELEMENT_W_DOMAIN_BODY = f''' + + + +domain +ABC + +0 +0 +Tst DTEL +10 +10 +Test Label Medium +20 +20 +Test Label Long +40 +40 +Test Label Heading +55 +55 + + + + +false +false +false +false + +''' + +DEFINE_DATA_ELEMENT_W_PREDEFINED_ABAP_TYPE_BODY = f''' + + + +predefinedAbapType + +STRING +200 +0 +Tst DTEL +10 +10 +Test Label Medium +20 +20 +Test Label Long +40 +40 +Test Label Heading +55 +55 + + + + +false +false +false +false + +''' + +FAKE_LOCK_HANDLE = 'lock_handle' + +ACTIVATE_DATA_ELEMENT_BODY = f''' + + +''' + +ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS=f'''Resource Data Element {DATA_ELEMENT_NAME} does already exist.Resource Package $SAPCLI_TEST_ROOT does already exist.''' diff --git a/test/unit/test_sap_adt_dataelement.py b/test/unit/test_sap_adt_dataelement.py new file mode 100644 index 00000000..0d1a9394 --- /dev/null +++ b/test/unit/test_sap_adt_dataelement.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import unittest +import sap.adt +import sap.adt.dataelement + +from mock import Connection, Response, Request +from fixtures_adt_dataelement import DATA_ELEMENT_DEFINITION_ADT_XML, DATA_ELEMENT_NAME, CREATE_DATA_ELEMENT_ADT_XML + + +class TestADTDataElement(unittest.TestCase): + + def test_data_element_fetch(self): + connection = Connection([Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={})]) + + data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME) + data_element.fetch() + + self.assertEqual(data_element.name, DATA_ELEMENT_NAME) + self.assertEqual(data_element.active, 'active') + self.assertEqual(data_element.master_language, 'EN') + self.assertEqual(data_element.description, 'Test data element') + + def test_data_element_serialize(self): + connection = Connection() + + metadata = sap.adt.ADTCoreData(description='Test data element', language='EN', master_language='EN', + responsible='ANZEIGER') + data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME, package='PACKAGE', metadata=metadata) + data_element.create() + + expected_request = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.maxDiff = None + expected_request.assertEqual(connection.execs[0], self) diff --git a/test/unit/test_sap_cli_dataelement.py b/test/unit/test_sap_cli_dataelement.py new file mode 100644 index 00000000..2198455c --- /dev/null +++ b/test/unit/test_sap_cli_dataelement.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python3 + +import unittest +from unittest.mock import call, patch, Mock +from sap.adt.errors import ExceptionResourceAlreadyExists + +import sap.cli.dataelement +from sap.errors import SAPCliError + +from mock import ( + Connection, + Response, + Request, + patch, +) +from io import StringIO + +from infra import generate_parse_args +from fixtures_adt_dataelement import ( + DATA_ELEMENT_NAME, + CREATE_DATA_ELEMENT_ADT_XML, + READ_DATA_ELEMENT_BODY, + FAKE_LOCK_HANDLE, + ACTIVATE_DATA_ELEMENT_BODY, + DATA_ELEMENT_DEFINITION_ADT_XML, + DEFINE_DATA_ELEMENT_W_DOMAIN_BODY, + DEFINE_DATA_ELEMENT_W_PREDEFINED_ABAP_TYPE_BODY, + ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS +) +from fixtures_adt_wb import RESPONSE_ACTIVATION_OK + +parse_args = generate_parse_args(sap.cli.dataelement.CommandGroup()) + + +class TestDataElementCreate(unittest.TestCase): + + def data_element_create_cmd(self, *args, **kwargs): + return parse_args('create', *args, **kwargs) + + def test_create(self): + connection = Connection() + + the_cmd = self.data_element_create_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package') + the_cmd.execute(connection, the_cmd) + + expected_request = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + expected_request.assertEqual(connection.execs[0], self) + + +class TestDataElementActivate(unittest.TestCase): + + def data_element_activate_cmd(self, *args, **kwargs): + return parse_args('activate', *args, **kwargs) + + def test_activate(self): + connection = Connection([ + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ]) + + the_cmd = self.data_element_activate_cmd(DATA_ELEMENT_NAME) + the_cmd.execute(connection, the_cmd) + + expected_request = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[0], expected_request) + + +class TestDataElementDefine(unittest.TestCase): + + def data_element_define_cmd(self, *args, **kwargs): + return parse_args('define', *args, **kwargs) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_w_domain(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_called_once() + fake_unlock.assert_called_once() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 5) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + expected_request_push = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='PUT', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(DEFINE_DATA_ELEMENT_W_DOMAIN_BODY, 'utf-8'), + params={'lockHandle': FAKE_LOCK_HANDLE} + ) + + expected_request_push.assertEqual(connection.execs[2], self) + + expected_request_activate = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[3], expected_request_activate) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[4], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_w_predefined_abap_type(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=predefinedAbapType', '--data_type=STRING', '--data_type_length=200', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_called_once() + fake_unlock.assert_called_once() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 5) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + expected_request_push = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='PUT', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(DEFINE_DATA_ELEMENT_W_PREDEFINED_ABAP_TYPE_BODY, 'utf-8'), + params={'lockHandle': FAKE_LOCK_HANDLE} + ) + + self.assertEqual(connection.execs[2], expected_request_push) + + expected_request_activate = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[3], expected_request_activate) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[4], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_domain_not_provided(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + # --domain_name argument is missing + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=domain', '--data_type=STRING', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_not_called() + fake_unlock.assert_not_called() + fake_console_print_err.assert_called_once_with('Domain name must be provided (--domain_name) if the type (--type) is "domain"') + + self.assertEqual(len(connection.execs), 2) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_predefined_abap_type_not_provided(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + # --data_type argument is missing + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=predefinedAbapType', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_not_called() + fake_unlock.assert_not_called() + fake_console_print_err.assert_called_once_with('Data type name must be provided (--data_type) if the type (--type) is "predefinedAbapType"') + + self.assertEqual(len(connection.execs), 2) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_data_element_already_exists(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text=ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS, + status_code=500, + headers={'content-type': 'application/xml'} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + try: + the_cmd.execute(connection, the_cmd) + + self.fail('Exception should be raised but it has not been') + except ExceptionResourceAlreadyExists as e: + self.assertEqual('Resource Data Element TEST_DATA_ELEMENT does already exist.', str(e)) + + fake_lock.assert_not_called() + fake_unlock.assert_not_called() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 1) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_data_element_already_exists_but_skipped(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text=ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS, + status_code=500, + headers={'content-type': 'application/xml'} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--no-error-existing', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_called_once() + fake_unlock.assert_called_once() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 5) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + expected_request_push = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='PUT', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(DEFINE_DATA_ELEMENT_W_DOMAIN_BODY, 'utf-8'), + params={'lockHandle': FAKE_LOCK_HANDLE} + ) + + self.assertEqual(connection.execs[2], expected_request_push) + + expected_request_activate = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[3], expected_request_activate) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[4], expected_request_fetch) + + @patch('sap.adt.DataElement') + def test_define_element_forgotten_issue(self, mock_data_element): + mock_data_element.return_value.validate.return_value = 9999 + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--no-error-existing', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + + with self.assertRaises(SAPCliError) as caught: + the_cmd.execute(Mock(), the_cmd) + + self.assertEqual( + 'BUG: please report a forgotten case DataElementValidationIssues(9999)', + str(caught.exception))