Path: blob/develop/tests/unit/customizations/history/test_show.py
1569 views
# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12import os13import argparse14import xml.dom.minidom1516from botocore.session import Session1718from awscli.compat import ensure_text_type19from awscli.compat import BytesIO20from awscli.utils import OutputStreamFactory21from awscli.testutils import unittest, mock, FileCreator22from awscli.customizations.history.show import ShowCommand23from awscli.customizations.history.show import Formatter24from awscli.customizations.history.show import DetailedFormatter25from awscli.customizations.history.db import DatabaseRecordReader262728class FakeError(Exception):29pass303132class RecordingFormatter(Formatter):33def __init__(self, output=None, include=None, exclude=None):34super(RecordingFormatter, self).__init__(35output=output, include=include, exclude=exclude)36self.history = []3738def _display(self, event_record):39self.history.append(event_record)404142class TestFormatter(unittest.TestCase):43def test_display_no_filters(self):44formatter = RecordingFormatter()45event_record = {'event_type': 'my_event'}46other_event_record = {'event_type': 'my_other_event'}47formatter.display(event_record)48formatter.display(other_event_record)49self.assertEqual(50formatter.history,51[52event_record,53other_event_record54]55)5657def test_include_specified_event_type(self):58formatter = RecordingFormatter(include=['my_event'])59event_record = {'event_type': 'my_event'}60formatter.display(event_record)61self.assertEqual(formatter.history, [event_record])6263def test_include_on_unspecified_event_type(self):64formatter = RecordingFormatter(include=['my_event'])65other_event_record = {'event_type': 'my_other_event'}66formatter.display(other_event_record)67self.assertEqual(formatter.history, [])6869def test_exclude_on_specified_event_type(self):70formatter = RecordingFormatter(exclude=['my_event'])71event_record = {'event_type': 'my_event'}72formatter.display(event_record)73self.assertEqual(formatter.history, [])7475def test_exclude_on_unspecified_event_type(self):76formatter = RecordingFormatter(exclude=['my_event'])77other_event_record = {'event_type': 'my_other_event'}78formatter.display(other_event_record)79self.assertEqual(80formatter.history, [other_event_record])8182def test_raises_error_when_both_include_and_exclude(self):83with self.assertRaises(ValueError):84Formatter(include=['one_event'], exclude=['other_event'])858687class TestDetailedFormatter(unittest.TestCase):88def setUp(self):89self.output = BytesIO()90self.formatter = DetailedFormatter(self.output, colorize=False)9192def get_pretty_xml(self, xml_str):93xml_dom = xml.dom.minidom.parseString(xml_str)94return xml_dom.toprettyxml(indent=' '*4, newl='\n')9596def assert_output(self, for_event, contains):97self.formatter.display(for_event)98collected_output = ensure_text_type(self.output.getvalue())99for line in contains:100self.assertIn(line, collected_output)101102def test_display_cli_version(self):103self.assert_output(104for_event={105'event_type': 'CLI_VERSION',106'id': 'my-id',107'payload': 'aws-cli/1.11.188',108'timestamp': 86400000,109'request_id': None110},111contains=[112'AWS CLI command entered',113'with AWS CLI version: aws-cli/1.11.188'114]115)116117def test_can_use_color(self):118self.formatter = DetailedFormatter(self.output, colorize=True)119self.assert_output(120for_event={121'event_type': 'CLI_VERSION',122'id': 'my-id',123'payload': 'aws-cli/1.11.188',124'timestamp': 86400000,125'request_id': None126},127contains=[128'\x1b[1mAWS CLI command entered',129'\x1b[36mwith AWS CLI version:'130]131)132133def test_display_cli_arguments(self):134self.assert_output(135for_event={136'event_type': 'CLI_ARGUMENTS',137'id': 'my-id',138'payload': ['ec2', 'describe-regions'],139'timestamp': 86400000,140'request_id': None141},142contains=[143"with arguments: ['ec2', 'describe-regions']"144]145)146147def test_display_api_call(self):148self.assert_output(149for_event={150'event_type': 'API_CALL',151'id': 'my-id',152'request_id': 'some-id',153'payload': {154'service': 'ec2',155'operation': 'DescribeRegions',156'params': {}157},158'timestamp': 86400000,159},160contains=[161'to service: ec2\n',162'using operation: DescribeRegions\n',163'with parameters: {}\n'164]165)166167def test_two_different_api_calls_have_different_numbers(self):168event = {169'event_type': 'API_CALL',170'id': 'my-id',171'request_id': 'some-id',172'payload': {173'service': 'ec2',174'operation': 'DescribeRegions',175'params': {}176},177'timestamp': 86400000,178}179self.formatter.display(event)180collected_output = ensure_text_type(self.output.getvalue())181self.assertIn('[0] API call made', collected_output)182183other_event = {184'event_type': 'API_CALL',185'id': 'my-id',186'request_id': 'other-id',187'payload': {188'service': 'ec2',189'operation': 'DescribeRegions',190'params': {}191},192'timestamp': 86400000,193}194self.formatter.display(other_event)195new_output = ensure_text_type(self.output.getvalue())[196len(collected_output):]197self.assertIn('[1] API call made', new_output)198199def test_display_http_request(self):200self.assert_output(201for_event={202'event_type': 'HTTP_REQUEST',203'id': 'my-id',204'request_id': 'some-id',205'payload': {206'method': 'GET',207'url': 'https://myservice.us-west-2.amazonaws.com',208'headers': {},209'body': 'This is my body'210},211'timestamp': 86400000,212},213contains=[214'to URL: https://myservice.us-west-2.amazonaws.com\n',215'with method: GET\n',216'with headers: {}\n',217'with body: This is my body\n'218]219)220221def test_display_http_request_filter_signature(self):222self.assert_output(223for_event={224'event_type': 'HTTP_REQUEST',225'id': 'my-id',226'request_id': 'some-id',227'payload': {228'method': 'GET',229'url': 'https://myservice.us-west-2.amazonaws.com',230'headers': {231'Authorization': (232'Signature=d7fa4de082b598a0ac08b756db438c630a6'233'cc79c4f3d1636cf69fac0e7c1abcd'234)235},236'body': 'This is my body'237},238'timestamp': 86400000,239},240contains=[241'"Authorization": "Signature=d7fa..."'242]243)244245def test_display_http_request_with_streaming_body(self):246self.assert_output(247for_event={248'event_type': 'HTTP_REQUEST',249'id': 'my-id',250'request_id': 'some-id',251'payload': {252'method': 'GET',253'url': 'https://myservice.us-west-2.amazonaws.com',254'headers': {},255'body': 'This should not be printed out',256'streaming': True257},258'timestamp': 86400000,259},260contains=[261'with body: The body is a stream and will not be displayed',262]263)264265def test_display_http_request_with_no_payload(self):266self.assert_output(267for_event={268'event_type': 'HTTP_REQUEST',269'id': 'my-id',270'request_id': 'some-id',271'payload': {272'method': 'GET',273'url': 'https://myservice.us-west-2.amazonaws.com',274'headers': {},275'body': None276},277'timestamp': 86400000,278},279contains=[280'with body: There is no associated body'281]282)283284def test_display_http_request_with_empty_string_payload(self):285self.assert_output(286for_event={287'event_type': 'HTTP_REQUEST',288'id': 'my-id',289'request_id': 'some-id',290'payload': {291'method': 'GET',292'url': 'https://myservice.us-west-2.amazonaws.com',293'headers': {},294'body': ''295},296'timestamp': 86400000,297},298contains=[299'with body: There is no associated body'300]301)302303def test_display_http_request_with_xml_payload(self):304xml_body = '<?xml version="1.0" ?><foo><bar>text</bar></foo>'305self.assert_output(306for_event={307'event_type': 'HTTP_REQUEST',308'id': 'my-id',309'request_id': 'some-id',310'payload': {311'method': 'GET',312'url': 'https://myservice.us-west-2.amazonaws.com',313'headers': {},314'body': xml_body315},316'timestamp': 86400000,317},318contains=[319'with body: ' + self.get_pretty_xml(xml_body)320]321)322323def test_display_http_request_with_xml_payload_and_whitespace(self):324xml_body = '<?xml version="1.0" ?><foo><bar>text</bar></foo>'325self.assert_output(326for_event={327'event_type': 'HTTP_REQUEST',328'id': 'my-id',329'request_id': 'some-id',330'payload': {331'method': 'GET',332'url': 'https://myservice.us-west-2.amazonaws.com',333'headers': {},334'body': self.get_pretty_xml(xml_body)335},336'timestamp': 86400000,337},338# The XML should not be prettified more than once if the body339# of the request was already prettied.340contains=[341'with body: ' + self.get_pretty_xml(xml_body)342]343)344345def test_display_http_request_with_json_struct_payload(self):346self.assert_output(347for_event={348'event_type': 'HTTP_REQUEST',349'id': 'my-id',350'request_id': 'some-id',351'payload': {352'method': 'GET',353'url': 'https://myservice.us-west-2.amazonaws.com',354'headers': {},355'body': '{"foo": "bar"}'356},357'timestamp': 86400000,358},359contains=[360'with body: {\n'361' "foo": "bar"\n'362'}'363]364)365366def test_shares_api_number_across_events_of_same_api_call(self):367self.assert_output(368for_event={369'event_type': 'API_CALL',370'id': 'my-id',371'request_id': 'some-id',372'payload': {373'service': 'ec2',374'operation': 'DescribeRegions',375'params': {}376},377'timestamp': 86400000,378},379contains=[380'[0] API call made'381]382)383self.assert_output(384for_event={385'event_type': 'HTTP_REQUEST',386'id': 'my-id',387'request_id': 'some-id',388'payload': {389'method': 'GET',390'url': 'https://myservice.us-west-2.amazonaws.com',391'headers': {},392'body': 'This is my body'393},394'timestamp': 86400000,395},396contains=[397'[0] HTTP request sent'398]399)400401def test_display_http_response(self):402self.assert_output(403for_event={404'event_type': 'HTTP_RESPONSE',405'id': 'my-id',406'request_id': 'some-id',407'payload': {408'status_code': 200,409'headers': {},410'body': 'This is my body'411},412'timestamp': 86400000,413},414contains=[415'[0] HTTP response received',416'with status code: 200\n',417'with headers: {}\n',418'with body: This is my body\n'419420]421)422423def test_display_http_response_with_streaming_body(self):424self.assert_output(425for_event={426'event_type': 'HTTP_RESPONSE',427'id': 'my-id',428'request_id': 'some-id',429'payload': {430'status_code': 200,431'headers': {},432'body': 'This should not be printed out',433'streaming': True434},435'timestamp': 86400000,436},437contains=[438'with body: The body is a stream and will not be displayed'439]440)441442def test_display_http_response_with_no_payload(self):443self.assert_output(444for_event={445'event_type': 'HTTP_RESPONSE',446'id': 'my-id',447'request_id': 'some-id',448'payload': {449'status_code': 200,450'headers': {},451'body': None452},453'timestamp': 86400000,454},455contains=[456'with body: There is no associated body'457]458)459460def test_display_http_response_with_empty_string_payload(self):461self.assert_output(462for_event={463'event_type': 'HTTP_RESPONSE',464'id': 'my-id',465'request_id': 'some-id',466'payload': {467'status_code': 200,468'headers': {},469'body': ''470},471'timestamp': 86400000,472},473contains=[474'with body: There is no associated body'475]476)477478def test_display_http_response_with_xml_payload(self):479xml_body = '<?xml version="1.0" ?><foo><bar>text</bar></foo>'480self.assert_output(481for_event={482'event_type': 'HTTP_RESPONSE',483'id': 'my-id',484'request_id': 'some-id',485'payload': {486'status_code': 200,487'headers': {},488'body': xml_body489},490'timestamp': 86400000,491},492contains=[493'with body: ' + self.get_pretty_xml(xml_body)494]495)496497def test_display_http_response_with_xml_payload_and_whitespace(self):498xml_body = '<?xml version="1.0" ?><foo><bar>text</bar></foo>'499self.assert_output(500for_event={501'event_type': 'HTTP_RESPONSE',502'id': 'my-id',503'request_id': 'some-id',504'payload': {505'status_code': 200,506'headers': {},507'body': self.get_pretty_xml(xml_body)508},509'timestamp': 86400000,510},511# The XML should not be prettified more than once if the body512# of the response was already prettied.513contains=[514'with body: ' + self.get_pretty_xml(xml_body)515]516)517518def test_display_http_response_with_json_struct_payload(self):519self.assert_output(520for_event={521'event_type': 'HTTP_RESPONSE',522'id': 'my-id',523'request_id': 'some-id',524'payload': {525'status_code': 200,526'headers': {},527'body': '{"foo": "bar"}'528},529'timestamp': 86400000,530},531contains=[532'with body: {\n',533' "foo": "bar"\n',534'}',535]536)537538def test_display_parsed_response(self):539self.assert_output(540for_event={541'event_type': 'PARSED_RESPONSE',542'id': 'my-id',543'request_id': 'some-id',544'payload': {},545'timestamp': 86400000,546},547contains=[548'[0] HTTP response parsed',549'parsed to: {}'550]551)552553def test_display_cli_rc(self):554self.assert_output(555for_event={556'event_type': 'CLI_RC',557'id': 'my-id',558'payload': 0,559'timestamp': 86400000,560'request_id': None561},562contains=[563'AWS CLI command exited',564'with return code: 0'565]566)567568def test_display_unknown_type(self):569event = {570'event_type': 'UNKNOWN',571'id': 'my-id',572'payload': 'foo',573'timestamp': 86400000,574'request_id': None575}576self.formatter.display(event)577collected_output = ensure_text_type(self.output.getvalue())578self.assertEqual('', collected_output)579580581class TestShowCommand(unittest.TestCase):582def setUp(self):583self.session = mock.Mock(Session)584585self.output_stream_factory = mock.Mock(OutputStreamFactory)586587# MagicMock is needed because it can handle context managers.588# Normal Mock will throw AttributeErrors589output_stream_context = mock.MagicMock()590self.output_stream = mock.Mock()591output_stream_context.__enter__.return_value = self.output_stream592593self.output_stream_factory.get_pager_stream.return_value = \594output_stream_context595596self.output_stream_factory.get_stdout_stream.return_value = \597output_stream_context598599self.db_reader = mock.Mock(DatabaseRecordReader)600self.db_reader.iter_latest_records.return_value = []601self.db_reader.iter_records.return_value = []602603self.show_cmd = ShowCommand(604self.session, self.db_reader, self.output_stream_factory)605606self.formatter = mock.Mock(Formatter)607self.add_formatter('mock', self.formatter)608609self.parsed_args = argparse.Namespace()610self.parsed_args.format = 'mock'611self.parsed_args.include = None612self.parsed_args.exclude = None613614self.parsed_globals = argparse.Namespace()615self.parsed_globals.color = 'auto'616617self.files = FileCreator()618619def tearDown(self):620self.files.remove_all()621622def test_does_close_connection(self):623self.parsed_args.command_id = 'latest'624self.show_cmd._run_main(self.parsed_args, self.parsed_globals)625self.assertTrue(self.db_reader.close.called)626627def test_does_close_connection_with_error(self):628self.parsed_args.command_id = 'latest'629self.db_reader.iter_latest_records.side_effect = FakeError('ERROR')630with self.assertRaises(FakeError):631self.show_cmd._run_main(self.parsed_args, self.parsed_globals)632self.assertTrue(self.db_reader.close.called)633634def test_detects_if_history_exists(self):635self.show_cmd = ShowCommand(self.session)636self.parsed_args.command_id = 'latest'637638db_filename = os.path.join(self.files.rootdir, 'name.db')639with mock.patch('os.environ', {'AWS_CLI_HISTORY_FILE': db_filename}):640with self.assertRaisesRegex(641RuntimeError, 'Could not locate history'):642self.show_cmd._run_main(self.parsed_args, self.parsed_globals)643644def add_formatter(self, formatter_name, formatter):645# We do not want to be adding to the dictionary directly because646# the dictionary is scoped to the class as well so even a formatter647# to an instance of the class will add it to the class as well.648formatters = self.show_cmd.FORMATTERS.copy()649formatters[formatter_name] = formatter650self.show_cmd.FORMATTERS = formatters651652def test_show_latest(self):653self.parsed_args.command_id = 'latest'654self.show_cmd._run_main(self.parsed_args, self.parsed_globals)655self.assertTrue(self.db_reader.iter_latest_records.called)656657def test_show_specific_id(self):658self.parsed_args.command_id = 'some-specific-id'659self.show_cmd._run_main(self.parsed_args, self.parsed_globals)660self.assertEqual(661self.db_reader.iter_records.call_args,662mock.call('some-specific-id')663)664665def test_uses_format(self):666formatter = mock.Mock(Formatter)667self.add_formatter('myformatter', formatter)668669return_record = {'id': 'myid', 'event_type': 'CLI_RC', 'payload': 0}670self.db_reader.iter_latest_records.return_value = [return_record]671672self.parsed_args.format = 'myformatter'673self.parsed_args.command_id = 'latest'674self.show_cmd._run_main(self.parsed_args, self.parsed_globals)675676self.assertTrue(formatter.called)677self.assertEqual(678formatter.return_value.display.call_args_list,679[mock.call(return_record)]680)681682def test_uses_include(self):683self.parsed_args.command_id = 'latest'684self.parsed_args.include = ['API_CALL']685self.parsed_args.exclude = None686self.show_cmd._run_main(self.parsed_args, self.parsed_globals)687688self.assertEqual(689self.formatter.call_args,690mock.call(691include=['API_CALL'], exclude=None,692output=self.output_stream)693)694695def test_uses_exclude(self):696self.parsed_args.command_id = 'latest'697self.parsed_args.include = None698self.parsed_args.exclude = ['CLI_RC']699self.show_cmd._run_main(self.parsed_args, self.parsed_globals)700701self.assertEqual(702self.formatter.call_args,703mock.call(704include=None, exclude=['CLI_RC'],705output=self.output_stream)706)707708def test_raises_error_when_both_include_and_exclude(self):709self.parsed_args.include = ['API_CALL']710self.parsed_args.exclude = ['CLI_RC']711with self.assertRaises(ValueError):712self.show_cmd._run_main(self.parsed_args, self.parsed_globals)713714@mock.patch('awscli.customizations.history.commands.is_windows', False)715@mock.patch('awscli.customizations.history.commands.is_a_tty')716def test_detailed_formatter_is_a_tty(self, mock_is_a_tty):717mock_is_a_tty.return_value = True718self.formatter = mock.Mock(DetailedFormatter)719self.add_formatter('detailed', self.formatter)720self.parsed_args.format = 'detailed'721self.parsed_args.command_id = 'latest'722723self.show_cmd._run_main(self.parsed_args, self.parsed_globals)724call = self.output_stream_factory.get_pager_stream.call_args725self.assertEqual(726self.formatter.call_args,727mock.call(728include=None, exclude=None,729output=self.output_stream, colorize=True730)731)732733@mock.patch('awscli.customizations.history.commands.is_windows', False)734@mock.patch('awscli.customizations.history.commands.is_a_tty')735def test_detailed_formatter_not_a_tty(self, mock_is_a_tty):736mock_is_a_tty.return_value = False737self.formatter = mock.Mock(DetailedFormatter)738self.add_formatter('detailed', self.formatter)739self.parsed_args.format = 'detailed'740self.parsed_args.command_id = 'latest'741742self.show_cmd._run_main(self.parsed_args, self.parsed_globals)743self.assertTrue(744self.output_stream_factory.get_stdout_stream.called)745self.assertEqual(746self.formatter.call_args,747mock.call(748include=None, exclude=None,749output=self.output_stream, colorize=False750)751)752753@mock.patch('awscli.customizations.history.commands.is_windows', True)754def test_detailed_formatter_no_color_for_windows(self):755self.formatter = mock.Mock(DetailedFormatter)756self.add_formatter('detailed', self.formatter)757self.parsed_args.format = 'detailed'758self.parsed_args.command_id = 'latest'759760self.show_cmd._run_main(self.parsed_args, self.parsed_globals)761self.assertEqual(762self.formatter.call_args,763mock.call(764include=None, exclude=None,765output=self.output_stream, colorize=False766)767)768769@mock.patch('awscli.customizations.history.commands.is_windows', True)770@mock.patch('awscli.customizations.history.commands.is_a_tty')771def test_force_color(self, mock_is_a_tty):772self.formatter = mock.Mock(DetailedFormatter)773self.add_formatter('detailed', self.formatter)774self.parsed_args.format = 'detailed'775self.parsed_args.command_id = 'latest'776777self.parsed_globals.color = 'on'778# Even with settings that would typically turn off color, it779# should be turned on because it was explicitly turned on780mock_is_a_tty.return_value = False781782self.show_cmd._run_main(self.parsed_args, self.parsed_globals)783self.assertEqual(784self.formatter.call_args,785mock.call(786include=None, exclude=None,787output=self.output_stream, colorize=True788)789)790791@mock.patch('awscli.customizations.history.commands.is_windows', False)792@mock.patch('awscli.customizations.history.commands.is_a_tty')793def test_disable_color(self, mock_is_a_tty):794self.formatter = mock.Mock(DetailedFormatter)795self.add_formatter('detailed', self.formatter)796self.parsed_args.format = 'detailed'797self.parsed_args.command_id = 'latest'798799self.parsed_globals.color = 'off'800# Even with settings that would typically enable color, it801# should be turned off because it was explicitly turned off802mock_is_a_tty.return_value = True803804self.show_cmd._run_main(self.parsed_args, self.parsed_globals)805self.assertEqual(806self.formatter.call_args,807mock.call(808include=None, exclude=None,809output=self.output_stream, colorize=False810)811)812813814