def test_catch_bad_metadata_file_requests_metadata_is_empty(tmpdir):
    metadata_writer = MetadataWriter(suppress_output=False)(output_file="")
    # __call__ resolves empty output file to current directory
    metadata_writer.output_file = ""

    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli._catch_bad_metadata_file_requests(metadata_writer, "", "")

    excinfo.match("Metadata output file name cannot be empty")
def test_process_cli_request_source_file_destination_file(
        tmpdir, patch_iohandler):
    source = tmpdir.join("source")
    source.write("some data")
    destination = tmpdir.join("destination")

    aws_encryption_sdk_cli.process_cli_request(
        stream_args={"mode": sentinel.mode},
        parsed_args=MagicMock(
            input=str(source),
            output=str(destination),
            recursive=False,
            interactive=sentinel.interactive,
            no_overwrite=sentinel.no_overwrite,
            decode=sentinel.decode_input,
            encode=sentinel.encode_output,
            metadata_output=MetadataWriter(True)(),
            commitment_policy=CommitmentPolicyArgs.
            require_encrypt_require_decrypt,
        ),
    )
    assert not patch_iohandler.return_value.process_dir.called
    assert not patch_iohandler.return_value.process_single_operation.called
    patch_iohandler.return_value.process_single_file.assert_called_once_with(
        stream_args={"mode": sentinel.mode},
        source=str(source),
        destination=str(destination))
def test_process_cli_request_source_dir_destination_dir(
        tmpdir, patch_iohandler):
    source = tmpdir.mkdir("source_dir")
    destination = tmpdir.mkdir("destination_dir")
    aws_encryption_sdk_cli.process_cli_request(
        stream_args=sentinel.stream_args,
        parsed_args=MagicMock(
            input=str(source),
            output=str(destination),
            recursive=True,
            interactive=sentinel.interactive,
            no_overwrite=sentinel.no_overwrite,
            suffix=sentinel.suffix,
            decode=sentinel.decode_input,
            encode=sentinel.encode_output,
            metadata_output=MetadataWriter(True)(),
            commitment_policy=CommitmentPolicyArgs.
            require_encrypt_require_decrypt,
        ),
    )

    patch_iohandler.return_value.process_dir.assert_called_once_with(
        stream_args=sentinel.stream_args,
        source=str(source),
        destination=str(destination),
        suffix=sentinel.suffix)
    assert not patch_iohandler.return_value.process_single_file.called
    assert not patch_iohandler.return_value.process_single_operation.called
def test_process_cli_request_no_commitment_policy(tmpdir, patch_iohandler):
    source = tmpdir.mkdir("source")
    destination = tmpdir.mkdir("destination")
    metadata_writer = MetadataWriter(True)()
    aws_encryption_sdk_cli.process_cli_request(
        stream_args=sentinel.stream_args,
        parsed_args=MagicMock(
            input=str(source),
            output=str(destination),
            recursive=False,
            interactive=sentinel.interactive,
            no_overwrite=sentinel.no_overwrite,
            metadata_output=metadata_writer,
            decode=sentinel.decode_input,
            encode=sentinel.encode_output,
            encryption_context=sentinel.encryption_context,
            required_encryption_context_keys=sentinel.required_keys,
            commitment_policy=None,
        ),
    )

    patch_iohandler.assert_called_once_with(
        metadata_writer=metadata_writer,
        interactive=sentinel.interactive,
        no_overwrite=sentinel.no_overwrite,
        decode_input=sentinel.decode_input,
        encode_output=sentinel.encode_output,
        required_encryption_context=sentinel.encryption_context,
        required_encryption_context_keys=sentinel.required_keys,
        commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT,
    )
    assert not patch_iohandler.return_value.process_single_operation.called
    assert not patch_iohandler.return_value.process_dir.called
    assert not patch_iohandler.return_value.process_single_file.called
def test_catch_bad_metadata_file_requests_metadata_is_not_stdout_but_input_and_output_are_pipes(
        tmpdir):
    metadata_writer = MetadataWriter(suppress_output=False)(
        output_file=str(tmpdir.join("metadata")))

    aws_encryption_sdk_cli._catch_bad_metadata_file_requests(
        metadata_writer, "-", "-")
def test_process_cli_request_source_contains_directory_nonrecursive(tmpdir, patch_iohandler):
    plaintext_dir = tmpdir.mkdir("plaintext")
    test_file_a = plaintext_dir.join("testing.aa")
    test_file_a.write(b"some data here!")
    test_file_c = plaintext_dir.join("testing.cc")
    test_file_c.write(b"some data here!")
    plaintext_dir.mkdir("testing.bb")
    ciphertext_dir = tmpdir.mkdir("ciphertext")
    source = os.path.join(str(plaintext_dir), "testing.*")

    aws_encryption_sdk_cli.process_cli_request(
        stream_args={"mode": "encrypt"},
        parsed_args=MagicMock(
            input=source,
            output=str(ciphertext_dir),
            recursive=False,
            interactive=False,
            no_overwrite=False,
            encode=False,
            decode=False,
            metadata_output=MetadataWriter(True)(),
            commitment_policy=CommitmentPolicyArgs.REQUIRE_ENCRYPT_REQUIRE_DECRYPT,
            buffer=False,
        ),
    )

    assert not patch_iohandler.return_value.process_dir.called
    patch_iohandler.return_value.process_single_file.assert_has_calls(
        calls=[
            call(stream_args={"mode": "encrypt"}, source=str(source_file), destination=ANY)
            for source_file in (test_file_a, test_file_c)
        ],
        any_order=True,
    )
def test_catch_bad_metadata_file_requests_metadata_is_dir(tmpdir):
    metadata_writer = MetadataWriter(suppress_output=False)(output_file=str(tmpdir))

    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli._catch_bad_metadata_file_requests(metadata_writer, "-", "-")

    excinfo.match(r"Metadata output cannot be a directory")
def test_catch_bad_metadata_file_requests_metadata_and_output_are_stdout():
    metadata_writer = MetadataWriter(suppress_output=False)(output_file="-")

    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli._catch_bad_metadata_file_requests(metadata_writer, "-", "-")

    excinfo.match(r"Metadata output cannot be stdout when output is stdout")
def test_catch_bad_metadata_file_requests_metadata_all_are_unique_files(tmpdir):
    source = tmpdir.join("source")
    metadata_file = tmpdir.join("metadata")
    destination = tmpdir.join("destination")

    metadata_writer = MetadataWriter(suppress_output=False)(output_file=str(metadata_file))

    aws_encryption_sdk_cli._catch_bad_metadata_file_requests(metadata_writer, str(source), str(destination))
def test_catch_bad_metadata_file_requests_metadata_in_source_or_dest_dir(tmpdir, match):
    source = tmpdir.mkdir("source")
    destination = tmpdir.mkdir("destination")
    if match == "input":
        metadata_file = source.join("metadata")
    else:
        metadata_file = destination.join("metadata")

    metadata_writer = MetadataWriter(suppress_output=False)(output_file=str(metadata_file))

    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli._catch_bad_metadata_file_requests(metadata_writer, str(source), str(destination))

    excinfo.match(r"Metadata output file cannot be in the {} directory".format(match))
def test_catch_bad_metadata_file_requests_metadata_is_source_or_dest(
    tmpdir, metadata_is_symlink, match_is_symlink, match
):
    if match == "source":
        source, metadata_file = build_same_files_and_dirs(tmpdir, metadata_is_symlink, match_is_symlink, True)
        destination = tmpdir.join("destination")
    else:
        source = tmpdir.join("source")
        destination, metadata_file = build_same_files_and_dirs(tmpdir, metadata_is_symlink, match_is_symlink, True)

    metadata_writer = MetadataWriter(suppress_output=False)(output_file=str(metadata_file))

    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli._catch_bad_metadata_file_requests(metadata_writer, str(source), str(destination))

    excinfo.match(r"Metadata output file cannot be the input or output")
def test_process_cli_request_source_stdin(tmpdir, patch_iohandler):
    destination = tmpdir.join("destination")
    mock_parsed_args = MagicMock(
        input="-",
        output=str(destination),
        recursive=False,
        interactive=sentinel.interactive,
        no_overwrite=sentinel.no_overwrite,
        decode=sentinel.decode_input,
        encode=sentinel.encode_output,
        metadata_output=MetadataWriter(True)(),
        commitment_policy=CommitmentPolicyArgs.REQUIRE_ENCRYPT_REQUIRE_DECRYPT,
        buffer=sentinel.buffer_output,
    )
    aws_encryption_sdk_cli.process_cli_request(stream_args=sentinel.stream_args, parsed_args=mock_parsed_args)
    assert not patch_iohandler.return_value.process_dir.called
    assert not patch_iohandler.return_value.process_single_file.called
    patch_iohandler.return_value.process_single_operation.assert_called_once_with(
        stream_args=sentinel.stream_args, source="-", destination=str(destination)
    )
def test_process_cli_request_invalid_source(tmpdir):
    target = os.path.join(str(tmpdir), "test_targets.*")
    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli.process_cli_request(
            stream_args={},
            parsed_args=MagicMock(
                input=target,
                output="a specific destination",
                recursive=False,
                interactive=False,
                no_overwrite=False,
                decode=False,
                encode=False,
                metadata_output=MetadataWriter(True)(),
                encryption_context={},
                required_encryption_context_keys=[],
            ),
        )
    excinfo.match(
        r"Invalid source.  Must be a valid pathname pattern or stdin \(-\)")
def test_process_cli_request_source_dir_destination_nondir(tmpdir):
    source = tmpdir.mkdir("source")
    with pytest.raises(BadUserArgumentError) as excinfo:
        aws_encryption_sdk_cli.process_cli_request(
            stream_args={"mode": "encrypt"},
            parsed_args=MagicMock(
                input=str(source),
                output=str(tmpdir.join("destination")),
                recursive=True,
                interactive=False,
                no_overwrite=False,
                decode=False,
                encode=False,
                metadata_output=MetadataWriter(True)(),
                encryption_context={},
                required_encryption_context_keys=[],
            ),
        )
    excinfo.match(
        r"If operating on a source directory, destination must be an existing directory"
    )
def _build_parser():
    # type: () -> CommentIgnoringArgumentParser
    """Builds the argument parser.

    :returns: Constructed argument parser
    :rtype: argparse.ArgumentParser
    """
    parser = CommentIgnoringArgumentParser(
        description="Encrypt or decrypt data using the AWS Encryption SDK",
        epilog="For more usage instructions and examples, see: http://aws-encryption-sdk-cli.readthedocs.io/en/latest/",
        fromfile_prefix_chars="@",
    )

    # For each argument added to this group, a dummy redirect argument must
    # be added to the parent parser for each long form option string.
    version_or_action = parser.add_mutually_exclusive_group(required=True)

    version_or_action.add_argument("--version", action="version", version=_version_report())
    parser.add_dummy_redirect_argument("--version")

    # For each argument added to this group, a dummy redirect argument must
    # be added to the parent parser for each long form option string.
    operating_action = version_or_action.add_mutually_exclusive_group()
    operating_action.add_argument(
        "-e", "--encrypt", dest="action", action="store_const", const="encrypt", help="Encrypt data"
    )
    parser.add_dummy_redirect_argument("--encrypt")
    operating_action.add_argument(
        "-d", "--decrypt", dest="action", action="store_const", const="decrypt", help="Decrypt data"
    )
    parser.add_dummy_redirect_argument("--decrypt")

    # For each argument added to this group, a dummy redirect argument must
    # be added to the parent parser for each long form option string.
    metadata_group = parser.add_mutually_exclusive_group(required=True)

    metadata_group.add_argument(
        "-S",
        "--suppress-metadata",
        action="store_const",
        const=MetadataWriter(suppress_output=True)(),
        dest="metadata_output",
        help="Suppress metadata output.",
    )
    parser.add_dummy_redirect_argument("--suppress-metadata")

    metadata_group.add_argument(
        "--metadata-output", type=MetadataWriter(), help="File to which to write metadata records"
    )
    parser.add_dummy_redirect_argument("--metadata-output")

    parser.add_argument(
        "--overwrite-metadata",
        action="store_true",
        help="Force metadata output to overwrite contents of file rather than appending to file",
    )

    parser.add_argument(
        "-m",
        "--master-keys",
        nargs="+",
        action="append",
        required=False,
        help=(
            "Identifying information for a master key provider and master keys. Each instance must include "
            "a master key provider identifier and identifiers for one or more master key supplied by that provider. "
            "ex: "
            "--master-keys provider=aws-kms key=$AWS_KMS_KEY_ARN"
        ),
    )

    parser.add_argument(
        "--caching",
        nargs="+",
        required=False,
        action=UniqueStoreAction,
        help=(
            "Configuration options for a caching cryptographic materials manager and local cryptographic materials "
            'cache. Must consist of "key=value" pairs. If caching, at least "capacity" and "max_age" must be defined. '
            "ex: "
            "--caching capacity=10 max_age=100.0"
        ),
    )

    parser.add_argument(
        "-i",
        "--input",
        required=True,
        action=UniqueStoreAction,
        help='Input file or directory for encrypt/decrypt operation, or "-" for stdin.',
    )
    parser.add_argument(
        "-o",
        "--output",
        required=True,
        action=UniqueStoreAction,
        help="Output file or directory for encrypt/decrypt operation, or - for stdout.",
    )

    parser.add_argument("--encode", action="store_true", help="Base64-encode output after processing")
    parser.add_argument("--decode", action="store_true", help="Base64-decode input before processing")

    parser.add_argument(
        "-c",
        "--encryption-context",
        nargs="+",
        action=UniqueStoreAction,
        help=(
            'key-value pair encryption context values (encryption only). Must a set of "key=value" pairs. '
            "ex: "
            "-c key1=value1 key2=value2"
        ),
    )

    # Note: This is added as an argument for argparse API consistency, but it should not be used directly.
    parser.add_argument(
        "--required-encryption-context-keys", nargs="+", action=UniqueStoreAction, help=argparse.SUPPRESS
    )

    parser.add_argument(
        "--algorithm", action=UniqueStoreAction, help="Algorithm name (encryption only)", choices=ALGORITHM_NAMES
    )

    parser.add_argument(
        "--frame-length",
        dest="frame_length",
        type=int,
        action=UniqueStoreAction,
        help="Frame length in bytes (encryption only)",
    )

    parser.add_argument(
        "--max-length",
        type=int,
        action=UniqueStoreAction,
        help=(
            "Maximum frame length (for framed messages) or content length (for "
            "non-framed messages) (decryption only)"
        ),
    )

    parser.add_argument(
        "--suffix",
        nargs="?",
        const="",
        action=UniqueStoreAction,
        help="Custom suffix to use when target filename is not specified (empty if specified but no value provided)",
    )

    parser.add_argument(
        "--interactive",
        action="store_true",
        help="Force aws-encryption-cli to prompt you for verification before overwriting existing files",
    )

    parser.add_argument("--no-overwrite", action="store_true", help="Never overwrite existing files")

    parser.add_argument("-r", "-R", "--recursive", action="store_true", help="Allow operation on directories as input")

    parser.add_argument(
        "-v",
        dest="verbosity",
        action="count",
        help="Enables logging and sets detail level. Multiple -v options increases verbosity (max: 4).",
    )
    parser.add_argument("-q", "--quiet", action="store_true", help="Suppresses most warning and diagnostic messages")
    return parser
def test_catch_bad_metadata_file_requests_metadata_metadata_is_stdout_but_output_is_not(
):
    metadata_writer = MetadataWriter(suppress_output=False)(output_file="-")

    aws_encryption_sdk_cli._catch_bad_metadata_file_requests(
        metadata_writer, "not-std-in", "not-std-out")