kopia lustrzana https://github.com/simonw/s3-credentials
1279 wiersze
46 KiB
Python
1279 wiersze
46 KiB
Python
import botocore
|
|
from click.testing import CliRunner
|
|
import s3_credentials
|
|
from s3_credentials.cli import cli
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import pytest
|
|
from unittest.mock import call, Mock
|
|
from botocore.stub import Stubber
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_iam(mocker):
|
|
client = botocore.session.get_session().create_client("iam")
|
|
stubber = Stubber(client)
|
|
stubber.activate()
|
|
mocker.patch("s3_credentials.cli.make_client", return_value=client)
|
|
return stubber
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_s3(mocker):
|
|
client = botocore.session.get_session().create_client("s3")
|
|
stubber = Stubber(client)
|
|
stubber.activate()
|
|
mocker.patch("s3_credentials.cli.make_client", return_value=client)
|
|
return stubber
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_sts(mocker):
|
|
client = botocore.session.get_session().create_client("sts")
|
|
stubber = Stubber(client)
|
|
stubber.activate()
|
|
mocker.patch("s3_credentials.cli.make_client", return_value=client)
|
|
return stubber
|
|
|
|
|
|
def test_whoami(mocker, stub_sts):
|
|
stub_sts.add_response(
|
|
"get_caller_identity",
|
|
{
|
|
"UserId": "AEONAUTHOUNTOHU",
|
|
"Account": "123456",
|
|
"Arn": "arn:aws:iam::123456:user/user-name",
|
|
"ResponseMetadata": {},
|
|
},
|
|
)
|
|
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["whoami"])
|
|
assert result.exit_code == 0
|
|
assert json.loads(result.output) == {
|
|
"UserId": "AEONAUTHOUNTOHU",
|
|
"Account": "123456",
|
|
"Arn": "arn:aws:iam::123456:user/user-name",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"option,expected",
|
|
(
|
|
(
|
|
"",
|
|
"[\n"
|
|
" {\n"
|
|
' "Path": "/",\n'
|
|
' "UserName": "NameA",\n'
|
|
' "UserId": "AID000000000000000001",\n'
|
|
' "Arn": "arn:aws:iam::000000000000:user/NameB",\n'
|
|
' "CreateDate": "2020-01-01 00:00:00+00:00"\n'
|
|
" },\n"
|
|
" {\n"
|
|
' "Path": "/",\n'
|
|
' "UserName": "NameA",\n'
|
|
' "UserId": "AID000000000000000000",\n'
|
|
' "Arn": "arn:aws:iam::000000000000:user/NameB",\n'
|
|
' "CreateDate": "2020-01-01 00:00:00+00:00"\n'
|
|
" }\n"
|
|
"]\n",
|
|
),
|
|
(
|
|
"--nl",
|
|
'{"Path": "/", "UserName": "NameA", "UserId": "AID000000000000000001", "Arn": "arn:aws:iam::000000000000:user/NameB", "CreateDate": "2020-01-01 00:00:00+00:00"}\n'
|
|
'{"Path": "/", "UserName": "NameA", "UserId": "AID000000000000000000", "Arn": "arn:aws:iam::000000000000:user/NameB", "CreateDate": "2020-01-01 00:00:00+00:00"}\n',
|
|
),
|
|
(
|
|
"--csv",
|
|
(
|
|
"UserName,UserId,Arn,Path,CreateDate,PasswordLastUsed,PermissionsBoundary,Tags\n"
|
|
"NameA,AID000000000000000001,arn:aws:iam::000000000000:user/NameB,/,2020-01-01 00:00:00+00:00,,,\n"
|
|
"NameA,AID000000000000000000,arn:aws:iam::000000000000:user/NameB,/,2020-01-01 00:00:00+00:00,,,\n"
|
|
),
|
|
),
|
|
(
|
|
"--tsv",
|
|
(
|
|
"UserName\tUserId\tArn\tPath\tCreateDate\tPasswordLastUsed\tPermissionsBoundary\tTags\n"
|
|
"NameA\tAID000000000000000001\tarn:aws:iam::000000000000:user/NameB\t/\t2020-01-01 00:00:00+00:00\t\t\t\n"
|
|
"NameA\tAID000000000000000000\tarn:aws:iam::000000000000:user/NameB\t/\t2020-01-01 00:00:00+00:00\t\t\t\n"
|
|
),
|
|
),
|
|
),
|
|
)
|
|
def test_list_users(option, expected, stub_iam):
|
|
stub_iam.add_response(
|
|
"list_users",
|
|
{
|
|
"Users": [
|
|
{
|
|
"Path": "/",
|
|
"UserName": "NameA",
|
|
"UserId": "AID000000000000000001",
|
|
"Arn": "arn:aws:iam::000000000000:user/NameB",
|
|
"CreateDate": "2020-01-01 00:00:00+00:00",
|
|
},
|
|
{
|
|
"Path": "/",
|
|
"UserName": "NameA",
|
|
"UserId": "AID000000000000000000",
|
|
"Arn": "arn:aws:iam::000000000000:user/NameB",
|
|
"CreateDate": "2020-01-01 00:00:00+00:00",
|
|
},
|
|
]
|
|
},
|
|
)
|
|
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-users"] + ([option] if option else []))
|
|
assert result.exit_code == 0
|
|
assert result.output == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"options,expected",
|
|
(
|
|
(
|
|
[],
|
|
(
|
|
"[\n"
|
|
" {\n"
|
|
' "Name": "bucket-one",\n'
|
|
' "CreationDate": "2020-01-01 00:00:00+00:00"\n'
|
|
" },\n"
|
|
" {\n"
|
|
' "Name": "bucket-two",\n'
|
|
' "CreationDate": "2020-02-01 00:00:00+00:00"\n'
|
|
" }\n"
|
|
"]\n"
|
|
),
|
|
),
|
|
(
|
|
["--nl"],
|
|
'{"Name": "bucket-one", "CreationDate": "2020-01-01 00:00:00+00:00"}\n'
|
|
'{"Name": "bucket-two", "CreationDate": "2020-02-01 00:00:00+00:00"}\n',
|
|
),
|
|
(
|
|
["--nl", "bucket-one"],
|
|
'{"Name": "bucket-one", "CreationDate": "2020-01-01 00:00:00+00:00"}\n',
|
|
),
|
|
),
|
|
)
|
|
def test_list_buckets(stub_s3, options, expected):
|
|
stub_s3.add_response(
|
|
"list_buckets",
|
|
{
|
|
"Buckets": [
|
|
{
|
|
"Name": "bucket-one",
|
|
"CreationDate": "2020-01-01 00:00:00+00:00",
|
|
},
|
|
{
|
|
"Name": "bucket-two",
|
|
"CreationDate": "2020-02-01 00:00:00+00:00",
|
|
},
|
|
]
|
|
},
|
|
)
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-buckets"] + options)
|
|
assert result.exit_code == 0
|
|
assert result.output == expected
|
|
|
|
|
|
def test_list_buckets_details(stub_s3):
|
|
stub_s3.add_response(
|
|
"list_buckets",
|
|
{
|
|
"Buckets": [
|
|
{
|
|
"Name": "bucket-one",
|
|
"CreationDate": "2020-01-01 00:00:00+00:00",
|
|
}
|
|
]
|
|
},
|
|
)
|
|
stub_s3.add_response(
|
|
"get_bucket_acl",
|
|
{
|
|
"Owner": {
|
|
"DisplayName": "swillison",
|
|
"ID": "36b2eeee501c5952a8ac119f9e5212277a4c01eccfa8d6a9d670bba1e2d5f441",
|
|
},
|
|
"Grants": [
|
|
{
|
|
"Grantee": {
|
|
"DisplayName": "swillison",
|
|
"ID": "36b2eeee501c5952a8ac119f9e5212277a4c01eccfa8d6a9d670bba1e2d5f441",
|
|
"Type": "CanonicalUser",
|
|
},
|
|
"Permission": "FULL_CONTROL",
|
|
}
|
|
],
|
|
"ResponseMetadata": {},
|
|
},
|
|
)
|
|
stub_s3.add_response(
|
|
"get_bucket_location",
|
|
{
|
|
"LocationConstraint": "us-west-2",
|
|
},
|
|
)
|
|
stub_s3.add_response(
|
|
"get_public_access_block",
|
|
{
|
|
"PublicAccessBlockConfiguration": {
|
|
"BlockPublicAcls": True,
|
|
"IgnorePublicAcls": True,
|
|
"BlockPublicPolicy": True,
|
|
"RestrictPublicBuckets": True,
|
|
},
|
|
},
|
|
)
|
|
stub_s3.add_response(
|
|
"get_bucket_website",
|
|
{
|
|
"IndexDocument": {"Suffix": "index.html"},
|
|
"ErrorDocument": {"Key": "error.html"},
|
|
},
|
|
)
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-buckets", "--details"])
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"[\n"
|
|
" {\n"
|
|
' "Name": "bucket-one",\n'
|
|
' "CreationDate": "2020-01-01 00:00:00+00:00",\n'
|
|
' "region": "us-west-2",\n'
|
|
' "bucket_acl": {\n'
|
|
' "Owner": {\n'
|
|
' "DisplayName": "swillison",\n'
|
|
' "ID": "36b2eeee501c5952a8ac119f9e5212277a4c01eccfa8d6a9d670bba1e2d5f441"\n'
|
|
" },\n"
|
|
' "Grants": [\n'
|
|
" {\n"
|
|
' "Grantee": {\n'
|
|
' "DisplayName": "swillison",\n'
|
|
' "ID": "36b2eeee501c5952a8ac119f9e5212277a4c01eccfa8d6a9d670bba1e2d5f441",\n'
|
|
' "Type": "CanonicalUser"\n'
|
|
" },\n"
|
|
' "Permission": "FULL_CONTROL"\n'
|
|
" }\n"
|
|
" ]\n"
|
|
" },\n"
|
|
' "public_access_block": {\n'
|
|
' "BlockPublicAcls": true,\n'
|
|
' "IgnorePublicAcls": true,\n'
|
|
' "BlockPublicPolicy": true,\n'
|
|
' "RestrictPublicBuckets": true\n'
|
|
" },\n"
|
|
' "bucket_website": {\n'
|
|
' "IndexDocument": {\n'
|
|
' "Suffix": "index.html"\n'
|
|
" },\n"
|
|
' "ErrorDocument": {\n'
|
|
' "Key": "error.html"\n'
|
|
" },\n"
|
|
' "url": "http://bucket-one.s3-website.us-west-2.amazonaws.com/"\n'
|
|
" }\n"
|
|
" }\n"
|
|
"]\n"
|
|
)
|
|
|
|
|
|
CUSTOM_POLICY = '{"custom": "policy", "bucket": "$!BUCKET_NAME!$"}'
|
|
READ_WRITE_POLICY = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectAcl", "s3:GetObjectLegalHold", "s3:GetObjectRetention", "s3:GetObjectTagging"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}, {"Effect": "Allow", "Action": ["s3:PutObject", "s3:DeleteObject"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}'
|
|
READ_ONLY_POLICY = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectAcl", "s3:GetObjectLegalHold", "s3:GetObjectRetention", "s3:GetObjectTagging"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}'
|
|
WRITE_ONLY_POLICY = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["s3:PutObject"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}'
|
|
PREFIX_POLICY = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["s3:GetBucketLocation"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"], "Condition": {"StringLike": {"s3:prefix": ["my-prefix/*"]}}}, {"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectAcl", "s3:GetObjectLegalHold", "s3:GetObjectRetention", "s3:GetObjectTagging"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/my-prefix/*"]}, {"Effect": "Allow", "Action": ["s3:PutObject", "s3:DeleteObject"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/my-prefix/*"]}]}'
|
|
EXTRA_STATEMENTS_POLICY = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectAcl", "s3:GetObjectLegalHold", "s3:GetObjectRetention", "s3:GetObjectTagging"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}, {"Effect": "Allow", "Action": ["s3:PutObject", "s3:DeleteObject"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}, {"Effect": "Allow", "Action": "textract:*", "Resource": "*"}]}'
|
|
|
|
# Used by both test_create and test_create_duration
|
|
CREATE_TESTS = (
|
|
# options,use_policy_stdin,expected_policy,expected_name_fragment
|
|
([], False, READ_WRITE_POLICY, "read-write"),
|
|
(["--read-only"], False, READ_ONLY_POLICY, "read-only"),
|
|
(["--write-only"], False, WRITE_ONLY_POLICY, "write-only"),
|
|
(["--prefix", "my-prefix/"], False, PREFIX_POLICY, "read-write"),
|
|
(["--policy", "POLICYFILEPATH"], False, CUSTOM_POLICY, "custom"),
|
|
(["--policy", "-"], True, CUSTOM_POLICY, "custom"),
|
|
(["--policy", CUSTOM_POLICY], False, CUSTOM_POLICY, "custom"),
|
|
(
|
|
["--statement", '{"Effect": "Allow", "Action": "textract:*", "Resource": "*"}'],
|
|
False,
|
|
EXTRA_STATEMENTS_POLICY,
|
|
"custom",
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"options,use_policy_stdin,expected_policy,expected_name_fragment",
|
|
CREATE_TESTS,
|
|
)
|
|
def test_create(
|
|
mocker, tmpdir, options, use_policy_stdin, expected_policy, expected_name_fragment
|
|
):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.create_access_key.return_value = {
|
|
"AccessKey": {"AccessKeyId": "access", "SecretAccessKey": "secret"}
|
|
}
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
filepath = str(tmpdir / "policy.json")
|
|
open(filepath, "w").write(CUSTOM_POLICY)
|
|
fixed_options = [
|
|
filepath if option == "POLICYFILEPATH" else option for option in options
|
|
]
|
|
args = ["create", "pytest-bucket-simonw-1", "-c"] + fixed_options
|
|
kwargs = {}
|
|
if use_policy_stdin:
|
|
kwargs["input"] = CUSTOM_POLICY
|
|
result = runner.invoke(cli, args, **kwargs, catch_exceptions=False)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"Attached policy s3.NAME_FRAGMENT.pytest-bucket-simonw-1 to user s3.NAME_FRAGMENT.pytest-bucket-simonw-1\n"
|
|
"Created access key for user: s3.NAME_FRAGMENT.pytest-bucket-simonw-1\n"
|
|
'{\n "AccessKeyId": "access",\n "SecretAccessKey": "secret"\n}\n'
|
|
).replace("NAME_FRAGMENT", expected_name_fragment)
|
|
assert [str(c) for c in boto3.mock_calls] == [
|
|
"call('s3')",
|
|
"call('iam')",
|
|
"call('sts')",
|
|
"call().head_bucket(Bucket='pytest-bucket-simonw-1')",
|
|
"call().get_user(UserName='s3.{}.pytest-bucket-simonw-1')".format(
|
|
expected_name_fragment
|
|
),
|
|
"call().put_user_policy(PolicyDocument='{}', PolicyName='s3.{}.pytest-bucket-simonw-1', UserName='s3.{}.pytest-bucket-simonw-1')".format(
|
|
expected_policy.replace("$!BUCKET_NAME!$", "pytest-bucket-simonw-1"),
|
|
expected_name_fragment,
|
|
expected_name_fragment,
|
|
),
|
|
"call().create_access_key(UserName='s3.{}.pytest-bucket-simonw-1')".format(
|
|
expected_name_fragment
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"statement,expected_error",
|
|
(
|
|
("", "Invalid JSON string"),
|
|
("{}", "missing required keys: Action, Effect, Resource"),
|
|
('{"Action": 1}', "missing required keys: Effect, Resource"),
|
|
('{"Action": 1, "Effect": 2}', "missing required keys: Resource"),
|
|
),
|
|
)
|
|
def test_create_statement_error(statement, expected_error):
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["create", "--statement", statement])
|
|
assert result.exit_code == 2
|
|
assert expected_error in result.output
|
|
|
|
|
|
@pytest.fixture
|
|
def mocked_for_duration(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.create_access_key.return_value = {
|
|
"AccessKey": {"AccessKeyId": "access", "SecretAccessKey": "secret"}
|
|
}
|
|
boto3.return_value.get_caller_identity.return_value = {"Account": "1234"}
|
|
boto3.return_value.get_role.return_value = {"Role": {"Arn": "arn:::role"}}
|
|
boto3.return_value.assume_role.return_value = {
|
|
"Credentials": {
|
|
"AccessKeyId": "access",
|
|
"SecretAccessKey": "secret",
|
|
"SessionToken": "session",
|
|
}
|
|
}
|
|
return boto3
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"options,use_policy_stdin,expected_policy,expected_name_fragment",
|
|
CREATE_TESTS,
|
|
)
|
|
def test_create_duration(
|
|
mocked_for_duration,
|
|
tmpdir,
|
|
options,
|
|
use_policy_stdin,
|
|
expected_policy,
|
|
expected_name_fragment,
|
|
):
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
filepath = str(tmpdir / "policy.json")
|
|
open(filepath, "w").write(CUSTOM_POLICY)
|
|
fixed_options = [
|
|
filepath if option == "POLICYFILEPATH" else option for option in options
|
|
]
|
|
args = [
|
|
"create",
|
|
"pytest-bucket-simonw-1",
|
|
"-c",
|
|
"--duration",
|
|
"15m",
|
|
] + fixed_options
|
|
kwargs = {}
|
|
if use_policy_stdin:
|
|
kwargs["input"] = CUSTOM_POLICY
|
|
result = runner.invoke(cli, args, **kwargs, catch_exceptions=False)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"Assume role against arn:::role for 900s\n"
|
|
"{\n"
|
|
' "AccessKeyId": "access",\n'
|
|
' "SecretAccessKey": "secret",\n'
|
|
' "SessionToken": "session"\n'
|
|
"}\n"
|
|
)
|
|
assert mocked_for_duration.mock_calls == [
|
|
call("s3"),
|
|
call("iam"),
|
|
call("sts"),
|
|
call().head_bucket(Bucket="pytest-bucket-simonw-1"),
|
|
call().get_caller_identity(),
|
|
call().get_role(RoleName="s3-credentials.AmazonS3FullAccess"),
|
|
call().assume_role(
|
|
RoleArn="arn:::role",
|
|
RoleSessionName="s3.{fragment}.pytest-bucket-simonw-1".format(
|
|
fragment=expected_name_fragment
|
|
),
|
|
Policy="{policy}".format(
|
|
policy=expected_policy.replace(
|
|
"$!BUCKET_NAME!$", "pytest-bucket-simonw-1"
|
|
),
|
|
),
|
|
DurationSeconds=900,
|
|
),
|
|
]
|
|
|
|
|
|
def test_create_public(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.create_access_key.return_value = {
|
|
"AccessKey": {"AccessKeyId": "access", "SecretAccessKey": "secret"}
|
|
}
|
|
# Fake that the bucket does not exist
|
|
boto3.return_value.head_bucket.side_effect = botocore.exceptions.ClientError(
|
|
error_response={}, operation_name=""
|
|
)
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
args = ["create", "pytest-bucket-simonw-1", "-c", "--public"]
|
|
result = runner.invoke(cli, args, catch_exceptions=False)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"Created bucket: pytest-bucket-simonw-1\n"
|
|
"Attached bucket policy allowing public access\n"
|
|
"Attached policy s3.read-write.pytest-bucket-simonw-1 to user s3.read-write.pytest-bucket-simonw-1\n"
|
|
"Created access key for user: s3.read-write.pytest-bucket-simonw-1\n"
|
|
"{\n"
|
|
' "AccessKeyId": "access",\n'
|
|
' "SecretAccessKey": "secret"\n'
|
|
"}\n"
|
|
)
|
|
assert [str(c) for c in boto3.mock_calls] == [
|
|
"call('s3')",
|
|
"call('iam')",
|
|
"call('sts')",
|
|
"call().head_bucket(Bucket='pytest-bucket-simonw-1')",
|
|
"call().create_bucket(Bucket='pytest-bucket-simonw-1')",
|
|
"call().put_bucket_policy(Bucket='pytest-bucket-simonw-1', "
|
|
'Policy=\'{"Version": "2012-10-17", "Statement": [{"Sid": '
|
|
'"AllowAllGetObject", "Effect": "Allow", "Principal": "*", "Action": '
|
|
'["s3:GetObject"], "Resource": '
|
|
'["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}\')',
|
|
"call().get_user(UserName='s3.read-write.pytest-bucket-simonw-1')",
|
|
"call().put_user_policy(PolicyDocument='{}', PolicyName='s3.read-write.pytest-bucket-simonw-1', UserName='s3.read-write.pytest-bucket-simonw-1')".format(
|
|
READ_WRITE_POLICY.replace("$!BUCKET_NAME!$", "pytest-bucket-simonw-1"),
|
|
),
|
|
"call().create_access_key(UserName='s3.read-write.pytest-bucket-simonw-1')",
|
|
]
|
|
|
|
|
|
def test_create_website(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.create_access_key.return_value = {
|
|
"AccessKey": {"AccessKeyId": "access", "SecretAccessKey": "secret"}
|
|
}
|
|
# Fake that the bucket does not exist
|
|
boto3.return_value.head_bucket.side_effect = botocore.exceptions.ClientError(
|
|
error_response={}, operation_name=""
|
|
)
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
args = ["create", "pytest-bucket-simonw-1", "-c", "--website"]
|
|
result = runner.invoke(cli, args, catch_exceptions=False)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"Created bucket: pytest-bucket-simonw-1\n"
|
|
"Attached bucket policy allowing public access\n"
|
|
"Configured website: IndexDocument=index.html, ErrorDocument=error.html\n"
|
|
"Attached policy s3.read-write.pytest-bucket-simonw-1 to user s3.read-write.pytest-bucket-simonw-1\n"
|
|
"Created access key for user: s3.read-write.pytest-bucket-simonw-1\n"
|
|
"{\n"
|
|
' "AccessKeyId": "access",\n'
|
|
' "SecretAccessKey": "secret"\n'
|
|
"}\n"
|
|
)
|
|
assert [str(c) for c in boto3.mock_calls] == [
|
|
"call('s3')",
|
|
"call('iam')",
|
|
"call('sts')",
|
|
"call().head_bucket(Bucket='pytest-bucket-simonw-1')",
|
|
"call().create_bucket(Bucket='pytest-bucket-simonw-1')",
|
|
"call().put_bucket_policy(Bucket='pytest-bucket-simonw-1', "
|
|
'Policy=\'{"Version": "2012-10-17", "Statement": [{"Sid": '
|
|
'"AllowAllGetObject", "Effect": "Allow", "Principal": "*", "Action": '
|
|
'["s3:GetObject"], "Resource": '
|
|
'["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}\')',
|
|
"call().put_bucket_website(Bucket='pytest-bucket-simonw-1', "
|
|
"WebsiteConfiguration={'ErrorDocument': {'Key': 'error.html'}, "
|
|
"'IndexDocument': {'Suffix': 'index.html'}})",
|
|
"call().get_user(UserName='s3.read-write.pytest-bucket-simonw-1')",
|
|
"call().put_user_policy(PolicyDocument='{}', PolicyName='s3.read-write.pytest-bucket-simonw-1', UserName='s3.read-write.pytest-bucket-simonw-1')".format(
|
|
READ_WRITE_POLICY.replace("$!BUCKET_NAME!$", "pytest-bucket-simonw-1"),
|
|
),
|
|
"call().create_access_key(UserName='s3.read-write.pytest-bucket-simonw-1')",
|
|
]
|
|
|
|
|
|
def test_create_format_ini(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.create_access_key.return_value = {
|
|
"AccessKey": {
|
|
"AccessKeyId": "access",
|
|
"SecretAccessKey": "secret",
|
|
"SessionToken": "session",
|
|
}
|
|
}
|
|
runner = CliRunner(mix_stderr=False)
|
|
result = runner.invoke(
|
|
cli,
|
|
["create", "test-bucket", "-c", "-f", "ini"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert (
|
|
result.stdout
|
|
== "[default]\naws_access_key_id=access\naws_secret_access_key=secret\n"
|
|
)
|
|
|
|
|
|
def test_create_format_duration_ini(mocked_for_duration):
|
|
runner = CliRunner(mix_stderr=False)
|
|
result = runner.invoke(
|
|
cli,
|
|
["create", "test-bucket", "-c", "--duration", "15m", "-f", "ini"],
|
|
catch_exceptions=False,
|
|
)
|
|
assert result.exit_code == 0
|
|
assert result.stdout == (
|
|
"[default]\n"
|
|
"aws_access_key_id=access\n"
|
|
"aws_secret_access_key=secret\n"
|
|
"aws_session_token=session\n"
|
|
)
|
|
|
|
|
|
def test_list_user_policies(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.get_user_policy.return_value = {
|
|
"PolicyDocument": {"policy": "here"}
|
|
}
|
|
|
|
def get_paginator(type):
|
|
m = Mock()
|
|
if type == "list_users":
|
|
m.paginate.return_value = [
|
|
{"Users": [{"UserName": "one"}, {"UserName": "two"}]}
|
|
]
|
|
elif type == "list_user_policies":
|
|
m.paginate.return_value = [{"PolicyNames": ["policy-one", "policy-two"]}]
|
|
return m
|
|
|
|
boto3().get_paginator.side_effect = get_paginator
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-user-policies"], catch_exceptions=False)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"User: one\n"
|
|
"PolicyName: policy-one\n"
|
|
"{\n"
|
|
' "policy": "here"\n'
|
|
"}\n"
|
|
"PolicyName: policy-two\n"
|
|
"{\n"
|
|
' "policy": "here"\n'
|
|
"}\n"
|
|
"User: two\n"
|
|
"PolicyName: policy-one\n"
|
|
"{\n"
|
|
' "policy": "here"\n'
|
|
"}\n"
|
|
"PolicyName: policy-two\n"
|
|
"{\n"
|
|
' "policy": "here"\n'
|
|
"}\n"
|
|
)
|
|
assert boto3.mock_calls == [
|
|
call(),
|
|
call("iam"),
|
|
call().get_paginator("list_users"),
|
|
call().get_paginator("list_user_policies"),
|
|
call().get_user_policy(UserName="one", PolicyName="policy-one"),
|
|
call().get_user_policy(UserName="one", PolicyName="policy-two"),
|
|
call().get_paginator("list_user_policies"),
|
|
call().get_user_policy(UserName="two", PolicyName="policy-one"),
|
|
call().get_user_policy(UserName="two", PolicyName="policy-two"),
|
|
]
|
|
|
|
|
|
def test_delete_user(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.get_user_policy.return_value = {
|
|
"PolicyDocument": {"policy": "here"}
|
|
}
|
|
|
|
def get_paginator(type):
|
|
m = Mock()
|
|
if type == "list_access_keys":
|
|
m.paginate.return_value = [
|
|
{"AccessKeyMetadata": [{"AccessKeyId": "one"}, {"AccessKeyId": "two"}]}
|
|
]
|
|
elif type == "list_user_policies":
|
|
m.paginate.return_value = [{"PolicyNames": ["policy-one"]}]
|
|
return m
|
|
|
|
boto3().get_paginator.side_effect = get_paginator
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["delete-user", "user-123"], catch_exceptions=False)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"User: user-123\n"
|
|
" Deleted policy: policy-one\n"
|
|
" Deleted access key: one\n"
|
|
" Deleted access key: two\n"
|
|
" Deleted user\n"
|
|
)
|
|
assert boto3.mock_calls == [
|
|
call(),
|
|
call("iam"),
|
|
call().get_paginator("list_user_policies"),
|
|
call().delete_user_policy(UserName="user-123", PolicyName="policy-one"),
|
|
call().get_paginator("list_access_keys"),
|
|
call().delete_access_key(UserName="user-123", AccessKeyId="one"),
|
|
call().delete_access_key(UserName="user-123", AccessKeyId="two"),
|
|
call().delete_user(UserName="user-123"),
|
|
]
|
|
|
|
|
|
def test_get_cors_policy(mocker):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.get_bucket_cors.return_value = {
|
|
"CORSRules": [
|
|
{
|
|
"ID": "set-by-s3-credentials",
|
|
"AllowedMethods": ["GET"],
|
|
"AllowedOrigins": ["*"],
|
|
}
|
|
]
|
|
}
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(
|
|
cli, ["get-cors-policy", "my-bucket"], catch_exceptions=False
|
|
)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"["
|
|
"\n {"
|
|
'\n "ID": "set-by-s3-credentials",'
|
|
'\n "AllowedMethods": ['
|
|
'\n "GET"'
|
|
"\n ],"
|
|
'\n "AllowedOrigins": ['
|
|
'\n "*"'
|
|
"\n ]"
|
|
"\n }"
|
|
"\n]\n"
|
|
)
|
|
|
|
assert boto3.mock_calls == [
|
|
call("s3"),
|
|
call().get_bucket_cors(Bucket="my-bucket"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"options,expected_json",
|
|
(
|
|
(
|
|
[],
|
|
{
|
|
"ID": "set-by-s3-credentials",
|
|
"AllowedOrigins": ["*"],
|
|
"AllowedHeaders": (),
|
|
"AllowedMethods": ["GET"],
|
|
"ExposeHeaders": (),
|
|
},
|
|
),
|
|
(
|
|
[
|
|
"--allowed-method",
|
|
"GET",
|
|
"--allowed-method",
|
|
"PUT",
|
|
"--allowed-origin",
|
|
"https://www.example.com/",
|
|
"--expose-header",
|
|
"ETag",
|
|
],
|
|
{
|
|
"ID": "set-by-s3-credentials",
|
|
"AllowedOrigins": ("https://www.example.com/",),
|
|
"AllowedHeaders": (),
|
|
"AllowedMethods": ("GET", "PUT"),
|
|
"ExposeHeaders": ("ETag",),
|
|
},
|
|
),
|
|
(
|
|
["--max-age-seconds", 60],
|
|
{
|
|
"ID": "set-by-s3-credentials",
|
|
"AllowedOrigins": ["*"],
|
|
"AllowedHeaders": (),
|
|
"AllowedMethods": ["GET"],
|
|
"ExposeHeaders": (),
|
|
"MaxAgeSeconds": 60,
|
|
},
|
|
),
|
|
),
|
|
)
|
|
def test_set_cors_policy(mocker, options, expected_json):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3.return_value.put_bucket_cors.return_value = {}
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(
|
|
cli, ["set-cors-policy", "my-bucket"] + options, catch_exceptions=False
|
|
)
|
|
assert result.exit_code == 0
|
|
assert result.output == ""
|
|
assert boto3.mock_calls == [
|
|
call("s3"),
|
|
call().head_bucket(Bucket="my-bucket"),
|
|
call().put_bucket_cors(
|
|
Bucket="my-bucket", CORSConfiguration={"CORSRules": [expected_json]}
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"strategy,expected_error",
|
|
(
|
|
("stdin", "Input contained invalid JSON"),
|
|
("filepath", "File contained invalid JSON"),
|
|
("string", "Invalid JSON string"),
|
|
),
|
|
)
|
|
@pytest.mark.parametrize("use_valid_string", (True, False))
|
|
def test_verify_create_policy_option(
|
|
tmpdir, mocker, strategy, expected_error, use_valid_string
|
|
):
|
|
# Ensure "bucket does not exist" error to terminate after verification
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value.head_bucket.side_effect = botocore.exceptions.ClientError(
|
|
error_response={}, operation_name=""
|
|
)
|
|
if use_valid_string:
|
|
content = '{"policy": "..."}'
|
|
else:
|
|
content = "{Invalid JSON"
|
|
# Only used by strategy==filepath
|
|
filepath = str(tmpdir / "policy.json")
|
|
open(filepath, "w").write(content)
|
|
|
|
runner = CliRunner()
|
|
args = ["create", "my-bucket", "--policy"]
|
|
kwargs = {}
|
|
if strategy == "stdin":
|
|
args.append("-")
|
|
kwargs["input"] = content
|
|
elif strategy == "filepath":
|
|
args.append(filepath)
|
|
elif strategy == "string":
|
|
args.append(content)
|
|
|
|
result = runner.invoke(cli, args, **kwargs)
|
|
if use_valid_string:
|
|
assert result.exit_code == 1
|
|
assert (
|
|
result.output
|
|
== "Error: Bucket does not exist: my-bucket - try --create-bucket to create it\n"
|
|
)
|
|
else:
|
|
assert result.exit_code
|
|
assert (
|
|
"Error: Invalid value for '--policy': {}".format(expected_error)
|
|
in result.output
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"content",
|
|
(
|
|
'{"AccessKeyId": "access", "SecretAccessKey": "secret"}',
|
|
"[default]\naws_access_key_id=access\naws_secret_access_key=secret",
|
|
),
|
|
)
|
|
@pytest.mark.parametrize("use_stdin", (True, False))
|
|
def test_auth_option(tmpdir, mocker, content, use_stdin):
|
|
boto3 = mocker.patch("boto3.client")
|
|
boto3.return_value = Mock()
|
|
boto3().get_paginator().paginate.return_value = [{"Users": []}]
|
|
|
|
filepath = None
|
|
if use_stdin:
|
|
input = content
|
|
arg = "-"
|
|
else:
|
|
input = None
|
|
filepath = str(tmpdir / "input")
|
|
open(filepath, "w").write(content)
|
|
arg = filepath
|
|
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(
|
|
cli, ["list-users", "-a", arg], catch_exceptions=False, input=input
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
assert boto3.mock_calls == [
|
|
call(),
|
|
call().get_paginator(),
|
|
call("iam", aws_access_key_id="access", aws_secret_access_key="secret"),
|
|
call().get_paginator("list_users"),
|
|
call().get_paginator().paginate(),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"extra_option", ["--access-key", "--secret-key", "--session-token"]
|
|
)
|
|
def test_auth_option_errors(extra_option):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["list-users", "-a", "-", extra_option, "blah"],
|
|
catch_exceptions=False,
|
|
input="",
|
|
)
|
|
assert result.exit_code == 1
|
|
assert (
|
|
result.output
|
|
== "Error: --auth cannot be used with --access-key, --secret-key or --session-token\n"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"options,expected",
|
|
(
|
|
([], READ_WRITE_POLICY),
|
|
(["--read-only"], READ_ONLY_POLICY),
|
|
(["--write-only"], WRITE_ONLY_POLICY),
|
|
(["--prefix", "my-prefix/"], PREFIX_POLICY),
|
|
(
|
|
[
|
|
"--statement",
|
|
'{"Effect": "Allow", "Action": "textract:*", "Resource": "*"}',
|
|
],
|
|
EXTRA_STATEMENTS_POLICY,
|
|
),
|
|
),
|
|
)
|
|
def test_policy(options, expected):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["policy", "pytest-bucket-simonw-1"] + options,
|
|
catch_exceptions=False,
|
|
)
|
|
assert json.loads(result.output) == json.loads(expected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"options,expected",
|
|
(
|
|
(
|
|
[],
|
|
(
|
|
"[\n"
|
|
" {\n"
|
|
' "Key": "yolo-causeway-1.jpg",\n'
|
|
' "LastModified": "2019-12-26 17:00:22+00:00",\n'
|
|
' "ETag": "\\"87abea888b22089cabe93a0e17cf34a4\\"",\n'
|
|
' "Size": 5923104,\n'
|
|
' "StorageClass": "STANDARD"\n'
|
|
" },\n"
|
|
" {\n"
|
|
' "Key": "yolo-causeway-2.jpg",\n'
|
|
' "LastModified": "2019-12-26 17:00:22+00:00",\n'
|
|
' "ETag": "\\"87abea888b22089cabe93a0e17cf34a4\\"",\n'
|
|
' "Size": 5923104,\n'
|
|
' "StorageClass": "STANDARD"\n'
|
|
" }\n"
|
|
"]\n"
|
|
),
|
|
),
|
|
(
|
|
["--nl"],
|
|
(
|
|
'{"Key": "yolo-causeway-1.jpg", "LastModified": "2019-12-26 17:00:22+00:00", "ETag": "\\"87abea888b22089cabe93a0e17cf34a4\\"", "Size": 5923104, "StorageClass": "STANDARD"}\n'
|
|
'{"Key": "yolo-causeway-2.jpg", "LastModified": "2019-12-26 17:00:22+00:00", "ETag": "\\"87abea888b22089cabe93a0e17cf34a4\\"", "Size": 5923104, "StorageClass": "STANDARD"}\n'
|
|
),
|
|
),
|
|
(
|
|
["--tsv"],
|
|
(
|
|
"Key\tLastModified\tETag\tSize\tStorageClass\tOwner\n"
|
|
'yolo-causeway-1.jpg\t2019-12-26 17:00:22+00:00\t"""87abea888b22089cabe93a0e17cf34a4"""\t5923104\tSTANDARD\t\n'
|
|
'yolo-causeway-2.jpg\t2019-12-26 17:00:22+00:00\t"""87abea888b22089cabe93a0e17cf34a4"""\t5923104\tSTANDARD\t\n'
|
|
),
|
|
),
|
|
(
|
|
["--csv"],
|
|
(
|
|
"Key,LastModified,ETag,Size,StorageClass,Owner\n"
|
|
'yolo-causeway-1.jpg,2019-12-26 17:00:22+00:00,"""87abea888b22089cabe93a0e17cf34a4""",5923104,STANDARD,\n'
|
|
'yolo-causeway-2.jpg,2019-12-26 17:00:22+00:00,"""87abea888b22089cabe93a0e17cf34a4""",5923104,STANDARD,\n'
|
|
),
|
|
),
|
|
),
|
|
)
|
|
def test_list_bucket(stub_s3, options, expected):
|
|
stub_s3.add_response(
|
|
"list_objects_v2",
|
|
{
|
|
"Contents": [
|
|
{
|
|
"Key": "yolo-causeway-1.jpg",
|
|
"LastModified": "2019-12-26 17:00:22+00:00",
|
|
"ETag": '"87abea888b22089cabe93a0e17cf34a4"',
|
|
"Size": 5923104,
|
|
"StorageClass": "STANDARD",
|
|
},
|
|
{
|
|
"Key": "yolo-causeway-2.jpg",
|
|
"LastModified": "2019-12-26 17:00:22+00:00",
|
|
"ETag": '"87abea888b22089cabe93a0e17cf34a4"',
|
|
"Size": 5923104,
|
|
"StorageClass": "STANDARD",
|
|
},
|
|
]
|
|
},
|
|
)
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-bucket", "test-bucket"] + options)
|
|
assert result.exit_code == 0
|
|
assert result.output == expected
|
|
|
|
|
|
def test_list_bucket_empty(stub_s3):
|
|
stub_s3.add_response("list_objects_v2", {})
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-bucket", "test-bucket"])
|
|
assert result.exit_code == 0
|
|
assert result.output == "[]\n"
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_iam_for_list_roles(stub_iam):
|
|
stub_iam.add_response(
|
|
"list_roles",
|
|
{
|
|
"Roles": [
|
|
{
|
|
"RoleName": "role-one",
|
|
"Path": "/",
|
|
"Arn": "arn:aws:iam::462092780466:role/role-one",
|
|
"RoleId": "36b2eeee501c5952a8ac119f9e521",
|
|
"CreateDate": "2020-01-01 00:00:00+00:00",
|
|
}
|
|
]
|
|
},
|
|
)
|
|
stub_iam.add_response(
|
|
"list_role_policies",
|
|
{"PolicyNames": ["policy-one"]},
|
|
)
|
|
stub_iam.add_response(
|
|
"get_role_policy",
|
|
{
|
|
"RoleName": "role-one",
|
|
"PolicyName": "policy-one",
|
|
"PolicyDocument": '{"foo": "bar}',
|
|
},
|
|
)
|
|
stub_iam.add_response(
|
|
"list_attached_role_policies",
|
|
{"AttachedPolicies": [{"PolicyArn": "arn:123:must-be-at-least-tweny-chars"}]},
|
|
)
|
|
stub_iam.add_response(
|
|
"get_policy",
|
|
{"Policy": {"DefaultVersionId": "v1"}},
|
|
)
|
|
stub_iam.add_response(
|
|
"get_policy_version",
|
|
{"PolicyVersion": {"CreateDate": "2020-01-01 00:00:00+00:00"}},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("details", (False, True))
|
|
def test_list_roles_details(stub_iam_for_list_roles, details):
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-roles"] + (["--details"] if details else []))
|
|
assert result.exit_code == 0
|
|
expected = {
|
|
"RoleName": "role-one",
|
|
"Path": "/",
|
|
"Arn": "arn:aws:iam::462092780466:role/role-one",
|
|
"RoleId": "36b2eeee501c5952a8ac119f9e521",
|
|
"CreateDate": "2020-01-01 00:00:00+00:00",
|
|
"inline_policies": [
|
|
{
|
|
"RoleName": "role-one",
|
|
"PolicyName": "policy-one",
|
|
"PolicyDocument": '{"foo": "bar}',
|
|
}
|
|
],
|
|
"attached_policies": [
|
|
{
|
|
"DefaultVersionId": "v1",
|
|
"PolicyVersion": {"CreateDate": "2020-01-01 00:00:00+00:00"},
|
|
}
|
|
],
|
|
}
|
|
if not details:
|
|
expected.pop("inline_policies")
|
|
expected.pop("attached_policies")
|
|
assert json.loads(result.output) == [expected]
|
|
|
|
|
|
def test_list_roles_csv(stub_iam_for_list_roles):
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(cli, ["list-roles", "--csv", "--details"])
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"Path,RoleName,RoleId,Arn,CreateDate,AssumeRolePolicyDocument,Description,MaxSessionDuration,PermissionsBoundary,Tags,RoleLastUsed,inline_policies,attached_policies\n"
|
|
'/,role-one,36b2eeee501c5952a8ac119f9e521,arn:aws:iam::462092780466:role/role-one,2020-01-01 00:00:00+00:00,,,,,,,"[\n'
|
|
" {\n"
|
|
' ""RoleName"": ""role-one"",\n'
|
|
' ""PolicyName"": ""policy-one"",\n'
|
|
' ""PolicyDocument"": ""{\\""foo\\"": \\""bar}""\n'
|
|
" }\n"
|
|
']","[\n'
|
|
" {\n"
|
|
' ""DefaultVersionId"": ""v1"",\n'
|
|
' ""PolicyVersion"": {\n'
|
|
' ""CreateDate"": ""2020-01-01 00:00:00+00:00""\n'
|
|
" }\n"
|
|
" }\n"
|
|
']"\n'
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"files,patterns,expected,error",
|
|
(
|
|
# Without arguments return everything
|
|
(None, None, {"one.txt", "directory/two.txt", "directory/three.json"}, None),
|
|
# Positional arguments returns files
|
|
(["one.txt"], None, {"one.txt"}, None),
|
|
(["directory/two.txt"], None, {"directory/two.txt"}, None),
|
|
(["one.txt"], None, {"one.txt"}, None),
|
|
(
|
|
["directory/two.txt", "directory/three.json"],
|
|
None,
|
|
{"directory/two.txt", "directory/three.json"},
|
|
None,
|
|
),
|
|
# Invalid positional argument downloads file and shows error
|
|
(
|
|
["directory/two.txt", "directory/bad.json"],
|
|
None,
|
|
{"directory/two.txt"},
|
|
"Not found: directory/bad.json",
|
|
),
|
|
# --pattern returns files matching pattern
|
|
(None, ["*e.txt"], {"one.txt"}, None),
|
|
(None, ["*e.txt", "invalid-pattern"], {"one.txt"}, None),
|
|
(None, ["directory/*"], {"directory/two.txt", "directory/three.json"}, None),
|
|
# positional and patterns can be combined
|
|
(["one.txt"], ["directory/*.json"], {"one.txt", "directory/three.json"}, None),
|
|
),
|
|
)
|
|
@pytest.mark.parametrize("output", (None, "out"))
|
|
def test_get_objects(moto_s3_populated, output, files, patterns, expected, error):
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
args = ["get-objects", "my-bucket"] + (files or [])
|
|
if patterns:
|
|
for pattern in patterns:
|
|
args.extend(["--pattern", pattern])
|
|
if output:
|
|
args.extend(["--output", output])
|
|
result = runner.invoke(cli, args, catch_exceptions=False)
|
|
if error:
|
|
assert result.exit_code != 0
|
|
else:
|
|
assert result.exit_code == 0
|
|
# Build list of all files in output directory using glob
|
|
output_dir = pathlib.Path(output or ".")
|
|
all_files = {
|
|
str(p.relative_to(output_dir))
|
|
for p in output_dir.glob("**/*")
|
|
if p.is_file()
|
|
}
|
|
assert all_files == expected
|
|
if error:
|
|
assert error in result.output
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"args,expected,expected_output",
|
|
(
|
|
(["."], {"one.txt", "directory/two.txt", "directory/three.json"}, None),
|
|
(["one.txt"], {"one.txt"}, None),
|
|
(["directory"], {"directory/two.txt", "directory/three.json"}, None),
|
|
(
|
|
["directory", "--prefix", "o"],
|
|
{"o/directory/two.txt", "o/directory/three.json"},
|
|
None,
|
|
),
|
|
# --dry-run tests
|
|
(
|
|
["directory", "--prefix", "o", "--dry-run"],
|
|
None,
|
|
(
|
|
"directory/two.txt => s3://my-bucket/o/directory/two.txt\n"
|
|
"directory/three.json => s3://my-bucket/o/directory/three.json\n"
|
|
),
|
|
),
|
|
(
|
|
[".", "--prefix", "p"],
|
|
{"p/one.txt", "p/directory/two.txt", "p/directory/three.json"},
|
|
None,
|
|
),
|
|
),
|
|
)
|
|
def test_put_objects(moto_s3, args, expected, expected_output):
|
|
runner = CliRunner(mix_stderr=False)
|
|
with runner.isolated_filesystem():
|
|
# Create files
|
|
pathlib.Path("one.txt").write_text("one")
|
|
pathlib.Path("directory").mkdir()
|
|
pathlib.Path("directory/two.txt").write_text("two")
|
|
pathlib.Path("directory/three.json").write_text('{"three": 3}')
|
|
result = runner.invoke(
|
|
cli, ["put-objects", "my-bucket"] + args, catch_exceptions=False
|
|
)
|
|
assert result.exit_code == 0, result.output
|
|
assert set(result.output.split("\n")) == set(
|
|
(expected_output or "").split("\n")
|
|
)
|
|
# Check files were uploaded
|
|
keys = {
|
|
obj["Key"]
|
|
for obj in moto_s3.list_objects(Bucket="my-bucket").get("Contents") or []
|
|
}
|
|
assert keys == (expected or set())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"args,expected,expected_error",
|
|
(
|
|
([], None, "Error: Specify one or more keys or use --prefix"),
|
|
(
|
|
["one.txt", "--prefix", "directory/"],
|
|
None,
|
|
"Cannot pass both keys and --prefix",
|
|
),
|
|
(["one.txt"], ["directory/two.txt", "directory/three.json"], None),
|
|
(["one.txt", "directory/two.txt"], ["directory/three.json"], None),
|
|
(["--prefix", "directory/"], ["one.txt"], None),
|
|
),
|
|
)
|
|
def test_delete_objects(moto_s3_populated, args, expected, expected_error):
|
|
runner = CliRunner(mix_stderr=False)
|
|
with runner.isolated_filesystem():
|
|
result = runner.invoke(
|
|
cli, ["delete-objects", "my-bucket"] + args, catch_exceptions=False
|
|
)
|
|
if expected_error:
|
|
assert result.exit_code != 0
|
|
assert expected_error in result.stderr
|
|
else:
|
|
assert result.exit_code == 0, result.output
|
|
# Check expected files are left in bucket
|
|
keys = {
|
|
obj["Key"]
|
|
for obj in moto_s3_populated.list_objects(Bucket="my-bucket").get(
|
|
"Contents"
|
|
)
|
|
or []
|
|
}
|
|
assert keys == set(expected)
|
|
|
|
|
|
@pytest.mark.parametrize("arg", ("-d", "--dry-run"))
|
|
def test_delete_objects_dry_run(moto_s3_populated, arg):
|
|
runner = CliRunner(mix_stderr=False)
|
|
|
|
def get_keys():
|
|
return {
|
|
obj["Key"]
|
|
for obj in moto_s3_populated.list_objects(Bucket="my-bucket").get(
|
|
"Contents"
|
|
)
|
|
or []
|
|
}
|
|
|
|
with runner.isolated_filesystem():
|
|
before_keys = get_keys()
|
|
result = runner.invoke(
|
|
cli, ["delete-objects", "my-bucket", "--prefix", "directory/", arg]
|
|
)
|
|
assert result.exit_code == 0
|
|
assert result.output == (
|
|
"The following keys would be deleted:\n"
|
|
"directory/three.json\n"
|
|
"directory/two.txt\n"
|
|
)
|
|
after_keys = get_keys()
|
|
assert before_keys == after_keys
|