2323#include < iostream>
2424#include < memory>
2525#include < optional>
26+ #include < span>
2627#include < string>
2728#include < string_view>
2829#include < thread>
@@ -239,28 +240,6 @@ TEST_F(ArrowS3FileIOTest, WarnsWhenNoCredentialApplies) {
239240 EXPECT_TRUE (HasWarning (*logger));
240241}
241242
242- TEST_F (ArrowS3FileIOTest, DeleteFilesDispatchesAcrossCredentialPrefixes) {
243- auto result = MakeS3FileIO ({});
244- ASSERT_THAT (result, IsOk ());
245- auto * credentialed = result.value ()->AsSupportsStorageCredentials ();
246- ASSERT_NE (credentialed, nullptr );
247-
248- auto credential = [](std::string_view prefix, std::string_view access_key) {
249- return StorageCredential{
250- .prefix = std::string (prefix),
251- .config = {{std::string (S3Properties::kAccessKeyId ), std::string (access_key)},
252- {std::string (S3Properties::kSecretAccessKey ), " secret" }}};
253- };
254- ASSERT_THAT (credentialed->SetStorageCredentials ({credential (" s3://bucket-a" , " key-a" ),
255- credential (" s3://bucket-b" , " key-b" )}),
256- IsOk ());
257-
258- auto status = result.value ()->DeleteFiles ({" s3://bucket-a/%ZZ.parquet" ,
259- " s3://bucket-a/second.parquet" ,
260- " s3://bucket-b/other.parquet" });
261- EXPECT_THAT (status, HasErrorMessage (" Cannot parse URI" ));
262- }
263-
264243TEST_F (ArrowS3FileIOTest, OperationsSurviveConcurrentCredentialInstalls) {
265244 auto result = MakeS3FileIO ({});
266245 ASSERT_THAT (result, IsOk ());
@@ -276,18 +255,28 @@ TEST_F(ArrowS3FileIOTest, OperationsSurviveConcurrentCredentialInstalls) {
276255 ASSERT_THAT (credentialed->SetStorageCredentials ({credential (" first" )}), IsOk ());
277256
278257 std::atomic<bool > stop = false ;
258+ std::atomic<int > started = 0 ;
279259 std::atomic<int > failures = 0 ;
280260 std::vector<std::thread> operations;
281261 operations.reserve (4 );
282262 for (int i = 0 ; i < 4 ; ++i) {
283263 operations.emplace_back ([&] {
284- while (!stop. load ()) {
264+ auto open = [&] {
285265 if (!result.value ()->NewInputFile (" s3://bucket/key" ).has_value ()) {
286266 ++failures;
287267 }
268+ };
269+ open ();
270+ ++started;
271+ while (!stop.load ()) {
272+ open ();
288273 }
289274 });
290275 }
276+ // Every worker has run and is still looping before the first install.
277+ while (started.load () < 4 ) {
278+ std::this_thread::yield ();
279+ }
291280 // No assertions until the threads are joined: a fatal assertion here would
292281 // destroy joinable threads and terminate the binary, masking the failure.
293282 Status install_status = {};
@@ -406,6 +395,90 @@ TEST_F(ArrowS3FileIOTest, AppliesOssCredentialInRealRoundTrip) {
406395 EXPECT_THAT (CheckReadWrite (*io, s3_uri, " hello oss with vended credentials" ), IsOk ());
407396}
408397
398+ TEST_F (ArrowS3FileIOTest, DeleteFilesReachesEveryCredentialPrefix) {
399+ if (!HasIntegrationEnv ()) {
400+ GTEST_SKIP () << " Set ICEBERG_TEST_S3_URI to enable S3 IO test" ;
401+ }
402+
403+ auto properties = PropertiesFromEnv ();
404+ if (!properties.contains (std::string (S3Properties::kAccessKeyId )) ||
405+ !properties.contains (std::string (S3Properties::kSecretAccessKey ))) {
406+ GTEST_SKIP () << " Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to enable "
407+ " credential routing test" ;
408+ }
409+
410+ // Only the prefix delegates can authenticate, so a location that falls to
411+ // the default one fails the batch instead of passing unnoticed.
412+ auto bad_defaults = properties;
413+ for (const auto & [key, value] : BadS3Credentials ()) {
414+ bad_defaults.insert_or_assign (key, value);
415+ }
416+ ICEBERG_UNWRAP_OR_FAIL (auto io, MakeS3FileIO (std::move (bad_defaults)));
417+ auto * credentialed = io->AsSupportsStorageCredentials ();
418+ ASSERT_NE (credentialed, nullptr );
419+
420+ const auto a = ObjectUri (" delete_files_a/" );
421+ const auto b = ObjectUri (" delete_files_b/" );
422+ ASSERT_THAT (credentialed->SetStorageCredentials ({{.prefix = a, .config = properties},
423+ {.prefix = b, .config = properties}}),
424+ IsOk ());
425+
426+ const std::vector<std::string> paths = {a + " first" , b + " only" , a + " second" };
427+ for (const auto & path : paths) {
428+ ASSERT_THAT (io->WriteFile (path, " payload" ), IsOk ());
429+ }
430+ ASSERT_THAT (io->DeleteFiles (paths), IsOk ());
431+ // Deleting a missing key succeeds on S3, so check the objects are gone. The
432+ // writes above authenticated on these paths, so a failed read means absent.
433+ for (const auto & path : paths) {
434+ EXPECT_FALSE (io->ReadFile (path, std::nullopt ).has_value ()) << path;
435+ }
436+ }
437+
438+ TEST_F (ArrowS3FileIOTest, InputFileOutlivesCredentialInstall) {
439+ if (!HasIntegrationEnv ()) {
440+ GTEST_SKIP () << " Set ICEBERG_TEST_S3_URI to enable S3 IO test" ;
441+ }
442+
443+ auto properties = PropertiesFromEnv ();
444+ if (!properties.contains (std::string (S3Properties::kAccessKeyId )) ||
445+ !properties.contains (std::string (S3Properties::kSecretAccessKey ))) {
446+ GTEST_SKIP () << " Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to enable "
447+ " credential routing test" ;
448+ }
449+
450+ auto bad_defaults = properties;
451+ for (const auto & [key, value] : BadS3Credentials ()) {
452+ bad_defaults.insert_or_assign (key, value);
453+ }
454+ ICEBERG_UNWRAP_OR_FAIL (auto io, MakeS3FileIO (std::move (bad_defaults)));
455+ auto * credentialed = io->AsSupportsStorageCredentials ();
456+ ASSERT_NE (credentialed, nullptr );
457+
458+ const auto prefix = ObjectUri (" retained_input/" );
459+ const std::vector<StorageCredential> credentials = {
460+ {.prefix = prefix, .config = properties}};
461+ ASSERT_THAT (credentialed->SetStorageCredentials (credentials), IsOk ());
462+
463+ const auto uri = prefix + " object" ;
464+ constexpr std::string_view kContent = " written before the credential install" ;
465+ ASSERT_THAT (io->WriteFile (uri, kContent ), IsOk ());
466+ ICEBERG_UNWRAP_OR_FAIL (auto file, io->NewInputFile (uri));
467+ ICEBERG_UNWRAP_OR_FAIL (auto opened_before, file->Open ());
468+
469+ // Retires the delegate `file` came from; both handles must still read.
470+ ASSERT_THAT (credentialed->SetStorageCredentials (credentials), IsOk ());
471+
472+ ICEBERG_UNWRAP_OR_FAIL (auto opened_after, file->Open ());
473+ for (auto * stream : {opened_before.get (), opened_after.get ()}) {
474+ std::string read (kContent .size (), ' \0 ' );
475+ EXPECT_THAT (stream->ReadFully (0 , std::as_writable_bytes (std::span (read))), IsOk ());
476+ EXPECT_EQ (read, kContent );
477+ EXPECT_THAT (stream->Close (), IsOk ());
478+ }
479+ EXPECT_THAT (io->DeleteFile (uri), IsOk ());
480+ }
481+
409482#if ICEBERG_S3_ENABLED
410483TEST_F (ArrowS3FileIOTest, ClientRegion) {
411484 auto result =
0 commit comments