diff --git a/lib/ex_aws/s3.ex b/lib/ex_aws/s3.ex index e6c1d38..98154b6 100644 --- a/lib/ex_aws/s3.ex +++ b/lib/ex_aws/s3.ex @@ -229,12 +229,39 @@ defmodule ExAws.S3 do ) end - @doc "List metadata about all versions of the objects in a bucket." + @type list_object_versions_opts :: [ + {:delimiter, binary} + | {:key_marker, binary} + | {:version_id_marker, binary} + | {:max_keys, 0..1000} + | {:prefix, binary} + | {:encoding_type, binary} + ] + + @doc """ + List metadata about all versions of the objects in a bucket. + + Can be streamed. + + ## Examples + ``` + S3.list_object_versions("my-bucket") |> ExAws.request + + S3.list_object_versions("my-bucket") |> ExAws.stream! + S3.list_object_versions("my-bucket", prefix: "backup/") |> ExAws.stream! + ``` + """ @spec list_object_versions(bucket :: binary) :: ExAws.Operation.S3.t() - @spec list_object_versions(bucket :: binary, opts :: Keyword.t()) :: + @spec list_object_versions(bucket :: binary, opts :: list_object_versions_opts) :: ExAws.Operation.S3.t() + @params [:delimiter, :key_marker, :version_id_marker, :max_keys, :prefix, :encoding_type] def list_object_versions(bucket, opts \\ []) do - request(:get, bucket, "/", [resource: "versions", params: opts], + params = + opts + |> format_and_take(@params) + + request(:get, bucket, "/", [resource: "versions", params: params], + stream_builder: &ExAws.S3.Lazy.stream_object_versions!(bucket, opts, &1), parser: &ExAws.S3.Parsers.parse_object_versions/1 ) end diff --git a/lib/ex_aws/s3/lazy.ex b/lib/ex_aws/s3/lazy.ex index 8770dd8..8e32a2c 100644 --- a/lib/ex_aws/s3/lazy.ex +++ b/lib/ex_aws/s3/lazy.ex @@ -54,6 +54,37 @@ defmodule ExAws.S3.Lazy do ) end + def stream_object_versions!(bucket, opts, config) do + request_fun = fn fun_opts -> + ExAws.S3.list_object_versions(bucket, Keyword.merge(opts, fun_opts)) + |> ExAws.request!(config) + |> Map.get(:body) + end + + Stream.resource( + fn -> {request_fun, []} end, + fn + :quit -> + {:halt, nil} + + {fun, args} -> + case fun.(args) do + results = %{is_truncated: "true"} -> + {add_version_results(results), + {fun, + [ + key_marker: results[:next_key_marker], + version_id_marker: results[:next_version_id_marker] + ]}} + + results -> + {add_version_results(results), :quit} + end + end, + & &1 + ) + end + def add_results(results, opts) do case Keyword.get(opts, :stream_prefixes, nil) do nil -> results.contents @@ -68,4 +99,8 @@ defmodule ExAws.S3.Lazy do end def next_marker(%{next_marker: marker}), do: marker + + def add_version_results(results) do + (results[:versions] || []) ++ (results[:delete_markers] || []) + end end diff --git a/test/lib/s3_minio_test.exs b/test/lib/s3_minio_test.exs index 2bad7fc..9e9e045 100644 --- a/test/lib/s3_minio_test.exs +++ b/test/lib/s3_minio_test.exs @@ -1,110 +1,128 @@ defmodule ExAws.S3MinioTest do - use ExUnit.Case, async: false + use ExUnit.Case alias ExAws.S3 @moduletag :minio - @test_bucket "ex-aws-s3-test-#{:rand.uniform(1_000_000)}" @test_object "test-object.txt" @test_content "Hello MinIO from ExAws.S3" @test_webhook_target "testhook" setup do - # Ensure test bucket is clean - S3.delete_object(@test_bucket, @test_object) |> ExAws.request() - S3.delete_bucket(@test_bucket) |> ExAws.request() + # Generate unique bucket name for this test + bucket = "ex-aws-s3-test-#{System.unique_integer([:positive])}" on_exit(fn -> - # Cleanup after all tests - S3.delete_object(@test_bucket, @test_object) |> ExAws.request() - S3.delete_bucket(@test_bucket) |> ExAws.request() + # Cleanup: delete all object versions + # Note: list_object_versions works for both versioned and non-versioned buckets. + # For non-versioned buckets, objects have a version ID of "null" + case S3.list_object_versions(bucket) |> ExAws.request() do + {:ok, %{body: body}} -> + # Delete all versions + for version <- body[:versions] || [] do + S3.delete_object(bucket, version.key, version_id: version.version_id) + |> ExAws.request!() + end + + # Delete all delete markers + for marker <- body[:delete_markers] || [] do + S3.delete_object(bucket, marker.key, version_id: marker.version_id) + |> ExAws.request!() + end + + _ -> + :ok + end + + # Delete the bucket itself + S3.delete_bucket(bucket) |> ExAws.request!() end) - :ok + {:ok, bucket: bucket} end describe "Bucket operations" do - test "put_bucket creates bucket in MinIO" do - result = S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + test "put_bucket creates bucket in MinIO", %{bucket: bucket} do + result = S3.put_bucket(bucket, "us-east-1") |> ExAws.request() assert {:ok, _} = result end - test "list_objects returns empty list for new bucket" do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + test "list_objects returns empty list for new bucket", %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request() - {:ok, result} = S3.list_objects(@test_bucket) |> ExAws.request() + {:ok, result} = S3.list_objects(bucket) |> ExAws.request() assert result.body.contents == [] end - test "list_objects_v2 returns empty list for new bucket" do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + test "list_objects_v2 returns empty list for new bucket", %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request() - {:ok, result} = S3.list_objects_v2(@test_bucket) |> ExAws.request() + {:ok, result} = S3.list_objects_v2(bucket) |> ExAws.request() assert result.body.contents == [] end end describe "Object operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() on_exit(fn -> # Clean up test object from this describe block - S3.delete_object(@test_bucket, @test_object) |> ExAws.request() + S3.delete_object(bucket, @test_object) |> ExAws.request!() end) :ok end - test "put_object uploads content to MinIO" do - result = S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + test "put_object uploads content to MinIO", %{bucket: bucket} do + result = S3.put_object(bucket, @test_object, @test_content) |> ExAws.request() assert {:ok, %{status_code: 200}} = result end - test "get_object retrieves content from MinIO" do - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + test "get_object retrieves content from MinIO", %{bucket: bucket} do + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request() - {:ok, result} = S3.get_object(@test_bucket, @test_object) |> ExAws.request() + {:ok, result} = S3.get_object(bucket, @test_object) |> ExAws.request() assert result.body == @test_content end - test "head_object checks object existence in MinIO" do - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + test "head_object checks object existence in MinIO", %{bucket: bucket} do + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request() - result = S3.head_object(@test_bucket, @test_object) |> ExAws.request() + result = S3.head_object(bucket, @test_object) |> ExAws.request() assert {:ok, %{status_code: 200}} = result end - test "delete_object removes object from MinIO" do - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + test "delete_object removes object from MinIO", %{bucket: bucket} do + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request() - result = S3.delete_object(@test_bucket, @test_object) |> ExAws.request() + result = S3.delete_object(bucket, @test_object) |> ExAws.request() assert {:ok, %{status_code: 204}} = result # Verify object is gone - result = S3.get_object(@test_bucket, @test_object) |> ExAws.request() + result = S3.get_object(bucket, @test_object) |> ExAws.request() assert {:error, {:http_error, 404, _}} = result end - test "list_objects shows uploaded objects" do - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + test "list_objects shows uploaded objects", %{bucket: bucket} do + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request() - {:ok, result} = S3.list_objects(@test_bucket) |> ExAws.request() + {:ok, result} = S3.list_objects(bucket) |> ExAws.request() assert length(result.body.contents) == 1 assert List.first(result.body.contents).key == @test_object end - test "list_objects_v2 shows uploaded objects" do - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + test "list_objects_v2 shows uploaded objects", %{bucket: bucket} do + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request() - {:ok, result} = S3.list_objects_v2(@test_bucket) |> ExAws.request() + {:ok, result} = S3.list_objects_v2(bucket) |> ExAws.request() assert length(result.body.contents) == 1 assert List.first(result.body.contents).key == @test_object end - test "put_object with metadata and headers" do + test "put_object with metadata and headers", %{bucket: bucket} do result = - S3.put_object(@test_bucket, @test_object, @test_content, + S3.put_object(bucket, @test_object, @test_content, content_type: "text/plain", meta: [foo: "bar", baz: "qux"] ) @@ -113,7 +131,7 @@ defmodule ExAws.S3MinioTest do assert {:ok, %{status_code: 200}} = result # Verify metadata was set - {:ok, head_result} = S3.head_object(@test_bucket, @test_object) |> ExAws.request() + {:ok, head_result} = S3.head_object(bucket, @test_object) |> ExAws.request() headers = head_result.headers assert Enum.any?(headers, fn {k, v} -> @@ -131,43 +149,43 @@ defmodule ExAws.S3MinioTest do end describe "Object copy operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request!() on_exit(fn -> # Clean up any copied objects that might have been created - S3.delete_object(@test_bucket, "copied-#{@test_object}") |> ExAws.request() + S3.delete_object(bucket, "copied-#{@test_object}") |> ExAws.request!() end) :ok end - test "put_object_copy duplicates object in MinIO" do + test "put_object_copy duplicates object in MinIO", %{bucket: bucket} do dest_object = "copied-#{@test_object}" result = - S3.put_object_copy(@test_bucket, dest_object, @test_bucket, @test_object) + S3.put_object_copy(bucket, dest_object, bucket, @test_object) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Verify copy exists and has same content - {:ok, get_result} = S3.get_object(@test_bucket, dest_object) |> ExAws.request() + {:ok, get_result} = S3.get_object(bucket, dest_object) |> ExAws.request() assert get_result.body == @test_content end end describe "Multiple object operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() on_exit(fn -> # Clean up any objects that might have been created - case S3.list_objects(@test_bucket) |> ExAws.request() do + case S3.list_objects(bucket) |> ExAws.request() do {:ok, %{body: %{contents: objects}}} -> for object <- objects do - S3.delete_object(@test_bucket, object.key) |> ExAws.request() + S3.delete_object(bucket, object.key) |> ExAws.request!() end _ -> @@ -178,50 +196,50 @@ defmodule ExAws.S3MinioTest do :ok end - test "delete_multiple_objects removes multiple objects" do + test "delete_multiple_objects removes multiple objects", %{bucket: bucket} do objects = ["obj1.txt", "obj2.txt", "obj3.txt"] # Upload multiple objects for obj <- objects do - S3.put_object(@test_bucket, obj, "content for #{obj}") |> ExAws.request() + S3.put_object(bucket, obj, "content for #{obj}") |> ExAws.request() end # Verify they exist - {:ok, list_result} = S3.list_objects(@test_bucket) |> ExAws.request() + {:ok, list_result} = S3.list_objects(bucket) |> ExAws.request() assert length(list_result.body.contents) == 3 # Delete multiple objects - result = S3.delete_multiple_objects(@test_bucket, objects) |> ExAws.request() + result = S3.delete_multiple_objects(bucket, objects) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Verify they're gone - {:ok, list_result} = S3.list_objects(@test_bucket) |> ExAws.request() + {:ok, list_result} = S3.list_objects(bucket) |> ExAws.request() assert list_result.body.contents == [] end end describe "Object tagging operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() - S3.put_object(@test_bucket, @test_object, @test_content) |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() + S3.put_object(bucket, @test_object, @test_content) |> ExAws.request!() on_exit(fn -> # Clean up test object and any tags from this describe block - S3.delete_object(@test_bucket, @test_object) |> ExAws.request() + S3.delete_object(bucket, @test_object) |> ExAws.request!() end) :ok end - test "put_object_tagging and get_object_tagging work with MinIO" do + test "put_object_tagging and get_object_tagging work with MinIO", %{bucket: bucket} do tags = [environment: "test", team: "engineering"] # Set tags - result = S3.put_object_tagging(@test_bucket, @test_object, tags) |> ExAws.request() + result = S3.put_object_tagging(bucket, @test_object, tags) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Get tags - {:ok, get_result} = S3.get_object_tagging(@test_bucket, @test_object) |> ExAws.request() + {:ok, get_result} = S3.get_object_tagging(bucket, @test_object) |> ExAws.request() returned_tags = get_result.body assert %{tags: tags} = returned_tags @@ -229,33 +247,33 @@ defmodule ExAws.S3MinioTest do assert %{"environment" => "test", "team" => "engineering"} = tag_map end - test "delete_object_tagging removes tags from object" do + test "delete_object_tagging removes tags from object", %{bucket: bucket} do tags = [environment: "test"] # Set tags first - S3.put_object_tagging(@test_bucket, @test_object, tags) |> ExAws.request() + S3.put_object_tagging(bucket, @test_object, tags) |> ExAws.request() # Delete tags - result = S3.delete_object_tagging(@test_bucket, @test_object) |> ExAws.request() + result = S3.delete_object_tagging(bucket, @test_object) |> ExAws.request() assert {:ok, %{status_code: 204}} = result # Verify tags are gone - {:ok, get_result} = S3.get_object_tagging(@test_bucket, @test_object) |> ExAws.request() + {:ok, get_result} = S3.get_object_tagging(bucket, @test_object) |> ExAws.request() assert get_result.body == %{tags: []} end end describe "Bucket versioning operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() on_exit(fn -> # Clean up all versions of objects when versioning is enabled - case S3.list_object_versions(@test_bucket) |> ExAws.request() do + case S3.list_object_versions(bucket) |> ExAws.request() do {:ok, %{body: %{versions: versions}}} -> for version <- versions do - S3.delete_object(@test_bucket, version.key, version_id: version.version_id) - |> ExAws.request() + S3.delete_object(bucket, version.key, version_id: version.version_id) + |> ExAws.request!() end _ -> @@ -266,67 +284,171 @@ defmodule ExAws.S3MinioTest do :ok end - test "put_bucket_versioning enables versioning in MinIO" do + test "put_bucket_versioning enables versioning in MinIO", %{bucket: bucket} do # Enable versioning with proper XML version_config = "Enabled" - result = S3.put_bucket_versioning(@test_bucket, version_config) |> ExAws.request() + result = S3.put_bucket_versioning(bucket, version_config) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Get versioning status - {:ok, get_result} = S3.get_bucket_versioning(@test_bucket) |> ExAws.request() + {:ok, get_result} = S3.get_bucket_versioning(bucket) |> ExAws.request() versioning_config = get_result.body # MinIO returns raw XML, so check the string content assert versioning_config =~ "Enabled" # Upload same object twice to test versioning - S3.put_object(@test_bucket, @test_object, "version 1") |> ExAws.request() - S3.put_object(@test_bucket, @test_object, "version 2") |> ExAws.request() + S3.put_object(bucket, @test_object, "version 1") |> ExAws.request() + S3.put_object(bucket, @test_object, "version 2") |> ExAws.request() # List object versions - {:ok, versions_result} = S3.list_object_versions(@test_bucket) |> ExAws.request() + {:ok, versions_result} = S3.list_object_versions(bucket) |> ExAws.request() versions = versions_result.body.versions # Should have 2 versions of the same object assert length(versions) == 2 assert Enum.all?(versions, fn version -> version.key == @test_object end) end + + test "list_object_versions can be streamed", %{bucket: bucket} do + # Enable versioning + version_config = + "Enabled" + + S3.put_bucket_versioning(bucket, version_config) |> ExAws.request() + + # Upload multiple versions of multiple objects + for obj <- ["obj1.txt", "obj2.txt", "obj3.txt"] do + S3.put_object(bucket, obj, "#{obj} version 1") |> ExAws.request() + S3.put_object(bucket, obj, "#{obj} version 2") |> ExAws.request() + end + + # Stream all versions + versions = + S3.list_object_versions(bucket) + |> ExAws.stream!() + |> Enum.to_list() + + # Should have 6 versions total (3 objects × 2 versions each) + assert length(versions) == 6 + + assert Enum.all?(versions, fn version -> + version.key in ["obj1.txt", "obj2.txt", "obj3.txt"] + end) + end + + test "list_object_versions streams with pagination when max_keys is set", %{bucket: bucket} do + # Enable versioning + version_config = + "Enabled" + + S3.put_bucket_versioning(bucket, version_config) |> ExAws.request() + + # Upload 11 objects with 3 versions each = 33 total versions + # With max_keys=5, this gives: 5+5+5+5+5+5+3 (7 pages, last page has only 3 items) + object_names = for i <- 1..11, do: "obj#{i}.txt" + + for obj <- object_names do + S3.put_object(bucket, obj, "#{obj} version 1") |> ExAws.request() + S3.put_object(bucket, obj, "#{obj} version 2") |> ExAws.request() + S3.put_object(bucket, obj, "#{obj} version 3") |> ExAws.request() + end + + # Stream all versions with max_keys set to 5 to force multiple requests with uneven last page + versions = + S3.list_object_versions(bucket, max_keys: 5) + |> ExAws.stream!() + |> Enum.to_list() + + # Should have 33 versions total (11 objects × 3 versions each) + assert length(versions) == 33 + + # Verify all objects are present + unique_keys = versions |> Enum.map(& &1.key) |> Enum.uniq() |> Enum.sort() + assert length(unique_keys) == 11 + assert unique_keys == Enum.sort(object_names) + + # Verify each object has 3 versions + for obj <- object_names do + obj_versions = Enum.filter(versions, fn v -> v.key == obj end) + assert length(obj_versions) == 3 + end + end + + test "list_object_versions includes delete markers in stream", %{bucket: bucket} do + # Enable versioning + version_config = + "Enabled" + + S3.put_bucket_versioning(bucket, version_config) |> ExAws.request() + + # Upload multiple objects + for obj <- ["obj1.txt", "obj2.txt", "obj3.txt", "obj4.txt"] do + S3.put_object(bucket, obj, "#{obj} version 1") |> ExAws.request() + S3.put_object(bucket, obj, "#{obj} version 2") |> ExAws.request() + end + + # Delete some objects (creates delete markers) + S3.delete_object(bucket, "obj2.txt") |> ExAws.request() + S3.delete_object(bucket, "obj4.txt") |> ExAws.request() + + # Stream all versions and delete markers + all_entries = + S3.list_object_versions(bucket) + |> ExAws.stream!() + |> Enum.to_list() + + # Should have 8 versions (4 objects × 2 versions) + 2 delete markers = 10 total + assert length(all_entries) == 10 + + # Separate versions from delete markers using split_with + {versions, delete_markers} = + Enum.split_with(all_entries, fn entry -> Map.has_key?(entry, :size) end) + + # Verify we have the expected counts + assert length(versions) == 8 + assert length(delete_markers) == 2 + + # Verify delete markers are for the deleted objects + delete_marker_keys = Enum.map(delete_markers, & &1.key) |> Enum.sort() + assert delete_marker_keys == ["obj2.txt", "obj4.txt"] + end end describe "Multipart upload operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() on_exit(fn -> # Clean up any completed multipart uploads - S3.delete_object(@test_bucket, @test_object) |> ExAws.request() + S3.delete_object(bucket, @test_object) |> ExAws.request!() end) :ok end - test "initiate and abort multipart upload" do + test "initiate and abort multipart upload", %{bucket: bucket} do # Initiate multipart upload {:ok, init_result} = - S3.initiate_multipart_upload(@test_bucket, @test_object) |> ExAws.request() + S3.initiate_multipart_upload(bucket, @test_object) |> ExAws.request() upload_id = init_result.body.upload_id assert is_binary(upload_id) # Abort multipart upload - result = S3.abort_multipart_upload(@test_bucket, @test_object, upload_id) |> ExAws.request() + result = S3.abort_multipart_upload(bucket, @test_object, upload_id) |> ExAws.request() assert {:ok, %{status_code: 204}} = result end - test "complete multipart upload workflow" do + test "complete multipart upload workflow", %{bucket: bucket} do # >5MB to ensure multipart large_content = String.duplicate("A", 5 * 1024 * 1024 + 1000) # Initiate multipart upload {:ok, init_result} = - S3.initiate_multipart_upload(@test_bucket, @test_object) |> ExAws.request() + S3.initiate_multipart_upload(bucket, @test_object) |> ExAws.request() upload_id = init_result.body.upload_id @@ -334,7 +456,7 @@ defmodule ExAws.S3MinioTest do part_content = String.slice(large_content, 0, 5 * 1024 * 1024) {:ok, part_result} = - S3.upload_part(@test_bucket, @test_object, upload_id, 1, part_content) |> ExAws.request() + S3.upload_part(bucket, @test_object, upload_id, 1, part_content) |> ExAws.request() etag = part_result.headers |> Enum.find(fn {k, _} -> String.downcase(k) == "etag" end) |> elem(1) @@ -343,37 +465,37 @@ defmodule ExAws.S3MinioTest do parts = %{1 => etag} result = - S3.complete_multipart_upload(@test_bucket, @test_object, upload_id, parts) + S3.complete_multipart_upload(bucket, @test_object, upload_id, parts) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Verify object exists - {:ok, _get_result} = S3.head_object(@test_bucket, @test_object) |> ExAws.request() + {:ok, _get_result} = S3.head_object(bucket, @test_object) |> ExAws.request() end end describe "Bucket notification operations" do - setup do - S3.put_bucket(@test_bucket, "us-east-1") |> ExAws.request() + setup %{bucket: bucket} do + S3.put_bucket(bucket, "us-east-1") |> ExAws.request!() on_exit(fn -> # Clean up notification configuration empty_config = %{} - S3.put_bucket_notification(@test_bucket, empty_config) |> ExAws.request() + S3.put_bucket_notification(bucket, empty_config) |> ExAws.request!() end) :ok end - test "get_bucket_notification returns empty configuration initially" do - {:ok, result} = S3.get_bucket_notification(@test_bucket) |> ExAws.request() + test "get_bucket_notification returns empty configuration initially", %{bucket: bucket} do + {:ok, result} = S3.get_bucket_notification(bucket) |> ExAws.request() # MinIO returns empty notification configuration as empty XML assert result.body =~ "NotificationConfiguration" end - test "put_bucket_notification configures webhook notifications" do + test "put_bucket_notification configures webhook notifications", %{bucket: bucket} do # Note: This test verifies the API works, but actual webhook delivery # requires MinIO to have webhook endpoints configured via mc admin config webhook_config = %{ @@ -396,11 +518,11 @@ defmodule ExAws.S3MinioTest do # This should succeed even if webhook endpoint isn't configured # (MinIO accepts the configuration but won't deliver events) - result = S3.put_bucket_notification(@test_bucket, webhook_config) |> ExAws.request() + result = S3.put_bucket_notification(bucket, webhook_config) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Verify configuration was set - {:ok, get_result} = S3.get_bucket_notification(@test_bucket) |> ExAws.request() + {:ok, get_result} = S3.get_bucket_notification(bucket) |> ExAws.request() config_body = get_result.body # Should contain the webhook configuration @@ -409,22 +531,22 @@ defmodule ExAws.S3MinioTest do assert config_body =~ "s3:ObjectCreated" end - test "delete_bucket_notification clears configuration" do + test "delete_bucket_notification clears configuration", %{bucket: bucket} do # First set a configuration config = %{ topic_arn: "arn:aws:sns:us-east-1:123456789012:test-topic", events: ["s3:ObjectCreated:Put"] } - S3.put_bucket_notification(@test_bucket, config) |> ExAws.request() + S3.put_bucket_notification(bucket, config) |> ExAws.request() # Clear the configuration empty_config = %{} - result = S3.put_bucket_notification(@test_bucket, empty_config) |> ExAws.request() + result = S3.put_bucket_notification(bucket, empty_config) |> ExAws.request() assert {:ok, %{status_code: 200}} = result # Verify configuration is empty - {:ok, get_result} = S3.get_bucket_notification(@test_bucket) |> ExAws.request() + {:ok, get_result} = S3.get_bucket_notification(bucket) |> ExAws.request() config_body = get_result.body # Should be empty notification configuration