@@ -153,6 +153,11 @@ bool HasWarning(const CapturingLogger& logger, std::string_view substring = {})
153153 });
154154}
155155
156+ class MockStorageCredentialProvider : public StorageCredentialProvider {
157+ public:
158+ MOCK_METHOD ((Result<std::vector<StorageCredential>>), Load, (), (override ));
159+ };
160+
156161constexpr auto kOutlastsAShortenedBackoff = std::chrono::milliseconds(1200 );
157162
158163std::vector<StorageCredential> ExpiringCredentials (std::chrono::milliseconds valid_for,
@@ -267,13 +272,16 @@ TEST_F(ArrowS3FileIOTest, RefreshesCredentialsCloseToExpiry) {
267272
268273 const auto refreshed = ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
269274 int refresh_calls = 0 ;
270- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
271- ++refresh_calls;
272- return refreshed;
273- });
274- ASSERT_THAT (credentialed->SetStorageCredentials (
275- ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" )),
275+ auto provider = std::make_shared<MockStorageCredentialProvider>();
276+ EXPECT_CALL (*provider, Load ())
277+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
278+ ++refresh_calls;
279+ return refreshed;
280+ });
281+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
282+ ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" ), provider),
276283 IsOk ());
284+ EXPECT_EQ (refresh_calls, 0 );
277285
278286 EXPECT_THAT (result.value ()->NewInputFile (" s3://bucket/key" ), IsOk ());
279287 EXPECT_EQ (refresh_calls, 1 );
@@ -290,13 +298,15 @@ TEST_F(ArrowS3FileIOTest, DoesNotRefreshCredentialsThatAreNotCloseToExpiry) {
290298 ASSERT_NE (credentialed, nullptr );
291299
292300 int refresh_calls = 0 ;
293- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
294- ++refresh_calls;
295- return std::vector<StorageCredential>{};
296- });
297-
298- ASSERT_THAT (credentialed->SetStorageCredentials (
299- ExpiringCredentials (std::chrono::hours (1 ), " access-key" )),
301+ auto provider = std::make_shared<MockStorageCredentialProvider>();
302+ EXPECT_CALL (*provider, Load ())
303+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
304+ ++refresh_calls;
305+ return std::vector<StorageCredential>{};
306+ });
307+
308+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
309+ ExpiringCredentials (std::chrono::hours (1 ), " access-key" ), provider),
300310 IsOk ());
301311 EXPECT_THAT (result.value ()->NewInputFile (" s3://bucket/key" ), IsOk ());
302312 EXPECT_EQ (refresh_calls, 0 );
@@ -310,19 +320,58 @@ TEST_F(ArrowS3FileIOTest, DoesNotRefreshCredentialsThatAreNotCloseToExpiry) {
310320 EXPECT_EQ (refresh_calls, 0 );
311321}
312322
323+ TEST_F (ArrowS3FileIOTest, KeepsProviderWhenCredentialsAreUpdated) {
324+ ICEBERG_UNWRAP_OR_FAIL (auto io, MakeS3FileIO ({}));
325+ auto * credentialed = io->AsSupportsStorageCredentials ();
326+ ASSERT_NE (credentialed, nullptr );
327+
328+ const auto refreshed = ExpiringCredentials (std::chrono::hours (2 ), " refreshed-key" );
329+ auto provider = std::make_shared<MockStorageCredentialProvider>();
330+ EXPECT_CALL (*provider, Load ()).WillOnce (::testing::Return (refreshed));
331+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
332+ ExpiringCredentials (std::chrono::hours (1 ), " initial-key" ), provider),
333+ IsOk ());
334+ ASSERT_THAT (credentialed->SetStorageCredentials (
335+ ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" )),
336+ IsOk ());
337+
338+ EXPECT_THAT (io->NewInputFile (" s3://bucket/key" ), IsOk ());
339+ EXPECT_EQ (credentialed->credentials (), refreshed);
340+ }
341+
342+ TEST_F (ArrowS3FileIOTest, FailedInitializationDoesNotInstallProvider) {
343+ ICEBERG_UNWRAP_OR_FAIL (auto io, MakeS3FileIO ({}));
344+ auto * credentialed = io->AsSupportsStorageCredentials ();
345+ ASSERT_NE (credentialed, nullptr );
346+
347+ const auto initial = ExpiringCredentials (std::chrono::minutes (1 ), " initial-key" );
348+ ASSERT_THAT (credentialed->InitializeStorageCredentials (initial, nullptr ), IsOk ());
349+ auto invalid = initial;
350+ invalid.front ().prefix .clear ();
351+ auto provider = std::make_shared<MockStorageCredentialProvider>();
352+ EXPECT_CALL (*provider, Load ()).Times (0 );
353+ EXPECT_THAT (credentialed->InitializeStorageCredentials (invalid, provider),
354+ IsError (ErrorKind::kValidationFailed ));
355+
356+ EXPECT_THAT (io->NewInputFile (" s3://bucket/key" ), IsOk ());
357+ EXPECT_EQ (credentialed->credentials (), initial);
358+ }
359+
313360TEST_F (ArrowS3FileIOTest, RefreshesOnceWhenOperationsRaceForIt) {
314361 auto result = MakeS3FileIO ({});
315362 ASSERT_THAT (result, IsOk ());
316363 auto * credentialed = result.value ()->AsSupportsStorageCredentials ();
317364 ASSERT_NE (credentialed, nullptr );
318365
319366 std::atomic<int > refresh_calls = 0 ;
320- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
321- ++refresh_calls;
322- return ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
323- });
324- ASSERT_THAT (credentialed->SetStorageCredentials (
325- ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" )),
367+ auto provider = std::make_shared<MockStorageCredentialProvider>();
368+ EXPECT_CALL (*provider, Load ())
369+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
370+ ++refresh_calls;
371+ return ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
372+ });
373+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
374+ ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" ), provider),
326375 IsOk ());
327376
328377 constexpr int kThreads = 8 ;
@@ -358,16 +407,18 @@ TEST_F(ArrowS3FileIOTest, RefreshesOnceWhenCredentialsHaveExpired) {
358407 bool release_refresh = false ;
359408 std::atomic<int > refresh_calls = 0 ;
360409
361- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
362- ++refresh_calls;
363- std::unique_lock lock (mutex);
364- refresh_started = true ;
365- cv.notify_all ();
366- cv.wait (lock, [&] { return release_refresh; });
367- return ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
368- });
369- ASSERT_THAT (credentialed->SetStorageCredentials (
370- ExpiringCredentials (-std::chrono::minutes (1 ), " expired-key" )),
410+ auto provider = std::make_shared<MockStorageCredentialProvider>();
411+ EXPECT_CALL (*provider, Load ())
412+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
413+ ++refresh_calls;
414+ std::unique_lock lock (mutex);
415+ refresh_started = true ;
416+ cv.notify_all ();
417+ cv.wait (lock, [&] { return release_refresh; });
418+ return ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
419+ });
420+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
421+ ExpiringCredentials (-std::chrono::minutes (1 ), " expired-key" ), provider),
371422 IsOk ());
372423
373424 std::thread winner (
@@ -410,15 +461,17 @@ TEST_F(ArrowS3FileIOTest, RefreshDoesNotUndoCredentialsInstalledWhileItRan) {
410461 bool refresh_started = false ;
411462 bool release_refresh = false ;
412463
413- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
414- std::unique_lock lock (mutex);
415- refresh_started = true ;
416- cv.notify_all ();
417- cv.wait (lock, [&] { return release_refresh; });
418- return ExpiringCredentials (std::chrono::hours (1 ), " fetched-by-refresh" );
419- });
420- ASSERT_THAT (credentialed->SetStorageCredentials (
421- ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" )),
464+ auto provider = std::make_shared<MockStorageCredentialProvider>();
465+ EXPECT_CALL (*provider, Load ())
466+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
467+ std::unique_lock lock (mutex);
468+ refresh_started = true ;
469+ cv.notify_all ();
470+ cv.wait (lock, [&] { return release_refresh; });
471+ return ExpiringCredentials (std::chrono::hours (1 ), " fetched-by-refresh" );
472+ });
473+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
474+ ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" ), provider),
422475 IsOk ());
423476
424477 std::thread operation (
@@ -430,14 +483,15 @@ TEST_F(ArrowS3FileIOTest, RefreshDoesNotUndoCredentialsInstalledWhileItRan) {
430483 std::unique_lock lock (mutex);
431484 cv.wait (lock, [&] { return refresh_started; });
432485 }
433- ASSERT_THAT ( credentialed->SetStorageCredentials (installed), IsOk () );
486+ auto install_status = credentialed->SetStorageCredentials (installed);
434487 {
435488 std::lock_guard lock (mutex);
436489 release_refresh = true ;
437490 }
438491 cv.notify_all ();
439492 operation.join ();
440493
494+ ASSERT_THAT (install_status, IsOk ());
441495 EXPECT_EQ (credentialed->credentials (), installed);
442496}
443497
@@ -450,10 +504,12 @@ TEST_F(ArrowS3FileIOTest, RefreshesSessionCredentialsWithoutAUsableExpiry) {
450504 ASSERT_NE (credentialed, nullptr );
451505
452506 int refresh_calls = 0 ;
453- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
454- ++refresh_calls;
455- return ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
456- });
507+ auto provider = std::make_shared<MockStorageCredentialProvider>();
508+ EXPECT_CALL (*provider, Load ())
509+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
510+ ++refresh_calls;
511+ return ExpiringCredentials (std::chrono::hours (1 ), " refreshed-key" );
512+ });
457513
458514 auto logger = std::make_shared<CapturingLogger>();
459515 ScopedDefaultLogger scoped (logger);
@@ -464,8 +520,8 @@ TEST_F(ArrowS3FileIOTest, RefreshesSessionCredentialsWithoutAUsableExpiry) {
464520 if (!expiry.empty ()) {
465521 config[std::string (S3Properties::kSessionTokenExpiresAtMs )] = std::string (expiry);
466522 }
467- ASSERT_THAT (credentialed->SetStorageCredentials (
468- {{.prefix = " s3" , .config = std::move (config)}}),
523+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
524+ {{.prefix = " s3" , .config = std::move (config)}}, provider ),
469525 IsOk ());
470526 EXPECT_TRUE (HasWarning (*logger, " session token" ));
471527
@@ -486,11 +542,13 @@ TEST_F(ArrowS3FileIOTest, BacksOffWhenReplacementsAlsoLackAnExpiry) {
486542 {std::string (S3Properties::kSecretAccessKey ), " secret" },
487543 {std::string (S3Properties::kSessionToken ), " token" }}}};
488544 int refresh_calls = 0 ;
489- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
490- ++refresh_calls;
491- return undatable;
492- });
493- ASSERT_THAT (credentialed->SetStorageCredentials (undatable), IsOk ());
545+ auto provider = std::make_shared<MockStorageCredentialProvider>();
546+ EXPECT_CALL (*provider, Load ())
547+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
548+ ++refresh_calls;
549+ return undatable;
550+ });
551+ ASSERT_THAT (credentialed->InitializeStorageCredentials (undatable, provider), IsOk ());
494552
495553 EXPECT_THAT (result.value ()->NewInputFile (" s3://bucket/key" ), IsOk ());
496554 ASSERT_EQ (refresh_calls, 1 );
@@ -507,10 +565,12 @@ TEST_F(ArrowS3FileIOTest, IgnoresUnparseableExpiry) {
507565 ASSERT_NE (credentialed, nullptr );
508566
509567 int refresh_calls = 0 ;
510- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
511- ++refresh_calls;
512- return std::vector<StorageCredential>{};
513- });
568+ auto provider = std::make_shared<MockStorageCredentialProvider>();
569+ EXPECT_CALL (*provider, Load ())
570+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
571+ ++refresh_calls;
572+ return std::vector<StorageCredential>{};
573+ });
514574
515575 auto logger = std::make_shared<CapturingLogger>();
516576 ScopedDefaultLogger scoped (logger);
@@ -520,7 +580,7 @@ TEST_F(ArrowS3FileIOTest, IgnoresUnparseableExpiry) {
520580 {std::string (S3Properties::kSessionTokenExpiresAtMs ), " not-a-number" }};
521581 const std::vector<StorageCredential> credentials = {
522582 {.prefix = " s3" , .config = std::move (config)}};
523- ASSERT_THAT (credentialed->SetStorageCredentials (credentials), IsOk ());
583+ ASSERT_THAT (credentialed->InitializeStorageCredentials (credentials, provider ), IsOk ());
524584 EXPECT_EQ (credentialed->credentials (), credentials);
525585
526586 EXPECT_THAT (result.value ()->NewInputFile (" s3://bucket/key" ), IsOk ());
@@ -534,12 +594,14 @@ TEST_F(ArrowS3FileIOTest, BacksOffWhenTheReplacementIsAlsoCloseToExpiry) {
534594 ASSERT_NE (credentialed, nullptr );
535595
536596 int refresh_calls = 0 ;
537- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
538- ++refresh_calls;
539- return ExpiringCredentials (std::chrono::minutes (1 ), " short-lived-key" );
540- });
541- ASSERT_THAT (credentialed->SetStorageCredentials (
542- ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" )),
597+ auto provider = std::make_shared<MockStorageCredentialProvider>();
598+ EXPECT_CALL (*provider, Load ())
599+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
600+ ++refresh_calls;
601+ return ExpiringCredentials (std::chrono::minutes (1 ), " short-lived-key" );
602+ });
603+ ASSERT_THAT (credentialed->InitializeStorageCredentials (
604+ ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" ), provider),
543605 IsOk ());
544606
545607 for (int i = 0 ; i < 3 ; ++i) {
@@ -555,12 +617,14 @@ TEST_F(ArrowS3FileIOTest, KeepsCredentialsWhenRefreshFails) {
555617 ASSERT_NE (credentialed, nullptr );
556618
557619 int refresh_calls = 0 ;
558- credentialed->SetCredentialRefresher ([&]() -> Result<std::vector<StorageCredential>> {
559- ++refresh_calls;
560- return NotFound (" catalog unreachable" );
561- });
620+ auto provider = std::make_shared<MockStorageCredentialProvider>();
621+ EXPECT_CALL (*provider, Load ())
622+ .WillRepeatedly ([&]() -> Result<std::vector<StorageCredential>> {
623+ ++refresh_calls;
624+ return NotFound (" catalog unreachable" );
625+ });
562626 const auto expiring = ExpiringCredentials (std::chrono::minutes (1 ), " expiring-key" );
563- ASSERT_THAT (credentialed->SetStorageCredentials (expiring), IsOk ());
627+ ASSERT_THAT (credentialed->InitializeStorageCredentials (expiring, provider ), IsOk ());
564628
565629 auto logger = std::make_shared<CapturingLogger>();
566630 ScopedDefaultLogger scoped (logger);
0 commit comments