"""Lambda main tests.""" from datetime import datetime import json from unittest.mock import ANY from unittest.mock import Mock from unittest.mock import patch import main def sns_event(source): """Mock SNS event. Args: source (str): SNS source to embed into message. Returns: dict: SNS message fixture. """ return { 'Records': [ { 'EventVersion': '1.0', 'EventSubscriptionArn': 'even', 'EventSource': 'aws:sns', 'Sns': { 'SignatureVersion': '1', 'MessageId': '1501aa88-0051-4453-a421-f59569044250', 'Message': json.dumps({ 'correlation_id': 'd0bf64d751e1490f9b3eedb7399aaf3d', 'source': source}), 'Type': 'Notification', 'UnsubscribeUrl': 'EXAMPLE', 'TopicArn': 'arn', 'Subject': 'TestInvoke'}}]} @patch('main.compare_snapshots') @patch('main.generate_snapshot_report') @patch('main.insert_new_snapshots') @patch('main.notify_slack') @patch('main.notify_sns') @patch('main.verify_event_source') def test_main_continue( verify_event_source, notify_sns, notify_slack, insert_new_snapshots, generate_snapshot_report, compare_snapshots): """Test lambda running due to proper source validation.""" verify_event_source.return_value = True comparison = ('good', 'bad') report = 'some report' compare_snapshots.return_value = comparison generate_snapshot_report.return_value = report main.main(None, None) assert insert_new_snapshots.called assert compare_snapshots.called generate_snapshot_report.assert_called_once_with(*comparison) notify_sns.assert_called_once_with(report) notify_slack.assert_called_once_with(report) @patch('main.compare_snapshots') @patch('main.generate_snapshot_report') @patch('main.insert_new_snapshots') @patch('main.notify_slack') @patch('main.notify_sns') @patch('main.verify_event_source') def test_main_stop( verify_event_source, notify_sns, notify_slack, insert_new_snapshots, generate_snapshot_report, compare_snapshots): """Test lambda stopping due to invalid source validation.""" verify_event_source.return_value = False main.main(None, None) assert not insert_new_snapshots.called assert not compare_snapshots.called assert not generate_snapshot_report.called assert not notify_sns.called assert not notify_slack.called @patch('main.datetime') @patch('main.get_database') def test_insert_new_snapshots(get_database, mock_datetime, db_conn): """Test snapshot procedure timestamp.""" test_now = datetime(2012, 12, 21, 1, 2, 3) mock_datetime.now.return_value = test_now get_database.return_value = db_conn main.insert_new_snapshots() assert db_conn._cursor.execute.called sql = db_conn._cursor.execute.call_args_list[0] assert str(test_now) in sql[0][0] assert db_conn.commit.called @patch('main.get_database') @patch('main.query') def test_get_upcs(query, get_database, db_conn, upc_rows, upcs): """Test UPC retrieval from the DB.""" sql = 'test the upcs' query.GET_UPCS = sql get_database.return_value = db_conn db_conn._cursor.fetchall.return_value = upc_rows results = main.get_upcs() assert db_conn._cursor.execute.called assert results == upcs @patch('main.get_database') def test_get_snapshots(get_database, db_conn, upcs, upc_snapshots): """Test UPC snapshot retrieval from the DB.""" get_database.return_value = db_conn db_conn._cursor.fetchone.side_effect = upc_snapshots results = main.get_snapshots(upcs) assert results == upc_snapshots assert db_conn._cursor.fetchone.call_count == len(upcs) assert db_conn._cursor.execute.call_count == len(upcs) for upc in upcs: db_conn._cursor.execute.assert_any_call(ANY, {'upc': upc}) @patch('main.get_snapshots') @patch('main.get_upcs') def test_compare_snapshots_bad( get_upcs, get_snapshots, upc_snapshots, upc_snapshots_slice_bad, upc_snapshots_slice_good, upc_snapshots_slice_new): """Test UPC snapshot comparison logic with some bad results.""" get_upcs.return_value = None # doesn't matter get_snapshots.return_value = upc_snapshots expected = ( upc_snapshots_slice_good + upc_snapshots_slice_new, upc_snapshots_slice_bad) results = main.compare_snapshots() assert results == expected @patch('main.get_snapshots') @patch('main.get_upcs') def test_compare_snapshots(get_upcs, get_snapshots, upc_snapshots_slice_good): """Test UPC snapshot comparison logic with no bad UPCs.""" get_upcs.return_value = None # doesn't matter get_snapshots.return_value = upc_snapshots_slice_good expected = (upc_snapshots_slice_good, []) results = main.compare_snapshots() assert results == expected @patch('main.config') def test_verify_event_source_dev(config): """Test event source verification in a dev environment.""" config.IS_DEV = True config.VALID_SOURCE = 'some-test-source' assert main.verify_event_source(sns_event(config.VALID_SOURCE)) assert main.verify_event_source(sns_event('!' + config.VALID_SOURCE)) @patch('main.config') def test_verify_event_source(config): """Test event source verification in a prod environment.""" config.IS_DEV = False config.VALID_SOURCE = 'some-test-source' assert main.verify_event_source(sns_event(config.VALID_SOURCE)) assert not main.verify_event_source(sns_event('!' + config.VALID_SOURCE)) @patch('main.config') def test_verify_event_source_bad_json(config): """Test bad JSON event source verification.""" config.IS_DEV = False sns_message = {'Records': [{'Sns': {'Message': '{}foo[]bar'}}]} assert not main.verify_event_source(sns_message) @patch('main._database_connection') @patch('main.mysql') def test_get_database_new(mysql, _conn): """Test db singleton when it was never called.""" new_connection = Mock() _conn.__bool__.return_value = False mysql._connection.return_value = new_connection assert main.get_database() == new_connection @patch('main._database_connection') @patch('main.mysql') def test_get_database_old(mysql, _conn): """Test db singleton when called and existing connection is reused.""" new_connection = Mock() mysql._connection.return_value = new_connection _conn.__bool__.return_value = True assert main.get_database() == _conn assert main.get_database() != new_connection @patch('main.datetime') def test_generate_snapshot_report( mock_datetime, upc_snapshots_slice_bad, upc_snapshots_slice_good, upc_snapshots_slice_new): """Test report generation with good and bad snapshots.""" test_now = datetime(2012, 12, 21, 1, 2, 3) mock_datetime.now.return_value = test_now good = upc_snapshots_slice_good + upc_snapshots_slice_new report = main.generate_snapshot_report(good, upc_snapshots_slice_bad) header, good_section, bad_section = report.split('\n\n') assert str(test_now) in report assert str(len(good)) in good_section for s in upc_snapshots_slice_bad: assert str(s['upc']) in bad_section assert str(s['amount_latest']) in bad_section assert str(s['amount_previous']) in bad_section @patch('main.datetime') def test_generate_snapshot_report_good( mock_datetime, upc_snapshots_slice_good): """Test report generation with only good snapshots.""" test_now = datetime(2012, 12, 21, 1, 2, 3) mock_datetime.now.return_value = test_now report = main.generate_snapshot_report(upc_snapshots_slice_good, []) header, good_section, bad_section = report.split('\n\n') assert str(test_now) in report assert str(len(upc_snapshots_slice_good)) in good_section assert 'No bad snapshots found' in bad_section @patch('main.datetime') def test_generate_snapshot_report_bad( mock_datetime, upc_snapshots_slice_bad): """Test report generation with only bad snapshots.""" test_now = datetime(2012, 12, 21, 1, 2, 3) mock_datetime.now.return_value = test_now report = main.generate_snapshot_report([], upc_snapshots_slice_bad) header, good_section, bad_section = report.split('\n\n') assert str(test_now) in report assert 'No good snapshots found' in good_section for s in upc_snapshots_slice_bad: assert str(s['upc']) in bad_section assert str(s['amount_latest']) in bad_section assert str(s['amount_latest']) in bad_section assert str(s['amount_previous']) in bad_section assert str(s['amount_previous']) in bad_section @patch('main.boto3') @patch('main.config') def test_notify_sns(config, boto): """Test sns publish of report.""" test_arn = 'this-is-a-test-arn' test_report = 'this is a test report' config.DATA_INTEGRITY_SNS_ARN = test_arn client = Mock() boto.client.return_value = client main.notify_sns(test_report) client.publish.assert_called_with(Message=test_report, TopicArn=test_arn) @patch('main.config') @patch('main.urllib') def test_notify_slack(urllib, config): """Test slack webhook of report.""" test_url = 'test-url' config.SLACK_WEBHOOK_URL = test_url test_report = 'this is a test report' main.notify_slack(test_report) call = urllib.request.urlopen.call_args assert call[0][0] == test_url data = json.loads(call[1]['data'], encoding='ascii') assert data['text'] == test_report @patch('main.config') @patch('main.urllib') def test_notify_slack_exits_if_url_is_none(urllib, config): """Test slack webhook of report if url is none.""" config.SLACK_WEBHOOK_URL = '' test_report = 'this is a test report' main.notify_slack(test_report) assert not urllib.request.urlopen.called