diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..21aea81 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, E501, F401, E402 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..20b02c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +include common.mk +MODULES=tests + +all: test + +lint: + flake8 $(MODULES) + +# Vars +# +tests:=$(wildcard tests/test_*.py) + +# Run standalone tests +# +test: + $(MAKE) -j1 $(tests) + +# A pattern rule that runs a single test script +# +$(tests): %.py : lint + coverage run -p --source=src $*.py + +.PHONY: all lint test safe_test serial_test all_test $(tests) diff --git a/common.mk b/common.mk new file mode 100644 index 0000000..01bc596 --- /dev/null +++ b/common.mk @@ -0,0 +1,5 @@ +SHELL=/bin/bash + +ifeq ($(shell which flake8),) +$(error Please install flake8 or activate your virtual environment) +endif diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..661fd38 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,4 @@ +flake8 >= 3.4.1 +coverage >= 4.4.1 +httpie >= 1.0.3 +yq >= 2.3.3 diff --git a/tests/test_operations.py b/tests/test_operations.py index 950d5f1..5615de4 100755 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -19,22 +19,19 @@ import typing from collections import namedtuple from unittest import mock -pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) # noqa +pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) # noqa sys.path.insert(0, pkg_root) # noqa -#from tests import skip_on_travis -#from tests.infra import testmode +# from tests.util import CaptureStdout, SwapStdin +# from tests import skip_on_travis +from tests.infra import testmode from eastwood.clint import EastwoodOperationsCommandDispatch from eastwood.clint import think, water, house -from tests.util import CaptureStdout, SwapStdin -from tests.infra import get_env, DSSUploadMixin, TestAuthMixin, DSSAssertMixin -from tests.infra.server import ThreadedLocalServer - def setUpModule(): - logging.basicConfig(**kwargs) + pass @testmode.standalone @@ -50,7 +47,7 @@ class TestOperations(unittest.TestCase): self._test_dispatch(action_overrides=True) def _test_dispatch(self, mutually_exclusive=None, action_overrides=False): - dispatch = DSSOperationsCommandDispatch() + dispatch = EastwoodOperationsCommandDispatch() target = dispatch.target( "my_target", arguments={ @@ -58,16 +55,19 @@ class TestOperations(unittest.TestCase): "--argument-a": None, "--argument-b": dict(default="bar"), }, - mutually_exclusive=(["--argument-a", "--argument-b"] if mutually_exclusive else None) + mutually_exclusive=(["--argument-a", "--argument-b"] if mutually_exclusive else None), ) if action_overrides: + @target.action("my_action", arguments={"foo": None, "--bar": dict(default="bars")}) def my_action(argv, args): self.assertEqual(args.argument_b, "LSDKFJ") self.assertEqual(args.foo, "24") self.assertEqual(args.bar, "bars") + else: + @target.action("my_action") def my_action(argv, args): self.assertEqual(args.argument_b, "LSDKFJ") @@ -75,1498 +75,496 @@ class TestOperations(unittest.TestCase): dispatch(["my_target", "my_action", "24", "--argument-b", "LSDKFJ"]) - def test_map_bucket(self): - with override_bucket_config(BucketConfig.TEST_FIXTURE): - for replica in Replica: - with self.subTest(replica=replica): - handle = Config.get_blobstore_handle(replica) - - count_list = 0 - for key in handle.list(replica.bucket, prefix="bundles/"): - count_list += 1 - - def counter(keys): - count = 0 - for key in keys: - count += 1 - return count - - total = 0 - for count in map_bucket_results(counter, handle, replica.bucket, "bundles/", 2): - total += count - - self.assertGreater(count_list, 0) - self.assertEqual(count_list, total) - - def test_repair_blob_metadata(self): - uploader = {Replica.aws: self._put_s3_file, Replica.gcp: self._put_gs_file} - with override_bucket_config(BucketConfig.TEST): - for replica in Replica: - handle = Config.get_blobstore_handle(replica) - key = str(uuid.uuid4()) - file_metadata = { - FileMetadata.SHA256: "foo", - FileMetadata.SHA1: "foo", - FileMetadata.S3_ETAG: "foo", - FileMetadata.CRC32C: "foo", - FileMetadata.CONTENT_TYPE: "foo" - } - blob_key = compose_blob_key(file_metadata) - uploader[replica](key, json.dumps(file_metadata).encode("utf-8"), "application/json") - uploader[replica](blob_key, b"123", "bar") - args = argparse.Namespace(keys=[key], entity_type="files", job_id="", replica=replica.name) - - with self.subTest("Blob content type repaired", replica=replica): - storage.repair_file_blob_metadata([], args).process_key(key) - self.assertEqual(handle.get_content_type(replica.bucket, blob_key), - file_metadata[FileMetadata.CONTENT_TYPE]) - - with self.subTest("Should handle arbitrary exceptions", replica=replica): - with mock.patch("dss.operations.storage.StorageOperationHandler.log_error") as log_error: - with mock.patch("dss.config.Config.get_native_handle") as thrower: - thrower.side_effect = Exception() - storage.repair_file_blob_metadata([], args).process_key(key) - log_error.assert_called() - self.assertEqual(log_error.call_args[0][0], "Exception") - - with self.subTest("Should handle missing file metadata", replica=replica): - with mock.patch("dss.operations.storage.StorageOperationHandler.log_warning") as log_warning: - storage.repair_file_blob_metadata([], args).process_key("wrong key") - self.assertEqual(log_warning.call_args[0][0], "BlobNotFoundError") - - with self.subTest("Should handle missing blob", replica=replica): - with mock.patch("dss.operations.storage.StorageOperationHandler.log_warning") as log_warning: - file_metadata[FileMetadata.SHA256] = "wrong" - uploader[replica](key, json.dumps(file_metadata).encode("utf-8"), "application/json") - storage.repair_file_blob_metadata([], args).process_key(key) - self.assertEqual(log_warning.call_args[0][0], "BlobNotFoundError") - - with self.subTest("Should handle corrupt file metadata", replica=replica): - with mock.patch("dss.operations.storage.StorageOperationHandler.log_warning") as log_warning: - uploader[replica](key, b"this is not json", "application/json") - storage.repair_file_blob_metadata([], args).process_key(key) - self.assertEqual(log_warning.call_args[0][0], "JSONDecodeError") - - def test_bundle_reference_list(self): - class MockHandler: - mock_file_data = {"uuid": "987", - "version": "987", - "sha256": "256k", - "sha1": "1thing", - "s3-etag": "s34me", - "crc32c": "wthisthis"} - mock_bundle_metadata = {"files": [mock_file_data]} - mock_bundle_key = 'bundles/123.456' - handle = mock.Mock() - - def get(self, bucket, key): - return json.dumps(self.mock_bundle_metadata) - - for replica in Replica: - with self.subTest("Test Bundle Reference"): - with override_bucket_config(BucketConfig.TEST): - with mock.patch("dss.operations.storage.Config") as mock_handle: - mock_handle.get_blobstore_handle = mock.MagicMock(return_value=MockHandler()) - args = argparse.Namespace(keys=[MockHandler.mock_bundle_key], - replica=replica.name, - entity_type='bundles', - job_id="") - res = storage.build_reference_list([], args).process_key(MockHandler.mock_bundle_key) - self.assertIn(MockHandler.mock_bundle_key, res) - self.assertIn(f'files/{MockHandler.mock_file_data["uuid"]}.' - f'{MockHandler.mock_file_data["version"]}', - res) - self.assertIn(compose_blob_key(MockHandler.mock_file_data), res) - - def test_update_content_type(self): - TestCase = namedtuple("TestCase", "replica upload size update initial_content_type expected_content_type") - with override_bucket_config(BucketConfig.TEST): - key = f"operations/{uuid.uuid4()}" - large_size = 64 * 1024 * 1024 + 1 - tests = [ - TestCase(Replica.aws, self._put_s3_file, 1, storage.update_aws_content_type, "a", "b"), - TestCase(Replica.aws, self._put_s3_file, large_size, storage.update_aws_content_type, "a", "b"), - TestCase(Replica.gcp, self._put_gs_file, 1, storage.update_gcp_content_type, "a", "b"), - ] - for test in tests: - data = os.urandom(test.size) - with self.subTest(test.replica.name): - handle = Config.get_blobstore_handle(test.replica) - native_handle = Config.get_native_handle(test.replica) - test.upload(key, data, test.initial_content_type) - old_checksum = handle.get_cloud_checksum(test.replica.bucket, key) - test.update(native_handle, test.replica.bucket, key, test.expected_content_type) - self.assertEqual(test.expected_content_type, handle.get_content_type(test.replica.bucket, key)) - self.assertEqual(handle.get(test.replica.bucket, key), data) - self.assertEqual(old_checksum, handle.get_cloud_checksum(test.replica.bucket, key)) - - def test_verify_blob_replication(self): - key = "blobs/alsdjflaskjdf" - from_handle = mock.Mock() - to_handle = mock.Mock() - from_handle.get_size = mock.Mock(return_value=10) - to_handle.get_size = mock.Mock(return_value=10) - - with self.subTest("no replication error"): - res = sync.verify_blob_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res, list()) - - with self.subTest("Unequal size blobs reports error"): - to_handle.get_size = mock.Mock(return_value=11) - res = sync.verify_blob_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("mismatch", res[0].anomaly) - - with self.subTest("Missing target blob reports error"): - to_handle.get_size.side_effect = BlobNotFoundError - res = sync.verify_blob_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("missing", res[0].anomaly) - - def test_verify_file_replication(self): - key = "blobs/alsdjflaskjdf" - from_handle = mock.Mock() - to_handle = mock.Mock() - file_metadata = json.dumps({'sha256': "", 'sha1': "", 's3-etag': "", 'crc32c': ""}) - from_handle.get = mock.Mock(return_value=file_metadata) - to_handle.get = mock.Mock(return_value=file_metadata) - - with self.subTest("no replication error"): - with mock.patch("dss.operations.sync.verify_blob_replication") as vbr: - vbr.return_value = list() - res = sync.verify_file_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res, list()) - - with self.subTest("Unequal file metadata"): - to_handle.get.return_value = "{}" - res = sync.verify_file_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("mismatch", res[0].anomaly) - - with self.subTest("Missing file metadata"): - to_handle.get.side_effect = BlobNotFoundError - res = sync.verify_file_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("missing", res[0].anomaly) - - def test_verify_bundle_replication(self): - key = "blobs/alsdjflaskjdf" - from_handle = mock.Mock() - to_handle = mock.Mock() - bundle_metadata = json.dumps({ - "creator_uid": 8008, - "files": [{"uuid": None, "version": None}] - }) - from_handle.get = mock.Mock(return_value=bundle_metadata) - to_handle.get = mock.Mock(return_value=bundle_metadata) - - with mock.patch("dss.operations.sync.verify_file_replication") as vfr: - with self.subTest("replication ok"): - vfr.return_value = list() - res = sync.verify_bundle_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res, []) - - with self.subTest("replication problem"): - vfr.return_value = [sync.ReplicationAnomaly(key="", anomaly="")] - res = sync.verify_bundle_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res, vfr.return_value) - - with self.subTest("Unequal bundle metadata"): - to_handle.get.return_value = "{}" - res = sync.verify_bundle_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("mismatch", res[0].anomaly) - - with self.subTest("Missing destination bundle metadata"): - to_handle.get.side_effect = BlobNotFoundError - res = sync.verify_bundle_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("missing on target", res[0].anomaly) - - with self.subTest("Missing source bundle metadata"): - from_handle.get.side_effect = BlobNotFoundError - res = sync.verify_bundle_replication(from_handle, to_handle, "", "", key) - self.assertEqual(res[0].key, key) - self.assertIn("missing on source", res[0].anomaly) - - def _put_s3_file(self, key, data, content_type="blah", part_size=None): - s3 = Config.get_native_handle(Replica.aws) - with io.BytesIO(data) as fh: - s3.upload_fileobj(Bucket=Replica.aws.bucket, - Key=key, - Fileobj=fh, - ExtraArgs=dict(ContentType=content_type), - Config=TransferConfig(multipart_chunksize=64 * 1024 * 1024)) - - def _put_gs_file(self, key, data, content_type="blah"): - gs = Config.get_native_handle(Replica.gcp) - gs_bucket = gs.bucket(Replica.gcp.bucket) - gs_blob = gs_bucket.blob(key, chunk_size=1 * 1024 * 1024) - with io.BytesIO(data) as fh: - gs_blob.upload_from_file(fh, content_type="application/octet-stream") - - def test_iam_aws_list_policies(self): - - def _get_aws_list_policies_kwargs(**kwargs): - # Set default kwarg values, then set any user-specified kwargs - custom_kwargs = dict( - cloud_provider="aws", - group_by=None, - output=None, - force=False, - include_managed=False, - exclude_headers=False, - quiet=True - ) - for kw, val in kwargs.items(): - custom_kwargs[kw] = val - return custom_kwargs - - def _get_fake_policy_document(): - """Utility function to get a fake policy document for mocking the AWS API""" - return { - "Version": "2000-01-01", - "Statement": [ - { - "Effect": "Allow", - "Action": ["fakeservice:*"], - "Resource": [ - "arn:aws:fakeservice:us-east-1:861229788715:foo:bar*", - "arn:aws:fakeservice:us-east-1:861229788715:foo:bar/baz*", - ], - } - ], - } - - with self.subTest("List AWS policies"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - # calling list_policies() will call list_aws_policies() - # which will call extract_aws_policies() - # which will call get_paginator("list_policies") - # which will call paginate() to ask for each page, - # and ask for ["Policies"] for the page items, - # and ["PolicyName"] for the items - class MockPaginator(object): - def paginate(self, *args, **kwargs): - # Return a mock page from the mock paginator - return [{"Policies": [{"PolicyName": "fake-policy"}]}] - - # Plain call to list_policies - iam_client.get_paginator.return_value = MockPaginator() - with CaptureStdout() as output: - kwargs = _get_aws_list_policies_kwargs() - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn("fake-policy", output) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-aws-list-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - iam_client.get_paginator.return_value = MockPaginator() - kwargs = _get_aws_list_policies_kwargs(output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn("fake-policy", output) - - # Define utility functions and classes to help test the --group-by flags - def _get_detail_lists(asset_type): - """Utility function to return a fake user detail list for mocking AWS API""" - if asset_type not in ['users', 'groups', 'roles']: - raise RuntimeError("Error: invalid asset type given, cannot mock AWS API") - - user_detail_list = [ - { - "UserName": "fake-user-1", - "UserId": random_alphanumeric_string(N=21).upper(), - "AttachedManagedPolicies": [], - "UserPolicyList": [ - { - "PolicyName": "fake-policy-attached-to-fake-user-1", - "PolicyDocument": _get_fake_policy_document(), - } - ], - } - ] - - group_detail_list = [ - { - "GroupName": "fake-group-1", - "GroupId": random_alphanumeric_string(N=21).upper(), - "AttachedManagedPolicies": [], - "GroupPolicyList": [ - { - "PolicyName": "fake-policy-attached-to-fake-group-1", - "PolicyDocument": _get_fake_policy_document(), - } - ], - } - ] - - role_detail_list = [ - { - "RoleName": "fake-role-1", - "RoleId": random_alphanumeric_string(N=21).upper(), - "AttachedManagedPolicies": [], - "RolePolicyList": [ - { - "PolicyName": "fake-policy-attached-to-fake-role-1", - "PolicyDocument": _get_fake_policy_document(), - } - ], - } - ] - - return { - "GroupDetailList": group_detail_list if asset_type == "groups" else [], - "RoleDetailList": role_detail_list if asset_type == "roles" else [], - "UserDetailList": user_detail_list if asset_type == "users" else [], - } - - class MockPaginator_UserPolicies(object): - def paginate(self, *args, **kwargs): - yield _get_detail_lists("users") - - class MockPaginator_GroupPolicies(object): - def paginate(self, *args, **kwargs): - yield _get_detail_lists("groups") - - class MockPaginator_RolePolicies(object): - def paginate(self, *args, **kwargs): - yield _get_detail_lists("roles") - - with self.subTest("List AWS policies grouped by user"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - # this will call list_aws_user_policies() - # which will call list_aws_policies_grouped() - # which will call get_paginator("get_account_authorization_details") - # (this is what we mock) - # then it calls paginate() to ask for each page, - # which we mock in the mock classes above. - iam_client.get_paginator.return_value = MockPaginator_UserPolicies() - - # Plain call to list_policies - with CaptureStdout() as output: - kwargs = _get_aws_list_policies_kwargs(group_by="users") - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join(["fake-user-1", "fake-policy-attached-to-fake-user-1"]), output) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-aws-list-users-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - iam_client.get_paginator.return_value = MockPaginator_UserPolicies() - kwargs = _get_aws_list_policies_kwargs(group_by="users", output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn( - IAMSEPARATOR.join(["fake-user-1", "fake-policy-attached-to-fake-user-1"]), output - ) - - with self.subTest("List AWS policies grouped by user"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - # calls list_aws_group_policies - # then list_aws_policies_grouped - # then get_paginator("get_account_authorization_details") - # (this is what we mock) - iam_client.get_paginator.return_value = MockPaginator_GroupPolicies() - - # Plain call to list_policies - with CaptureStdout() as output: - kwargs = _get_aws_list_policies_kwargs(group_by="groups") - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn( - IAMSEPARATOR.join(["fake-group-1", "fake-policy-attached-to-fake-group-1"]), output - ) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-aws-list-groups-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - iam_client.get_paginator.return_value = MockPaginator_GroupPolicies() - kwargs = _get_aws_list_policies_kwargs(group_by="groups", output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn( - IAMSEPARATOR.join(["fake-group-1", "fake-policy-attached-to-fake-group-1"]), output - ) - - with self.subTest("List AWS policies grouped by role"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - # calls list_aws_group_policies - # then list_aws_policies_grouped - # then get_paginator("get_account_authorization_details") - # (this is what we mock) - iam_client.get_paginator.return_value = MockPaginator_RolePolicies() - - # Plain call to list_policies - with CaptureStdout() as output: - kwargs = _get_aws_list_policies_kwargs(group_by="roles") - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn( - IAMSEPARATOR.join(["fake-role-1", "fake-policy-attached-to-fake-role-1"]), output - ) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-aws-list-roles-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - iam_client.get_paginator.return_value = MockPaginator_RolePolicies() - kwargs = _get_aws_list_policies_kwargs(group_by="roles", output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn( - IAMSEPARATOR.join(["fake-role-1", "fake-policy-attached-to-fake-role-1"]), output - ) - - # Make sure we can't overwrite without --force - with self.assertRaises(RuntimeError): - kwargs = _get_aws_list_policies_kwargs(group_by="roles", output=fname, force=False) - iam.list_policies([], argparse.Namespace(**kwargs)) - - # Test error-handling and exceptions last - with self.subTest("Test exceptions and error-handling for AWS IAM functions in dss-ops"): - - with self.assertRaises(RuntimeError): - kwargs = _get_aws_list_policies_kwargs(cloud_provider="invalid-cloud-provider") - iam.list_policies([], argparse.Namespace(**kwargs)) - - with self.assertRaises(RuntimeError): - kwargs = _get_aws_list_policies_kwargs(group_by="another-invalid-choice") - iam.list_policies([], argparse.Namespace(**kwargs)) - - def test_iam_fus_list_policies(self): - - def _get_fus_list_policies_kwargs(**kwargs): - # Set default kwargs values, then set user-specified kwargs - custom_kwargs = dict( - cloud_provider="fusillade", - group_by=None, - output=None, - force=False, - exclude_headers=False, - include_managed=False, - quiet=True - ) - for kw, val in kwargs.items(): - custom_kwargs[kw] = val - return custom_kwargs - - with self.subTest("Fusillade client"): - with mock.patch("dss.operations.iam.DCPServiceAccountManager") as SAM, \ - mock.patch("dss.operations.iam.requests") as req: - - # Mock the service account manager so it won't hit the fusillade server - class FakeServiceAcctMgr(object): - - def get_authorization_header(self, *args, **kwargs): - return {} - - SAM.from_secrets_manager = mock.MagicMock(return_value=FakeServiceAcctMgr()) - - # Create fake API response (one page) - class FakeResponse(object): - - def __init__(self): - self.headers = {} - - def raise_for_status(self, *args, **kwargs): - pass - - def json(self, *args, **kwargs): - return {"key": "value"} - - # Test call_api() - req.get = mock.MagicMock(return_value=FakeResponse()) - client = iam.FusilladeClient("testing") - result = client.call_api("/foobar", "key") - self.assertEqual(result, "value") - - # Mock paginated responses with and without Link header - class FakePaginatedResponse(object): - - def __init__(self): - self.headers = {} - - def raise_for_status(self, *args, **kwargs): - pass - - def json(self, *args, **kwargs): - return {"key": ["values", "values"]} - - class FakePaginatedResponseWithLink(FakePaginatedResponse): - - def __init__(self): - self.headers = {"Link": "<https://api.github.com/user/repos?page=3&per_page=100>;"} - - # Test paginate() - req.get = mock.MagicMock(side_effect=[FakePaginatedResponseWithLink(), FakePaginatedResponse()]) - result = client.paginate("/foobar", "key") - self.assertEqual(result, ["values"] * 4) - - def _wrap_policy(policy_doc): - """Wrap a policy doc the way Fusillade stores/returns them""" - return {"IAMPolicy": policy_doc} - - def _repatch_fus_client(fus_client): - """ - Re-patch a mock Fusillade client with the proper responses for no --group-by flag - or for the --group-by users flag. - """ - # When we call list_policies(), which calls list_fus_user_policies(), - # it calls the paginate() method to get a list of all users, - # then the paginate() method twice for each user (once for groups, once for roles), - side_effects = [ - [ - "fake-user@test-operations.data.humancellatlas.org", - "another-fake-user@test-operations.data.humancellatlas.org" - ], - ["fake-group"], ["fake-role"], - ["fake-group-2"], ["fake-role-2"] - ] - fus_client().paginate = mock.MagicMock(side_effect=side_effects) - - # Once we have called the paginate() methods, - # we call the call_api() method to get IAM policies attached to roles and groups - policy_docs = [ - '{"Id": "fake-group-policy"}', - '{"Id": "fake-role-policy"}', - '{"Id": "fake-group-2-policy"}', - '{"Id": "fake-role-2-policy"}', - ] - fus_client().call_api = mock.MagicMock(side_effect=[_wrap_policy(doc) for doc in policy_docs]) - - with self.subTest("List Fusillade policies"): - - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - # Note: Need to call _repatch_fus_client() before each test - - # Plain call to list_fus_policies - with CaptureStdout() as output: - _repatch_fus_client(fus_client) - kwargs = _get_fus_list_policies_kwargs() - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn("fake-group-policy", output) - self.assertIn("fake-role-policy", output) - self.assertIn("fake-group-2-policy", output) - self.assertIn("fake-role-2-policy", output) - - # Check exclude headers - with CaptureStdout() as output: - _repatch_fus_client(fus_client) - kwargs = _get_fus_list_policies_kwargs(exclude_headers=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn("fake-group-policy", output) - self.assertIn("fake-role-policy", output) - self.assertIn("fake-group-2-policy", output) - self.assertIn("fake-role-2-policy", output) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-fus-list-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - _repatch_fus_client(fus_client) - kwargs = _get_fus_list_policies_kwargs(output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn("fake-group-policy", output) - self.assertIn("fake-role-policy", output) - self.assertIn("fake-group-2-policy", output) - self.assertIn("fake-role-2-policy", output) - - with self.subTest("List Fusillade policies grouped by users"): - - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - - # List fusillade policies grouped by user - with CaptureStdout() as output: - _repatch_fus_client(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="users") - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join([ - "fake-user@test-operations.data.humancellatlas.org", "fake-group-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "fake-user@test-operations.data.humancellatlas.org", "fake-role-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "another-fake-user@test-operations.data.humancellatlas.org", "fake-group-2-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "another-fake-user@test-operations.data.humancellatlas.org", "fake-role-2-policy" - ]), output) - - # Check exclude headers - with CaptureStdout() as output: - _repatch_fus_client(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="users", exclude_headers=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join([ - "fake-user@test-operations.data.humancellatlas.org", "fake-group-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "fake-user@test-operations.data.humancellatlas.org", "fake-role-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "another-fake-user@test-operations.data.humancellatlas.org", "fake-group-2-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "another-fake-user@test-operations.data.humancellatlas.org", "fake-role-2-policy" - ]), output) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-fus-list-users-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - _repatch_fus_client(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="users", output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn(IAMSEPARATOR.join([ - "fake-user@test-operations.data.humancellatlas.org", "fake-group-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "fake-user@test-operations.data.humancellatlas.org", "fake-role-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "another-fake-user@test-operations.data.humancellatlas.org", "fake-group-2-policy" - ]), output) - self.assertIn(IAMSEPARATOR.join([ - "another-fake-user@test-operations.data.humancellatlas.org", "fake-role-2-policy" - ]), output) - - with self.subTest("List Fusillade policies grouped by groups"): - - # We can't use _repatch_fus_client() to repatch, - # since grouping by groups makes different function calls - def _repatch_fus_client_groups(fus_client): - """Re-patch a mock Fusillade client with the proper responses for using the --group-by groups flag""" - # When we call list_policies(), which calls list_fus_group_policies(), - # it calls paginate() to get all groups, - # then calls paginate() to get roles for each group - responses = [["fake-group", "fake-group-2"], ["fake-role"], ["fake-role-2"]] - fus_client().paginate = mock.MagicMock(side_effect=responses) - - # For each role, list_fus_group_policies() calls get_fus_role_attached_policies(), - # which calls call_api() on each role and returns a corresponding policy document - # @chmreid TODO: should this be calling get policy on each group, too? (inline policies) - policy_docs = ['{"Id": "fake-role-policy"}', '{"Id": "fake-role-2-policy"}'] - fus_client().call_api = mock.MagicMock(side_effect=[_wrap_policy(doc) for doc in policy_docs]) - - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - - # List fusillade policies grouped by groups - with CaptureStdout() as output: - _repatch_fus_client_groups(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="groups") - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join(["fake-group", "fake-role-policy"]), output) - self.assertIn(IAMSEPARATOR.join(["fake-group-2", "fake-role-2-policy"]), output) - - # Check exclude headers - with CaptureStdout() as output: - _repatch_fus_client_groups(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="groups", exclude_headers=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join(["fake-group", "fake-role-policy"]), output) - self.assertIn(IAMSEPARATOR.join(["fake-group-2", "fake-role-2-policy"]), output) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-fus-list-groups-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - _repatch_fus_client_groups(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="groups", output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn(IAMSEPARATOR.join(["fake-group", "fake-role-policy"]), output) - self.assertIn(IAMSEPARATOR.join(["fake-group-2", "fake-role-2-policy"]), output) - - with self.subTest("List Fusillade policies grouped by roles"): - - # repatch the fusillade client for calling a list of policies grouped by roles - def _repatch_fus_client_roles(fus_client): - """Re-patch a mock Fusillade client with the proper responses for using the --group-by roles flag""" - # When we call list_policies, which calls list_fus_role_policies(), - # it calls paginate() to get the list of all roles, - side_effects = [["fake-role", "fake-role-2"]] - fus_client().paginate = mock.MagicMock(side_effect=side_effects) - - # list_fus_role_policies then calls get_fus_role_attached_policies() - # to get a list of policies attached to the role, - # which calls call_api() for each role returned by the paginate command - policy_docs = ['{"Id": "fake-role-policy"}', '{"Id": "fake-role-2-policy"}'] - fus_client().call_api = mock.MagicMock(side_effect=[_wrap_policy(doc) for doc in policy_docs]) - - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - - # List fusillade policies grouped by roles - with CaptureStdout() as output: - _repatch_fus_client_roles(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="roles") - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join(["fake-role", "fake-role-policy"]), output) - self.assertIn(IAMSEPARATOR.join(["fake-role-2", "fake-role-2-policy"]), output) - - # Check exclude headers - with CaptureStdout() as output: - _repatch_fus_client_roles(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="roles", exclude_headers=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - self.assertIn(IAMSEPARATOR.join(["fake-role", "fake-role-policy"]), output) - self.assertIn(IAMSEPARATOR.join(["fake-role-2", "fake-role-2-policy"]), output) - - # Check write to output file - temp_prefix = "dss-test-operations-iam-list-roles-temp-output" - f, fname = tempfile.mkstemp(prefix=temp_prefix) - _repatch_fus_client_roles(fus_client) - kwargs = _get_fus_list_policies_kwargs(group_by="roles", output=fname, force=True) - iam.list_policies([], argparse.Namespace(**kwargs)) - with open(fname, "r") as f: - output = f.read() - self.assertIn(IAMSEPARATOR.join(["fake-role", "fake-role-policy"]), output) - self.assertIn(IAMSEPARATOR.join(["fake-role-2", "fake-role-2-policy"]), output) - - def test_iam_aws_list_assets(self): - - def _get_aws_list_assets_kwargs(**kwargs): - # Set default kwargs values, then set user-specified kwargs - custom_kwargs = dict( - cloud_provider="aws", - output=None, - force=False, - exclude_headers=False, - ) - for kw, val in kwargs.items(): - custom_kwargs[kw] = val - return custom_kwargs - - with self.subTest("AWS list users"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - class MockPaginator_Users(object): - def paginate(self, *args, **kwargs): - return [{"Users": [ - {"UserName": "fake-user-1@test-operations.data.humancellatlas.org"}, - {"UserName": "fake-user-2@test-operations.data.humancellatlas.org"} - ]}] - iam_client.get_paginator.return_value = MockPaginator_Users() - with CaptureStdout() as output: - kwargs = _get_aws_list_assets_kwargs() - iam.list_users([], argparse.Namespace(**kwargs)) - self.assertIn("fake-user-1@test-operations.data.humancellatlas.org", output) - self.assertIn("fake-user-2@test-operations.data.humancellatlas.org", output) - - with self.subTest("AWS list groups"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - class MockPaginator_Groups(object): - def paginate(self, *args, **kwargs): - return [{"Groups": [{"GroupName": "fake-group-1"}, {"GroupName": "fake-group-2"}]}] - iam_client.get_paginator.return_value = MockPaginator_Groups() - with CaptureStdout() as output: - kwargs = _get_aws_list_assets_kwargs() - iam.list_groups([], argparse.Namespace(**kwargs)) - self.assertIn("fake-group-1", output) - self.assertIn("fake-group-2", output) - - with self.subTest("AWS list roles"): - with mock.patch("dss.operations.iam.iam_client") as iam_client: - class MockPaginator_Roles(object): - def paginate(self, *args, **kwargs): - return [{"Roles": [{"RoleName": "fake-role-1"}, {"RoleName": "fake-role-2"}]}] - iam_client.get_paginator.return_value = MockPaginator_Roles() - with CaptureStdout() as output: - kwargs = _get_aws_list_assets_kwargs() - iam.list_roles([], argparse.Namespace(**kwargs)) - self.assertIn("fake-role-1", output) - self.assertIn("fake-role-2", output) - - def test_iam_fus_list_assets(self): - - def _get_fus_list_assets_kwargs(**kwargs): - # Set default kwargs values, then set user-specified kwargs - custom_kwargs = dict( - cloud_provider="fusillade", - output=None, - force=False, - exclude_headers=False, - ) - for kw, val in kwargs.items(): - custom_kwargs[kw] = val - return custom_kwargs - - with self.subTest("Fusillade list users"): - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - side_effects = [[ - "fake-user-1@test-operations.data.humancellatlas.org", - "fake-user-2@test-operations.data.humancellatlas.org" - ]] - fus_client().paginate = mock.MagicMock(side_effect=side_effects) - kwargs = _get_fus_list_assets_kwargs() - with CaptureStdout() as output: - iam.list_users([], argparse.Namespace(**kwargs)) - self.assertIn("fake-user-1@test-operations.data.humancellatlas.org", output) - self.assertIn("fake-user-2@test-operations.data.humancellatlas.org", output) - - with self.subTest("Fusillade list groups"): - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - side_effects = [["fake-group-1", "fake-group-2"]] - fus_client().paginate = mock.MagicMock(side_effect=side_effects) - kwargs = _get_fus_list_assets_kwargs() - with CaptureStdout() as output: - iam.list_groups([], argparse.Namespace(**kwargs)) - self.assertIn("fake-group-1", output) - self.assertIn("fake-group-2", output) - - with self.subTest("Fusillade list roles"): - with mock.patch("dss.operations.iam.FusilladeClient") as fus_client: - side_effects = [["fake-role-1", "fake-role-2"]] - fus_client().paginate = mock.MagicMock(side_effect=side_effects) - kwargs = _get_fus_list_assets_kwargs() - with CaptureStdout() as output: - iam.list_roles([], argparse.Namespace(**kwargs)) - self.assertIn("fake-role-1", output) - self.assertIn("fake-role-2", output) - - def test_secrets_crud(self): - # CRUD (create read update delete) test procedure: - # - create new secret - # - list secrets and verify new secret shows up - # - get secret value and verify it is correct - # - update secret value - # - get secret value and verify it is correct - # - delete secret - which_stage = os.environ["DSS_DEPLOYMENT_STAGE"] - which_store = os.environ["DSS_SECRETS_STORE"] - - secret_name = random_alphanumeric_string() - testvar_name = f"{which_store}/{which_stage}/{secret_name}" - testvar_value = "Hello world!" - testvar_value2 = "Goodbye world!" - - unusedvar_name = f"{which_store}/{which_stage}/admin_user_emails" - - with self.subTest("Create a new secret"): - # Monkeypatch the secrets manager - with mock.patch("dss.operations.secrets.sm_client") as sm: - # Creating a new variable will first call get, which will not find it - sm.get_secret_value = mock.MagicMock(return_value=None, side_effect=ClientError({}, None)) - # Next we will use the create secret command - sm.create_secret = mock.MagicMock(return_value=None) - - # Create initial secret value: - # Dry run first - with SwapStdin(testvar_value): - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=True, infile=None, quiet=True, force=True - ), - ) - - # Provide secret via stdin - with SwapStdin(testvar_value): - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=False, infile=None, quiet=True, force=True - ), - ) - - # Provide secret via infile - with tempfile.NamedTemporaryFile(prefix='dss-test-operations-new-secret-temp-input', mode='w') as f: - f.write(testvar_value) - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=False, infile=f.name, force=True, quiet=True - ), - ) - - # Check error-catching with non-existent infile - mf = 'this-file-is-not-here' - with self.assertRaises(RuntimeError): - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=False, infile=mf, force=True, quiet=True - ), - ) - - with self.subTest("List secrets"): - with mock.patch("dss.operations.secrets.sm_client") as sm: - # Listing secrets requires creating a paginator first, - # so mock what the paginator returns - class MockPaginator(object): - def paginate(self): - # Return a mock page from the mock paginator - return [{"SecretList": [{"Name": testvar_name}, {"Name": unusedvar_name}]}] - sm.get_paginator.return_value = MockPaginator() - - # Non-JSON output first - with CaptureStdout() as output: - secrets.list_secrets([], argparse.Namespace(json=False)) - self.assertIn(testvar_name, output) - - # JSON output - with CaptureStdout() as output: - secrets.list_secrets([], argparse.Namespace(json=True)) - all_secrets_output = json.loads("\n".join(output)) - self.assertIn(testvar_name, all_secrets_output) - - with self.subTest("Get secret value"): - with mock.patch("dss.operations.secrets.sm_client") as sm: - # Requesting the variable will try to get secret value and succeed - sm.get_secret_value.return_value = {"SecretString": testvar_value} - # Now run get secret value in JSON mode and non-JSON mode - # and verify variable name/value is in both. - - # New output file - with tempfile.NamedTemporaryFile(prefix='dss-test-operations-get-secret-temp-output', mode='w') as f: - # Try to overwrite outfile without --force - with self.assertRaises(RuntimeError): - secrets.get_secret( - [], argparse.Namespace(secret_name=testvar_name, outfile=f.name, force=False) - ) - - # Overwrite outfile with --force - secrets.get_secret( - [], argparse.Namespace(secret_name=testvar_name, outfile=f.name, force=True) - ) - with open(f.name, 'r') as fr: - file_contents = fr.read() - self.assertIn(testvar_value, file_contents) - - # Output secret to stdout - with CaptureStdout() as output: - secrets.get_secret( - [], argparse.Namespace(secret_name=testvar_name, outfile=None, force=False) - ) - self.assertIn(testvar_value, "\n".join(output)) - - with self.subTest("Update existing secret"): - with mock.patch("dss.operations.secrets.sm_client") as sm: - # Updating the variable will try to get secret value and succeed - sm.get_secret_value = mock.MagicMock(return_value={"SecretString": testvar_value}) - # Next we will call the update secret command - sm.update_secret = mock.MagicMock(return_value=None) - - # Update secret: - # Dry run first - with SwapStdin(testvar_value2): - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=True, infile=None, force=True, quiet=True - ), - ) - - # Use stdin - with SwapStdin(testvar_value2): - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=False, infile=None, force=True, quiet=True - ), - ) - - # Use input file - with tempfile.NamedTemporaryFile(prefix='dss-test-operations-update-secret-temp-input', mode='w') as f: - f.write(testvar_value2) - secrets.set_secret( - [], - argparse.Namespace( - secret_name=testvar_name, dry_run=False, infile=f.name, force=True, quiet=True - ), - ) - - with self.subTest("Delete secret"): - with mock.patch("dss.operations.secrets.sm_client") as sm: - # Deleting the variable will try to get secret value and succeed - sm.get_secret_value = mock.MagicMock(return_value={"SecretString": testvar_value}) - sm.delete_secret = mock.MagicMock(return_value=None) - - # Delete secret - # Dry run first - secrets.del_secret( - [], argparse.Namespace(secret_name=testvar_name, force=True, dry_run=True, quiet=True) - ) - - # Real thing - secrets.del_secret( - [], argparse.Namespace(secret_name=testvar_name, force=True, dry_run=False, quiet=True) - ) - - def test_ssmparams_utilities(self): - prefix = f"/{os.environ['DSS_PARAMETER_STORE']}/{os.environ['DSS_DEPLOYMENT_STAGE']}" - gold_var = f"{prefix}/dummy_variable" - - var = "dummy_variable" - new_var = fix_ssm_variable_prefix(var) - self.assertEqual(new_var, gold_var) - - var = "/dummy_variable" - new_var = fix_ssm_variable_prefix(var) - self.assertEqual(new_var, gold_var) - - var = f"{prefix}/dummy_variable" - new_var = fix_ssm_variable_prefix(var) - self.assertEqual(new_var, gold_var) - - var = f"{prefix}/dummy_variable/" - new_var = fix_ssm_variable_prefix(var) - self.assertEqual(new_var, gold_var) - - def test_ssmparams_crud(self): - # CRUD (create read update delete) test for setting environment variables in SSM param store - testvar_name = random_alphanumeric_string() - testvar_value = "Hello world!" - - # Assemble environment to return - old_env = {"DUMMY_VARIABLE": "dummy_value"} - new_env = dict(**old_env) - new_env[testvar_name] = testvar_value - ssm_new_env = self._wrap_ssm_env(new_env) - - with self.subTest("Print the SSM environment"): - with mock.patch("dss.operations.lambda_params.ssm_client") as ssm: - # listing params will call ssm.get_parameter to get the entire environment - ssm.get_parameter = mock.MagicMock(return_value=ssm_new_env) - - # Now call our params.py module. Output var=value on each line. - with CaptureStdout() as output: - lambda_params.ssm_environment([], argparse.Namespace(json=False)) - self.assertIn(f"{testvar_name}={testvar_value}", output) - - def test_lambdaparams_crud(self): - # CRUD (create read update delete) test for setting lambda function environment variables - testvar_name = random_alphanumeric_string() - testvar_value = "Hello world!" - testvar_value2 = "Goodbye world!" - - # Assemble an old and new environment to return - old_env = {"DUMMY_VARIABLE": "dummy_value"} - new_env = dict(**old_env) - new_env[testvar_name] = testvar_value - - ssm_old_env = self._wrap_ssm_env(old_env) - ssm_new_env = self._wrap_ssm_env(new_env) - - lam_old_env = self._wrap_lambda_env(old_env) - lam_new_env = self._wrap_lambda_env(new_env) - - with self.subTest("Create a new lambda parameter"): - with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ - mock.patch("dss.operations.lambda_params.lambda_client") as lam: - - # If this is not a dry run, lambda_set in params.py - # will update the SSM first, so we mock those first. - # Before we have set the new test variable for the - # first time, we will see the old environment. - ssm.put_parameter = mock.MagicMock(return_value=None) - ssm.get_parameter = mock.MagicMock(return_value=ssm_old_env) - - # The lambda_set func in params.py will update lambdas, - # so we mock the calls that those will make too. - lam.get_function = mock.MagicMock(return_value=None) - lam.get_function_configuration = mock.MagicMock(return_value=lam_old_env) - lam.update_function_configuration = mock.MagicMock(return_value=None) - - with SwapStdin(testvar_value): - lambda_params.lambda_set( - [], argparse.Namespace(name=testvar_name, dry_run=True, quiet=True) - ) - - with SwapStdin(testvar_value): - lambda_params.lambda_set( - [], argparse.Namespace(name=testvar_name, dry_run=False, quiet=True) - ) - with self.subTest("List lambda parameters"): - with mock.patch("dss.operations.lambda_params.lambda_client") as lam: - # The lambda_list func in params.py calls get_deployed_lambas, which calls lam.get_function() - # using daemon folder names (this function is called only to ensure no exception is thrown) - lam.get_function = mock.MagicMock(return_value=None) - # Next we call get_deployed_lambda_environment(), which calls lam.get_function_configuration - # (this returns the mocked new env vars json) - lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) - # Used to specify a lambda by name - stage = os.environ["DSS_DEPLOYMENT_STAGE"] - - # Non-JSON fmt - with CaptureStdout() as output: - lambda_params.lambda_list([], argparse.Namespace(json=False)) - # Check that all deployed lambdas are present - for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): - self.assertIn(f"{lambda_name}", output) - - # JSON fmt - with CaptureStdout() as output: - lambda_params.lambda_list([], argparse.Namespace(json=True)) - # Check that all deployed lambdas are present - all_lams_output = json.loads("\n".join(output)) - for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): - self.assertIn(lambda_name, all_lams_output) - - with self.subTest("Get environments of each lambda function"): - with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ - mock.patch("dss.operations.lambda_params.lambda_client") as lam: - - # lambda_environment() function in dss/operations/lambda_params.py calls get_deployed_lambdas() - # (which only does local operations) - # then it calls get_deployed_lambda_environment() on every lambda, - # which calls lambda_client.get_function() (only called to ensure no exception is thrown) - lam.get_function = mock.MagicMock(return_value=None) - # then calls lambda_client.get_function_configuration() - lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) - - # TODO: reduce copypasta - - # Non-JSON, no lambda name specified - with CaptureStdout() as output: - lambda_params.lambda_environment([], argparse.Namespace(lambda_name=None, json=False)) - # Check that all deployed lambdas are present - output = "\n".join(output) - for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): - self.assertIn(lambda_name, output) - - # Non-JSON, lambda name specified - with CaptureStdout() as output: - lambda_params.lambda_environment([], argparse.Namespace(lambda_name=f"dss-{stage}", json=False)) - output = "\n".join(output) - self.assertIn(f"dss-{stage}", output) - - # JSON, no lambda name specified - with CaptureStdout() as output: - lambda_params.lambda_environment([], argparse.Namespace(lambda_name=None, json=True)) - # Check that all deployed lambdas are present - all_lams_output = json.loads("\n".join(output)) - for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): - self.assertIn(lambda_name, all_lams_output) - - # JSON, lambda name specified - with CaptureStdout() as output: - lambda_params.lambda_environment([], argparse.Namespace(lambda_name=f"dss-{stage}", json=True)) - all_lams_output = json.loads("\n".join(output)) - self.assertIn(f"dss-{stage}", all_lams_output) - - with self.subTest("Update (set) existing lambda parameters"): - with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ - mock.patch("dss.operations.lambda_params.lambda_client") as lam: - # Mock the same way we did for create new param above. - # First we mock the SSM param store - ssm.get_parameter = mock.MagicMock(return_value=ssm_new_env) - ssm.put_parameter = mock.MagicMock(return_value=None) - # Next we mock the lambda client - lam.get_function = mock.MagicMock(return_value=None) - lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) - lam.update_function_configuration = mock.MagicMock(return_value=None) - - # Dry run then real (mocked) thing - with SwapStdin(testvar_value2): - lambda_params.lambda_set( - [], argparse.Namespace(name=testvar_name, dry_run=True, quiet=True) - ) - with SwapStdin(testvar_value2): - lambda_params.lambda_set( - [], argparse.Namespace(name=testvar_name, dry_run=False, quiet=True) - ) - - with self.subTest("Update lambda environment stored in SSM store under $DSS_DEPLOYMENT_STAGE/environment"): - with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ - mock.patch("dss.operations.lambda_params.lambda_client") as lam, \ - mock.patch("dss.operations.lambda_params.es_client") as es, \ - mock.patch("dss.operations.lambda_params.sm_client") as sm, \ - mock.patch("dss.operations.lambda_params.set_ssm_environment") as set_ssm: - # If we call lambda_update in dss/operations/lambda_params.py, - # it calls get_local_lambda_environment() - # (local operations only) - # lambda_update() then calls set_ssm_environment(), - # which we mocked above into set_ssm - set_ssm = mock.MagicMock(return_value=None) # noqa - - ssm.put_parameter = mock.MagicMock(return_value=None) - - # get_elasticsearch_endpoint() calls es.describe_elasticsearch_domain() - es_endpoint_secret = { - "DomainStatus": { - "Endpoint": "this-invalid-es-endpoint-value-comes-from-dss-test-operations" - } - } - es.describe_elasticsearch_domain = mock.MagicMock( - return_value=es_endpoint_secret - ) - - # get_admin_emails() calls sm.get_secret_value() several times: - # - google service acct secret (json string) - # - admin email secret - # use side_effect when returning multiple values - google_service_acct_secret = json.dumps( - {"client_email": "this-invalid-email-comes-from-dss-test-operations"} - ) - admin_email_secret = "this-invalid-email-list-comes-from-dss-test-operations" - - # Finally, we call set_ssm_environment - # which calls ssm.put_parameter() - # (mocked above). - - # If we also update deployed lambdas: - # get_deployed_lambdas() -> lam_client.get_function() - # get_deployed_lambda_environment() -> lam_client.get_function_configuration() - # set_deployed_lambda_environment() -> lam_client.update_function_configuration() - lam.get_function = mock.MagicMock(return_value=None) - lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) - lam.update_function_configuration = mock.MagicMock(return_value=None) - - # The function sm.get_secret_value() must return things in the right order - # Re-mock it before each call - email_side_effect = [ - self._wrap_secret(google_service_acct_secret), - self._wrap_secret(admin_email_secret), - ] - - # Dry run, then real (mocked) thing - sm.get_secret_value = mock.MagicMock(side_effect=email_side_effect) - lambda_params.lambda_update( - [], argparse.Namespace(update_deployed=False, dry_run=True, force=True, quiet=True) - ) - sm.get_secret_value = mock.MagicMock(side_effect=email_side_effect) - lambda_params.lambda_update( - [], argparse.Namespace(update_deployed=False, dry_run=False, force=True, quiet=True) - ) - sm.get_secret_value = mock.MagicMock(side_effect=email_side_effect) - lambda_params.lambda_update( - [], argparse.Namespace(update_deployed=True, dry_run=False, force=True, quiet=True) - ) - - with self.subTest("Unset lambda parameters"): - with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ - mock.patch("dss.operations.lambda_params.lambda_client") as lam: - # If this is not a dry run, lambda_set in params.py - # will update the SSM first, so we mock those first. - # Before we have set the new test variable for the - # first time, we will see the old environment. - ssm.put_parameter = mock.MagicMock(return_value=None) - # use deepcopy here to prevent delete operation from being permanent - ssm.get_parameter = mock.MagicMock(return_value=copy.deepcopy(ssm_new_env)) - - # The lambda_set func in params.py will update lambdas, - # so we mock the calls that those will make too. - lam.get_function = mock.MagicMock(return_value=None) - # use side effect here, and copy the environment for each lambda, so that deletes won't be permanent - lam.get_function_configuration = mock.MagicMock( - side_effect=[copy.deepcopy(lam_new_env) for j in get_deployed_lambdas()] - ) - lam.update_function_configuration = mock.MagicMock(return_value=None) - - lambda_params.lambda_unset([], argparse.Namespace(name=testvar_name, dry_run=True, quiet=True)) - lambda_params.lambda_unset([], argparse.Namespace(name=testvar_name, dry_run=False, quiet=True)) - - def test_events_operations_journal(self): - with self.subTest("Should forward to lambda when `starting_journal_id` is `None`"): - self._test_events_operations_journal(None, 0, 2) - - with self.subTest("Should execute journaling when `starting_journal_id` is not `None`"): - self._test_events_operations_journal("blah", 1, 0) - - def _test_events_operations_journal(self, - starting_journal_id: str, - expected_journal_flashflood_calls: int, - expected_sqs_messenger_calls: int): - sqs_messenger = mock.MagicMock() - with mock.patch("dss.operations.events.SQSMessenger", return_value=sqs_messenger), \ - mock.patch("dss.operations.events.list_new_flashflood_journals"), \ - mock.patch("dss.operations.events.journal_flashflood") as journal_flashflood, \ - mock.patch("dss.operations.events.monitor_logs"): - args = argparse.Namespace(prefix="pfx", - number_of_events=5, - starting_journal_id=starting_journal_id, - job_id=None) - events.journal([], args) - self.assertEqual(expected_journal_flashflood_calls, len(journal_flashflood.mock_calls)) - self.assertEqual(expected_sqs_messenger_calls, len(sqs_messenger.mock_calls)) - - def _wrap_ssm_env(self, e): - """ - Package up the SSM environment the way AWS returns it. - :param dict e: the dict containing the environment to package up and send to SSM store at - $DSS_DEPLOYMENT_STAGE/environment. - """ - # Value should be serialized JSON - ssm_e = {"Parameter": {"Name": "environment", "Value": json.dumps(e)}} - return ssm_e - - def _wrap_lambda_env(self, e): - """ - Package up the lambda environment (a.k.a. function configuration) the way AWS returns it. - :param dict e: the dict containing the lambda function's environment variables - """ - # Value should be a dict (NOT a string) - lam_e = {"Environment": {"Variables": e}} - return lam_e - - def _wrap_secret(self, val): - """ - Package up the secret the way AWS returns it. - """ - return {"SecretString": val} - - -@testmode.integration -class TestOperationsIntegration(TestBundleApiMixin): - @classmethod - def setUpClass(cls): - cls.app = ThreadedLocalServer() - cls.app.start() - - @classmethod - def tearDownClass(cls): - cls.app.shutdown() - - def setUp(self): - Config.set_config(BucketConfig.TEST) - self.s3_test_bucket = get_env("DSS_S3_BUCKET_TEST") - self.gs_test_bucket = get_env("DSS_GS_BUCKET_TEST") - self.s3_test_fixtures_bucket = get_env("DSS_S3_BUCKET_TEST_FIXTURES") - self.gs_test_fixtures_bucket = get_env("DSS_GS_BUCKET_TEST_FIXTURES") - - def test_checkout_operations(self): - with override_bucket_config(BucketConfig.TEST): - for replica, fixture_bucket in [(Replica['aws'], - self.s3_test_fixtures_bucket), - (Replica['gcp'], - self.gs_test_fixtures_bucket)]: - bundle, bundle_uuid = self._create_bundle(replica, fixture_bucket) - args = argparse.Namespace(replica=replica.name, keys=[f'bundles/{bundle_uuid}.{bundle["version"]}']) - checkout_status = checkout.Verify([], args).process_keys() - for key in args.keys: - self.assertIn(key, checkout_status) - checkout.Remove([], args).process_keys() - checkout_status = checkout.Verify([], args).process_keys() - for key in args.keys: - for file in checkout_status[key]: - self.assertIs(False, file['bundle_checkout']) - self.assertIs(False, file['blob_checkout']) - checkout.Add([], args).process_keys() - checkout_status = checkout.Verify([], args).process_keys() - for key in args.keys: - for file in checkout_status[key]: - self.assertIs(True, file['bundle_checkout']) - self.assertIs(True, file['blob_checkout']) - self.delete_bundle(replica, bundle_uuid) - - def _create_bundle(self, replica: Replica, fixtures_bucket: str): - schema = replica.storage_schema - bundle_uuid = str(uuid.uuid4()) - file_uuid = str(uuid.uuid4()) - resp_obj = self.upload_file_wait( - f"{schema}://{fixtures_bucket}/test_good_source_data/0", - replica, - file_uuid, - bundle_uuid=bundle_uuid, - ) - file_version = resp_obj.json['version'] - bundle_version = datetime_to_version_format(datetime.datetime.utcnow()) - resp_obj = self.put_bundle(replica, - bundle_uuid, - [(file_uuid, file_version, "LICENSE")], - bundle_version) - return resp_obj.json, bundle_uuid - - -@testmode.integration -class TestSecretsChecker(unittest.TestCase): - """Test the SecretsChecker class defined in dss/operations/secrets.py""" - @skip_on_travis - def test_check_secrets(self): - """Check that the current stage's secrets conform to expected values""" - secrets.check_secrets([], argparse.Namespace()) - - @skip_on_travis - def test_custom_stage_secrets(self): - """ - The SecretsChecker should not test stages that are not in the list: - dev, staging, integration, prod. - """ - s = SecretsChecker(stage='somenonsensenamelikeprod') - s.run() - - @skip_on_travis - def test_invalid_secrets(self): - """Check that a ValueError is raised when an unqualified email is stored in a secret.""" - s = SecretsChecker(stage='dev') - # Override the email field obtained from terraform - s.email = ['nonsense'] - with self.assertRaises(ValueError): - s.run() - - -@testmode.integration -class TestFlacTableOperations(unittest.TestCase): - """ - Test the dynamodb table with flac operations, tests with actual deployed infra - """ - file_keys = [f'files/{uuid.uuid4()}.{datetime_to_version_format(datetime.datetime.now())}' for x in range(4)] - bundle_keys = [f'bundles/{uuid.uuid4()}.{datetime_to_version_format(datetime.datetime.now())}' for x in range(1)] - all_keys = file_keys + bundle_keys - groups = ["operations", "testing"] - - def test_flac_flow(self): - self._test_upload_keys() - self._test_get_keys(self.all_keys, self.groups) - self._test_modify_key() - self._test_remove_keys() - - def _test_upload_keys(self): - args = argparse.Namespace(keys=self.all_keys, - groups=self.groups) - resp = flac.Add([], args).process_keys() - for item in resp: - self._assert_obj(item, self._build_response_obj(item['key'], self.groups)) - - def _test_modify_key(self): - mod_groups = ["modify"] - mod_key = [self.all_keys[random.randint(0, len(self.all_keys) - 1)]] - args = argparse.Namespace(keys=mod_key, - groups=mod_groups) - resp = flac.Add([], args).process_keys() - for item in resp: - self._assert_obj(item, self._build_response_obj(mod_key[0], mod_groups)) - - def _test_remove_keys(self): - args = argparse.Namespace(keys=self.all_keys) - flac.Remove([], args).process_keys() - resp = flac.Get([], args).process_keys() - for item in resp: - self.assertEqual(item['inDatabase'], False) - - def _test_get_keys(self, keys: list, groups: list): - args = argparse.Namespace(keys=keys) - resp = flac.Get([], args).process_keys() - for item in resp: - self._assert_obj(item, self._build_response_obj(item['key'], groups)) - - def _assert_obj(self, first: dict, second: dict): - """ - Asserts that two dictionaries are equal, ensures that data types are checked correctly. - :param first: - :param second: - """ - for k, v in first.items(): - self.assertIn(k, second) - if type(v) is list: - self.assertListEqual(sorted(v), sorted(second[k])) - elif type(v) is dict: - self.assertDictEqual(v, second[k]) - else: - self.assertEqual(v, second[k]) - - def _build_response_obj(self, key: str, groups: list = None, ddb_status: bool = True): - temp: typing.Dict[typing.Any, typing.Any] = dict() - temp['key'] = key - temp['uuid'] = UUID_REGEX.search(key).group(0) - temp['inDatabase'] = ddb_status - if groups: - temp['groups'] = groups - return temp - -if __name__ == '__main__': +# # Check write to output file +# temp_prefix = "dss-test-operations-iam-aws-list-roles-temp-output" +# f, fname = tempfile.mkstemp(prefix=temp_prefix) +# with open(fname, "r") as f: +# output = f.read() + + +# def test_secrets_crud(self): +# # CRUD (create read update delete) test procedure: +# # - create new secret +# # - list secrets and verify new secret shows up +# # - get secret value and verify it is correct +# # - update secret value +# # - get secret value and verify it is correct +# # - delete secret +# which_stage = os.environ["Eastwood_DEPLOYMENT_STAGE"] +# which_store = os.environ["Eastwood_SECRETS_STORE"] +# +# secret_name = random_alphanumeric_string() +# testvar_name = f"{which_store}/{which_stage}/{secret_name}" +# testvar_value = "Hello world!" +# testvar_value2 = "Goodbye world!" +# +# unusedvar_name = f"{which_store}/{which_stage}/admin_user_emails" +# +# with self.subTest("Create a new secret"): +# # Monkeypatch the secrets manager +# with mock.patch("dss.operations.secrets.sm_client") as sm: +# # Creating a new variable will first call get, which will not find it +# sm.get_secret_value = mock.MagicMock(return_value=None, side_effect=ClientError({}, None)) +# # Next we will use the create secret command +# sm.create_secret = mock.MagicMock(return_value=None) +# +# # Create initial secret value: +# # Dry run first +# with SwapStdin(testvar_value): +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=True, infile=None, quiet=True, force=True +# ), +# ) +# +# # Provide secret via stdin +# with SwapStdin(testvar_value): +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=False, infile=None, quiet=True, force=True +# ), +# ) +# +# # Provide secret via infile +# with tempfile.NamedTemporaryFile(prefix='dss-test-operations-new-secret-temp-input', mode='w') as f: +# f.write(testvar_value) +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=False, infile=f.name, force=True, quiet=True +# ), +# ) +# +# # Check error-catching with non-existent infile +# mf = 'this-file-is-not-here' +# with self.assertRaises(RuntimeError): +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=False, infile=mf, force=True, quiet=True +# ), +# ) +# +# with self.subTest("List secrets"): +# with mock.patch("dss.operations.secrets.sm_client") as sm: +# # Listing secrets requires creating a paginator first, +# # so mock what the paginator returns +# class MockPaginator(object): +# def paginate(self): +# # Return a mock page from the mock paginator +# return [{"SecretList": [{"Name": testvar_name}, {"Name": unusedvar_name}]}] +# sm.get_paginator.return_value = MockPaginator() +# +# # Non-JSON output first +# with CaptureStdout() as output: +# secrets.list_secrets([], argparse.Namespace(json=False)) +# self.assertIn(testvar_name, output) +# +# # JSON output +# with CaptureStdout() as output: +# secrets.list_secrets([], argparse.Namespace(json=True)) +# all_secrets_output = json.loads("\n".join(output)) +# self.assertIn(testvar_name, all_secrets_output) +# +# with self.subTest("Get secret value"): +# with mock.patch("dss.operations.secrets.sm_client") as sm: +# # Requesting the variable will try to get secret value and succeed +# sm.get_secret_value.return_value = {"SecretString": testvar_value} +# # Now run get secret value in JSON mode and non-JSON mode +# # and verify variable name/value is in both. +# +# # New output file +# with tempfile.NamedTemporaryFile(prefix='dss-test-operations-get-secret-temp-output', mode='w') as f: +# # Try to overwrite outfile without --force +# with self.assertRaises(RuntimeError): +# secrets.get_secret( +# [], argparse.Namespace(secret_name=testvar_name, outfile=f.name, force=False) +# ) +# +# # Overwrite outfile with --force +# secrets.get_secret( +# [], argparse.Namespace(secret_name=testvar_name, outfile=f.name, force=True) +# ) +# with open(f.name, 'r') as fr: +# file_contents = fr.read() +# self.assertIn(testvar_value, file_contents) +# +# # Output secret to stdout +# with CaptureStdout() as output: +# secrets.get_secret( +# [], argparse.Namespace(secret_name=testvar_name, outfile=None, force=False) +# ) +# self.assertIn(testvar_value, "\n".join(output)) +# +# with self.subTest("Update existing secret"): +# with mock.patch("dss.operations.secrets.sm_client") as sm: +# # Updating the variable will try to get secret value and succeed +# sm.get_secret_value = mock.MagicMock(return_value={"SecretString": testvar_value}) +# # Next we will call the update secret command +# sm.update_secret = mock.MagicMock(return_value=None) +# +# # Update secret: +# # Dry run first +# with SwapStdin(testvar_value2): +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=True, infile=None, force=True, quiet=True +# ), +# ) +# +# # Use stdin +# with SwapStdin(testvar_value2): +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=False, infile=None, force=True, quiet=True +# ), +# ) +# +# # Use input file +# with tempfile.NamedTemporaryFile(prefix='dss-test-operations-update-secret-temp-input', mode='w') as f: +# f.write(testvar_value2) +# secrets.set_secret( +# [], +# argparse.Namespace( +# secret_name=testvar_name, dry_run=False, infile=f.name, force=True, quiet=True +# ), +# ) +# +# with self.subTest("Delete secret"): +# with mock.patch("dss.operations.secrets.sm_client") as sm: +# # Deleting the variable will try to get secret value and succeed +# sm.get_secret_value = mock.MagicMock(return_value={"SecretString": testvar_value}) +# sm.delete_secret = mock.MagicMock(return_value=None) +# +# # Delete secret +# # Dry run first +# secrets.del_secret( +# [], argparse.Namespace(secret_name=testvar_name, force=True, dry_run=True, quiet=True) +# ) +# +# # Real thing +# secrets.del_secret( +# [], argparse.Namespace(secret_name=testvar_name, force=True, dry_run=False, quiet=True) +# ) +# +# def test_ssmparams_utilities(self): +# prefix = f"/{os.environ['Eastwood_PARAMETER_STORE']}/{os.environ['Eastwood_DEPLOYMENT_STAGE']}" +# gold_var = f"{prefix}/dummy_variable" +# +# var = "dummy_variable" +# new_var = fix_ssm_variable_prefix(var) +# self.assertEqual(new_var, gold_var) +# +# var = "/dummy_variable" +# new_var = fix_ssm_variable_prefix(var) +# self.assertEqual(new_var, gold_var) +# +# var = f"{prefix}/dummy_variable" +# new_var = fix_ssm_variable_prefix(var) +# self.assertEqual(new_var, gold_var) +# +# var = f"{prefix}/dummy_variable/" +# new_var = fix_ssm_variable_prefix(var) +# self.assertEqual(new_var, gold_var) +# +# def test_ssmparams_crud(self): +# # CRUD (create read update delete) test for setting environment variables in SSM param store +# testvar_name = random_alphanumeric_string() +# testvar_value = "Hello world!" +# +# # Assemble environment to return +# old_env = {"DUMMY_VARIABLE": "dummy_value"} +# new_env = dict(**old_env) +# new_env[testvar_name] = testvar_value +# ssm_new_env = self._wrap_ssm_env(new_env) +# +# with self.subTest("Print the SSM environment"): +# with mock.patch("dss.operations.lambda_params.ssm_client") as ssm: +# # listing params will call ssm.get_parameter to get the entire environment +# ssm.get_parameter = mock.MagicMock(return_value=ssm_new_env) +# +# # Now call our params.py module. Output var=value on each line. +# with CaptureStdout() as output: +# lambda_params.ssm_environment([], argparse.Namespace(json=False)) +# self.assertIn(f"{testvar_name}={testvar_value}", output) +# +# def test_lambdaparams_crud(self): +# # CRUD (create read update delete) test for setting lambda function environment variables +# testvar_name = random_alphanumeric_string() +# testvar_value = "Hello world!" +# testvar_value2 = "Goodbye world!" +# +# # Assemble an old and new environment to return +# old_env = {"DUMMY_VARIABLE": "dummy_value"} +# new_env = dict(**old_env) +# new_env[testvar_name] = testvar_value +# +# ssm_old_env = self._wrap_ssm_env(old_env) +# ssm_new_env = self._wrap_ssm_env(new_env) +# +# lam_old_env = self._wrap_lambda_env(old_env) +# lam_new_env = self._wrap_lambda_env(new_env) +# +# with self.subTest("Create a new lambda parameter"): +# with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ +# mock.patch("dss.operations.lambda_params.lambda_client") as lam: +# +# # If this is not a dry run, lambda_set in params.py +# # will update the SSM first, so we mock those first. +# # Before we have set the new test variable for the +# # first time, we will see the old environment. +# ssm.put_parameter = mock.MagicMock(return_value=None) +# ssm.get_parameter = mock.MagicMock(return_value=ssm_old_env) +# +# # The lambda_set func in params.py will update lambdas, +# # so we mock the calls that those will make too. +# lam.get_function = mock.MagicMock(return_value=None) +# lam.get_function_configuration = mock.MagicMock(return_value=lam_old_env) +# lam.update_function_configuration = mock.MagicMock(return_value=None) +# +# with SwapStdin(testvar_value): +# lambda_params.lambda_set( +# [], argparse.Namespace(name=testvar_name, dry_run=True, quiet=True) +# ) +# +# with SwapStdin(testvar_value): +# lambda_params.lambda_set( +# [], argparse.Namespace(name=testvar_name, dry_run=False, quiet=True) +# ) +# with self.subTest("List lambda parameters"): +# with mock.patch("dss.operations.lambda_params.lambda_client") as lam: +# # The lambda_list func in params.py calls get_deployed_lambas, which calls lam.get_function() +# # using daemon folder names (this function is called only to ensure no exception is thrown) +# lam.get_function = mock.MagicMock(return_value=None) +# # Next we call get_deployed_lambda_environment(), which calls lam.get_function_configuration +# # (this returns the mocked new env vars json) +# lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) +# # Used to specify a lambda by name +# stage = os.environ["Eastwood_DEPLOYMENT_STAGE"] +# +# # Non-JSON fmt +# with CaptureStdout() as output: +# lambda_params.lambda_list([], argparse.Namespace(json=False)) +# # Check that all deployed lambdas are present +# for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): +# self.assertIn(f"{lambda_name}", output) +# +# # JSON fmt +# with CaptureStdout() as output: +# lambda_params.lambda_list([], argparse.Namespace(json=True)) +# # Check that all deployed lambdas are present +# all_lams_output = json.loads("\n".join(output)) +# for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): +# self.assertIn(lambda_name, all_lams_output) +# +# with self.subTest("Get environments of each lambda function"): +# with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ +# mock.patch("dss.operations.lambda_params.lambda_client") as lam: +# +# # lambda_environment() function in dss/operations/lambda_params.py calls get_deployed_lambdas() +# # (which only does local operations) +# # then it calls get_deployed_lambda_environment() on every lambda, +# # which calls lambda_client.get_function() (only called to ensure no exception is thrown) +# lam.get_function = mock.MagicMock(return_value=None) +# # then calls lambda_client.get_function_configuration() +# lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) +# +# # TODO: reduce copypasta +# +# # Non-JSON, no lambda name specified +# with CaptureStdout() as output: +# lambda_params.lambda_environment([], argparse.Namespace(lambda_name=None, json=False)) +# # Check that all deployed lambdas are present +# output = "\n".join(output) +# for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): +# self.assertIn(lambda_name, output) +# +# # Non-JSON, lambda name specified +# with CaptureStdout() as output: +# lambda_params.lambda_environment([], argparse.Namespace(lambda_name=f"dss-{stage}", json=False)) +# output = "\n".join(output) +# self.assertIn(f"dss-{stage}", output) +# +# # JSON, no lambda name specified +# with CaptureStdout() as output: +# lambda_params.lambda_environment([], argparse.Namespace(lambda_name=None, json=True)) +# # Check that all deployed lambdas are present +# all_lams_output = json.loads("\n".join(output)) +# for lambda_name in lambda_params.get_deployed_lambdas(quiet=True): +# self.assertIn(lambda_name, all_lams_output) +# +# # JSON, lambda name specified +# with CaptureStdout() as output: +# lambda_params.lambda_environment([], argparse.Namespace(lambda_name=f"dss-{stage}", json=True)) +# all_lams_output = json.loads("\n".join(output)) +# self.assertIn(f"dss-{stage}", all_lams_output) +# +# with self.subTest("Update (set) existing lambda parameters"): +# with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ +# mock.patch("dss.operations.lambda_params.lambda_client") as lam: +# # Mock the same way we did for create new param above. +# # First we mock the SSM param store +# ssm.get_parameter = mock.MagicMock(return_value=ssm_new_env) +# ssm.put_parameter = mock.MagicMock(return_value=None) +# # Next we mock the lambda client +# lam.get_function = mock.MagicMock(return_value=None) +# lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) +# lam.update_function_configuration = mock.MagicMock(return_value=None) +# +# # Dry run then real (mocked) thing +# with SwapStdin(testvar_value2): +# lambda_params.lambda_set( +# [], argparse.Namespace(name=testvar_name, dry_run=True, quiet=True) +# ) +# with SwapStdin(testvar_value2): +# lambda_params.lambda_set( +# [], argparse.Namespace(name=testvar_name, dry_run=False, quiet=True) +# ) +# +# with self.subTest("Update lambda environment stored in SSM store under $Eastwood_DEPLOYMENT_STAGE/environment"): +# with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ +# mock.patch("dss.operations.lambda_params.lambda_client") as lam, \ +# mock.patch("dss.operations.lambda_params.es_client") as es, \ +# mock.patch("dss.operations.lambda_params.sm_client") as sm, \ +# mock.patch("dss.operations.lambda_params.set_ssm_environment") as set_ssm: +# # If we call lambda_update in dss/operations/lambda_params.py, +# # it calls get_local_lambda_environment() +# # (local operations only) +# # lambda_update() then calls set_ssm_environment(), +# # which we mocked above into set_ssm +# set_ssm = mock.MagicMock(return_value=None) # noqa +# +# ssm.put_parameter = mock.MagicMock(return_value=None) +# +# # get_elasticsearch_endpoint() calls es.describe_elasticsearch_domain() +# es_endpoint_secret = { +# "DomainStatus": { +# "Endpoint": "this-invalid-es-endpoint-value-comes-from-dss-test-operations" +# } +# } +# es.describe_elasticsearch_domain = mock.MagicMock( +# return_value=es_endpoint_secret +# ) +# +# # get_admin_emails() calls sm.get_secret_value() several times: +# # - google service acct secret (json string) +# # - admin email secret +# # use side_effect when returning multiple values +# google_service_acct_secret = json.dumps( +# {"client_email": "this-invalid-email-comes-from-dss-test-operations"} +# ) +# admin_email_secret = "this-invalid-email-list-comes-from-dss-test-operations" +# +# # Finally, we call set_ssm_environment +# # which calls ssm.put_parameter() +# # (mocked above). +# +# # If we also update deployed lambdas: +# # get_deployed_lambdas() -> lam_client.get_function() +# # get_deployed_lambda_environment() -> lam_client.get_function_configuration() +# # set_deployed_lambda_environment() -> lam_client.update_function_configuration() +# lam.get_function = mock.MagicMock(return_value=None) +# lam.get_function_configuration = mock.MagicMock(return_value=lam_new_env) +# lam.update_function_configuration = mock.MagicMock(return_value=None) +# +# # The function sm.get_secret_value() must return things in the right order +# # Re-mock it before each call +# email_side_effect = [ +# self._wrap_secret(google_service_acct_secret), +# self._wrap_secret(admin_email_secret), +# ] +# +# # Dry run, then real (mocked) thing +# sm.get_secret_value = mock.MagicMock(side_effect=email_side_effect) +# lambda_params.lambda_update( +# [], argparse.Namespace(update_deployed=False, dry_run=True, force=True, quiet=True) +# ) +# sm.get_secret_value = mock.MagicMock(side_effect=email_side_effect) +# lambda_params.lambda_update( +# [], argparse.Namespace(update_deployed=False, dry_run=False, force=True, quiet=True) +# ) +# sm.get_secret_value = mock.MagicMock(side_effect=email_side_effect) +# lambda_params.lambda_update( +# [], argparse.Namespace(update_deployed=True, dry_run=False, force=True, quiet=True) +# ) +# +# with self.subTest("Unset lambda parameters"): +# with mock.patch("dss.operations.lambda_params.ssm_client") as ssm, \ +# mock.patch("dss.operations.lambda_params.lambda_client") as lam: +# # If this is not a dry run, lambda_set in params.py +# # will update the SSM first, so we mock those first. +# # Before we have set the new test variable for the +# # first time, we will see the old environment. +# ssm.put_parameter = mock.MagicMock(return_value=None) +# # use deepcopy here to prevent delete operation from being permanent +# ssm.get_parameter = mock.MagicMock(return_value=copy.deepcopy(ssm_new_env)) +# +# # The lambda_set func in params.py will update lambdas, +# # so we mock the calls that those will make too. +# lam.get_function = mock.MagicMock(return_value=None) +# # use side effect here, and copy the environment for each lambda, so that deletes won't be permanent +# lam.get_function_configuration = mock.MagicMock( +# side_effect=[copy.deepcopy(lam_new_env) for j in get_deployed_lambdas()] +# ) +# lam.update_function_configuration = mock.MagicMock(return_value=None) +# +# lambda_params.lambda_unset([], argparse.Namespace(name=testvar_name, dry_run=True, quiet=True)) +# lambda_params.lambda_unset([], argparse.Namespace(name=testvar_name, dry_run=False, quiet=True)) +# +# def test_events_operations_journal(self): +# with self.subTest("Should forward to lambda when `starting_journal_id` is `None`"): +# self._test_events_operations_journal(None, 0, 2) +# +# with self.subTest("Should execute journaling when `starting_journal_id` is not `None`"): +# self._test_events_operations_journal("blah", 1, 0) +# +# def _test_events_operations_journal(self, +# starting_journal_id: str, +# expected_journal_flashflood_calls: int, +# expected_sqs_messenger_calls: int): +# sqs_messenger = mock.MagicMock() +# with mock.patch("dss.operations.events.SQSMessenger", return_value=sqs_messenger), \ +# mock.patch("dss.operations.events.list_new_flashflood_journals"), \ +# mock.patch("dss.operations.events.journal_flashflood") as journal_flashflood, \ +# mock.patch("dss.operations.events.monitor_logs"): +# args = argparse.Namespace(prefix="pfx", +# number_of_events=5, +# starting_journal_id=starting_journal_id, +# job_id=None) +# events.journal([], args) +# self.assertEqual(expected_journal_flashflood_calls, len(journal_flashflood.mock_calls)) +# self.assertEqual(expected_sqs_messenger_calls, len(sqs_messenger.mock_calls)) +# +# def _wrap_ssm_env(self, e): +# """ +# Package up the SSM environment the way AWS returns it. +# :param dict e: the dict containing the environment to package up and send to SSM store at +# $Eastwood_DEPLOYMENT_STAGE/environment. +# """ +# # Value should be serialized JSON +# ssm_e = {"Parameter": {"Name": "environment", "Value": json.dumps(e)}} +# return ssm_e +# +# def _wrap_lambda_env(self, e): +# """ +# Package up the lambda environment (a.k.a. function configuration) the way AWS returns it. +# :param dict e: the dict containing the lambda function's environment variables +# """ +# # Value should be a dict (NOT a string) +# lam_e = {"Environment": {"Variables": e}} +# return lam_e +# +# def _wrap_secret(self, val): +# """ +# Package up the secret the way AWS returns it. +# """ +# return {"SecretString": val} + + +if __name__ == "__main__": unittest.main() diff --git a/tests/testmode.py b/tests/testmode.py deleted file mode 100644 index aae3b57..0000000 --- a/tests/testmode.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -import unittest - - -def standalone(f): - return unittest.skipUnless(is_standalone(), "Skipping standalone test")(f) - - -def is_standalone(): - return "standalone" in _test_mode() - - -def integration(f): - return unittest.skipUnless(is_integration(), "Skipping integration test")(f) - - -def is_integration(): - return "integration" in _test_mode() - - -def always(f): - return f - - -def _test_mode(): - return os.environ.get('CLINT_EASTWOOD_TEST_MODE', "standalone") diff --git a/tests/util.py b/tests/util.py index d2237d8..d218a00 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,6 +1,7 @@ import os import sys import random +import string from io import StringIO @@ -10,6 +11,7 @@ class CaptureStdout(list): of Python code. Subclass of list so that you can access stdout lines like a list. """ + def __init__(self, *args, **kwargs): super().__init__() @@ -42,13 +44,17 @@ class CaptureStdout(list): # up this context sys.stdout = self._stdout + class SwapStdin(object): """Utility object using a context manager to swap out stdin with user-provided data.""" + def __init__(self, swap_with): if swap_with is None: - raise RuntimeError("Error: SwapStdin constructor must be provided with a value to substitute for stdin!") + raise RuntimeError( + "Error: SwapStdin constructor must be provided with a value to substitute for stdin!" + ) elif isinstance(swap_with, type("")): - swap_with = bytes(swap_with, 'utf-8') + swap_with = bytes(swap_with, "utf-8") self.swap_with = swap_with def __enter__(self, *args, **kwargs): @@ -70,12 +76,10 @@ class SwapStdin(object): def random_alphanumeric_string(N=10): - return ''.join(random.choices(string.ascii_uppercase + string.digits, k=N)) + return "".join(random.choices(string.ascii_uppercase + string.digits, k=N)) def get_env(varname): if varname not in os.environ: - raise RuntimeError( - "Please set the {} environment variable".format(varname)) + raise RuntimeError("Please set the {} environment variable".format(varname)) return os.environ[varname] -