@@ -511,6 +511,82 @@ final class LoginCredentialProviderTests {
511511 }
512512 }
513513
514+ @Test ( " Get credentials succeeds when token file write fails (sandbox) " )
515+ func getCredentialsDespiteWriteFailure( ) async throws {
516+ let tempDirectory = try createTempDirectory ( )
517+ defer {
518+ // Restore permissions so cleanup can succeed
519+ try ? FileManager . default. setAttributes ( [ . posixPermissions: 0o755 ] , ofItemAtPath: tempDirectory. path)
520+ removeTempDirectory ( tempDirectory)
521+ }
522+
523+ // Setup expired token file
524+ let privateKey = P256 . Signing. PrivateKey ( )
525+ let pemKey = privateKey. pemRepresentation
526+ let escapedPemKey = pemKey. replacing ( " \n " , with: " \\ n " )
527+
528+ let tokenData = """
529+ {
530+ " accessToken " : {
531+ " accessKeyId " : " AKIAOLD123 " ,
532+ " secretAccessKey " : " oldsecret " ,
533+ " sessionToken " : " oldsession " ,
534+ " accountId " : " 123456789012 " ,
535+ " expiresAt " : " 2020-01-01T00:00:00Z "
536+ },
537+ " refreshToken " : " refresh-token-123 " ,
538+ " dpopKey " : " \( escapedPemKey) " ,
539+ " clientId " : " client-id-123 " ,
540+ " idToken " : " id-token-123 " ,
541+ " tokenType " : " urn:aws:params:oauth:token-type:access_token_sigv4 "
542+ }
543+ """
544+
545+ let sessionData = Data ( " test-session " . utf8)
546+ let hash = SHA256 . hash ( data: sessionData)
547+ let hashString = hash. compactMap { String ( format: " %02x " , $0) } . joined ( )
548+
549+ let tokenPath = tempDirectory. appendingPathComponent ( " \( hashString) .json " )
550+ try tokenData. write ( to: tokenPath, atomically: true , encoding: . utf8)
551+
552+ // Make directory read-only AFTER writing the token file (simulates SwiftPM sandbox
553+ // where reads are allowed but writes outside the package directory are blocked)
554+ try FileManager . default. setAttributes ( [ . posixPermissions: 0o555 ] , ofItemAtPath: tempDirectory. path)
555+
556+ // Mock HTTP client returns fresh credentials
557+ let mockHTTPClient = MockAWSHTTPClient { _ in
558+ let responseJSON = """
559+ {
560+ " accessToken " : {
561+ " accessKeyId " : " AKIANEW456 " ,
562+ " secretAccessKey " : " newsecret456 " ,
563+ " sessionToken " : " newsession456 "
564+ },
565+ " tokenType " : " urn:aws:params:oauth:token-type:access_token_sigv4 " ,
566+ " expiresIn " : 3600,
567+ " refreshToken " : " new-refresh-token-456 "
568+ }
569+ """
570+ return ( . ok, responseJSON. data ( using: . utf8) !)
571+ }
572+
573+ let provider = try LoginCredentialProvider . create (
574+ loginSession: " test-session " ,
575+ loginRegion: . useast1,
576+ cacheDirectoryOverride: tempDirectory. path,
577+ httpClient: mockHTTPClient
578+ )
579+
580+ // Even though saveToken will fail (read-only directory), the refresh succeeded
581+ // and we should still get the fresh credentials back from the server.
582+ let logger = Logger ( label: " test " )
583+ let credential = try await provider. getCredential ( logger: logger)
584+
585+ #expect( credential. accessKeyId == " AKIANEW456 " )
586+ #expect( credential. secretAccessKey == " newsecret456 " )
587+ #expect( credential. sessionToken == " newsession456 " )
588+ }
589+
514590 @Test ( " Provider is immutable " )
515591 func providerIsImmutable( ) throws {
516592 let tempDirectory = try createTempDirectory ( )
0 commit comments