diff --git a/requirements-test-blackbox-lock.txt b/requirements-test-blackbox-lock.txt index acdb388276c0..cdcfde49600f 100644 --- a/requirements-test-blackbox-lock.txt +++ b/requirements-test-blackbox-lock.txt @@ -282,7 +282,12 @@ pytest==8.4.0 \ --hash=sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e # via # -r requirements-test-blackbox.txt + # pytest-asyncio # pytest-xdist +pytest-asyncio==1.4.0 \ + --hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 \ + --hash=sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42 + # via -r requirements-test-blackbox.txt pytest-xdist==3.6.1 \ --hash=sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7 \ --hash=sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d diff --git a/requirements-test-blackbox.txt b/requirements-test-blackbox.txt index 838348956847..04bc7c1b3c36 100644 --- a/requirements-test-blackbox.txt +++ b/requirements-test-blackbox.txt @@ -1,5 +1,6 @@ pytest==8.4.0 pytest-xdist==3.6.1 +pytest-asyncio==1.4.0 pip-tools==7.6.1 # localstub is a required dependency of # blackbox tests. diff --git a/tests/blackbox/test_cp_command.py b/tests/blackbox/test_cp_command.py index 17bc7e9ce263..f2e8151a63f1 100644 --- a/tests/blackbox/test_cp_command.py +++ b/tests/blackbox/test_cp_command.py @@ -2,11 +2,12 @@ from __future__ import annotations +import asyncio import os import pytest from localstub.handlers import handle_expect_header -from localstub.server import HTTPResponse +from localstub.server import DropConnection, FaultyTransmission, HTTPResponse from tests.blackbox.s3_assertions import ( assert_abort_multipart_upload, @@ -5440,6 +5441,7 @@ async def test_upload_key_with_spaces(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() + assert len(server.requests) == 1, format_requests(server) # Space must be percent-encoded as %20, not + or literal space assert server.requests[0].effective_path == "/my%20file.txt" @@ -5475,6 +5477,7 @@ async def test_download_unicode_key_from_s3(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() + assert len(server.requests) == 2, format_requests(server) assert (tmp_path / "données.txt").exists() @@ -5517,6 +5520,7 @@ async def test_user_agent_contains_cli_version(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() + assert len(server.requests) == 1, format_requests(server) ua = server.requests[0].headers.get("user-agent") assert ua is not None, "User-Agent header missing" assert "aws-cli/" in ua, f"Expected 'aws-cli/' in User-Agent: {ua}" @@ -5539,6 +5543,7 @@ async def test_user_agent_contains_command(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() + assert len(server.requests) == 1, format_requests(server) ua = server.requests[0].headers.get("user-agent") assert "s3.cp" in ua, f"Expected 's3.cp' in User-Agent: {ua}" @@ -5560,7 +5565,7 @@ async def test_acl_private(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", Key="foo.txt", ACL="private" ) @@ -5590,7 +5595,7 @@ async def test_acl_public_read(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", Key="foo.txt", ACL="public-read" ) @@ -5620,7 +5625,7 @@ async def test_acl_bucket_owner_full_control(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5653,7 +5658,7 @@ async def test_content_type_override(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5686,7 +5691,7 @@ async def test_content_type_html(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5719,7 +5724,7 @@ async def test_content_disposition(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5755,12 +5760,16 @@ async def test_content_disposition_non_ascii(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) req = server.requests[0] - # The × character (U+00D7) is sent as UTF-8 bytes \xc3\x97 on the wire + # The × character (U+00D7) is sent as UTF-8 bytes on the wire + cd = req.headers.get("Content-Disposition") or req.headers.get( + "content-disposition" + ) + assert cd is not None, "Content-Disposition header missing" assert ( - b'500\xc3\x97500.jpg' in req.wire_raw_bytes - ), "Expected UTF-8 encoded \u00d7 in wire data" + "500\u00d7500.jpg" in cd + ), f"Expected \u00d7 in Content-Disposition, got {cd!r}" @pytest.mark.asyncio @@ -5787,7 +5796,7 @@ async def test_content_encoding(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) # Content-Encoding on the wire combines user value with aws-chunked assert_put_object( server.requests[0], @@ -5821,7 +5830,7 @@ async def test_content_language(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5854,7 +5863,7 @@ async def test_cache_control(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5887,7 +5896,7 @@ async def test_expires(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5920,7 +5929,7 @@ async def test_metadata_single_key(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5953,7 +5962,7 @@ async def test_metadata_multiple_keys(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -5990,7 +5999,7 @@ async def test_metadata_directive_replace(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 2 + assert len(server.requests) == 2, format_requests(server) assert_copy_object( server.requests[1], Bucket="dst", @@ -6027,7 +6036,7 @@ async def test_metadata_directive_copy(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 2 + assert len(server.requests) == 2, format_requests(server) assert_copy_object( server.requests[1], Bucket="dst", @@ -6066,7 +6075,7 @@ async def test_metadata_directive_replace_with_metadata(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 2 + assert len(server.requests) == 2, format_requests(server) assert_copy_object( server.requests[1], Bucket="dst", @@ -6108,7 +6117,7 @@ async def test_combined_metadata_params(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -6138,7 +6147,7 @@ async def test_content_type_auto_guessed_from_extension(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -6164,7 +6173,7 @@ async def test_content_type_auto_guessed_json(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -6197,7 +6206,7 @@ async def test_metadata_value_with_spaces(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -6230,7 +6239,7 @@ async def test_metadata_value_with_equals(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -6263,7 +6272,7 @@ async def test_metadata_empty_value(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) assert_put_object( server.requests[0], Bucket="bucket", @@ -6289,7 +6298,7 @@ async def test_expires_numeric_value(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 1 + assert len(server.requests) == 1, format_requests(server) # CLI interprets "90" as Unix timestamp (90 seconds since epoch) assert_put_object( server.requests[0], @@ -6299,6 +6308,162 @@ async def test_expires_numeric_value(aws_cli, tmp_path): ) +@pytest.mark.asyncio +async def test_download_checksum_mismatch_fails(aws_cli, tmp_path): + """cp with --checksum-mode ENABLED fails if checksum doesn't match body.""" + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + head_object_response(), + # Body is b"foo" but checksum is wrong + get_object_response( + b"foo", **{"x-amz-checksum-crc32": "AAAAAA=="} + ), + ], + ) + stdout, stderr, rc = await run_cli( + aws_cli, + [ + "s3", + "cp", + "s3://bucket/key.txt", + str(tmp_path), + "--checksum-mode", + "ENABLED", + ], + cli_env(proxy), + ) + + assert rc == 1 + assert len(server.requests) == 2, format_requests(server) + assert ( + b"Expected checksum AAAAAA== did not " + b"match calculated checksum: jHNlIQ==" + ) in stderr + + +@pytest.mark.asyncio +async def test_upload_checksum_rejected_by_server(aws_cli, tmp_path): + """cp upload fails when server rejects with BadDigest.""" + src = tmp_path / "foo.txt" + src.write_text("content") + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + error_response( + "BadDigest", + "The CRC32 you specified did not match the calculated checksum.", + status=400, + ), + ], + ) + stdout, stderr, rc = await run_cli( + aws_cli, + ["s3", "cp", str(src), "s3://bucket/key.txt"], + cli_env(proxy), + ) + + assert rc == 1 + assert len(server.requests) == 1, format_requests(server) + assert ( + b"The CRC32 you specified did not " + b"match the calculated checksum." in stderr + ) + + +@pytest.mark.asyncio +async def test_download_content_length_mismatch_fails(aws_cli, tmp_path): + """cp download fails when body is shorter than Content-Length header.""" + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + head_object_response(content_length=100), + # Content-Length says 100 but body is only 3 bytes + HTTPResponse.raw( + b"foo", + status=200, + headers={ + "Content-Length": "100", + "ETag": '"foo-1"', + }, + ), + ], + ) + + async def inject_fault(): + # HeadObject completes + await server.next_request() + # Drop connection after sending the short body so the + # CLI sees EOF instead of blocking for remaining bytes. + server.set_transmission_strategy( + FaultyTransmission([DropConnection(after_bytes=3)]) + ) + + (stdout, stderr, rc), _ = await asyncio.gather( + run_cli( + aws_cli, + ["s3", "cp", "s3://bucket/key.txt", str(tmp_path)], + cli_env(proxy), + ), + inject_fault(), + ) + + assert rc == 1 + assert len(server.requests) == 2, format_requests(server) + assert ( + b"download failed" in stderr + and b"Connection broken: IncompleteRead" in stderr + ) + + +@pytest.mark.asyncio +async def test_multipart_upload_part_rejected_by_server(aws_cli, tmp_path): + """cp multipart upload fails when server rejects a part with BadDigest.""" + src = tmp_path / "foo.txt" + src.write_bytes(b"a" * 10 * (1024**2)) + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + create_mpu_response("foo"), + upload_part_response("etag1"), + error_response( + "BadDigest", + "The CRC32 you specified did not match the calculated checksum.", + status=400, + ), + abort_mpu_response(), + ], + ) + stdout, stderr, rc = await run_cli( + aws_cli, + ["s3", "cp", str(src), "s3://bucket/key.txt"], + cli_env(proxy), + ) + + assert rc == 1 + assert len(server.requests) == 4, format_requests(server) + assert ( + b"An error occurred (BadDigest) when " + b"calling the UploadPart operation" in stderr + ) + + @pytest.mark.asyncio async def test_content_type_not_guessed_on_s3_to_s3_copy(aws_cli, tmp_path): """cp s3->s3 without --content-type does NOT guess Content-Type. @@ -6324,7 +6489,7 @@ async def test_content_type_not_guessed_on_s3_to_s3_copy(aws_cli, tmp_path): ) assert rc == 0, stderr.decode() - assert len(server.requests) == 2 + assert len(server.requests) == 2, format_requests(server) # CopyObject should NOT have a Content-Type header set by the CLI req = server.requests[1] ct = req.headers.get("Content-Type") or req.headers.get("content-type") diff --git a/tests/blackbox/test_mb_command.py b/tests/blackbox/test_mb_command.py index 8934ebc27bba..cebd52590a97 100644 --- a/tests/blackbox/test_mb_command.py +++ b/tests/blackbox/test_mb_command.py @@ -321,5 +321,5 @@ async def test_create_bucket_with_non_ascii_tag_value(aws_cli): req = server.requests[0] body_text = req.body.decode("utf-8") if req.body else "" assert ( - "José" in body_text or "Jos" in body_text + "José" in body_text ), f"Expected non-ASCII tag value in body, got: {body_text[:200]}" diff --git a/tests/blackbox/test_mv_command.py b/tests/blackbox/test_mv_command.py index a00bb0ebe0a1..311d1e97ef82 100644 --- a/tests/blackbox/test_mv_command.py +++ b/tests/blackbox/test_mv_command.py @@ -2,11 +2,12 @@ from __future__ import annotations +import asyncio import os import pytest from localstub.handlers import handle_expect_header -from localstub.server import HTTPResponse +from localstub.server import DropConnection, FaultyTransmission, HTTPResponse from tests.blackbox.s3_assertions import ( assert_abort_multipart_upload, @@ -36,10 +37,10 @@ get_object_tagging_response, head_object_response, list_objects_xml, + mock_server, put_object_response, put_object_tagging_response, run_cli, - mock_server, setup_responses, upload_part_copy_response, upload_part_response, @@ -175,9 +176,7 @@ async def test_metadata_directive_copy(self, aws_cli, tmp_path): assert rc == 0, stderr.decode() assert len(server.requests) == 3, format_requests(server) - assert_head_object( - server.requests[0], Bucket="bucket", Key="key.txt" - ) + assert_head_object(server.requests[0], Bucket="bucket", Key="key.txt") assert_copy_object( server.requests[1], Bucket="bucket", @@ -212,9 +211,7 @@ async def test_no_metadata_directive_for_non_copy(self, aws_cli, tmp_path): assert rc == 0, stderr.decode() assert len(server.requests) == 1, format_requests(server) - assert_put_object( - server.requests[0], Bucket="bucket", Key="key.txt" - ) + assert_put_object(server.requests[0], Bucket="bucket", Key="key.txt") # MetadataDirective header should NOT be present for uploads assert ( server.requests[0].headers.get("x-amz-metadata-directive") is None @@ -458,9 +455,7 @@ async def test_mv_does_not_delete_source_on_failed_put_tagging( server.requests[5], Bucket="bucket", Key="key" ) # The delete should be for the destination (cleanup), not the source - assert_delete_object( - server.requests[6], Bucket="bucket", Key="key" - ) + assert_delete_object(server.requests[6], Bucket="bucket", Key="key") async def test_upload_with_checksum_algorithm_crc32( self, aws_cli, tmp_path @@ -533,9 +528,7 @@ async def test_download_with_checksum_mode_crc32(self, aws_cli, tmp_path): Key="foo", ChecksumMode="ENABLED", ) - assert_delete_object( - server.requests[2], Bucket="bucket", Key="foo" - ) + assert_delete_object(server.requests[2], Bucket="bucket", Key="foo") async def test_mv_no_overwrite_flag_when_object_not_exists_on_target( self, aws_cli, tmp_path @@ -719,7 +712,9 @@ async def test_mv_no_overwrite_flag_multipart_upload_when_object_exists_on_targe async def test_mv_no_overwrite_flag_on_copy_when_small_object_does_not_exist_on_target( self, aws_cli, tmp_path ): - """mv s3->s3 --no-overwrite small object sends IfNoneMatch on CopyObject and deletes source.""" + """mv s3->s3 --no-overwrite small object sends + IfNoneMatch on CopyObject and deletes source. + """ async with mock_server(on_headers_received=handle_expect_header) as ( server, proxy, @@ -746,9 +741,7 @@ async def test_mv_no_overwrite_flag_on_copy_when_small_object_does_not_exist_on_ assert rc == 0, stderr.decode() assert len(server.requests) == 3, format_requests(server) - assert_head_object( - server.requests[0], Bucket="bucket1", Key="key.txt" - ) + assert_head_object(server.requests[0], Bucket="bucket1", Key="key.txt") assert_copy_object( server.requests[1], Bucket="bucket2", @@ -792,9 +785,7 @@ async def test_mv_no_overwrite_flag_on_copy_when_small_object_exists_on_target( assert rc == 0, stderr.decode() assert len(server.requests) == 2, format_requests(server) - assert_head_object( - server.requests[0], Bucket="bucket1", Key="key.txt" - ) + assert_head_object(server.requests[0], Bucket="bucket1", Key="key.txt") assert_copy_object( server.requests[1], Bucket="bucket2", @@ -950,16 +941,16 @@ async def test_no_overwrite_flag_on_mv_download_when_single_object_exists_at_tar assert rc == 0, stderr.decode() assert len(server.requests) == 1, format_requests(server) - assert_head_object( - server.requests[0], Bucket="bucket", Key="foo.txt" - ) + assert_head_object(server.requests[0], Bucket="bucket", Key="foo.txt") # File should retain original content (not overwritten) assert target.read_text() == "existing content" async def test_no_overwrite_flag_on_mv_download_when_single_object_does_not_exist_at_target( self, aws_cli, tmp_path ): - """mv s3://bucket/foo.txt local --no-overwrite downloads and deletes source if no local file.""" + """mv s3://bucket/foo.txt local --no-overwrite downloads and + deletes source if no local file. + """ target = tmp_path / "foo.txt" async with mock_server(on_headers_received=handle_expect_header) as ( server, @@ -987,18 +978,167 @@ async def test_no_overwrite_flag_on_mv_download_when_single_object_does_not_exis assert rc == 0, stderr.decode() assert len(server.requests) == 3, format_requests(server) - assert_head_object( - server.requests[0], Bucket="bucket", Key="foo.txt" - ) - assert_get_object( - server.requests[1], Bucket="bucket", Key="foo.txt" - ) + assert_head_object(server.requests[0], Bucket="bucket", Key="foo.txt") + assert_get_object(server.requests[1], Bucket="bucket", Key="foo.txt") assert_delete_object( server.requests[2], Bucket="bucket", Key="foo.txt" ) assert target.read_text() == "foo" +@pytest.mark.asyncio +async def test_mv_download_checksum_mismatch_fails(aws_cli, tmp_path): + """mv s3->local --checksum-mode ENABLED fails if checksum doesn't match body.""" + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + head_object_response(), + get_object_response( + b"foo", **{"x-amz-checksum-crc32": "AAAAAA=="} + ), + ], + ) + stdout, stderr, rc = await run_cli( + aws_cli, + [ + "s3", + "mv", + "s3://bucket/key.txt", + str(tmp_path), + "--checksum-mode", + "ENABLED", + ], + cli_env(proxy), + ) + + assert rc == 1 + assert len(server.requests) == 2, format_requests(server) + assert ( + b"Expected checksum AAAAAA== did not " + b"match calculated checksum: jHNlIQ==" + ) in stderr + + +@pytest.mark.asyncio +async def test_mv_upload_checksum_rejected_by_server(aws_cli, tmp_path): + """mv upload fails when server rejects with BadDigest.""" + src = tmp_path / "foo.txt" + src.write_text("content") + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + error_response( + "BadDigest", + "The CRC32 you specified did not match the calculated checksum.", + status=400, + ), + ], + ) + stdout, stderr, rc = await run_cli( + aws_cli, + ["s3", "mv", str(src), "s3://bucket/key.txt"], + cli_env(proxy), + ) + + assert rc == 1 + assert len(server.requests) == 1, format_requests(server) + assert ( + b"The CRC32 you specified did not " + b"match the calculated checksum." in stderr + ) + # Source file should NOT be deleted on failed upload + assert src.exists() + + +@pytest.mark.asyncio +async def test_mv_download_content_length_mismatch_fails(aws_cli, tmp_path): + """mv download fails when body is shorter than Content-Length header.""" + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + head_object_response(content_length=100), + HTTPResponse.raw( + b"foo", + status=200, + headers={ + "Content-Length": "100", + "ETag": '"foo-1"', + }, + ), + ], + ) + + async def inject_fault(): + # HeadObject completes + await server.next_request() + server.set_transmission_strategy( + FaultyTransmission([DropConnection(after_bytes=3)]) + ) + + (stdout, stderr, rc), _ = await asyncio.gather( + run_cli( + aws_cli, + ["s3", "mv", "s3://bucket/key.txt", str(tmp_path)], + cli_env(proxy), + ), + inject_fault(), + ) + + assert rc == 1 + assert len(server.requests) == 2, format_requests(server) + assert b"move failed" in stderr + + +@pytest.mark.asyncio +async def test_mv_multipart_upload_part_rejected_by_server(aws_cli, tmp_path): + """mv multipart upload fails when server rejects a part with BadDigest.""" + src = tmp_path / "foo.txt" + src.write_bytes(b"a" * 10 * (1024**2)) + async with mock_server(on_headers_received=handle_expect_header) as ( + server, + proxy, + ): + setup_responses( + server, + [ + create_mpu_response("foo"), + upload_part_response("etag1"), + error_response( + "BadDigest", + "The CRC32 you specified did not match the calculated checksum.", + status=400, + ), + abort_mpu_response(), + ], + ) + stdout, stderr, rc = await run_cli( + aws_cli, + ["s3", "mv", str(src), "s3://bucket/key.txt"], + cli_env(proxy), + ) + + assert rc == 1 + assert len(server.requests) == 4, format_requests(server) + assert ( + b"An error occurred (BadDigest) when " + b"calling the UploadPart operation" in stderr + ) + # Source file should NOT be deleted on failed upload + assert src.exists() + + def get_access_point_response(bucket: str) -> HTTPResponse: body = ( '' @@ -1345,17 +1485,13 @@ async def test_mv_works_if_access_point_arn_resolves_to_different_bucket( assert rc == 0, stderr.decode() assert len(server.requests) == 4, format_requests(server) assert_get_access_point(server.requests[0]) - assert_head_object( - server.requests[1], Bucket="bucket", Key="key" - ) + assert_head_object(server.requests[1], Bucket="bucket", Key="key") assert_copy_object( server.requests[2], Bucket="arn:aws:s3:us-west-2:123456789012:accesspoint/myaccesspoint", Key="key", ) - assert_delete_object( - server.requests[3], Bucket="bucket", Key="key" - ) + assert_delete_object(server.requests[3], Bucket="bucket", Key="key") async def test_mv_works_if_access_point_alias_resolves_to_different_bucket( self, aws_cli, tmp_path @@ -1391,17 +1527,13 @@ async def test_mv_works_if_access_point_alias_resolves_to_different_bucket( assert len(server.requests) == 5, format_requests(server) assert_get_caller_identity(server.requests[0]) assert_get_access_point(server.requests[1]) - assert_head_object( - server.requests[2], Bucket="bucket", Key="key" - ) + assert_head_object(server.requests[2], Bucket="bucket", Key="key") assert_copy_object( server.requests[3], Bucket="myaccesspoint-foobar-s3alias", Key="key", ) - assert_delete_object( - server.requests[4], Bucket="bucket", Key="key" - ) + assert_delete_object(server.requests[4], Bucket="bucket", Key="key") async def test_mv_works_if_outpost_access_point_arn_resolves_to_different_bucket( self, aws_cli, tmp_path @@ -1435,17 +1567,13 @@ async def test_mv_works_if_outpost_access_point_arn_resolves_to_different_bucket assert rc == 0, stderr.decode() assert len(server.requests) == 4, format_requests(server) assert_get_access_point(server.requests[0]) - assert_head_object( - server.requests[1], Bucket="bucket", Key="key" - ) + assert_head_object(server.requests[1], Bucket="bucket", Key="key") assert_copy_object( server.requests[2], Bucket="arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-foobar/accesspoint/myaccesspoint", Key="key", ) - assert_delete_object( - server.requests[3], Bucket="bucket", Key="key" - ) + assert_delete_object(server.requests[3], Bucket="bucket", Key="key") async def test_skips_validation_if_keys_are_different_accesspoint_arn( self, aws_cli, tmp_path