From 512843d4645c37a83afa45dc12f35795461cad80 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 4 Sep 2026 13:23:09 -0400 Subject: [PATCH 1/5] Add more blackbox test coverage. --- requirements-test-blackbox-lock.txt | 5 + requirements-test-blackbox.txt | 1 + tests/blackbox/s3_assertions.py | 2 +- tests/blackbox/test_cp_command.py | 220 ++++++++++++++++++++++++---- tests/blackbox/test_mv_command.py | 152 ++++++++++++++++++- 5 files changed, 349 insertions(+), 31 deletions(-) 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/s3_assertions.py b/tests/blackbox/s3_assertions.py index 5d450d233271..be1a09f29195 100644 --- a/tests/blackbox/s3_assertions.py +++ b/tests/blackbox/s3_assertions.py @@ -1,10 +1,10 @@ """Per-operation S3 assertion helpers.""" - from __future__ import annotations import xml.etree.ElementTree as ET from urllib.parse import parse_qs, urlparse + # Known list wrapper elements in S3 XML request bodies. _LIST_ELEMENTS = {"Tags", "TagSet", "Parts"} diff --git a/tests/blackbox/test_cp_command.py b/tests/blackbox/test_cp_command.py index 17bc7e9ce263..f1e835dab541 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,17 @@ 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 - assert ( - b'500\xc3\x97500.jpg' in req.wire_raw_bytes - ), "Expected UTF-8 encoded \u00d7 in wire data" + # 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 "500\u00d7500.jpg" in cd, ( + f"Expected \u00d7 in Content-Disposition, got {cd!r}" + ) @pytest.mark.asyncio @@ -5787,7 +5797,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 +5831,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 +5864,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 +5897,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 +5930,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 +5963,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 +6000,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 +6037,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 +6076,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 +6118,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 +6148,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 +6174,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 +6207,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 +6240,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 +6273,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 +6299,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 +6309,158 @@ 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(): + await server.next_request() # HeadObject completes + # 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"BadDigest" in stderr or b"upload failed" 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 +6486,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_mv_command.py b/tests/blackbox/test_mv_command.py index a00bb0ebe0a1..373589bc3bf2 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, @@ -999,6 +1000,155 @@ async def test_no_overwrite_flag_on_mv_download_when_single_object_does_not_exis 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(): + await server.next_request() # HeadObject completes + 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"BadDigest" in stderr or b"upload failed" in stderr + # Source file should NOT be deleted on failed upload + assert src.exists() + + def get_access_point_response(bucket: str) -> HTTPResponse: body = ( '' From 5d56928ded3b2d1117da312e7f8f24dd352ca3a7 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 4 Sep 2026 14:56:10 -0400 Subject: [PATCH 2/5] Fix mb test for non-ASCIi body. --- tests/blackbox/test_mb_command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/blackbox/test_mb_command.py b/tests/blackbox/test_mb_command.py index 8934ebc27bba..9f185e0c78a6 100644 --- a/tests/blackbox/test_mb_command.py +++ b/tests/blackbox/test_mb_command.py @@ -320,6 +320,6 @@ async def test_create_bucket_with_non_ascii_tag_value(aws_cli): assert rc == 0, stderr.decode() 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 - ), f"Expected non-ASCII tag value in body, got: {body_text[:200]}" + assert "José" in body_text, ( + f"Expected non-ASCII tag value in body, got: {body_text[:200]}" + ) From 784fd21ab756deaf01d21010463b0a5c4eb86a6c Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 4 Sep 2026 15:28:44 -0400 Subject: [PATCH 3/5] Improve assertion for cp/mv tests. --- tests/blackbox/test_cp_command.py | 5 ++++- tests/blackbox/test_mv_command.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/blackbox/test_cp_command.py b/tests/blackbox/test_cp_command.py index f1e835dab541..67e5c3159ba7 100644 --- a/tests/blackbox/test_cp_command.py +++ b/tests/blackbox/test_cp_command.py @@ -6458,7 +6458,10 @@ async def test_multipart_upload_part_rejected_by_server(aws_cli, tmp_path): assert rc == 1 assert len(server.requests) == 4, format_requests(server) - assert b"BadDigest" in stderr or b"upload failed" in stderr + assert ( + b"An error occurred (BadDigest) when " + b"calling the UploadPart operation" in stderr + ) @pytest.mark.asyncio diff --git a/tests/blackbox/test_mv_command.py b/tests/blackbox/test_mv_command.py index 373589bc3bf2..b3419f57f4e4 100644 --- a/tests/blackbox/test_mv_command.py +++ b/tests/blackbox/test_mv_command.py @@ -1144,7 +1144,10 @@ async def test_mv_multipart_upload_part_rejected_by_server(aws_cli, tmp_path): assert rc == 1 assert len(server.requests) == 4, format_requests(server) - assert b"BadDigest" in stderr or b"upload failed" in stderr + 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() From 89c5e54f34fe42891b4ddfe4202b41196cc43a21 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 4 Sep 2026 15:32:20 -0400 Subject: [PATCH 4/5] Reduce usage of inline comments. --- tests/blackbox/test_cp_command.py | 3 ++- tests/blackbox/test_mv_command.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/blackbox/test_cp_command.py b/tests/blackbox/test_cp_command.py index 67e5c3159ba7..3d7cb1260cdb 100644 --- a/tests/blackbox/test_cp_command.py +++ b/tests/blackbox/test_cp_command.py @@ -6404,7 +6404,8 @@ async def test_download_content_length_mismatch_fails(aws_cli, tmp_path): ) async def inject_fault(): - await server.next_request() # HeadObject completes + # 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( diff --git a/tests/blackbox/test_mv_command.py b/tests/blackbox/test_mv_command.py index b3419f57f4e4..053cb0c9532f 100644 --- a/tests/blackbox/test_mv_command.py +++ b/tests/blackbox/test_mv_command.py @@ -720,7 +720,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, @@ -960,7 +962,9 @@ async def test_no_overwrite_flag_on_mv_download_when_single_object_exists_at_tar 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, @@ -1095,7 +1099,8 @@ async def test_mv_download_content_length_mismatch_fails(aws_cli, tmp_path): ) async def inject_fault(): - await server.next_request() # HeadObject completes + # HeadObject completes + await server.next_request() server.set_transmission_strategy( FaultyTransmission([DropConnection(after_bytes=3)]) ) From 27cfed8a99e1a6b87997c7e150b04c5d4cb2a509 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 4 Sep 2026 15:34:25 -0400 Subject: [PATCH 5/5] Formatting. --- tests/blackbox/s3_assertions.py | 2 +- tests/blackbox/test_cp_command.py | 15 +++---- tests/blackbox/test_mb_command.py | 6 +-- tests/blackbox/test_mv_command.py | 72 +++++++++---------------------- 4 files changed, 32 insertions(+), 63 deletions(-) diff --git a/tests/blackbox/s3_assertions.py b/tests/blackbox/s3_assertions.py index be1a09f29195..5d450d233271 100644 --- a/tests/blackbox/s3_assertions.py +++ b/tests/blackbox/s3_assertions.py @@ -1,10 +1,10 @@ """Per-operation S3 assertion helpers.""" + from __future__ import annotations import xml.etree.ElementTree as ET from urllib.parse import parse_qs, urlparse - # Known list wrapper elements in S3 XML request bodies. _LIST_ELEMENTS = {"Tags", "TagSet", "Parts"} diff --git a/tests/blackbox/test_cp_command.py b/tests/blackbox/test_cp_command.py index 3d7cb1260cdb..f2e8151a63f1 100644 --- a/tests/blackbox/test_cp_command.py +++ b/tests/blackbox/test_cp_command.py @@ -5763,14 +5763,13 @@ async def test_content_disposition_non_ascii(aws_cli, tmp_path): assert len(server.requests) == 1, format_requests(server) req = server.requests[0] # 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") + cd = req.headers.get("Content-Disposition") or req.headers.get( + "content-disposition" ) assert cd is not None, "Content-Disposition header missing" - assert "500\u00d7500.jpg" in cd, ( - f"Expected \u00d7 in Content-Disposition, got {cd!r}" - ) + assert ( + "500\u00d7500.jpg" in cd + ), f"Expected \u00d7 in Content-Disposition, got {cd!r}" @pytest.mark.asyncio @@ -6342,8 +6341,8 @@ async def test_download_checksum_mismatch_fails(aws_cli, tmp_path): assert rc == 1 assert len(server.requests) == 2, format_requests(server) assert ( - b"Expected checksum AAAAAA== did not " - b"match calculated checksum: jHNlIQ==" + b"Expected checksum AAAAAA== did not " + b"match calculated checksum: jHNlIQ==" ) in stderr diff --git a/tests/blackbox/test_mb_command.py b/tests/blackbox/test_mb_command.py index 9f185e0c78a6..cebd52590a97 100644 --- a/tests/blackbox/test_mb_command.py +++ b/tests/blackbox/test_mb_command.py @@ -320,6 +320,6 @@ async def test_create_bucket_with_non_ascii_tag_value(aws_cli): assert rc == 0, stderr.decode() req = server.requests[0] body_text = req.body.decode("utf-8") if req.body else "" - assert "José" in body_text, ( - f"Expected non-ASCII tag value in body, got: {body_text[:200]}" - ) + assert ( + "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 053cb0c9532f..311d1e97ef82 100644 --- a/tests/blackbox/test_mv_command.py +++ b/tests/blackbox/test_mv_command.py @@ -37,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, @@ -176,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", @@ -213,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 @@ -459,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 @@ -534,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 @@ -749,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", @@ -795,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", @@ -953,9 +941,7 @@ 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" @@ -992,12 +978,8 @@ 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" ) @@ -1036,9 +1018,9 @@ async def test_mv_download_checksum_mismatch_fails(aws_cli, tmp_path): 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 + b"Expected checksum AAAAAA== did not " + b"match calculated checksum: jHNlIQ==" + ) in stderr @pytest.mark.asyncio @@ -1069,8 +1051,8 @@ async def test_mv_upload_checksum_rejected_by_server(aws_cli, tmp_path): 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 + 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() @@ -1503,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 @@ -1549,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 @@ -1593,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