diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift index ed01a7a1bb..8b860798d0 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift @@ -56,6 +56,7 @@ class AWSS3StorageListObjectsTask: StorageListObjectsTask, DefaultLogger { } return StorageListResult.Item( path: path, + size: s3Object.size, eTag: s3Object.eTag, lastModified: s3Object.lastModified ) diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginStorageBucketTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginStorageBucketTests.swift new file mode 100644 index 0000000000..d5b9b2072c --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginStorageBucketTests.swift @@ -0,0 +1,174 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@_spi(InternalAmplifyConfiguration) @testable import Amplify +@testable import AWSS3StoragePlugin +@testable import AWSPluginsTestCommon + +class AWSS3StoragePluginStorageBucketTests: XCTestCase { + private var storagePlugin: AWSS3StoragePlugin! + private var defaultService: MockAWSS3StorageService! + private var authService: MockAWSAuthService! + private var queue: OperationQueue! + private let defaultBucketInfo = BucketInfo( + bucketName: "bucketName", + region: "us-east-1" + ) + private let additionalBucketInfo = BucketInfo( + bucketName: "anotherBucketName", + region: "us-east-2" + ) + + private var additionalS3Bucket: AmplifyOutputsData.Storage.Bucket { + return .init( + name: "anotherBucket", + bucketName: additionalBucketInfo.bucketName, + awsRegion: additionalBucketInfo.region + ) + } + + override func setUp() { + storagePlugin = AWSS3StoragePlugin() + defaultService = MockAWSS3StorageService() + authService = MockAWSAuthService() + queue = OperationQueue() + storagePlugin.configure( + defaultBucket: .fromBucketInfo(defaultBucketInfo), + storageService: defaultService, + authService: authService, + defaultAccessLevel: .guest, + queue: queue + ) + } + + override func tearDown() async throws { + try await Task.sleep(seconds: 0.1) // This is unfortunate but necessary to give the DB time to recover the URLSession tasks + await storagePlugin.reset() + queue.cancelAllOperations() + storagePlugin = nil + defaultService = nil + authService = nil + queue = nil + } + + /// Given: A configured AWSS3StoragePlugin + /// When: storageService(for:) is invoked with nil + /// Then: The default storage service should be returned + func testStorageService_withNil_shouldReturnDefaultService() throws { + let storageService = try storagePlugin.storageService(for: nil) + guard let mockService = storageService as? MockAWSS3StorageService else { + XCTFail("Expected a MockAWSS3StorageService, got \(type(of: storageService))") + return + } + XCTAssertTrue(mockService === defaultService) + } + + /// Given: A AWSS3StoragePlugin configured with additional bucket names + /// When: storageService(for:) is invoked with .fromOutputs with an existing value + /// Then: A valid AWSS3StorageService should be returned pointing to that bucket + func testStorageService_withBucketFromOutputs_shouldReturnStorageService() throws { + storagePlugin.additionalBucketsByName = [ + additionalS3Bucket.name: additionalS3Bucket + ] + let storageService = try storagePlugin.storageService(for: .fromOutputs(name: additionalS3Bucket.name)) + guard let newService = storageService as? AWSS3StorageService else { + XCTFail("Expected a AWSS3StorageService, got \(type(of: storageService))") + return + } + XCTAssertFalse(newService === defaultService) + XCTAssertEqual(newService.bucket, additionalS3Bucket.bucketName) + } + + /// Given: A AWSS3StoragePlugin configured without additional buckets (i.e. no AmplifyOutputs) + /// When: storageService(for:) is invoked with .fromOutputs + /// Then: A StorageError.validation error is thrown + func testStorageService_withBucketFromOutputs_withoutConfiguringOutputs_shouldThrowValidationException() { + storagePlugin.additionalBucketsByName = nil + do { + _ = try storagePlugin.storageService(for: .fromOutputs(name: "anotherBucket")) + XCTFail("Expected StorageError.validation to be thrown") + } catch { + guard let storageError = error as? StorageError, + case .validation(let field, _, _, _) = storageError else { + XCTFail("Expected StorageError.validation, got \(error)") + return + } + XCTAssertEqual(field, "bucket") + } + } + + /// Given: A AWSS3StoragePlugin configured with additional bucket names + /// When: storageService(for:) is invoked with .fromOutputs with a non-existing value + /// Then: A StorageError.validation error is thrown + func testStorageService_withInvalidBucketFromOutputs_shouldThrowValidationException() { + storagePlugin.additionalBucketsByName = [ + additionalS3Bucket.name: additionalS3Bucket + ] + do { + _ = try storagePlugin.storageService(for: .fromOutputs(name: "invalidBucket")) + XCTFail("Expected StorageError.validation to be thrown") + } catch { + guard let storageError = error as? StorageError, + case .validation(let field, _, _, _) = storageError else { + XCTFail("Expected StorageError.validation, got \(error)") + return + } + XCTAssertEqual(field, "bucket") + } + } + + /// Given: A configured AWSS3StoragePlugin + /// When: storageService(for:) is invoked with .fromBucketInfo + /// Then: A valid AWSS3StorageService should be returned pointing to that bucket + func testStorageService_withBucketFromBucketInfo_shouldReturnStorageService() throws { + let storageService = try storagePlugin.storageService(for: .fromBucketInfo(additionalBucketInfo)) + guard let newService = storageService as? AWSS3StorageService else { + XCTFail("Expected a AWSS3StorageService, got \(type(of: storageService))") + return + } + XCTAssertFalse(newService === defaultService) + XCTAssertEqual(newService.bucket, additionalBucketInfo.bucketName) + } + + /// Given: A configured AWSS3StoragePlugin + /// When: storageService(for:) is invoked with an invalid instance that conforms to StorageBucket + /// Then: A StorageError.validation error is thrown + func testStorageService_withInvalidStorageBucket_shouldThrowValidationException() { + do { + _ = try storagePlugin.storageService(for: InvalidBucket()) + XCTFail("Expected StorageError.validation to be thrown") + } catch { + guard let storageError = error as? StorageError, + case .validation(let field, _, _, _) = storageError else { + XCTFail("Expected StorageError.validation, got \(error)") + return + } + XCTAssertEqual(field, "bucket") + } + } + + /// Given: A configured AWSS3StoragePlugin + /// When: storageService(for:) is invoked for a bucket that was not accessed before (i.e. a new one) + /// Then: A new Storage Service should be created + func testStorageService_withNewBucket_shouldReturnNewService() throws { + XCTAssertEqual(storagePlugin.storageServicesByBucket.count, 1) + _ = try storagePlugin.storageService(for: .fromBucketInfo(additionalBucketInfo)) + XCTAssertEqual(storagePlugin.storageServicesByBucket.count, 2) + } + + /// Given: A configured AWSS3StoragePlugin + /// When: storageService(for:) is invoked for a bucket that was accessed before (e.g. the default one) + /// Then: A new Storage Service should not be created + func testStorageService_withPreviouslyAccessedBucket_shouldReturnExistingService() throws { + XCTAssertEqual(storagePlugin.storageServicesByBucket.count, 1) + _ = try storagePlugin.storageService(for: .fromBucketInfo(defaultBucketInfo)) + XCTAssertEqual(storagePlugin.storageServicesByBucket.count, 1) + } + + private struct InvalidBucket: StorageBucket {} +} diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginMultipleBucketTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginMultipleBucketTests.swift new file mode 100644 index 0000000000..db7e14ae72 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginMultipleBucketTests.swift @@ -0,0 +1,531 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(InternalAmplifyConfiguration) @testable import Amplify +@testable import AWSS3StoragePlugin +import XCTest + +class AWSS3StoragePluginMultipleBucketTests: AWSS3StoragePluginTestBase { + var customBucket: (any StorageBucket)! + + override func setUp() async throws { + guard let outputs = try? AmplifyOutputs.amplifyOutputs.resolveConfiguration(), + let additionalBucket = outputs.storage?.buckets?.first else { + throw XCTSkip("Multibucket has not been configured. Skipping test") + } + customBucket = .fromBucketInfo(.init( + bucketName: additionalBucket.bucketName, + region: additionalBucket.awsRegion + )) + try await super.setUp() + } + + override func tearDown() async throws { + try await Task.sleep(seconds: 0.1) + try await super.tearDown() + } + + /// Given: An data object + /// When: Upload the data to a custom buckets using keys + /// Then: The operation completes successfully + func testUploadData_toCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + + let uploaded = try await Amplify.Storage.uploadData( + key: key, + data: data, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, key) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: An data object + /// When: Upload the data to a custom bucket using StoragePath + /// Then: The operation completes successfully + func testUploadData_toCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let data = Data(id.utf8) + let path: StringStoragePath = .fromString("public/\(id)") + + let uploaded = try await Amplify.Storage.uploadData( + path: path, + data: data, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, path.string) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: A file with contents + /// When: Upload the file to a custom bucket using key + /// Then: The operation completes successfully and all URLSession and SDK requests include a user agent + func testUploadFile_toCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + let filePath = NSTemporaryDirectory() + key + ".tmp" + + let fileURL = URL(fileURLWithPath: filePath) + FileManager.default.createFile(atPath: filePath, contents: Data(key.utf8), attributes: nil) + + let uploaded = try await Amplify.Storage.uploadFile( + key: key, + local: fileURL, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, key) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: A file with contents + /// When: Upload the file to a custom bucket using StoragePath + /// Then: The operation completes successfully and all URLSession and SDK requests include a user agent + func testUploadFile_toCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let filePath = NSTemporaryDirectory() + id + ".tmp" + let path: StringStoragePath = .fromString("public/\(id)") + + let fileURL = URL(fileURLWithPath: filePath) + FileManager.default.createFile(atPath: filePath, contents: Data(id.utf8), attributes: nil) + + let uploaded = try await Amplify.Storage.uploadFile( + path: path, + local: fileURL, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, path.string) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: A large data object + /// When: Upload the data to a custom bucket using key + /// Then: The operation completes successfully + func testUploadLargeData_toCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + + let uploaded = try await Amplify.Storage.uploadData( + key: key, + data: AWSS3StoragePluginTestBase.largeDataObject, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, key) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: A large data object + /// When: Upload the data to a custom bucket using StoragePath + /// Then: The operation completes successfully + func testUploadLargeData_toCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let path: StringStoragePath = .fromString("public/\(id)") + + let uploaded = try await Amplify.Storage.uploadData( + path: path, + data: AWSS3StoragePluginTestBase.largeDataObject, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, path.string) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: A large file + /// When: Upload the file to a custom bucket using key + /// Then: The operation completes successfully + func testUploadLargeFile_toCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + let filePath = NSTemporaryDirectory() + key + ".tmp" + let fileURL = URL(fileURLWithPath: filePath) + + FileManager.default.createFile( + atPath: filePath, + contents: AWSS3StoragePluginTestBase.largeDataObject, + attributes: nil + ) + + let uploaded = try await Amplify.Storage.uploadFile( + key: key, + local: fileURL, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, key) + + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: A large file + /// When: Upload the file to a custom bucket using key + /// Then: The operation completes successfully + func testUploadLargeFile_toCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let filePath = NSTemporaryDirectory() + id + ".tmp" + let fileURL = URL(fileURLWithPath: filePath) + let path: StringStoragePath = .fromString("public/\(id)") + + FileManager.default.createFile( + atPath: filePath, + contents: AWSS3StoragePluginTestBase.largeDataObject, + attributes: nil + ) + + let uploaded = try await Amplify.Storage.uploadFile( + path: path, + local: fileURL, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(uploaded, path.string) + + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the downloadData API using key + /// Then: The operation completes successfully with the data retrieved + func testDownloadData_fromCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + try await uploadData( + key: key, + data: data, + options: .init(bucket: customBucket) + ) + + let downloaded = try await Amplify.Storage.downloadData( + key: key, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(data.count, downloaded.count) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the downloadData API using StoragePath + /// Then: The operation completes successfully with the data retrieved + func testDownloadData_fromCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let data = Data(id.utf8) + let path: StringStoragePath = .fromString("public/\(id)") + try await uploadData( + path: path, + data: data, + options: .init(bucket: customBucket) + ) + + let downloaded = try await Amplify.Storage.downloadData( + path: path, + options: .init(bucket: customBucket) + ).value + XCTAssertEqual(data.count, downloaded.count) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the downloadFile API using key + /// Then: The operation completes successfully the local file containing the data from the object + func testDownloadFile_fromCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + let timestamp = String(Date().timeIntervalSince1970) + let timestampData = Data(timestamp.utf8) + try await uploadData( + key: key, + data: timestampData, + options: .init(bucket: customBucket) + ) + let filePath = NSTemporaryDirectory() + key + ".tmp" + let fileURL = URL(fileURLWithPath: filePath) + removeFileIfExisting(fileURL) + + try await Amplify.Storage.downloadFile( + key: key, + local: fileURL, + options: .init(bucket: customBucket) + ).value + + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + do { + let result = try String(contentsOf: fileURL, encoding: .utf8) + XCTAssertEqual(result, timestamp) + } catch { + XCTFail("Failed to read downloaded file") + } + + removeFileIfExisting(fileURL) + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the downloadFile API using StoragePath + /// Then: The operation completes successfully the local file containing the data from the object + func testDownloadFile_fromCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let timestamp = String(Date().timeIntervalSince1970) + let timestampData = Data(timestamp.utf8) + let path: StringStoragePath = .fromString("public/\(id)") + try await uploadData( + path: path, + data: timestampData, + options: .init(bucket: customBucket) + ) + let filePath = NSTemporaryDirectory() + id + ".tmp" + let fileURL = URL(fileURLWithPath: filePath) + removeFileIfExisting(fileURL) + + try await Amplify.Storage.downloadFile( + path: path, + local: fileURL, + options: .init(bucket: customBucket) + ).value + + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + do { + let result = try String(contentsOf: fileURL, encoding: .utf8) + XCTAssertEqual(result, timestamp) + } catch { + XCTFail("Failed to read downloaded file") + } + + removeFileIfExisting(fileURL) + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the getURL API using key + /// Then: The operation completes successfully with the URL retrieved + func testGetRemoteURL_fromCustomBucket_usingKey_sholdSucceed() async throws { + let key = UUID().uuidString + try await uploadData( + key: key, + data: Data(key.utf8), + options: .init(bucket: customBucket) + ) + + let remoteURL = try await Amplify.Storage.getURL( + key: key, + options: .init(bucket: customBucket) + ) + + let (data, response) = try await URLSession.shared.data(from: remoteURL) + let httpResponse = try XCTUnwrap(response as? HTTPURLResponse) + XCTAssertEqual(httpResponse.statusCode, 200) + + let dataString = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertEqual(dataString, key) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the getURL API using StoragePath + /// Then: The operation completes successfully with the URL retrieved + func testGetRemoteURL_fromCustomBucket_usingStoragePath_sholdSucceed() async throws { + let id = UUID().uuidString + let path: StringStoragePath = .fromString("public/\(id)") + try await uploadData( + path: path, + data: Data(id.utf8), + options: .init(bucket: customBucket) + ) + + let remoteURL = try await Amplify.Storage.getURL( + path: path, + options: .init(bucket: customBucket) + ) + + let (data, response) = try await URLSession.shared.data(from: remoteURL) + let httpResponse = try XCTUnwrap(response as? HTTPURLResponse) + XCTAssertEqual(httpResponse.statusCode, 200) + + let dataString = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertEqual(dataString, id) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the list API using key + /// Then: The operation completes successfully with the key retrieved + func testList_fromOtherBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + try await uploadData( + key: key, + data: Data(key.utf8), + options: .init(bucket: customBucket) + ) + + let result = try await Amplify.Storage.list( + options: .init( + path: key, + bucket: customBucket + ) + ) + let items = try XCTUnwrap(result.items) + + XCTAssertEqual(items.count, 1) + let item = try XCTUnwrap(items.first) + XCTAssertEqual(item.key, key) + XCTAssertNotNil(item.eTag) + XCTAssertNotNil(item.lastModified) + XCTAssertNotNil(item.size) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the list API using StoragePath + /// Then: The operation completes successfully with the key retrieved + func testList_fromOtherBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let path: StringStoragePath = .fromString("public/\(id)") + try await uploadData( + path: path, + data: Data(id.utf8), + options: .init(bucket: customBucket) + ) + + let result = try await Amplify.Storage.list( + path: path, + options: .init(bucket: customBucket) + ) + let items = try XCTUnwrap(result.items) + + XCTAssertEqual(items.count, 1) + let item = try XCTUnwrap(items.first) + XCTAssertEqual(item.path, path.string) + XCTAssertNotNil(item.eTag) + XCTAssertNotNil(item.lastModified) + XCTAssertNotNil(item.size) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the remove API using key + /// Then: The operation completes successfully with the key removed from storage + func testRemoveKey_fromCustomBucket_usingKey_shouldSucceed() async throws { + let key = UUID().uuidString + try await uploadData( + key: key, + data: Data(key.utf8), + options: .init(bucket: customBucket) + ) + + let deleted = try await Amplify.Storage.remove( + key: key, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, key) + } + + /// Given: An object in storage in a custom bucket + /// When: Call the remove API using StoragePath + /// Then: The operation completes successfully with the key removed from storage + func testRemoveKey_fromCustomBucket_usingStoragePath_shouldSucceed() async throws { + let id = UUID().uuidString + let path: StringStoragePath = .fromString("public/\(id)") + try await uploadData( + path: path, + data: Data(id.utf8), + options: .init(bucket: customBucket) + ) + + let deleted = try await Amplify.Storage.remove( + path: path, + options: .init(bucket: customBucket) + ) + XCTAssertEqual(deleted, path.string) + } + + private func removeFileIfExisting(_ fileURL: URL) { + guard FileManager.default.fileExists(atPath: fileURL.path) else { + return + } + do { + try FileManager.default.removeItem(at: fileURL) + } catch { + XCTFail("Failed to remove file at \(fileURL)") + } + } +} + +private extension StringStoragePath { + var string: String { + return resolve("") + } +} diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift index 35e4721be2..fde9054957 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift @@ -87,13 +87,17 @@ class AWSS3StoragePluginTestBase: XCTestCase { Amplify.Storage.downloadData(key: key) } - func uploadData(key: String, data: Data) async throws { + func uploadData( + key: String, + data: Data, + options: StorageUploadDataRequest.Options? = nil + ) async throws { let completeInvoked = expectation(description: "Completed is invoked") Task { let result = try await Amplify.Storage.uploadData( key: key, data: data, - options: nil + options: options ).value XCTAssertNotNil(result) @@ -102,7 +106,27 @@ class AWSS3StoragePluginTestBase: XCTestCase { await fulfillment(of: [completeInvoked], timeout: 60) } - + + func uploadData( + path: any StoragePath, + data: Data, + options: StorageUploadDataRequest.Options? = nil + ) async throws { + let completeInvoked = expectation(description: "Completed is invoked") + Task { + let result = try await Amplify.Storage.uploadData( + path: path, + data: data, + options: options + ).value + + XCTAssertNotNil(result) + completeInvoked.fulfill() + } + + await fulfillment(of: [completeInvoked], timeout: 60) + } + func remove(key: String, accessLevel: StorageAccessLevel? = nil) async { var removeOptions: StorageRemoveRequest.Options? = nil if let accessLevel = accessLevel { @@ -196,15 +220,19 @@ class AWSS3StoragePluginTestBase: XCTestCase { private func invalidateCurrentSession() { Self.logger.debug("Invalidating URLSession") - guard let plugin = try? Amplify.Storage.getPlugin(for: "awsS3StoragePlugin") as? AWSS3StoragePlugin, - let service = plugin.storageService as? AWSS3StorageService else { - print("Unable to to cast to AWSS3StorageService") + guard let plugin = try? Amplify.Storage.getPlugin(for: "awsS3StoragePlugin") as? AWSS3StoragePlugin else { + print("Unable to to cast to AWSS3StoragePlugin") return } - if let delegate = service.urlSession.delegate as? StorageServiceSessionDelegate { - delegate.storageService = nil + for serviceBehaviour in plugin.storageServicesByBucket.values { + guard let service = serviceBehaviour as? AWSS3StorageService else { + continue + } + if let delegate = service.urlSession.delegate as? StorageServiceSessionDelegate { + delegate.storageService = nil + } + service.urlSession.invalidateAndCancel() } - service.urlSession.invalidateAndCancel() } } diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj index c363202fbf..a15257b61c 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 68828E4628C2736C006E7C0A /* AWSS3StoragePluginProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08C28BEAF8E00C8A6EB /* AWSS3StoragePluginProgressTests.swift */; }; 68828E4728C27745006E7C0A /* AWSS3StoragePluginPutDataResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08828BEAF8E00C8A6EB /* AWSS3StoragePluginPutDataResumabilityTests.swift */; }; 68828E4828C2AAA6006E7C0A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08B28BEAF8E00C8A6EB /* AWSS3StoragePluginGetDataResumabilityTests.swift */; }; + 68DAFA7A2C7796CD00346A43 /* AWSS3StoragePluginMultipleBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DAFA792C7796CD00346A43 /* AWSS3StoragePluginMultipleBucketTests.swift */; }; 734605222BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */; }; 734605242BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */; }; 901AB3E92AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */; }; @@ -166,6 +167,7 @@ 684FB0A928BEB07200C8A6EB /* AWSS3StoragePluginIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSS3StoragePluginIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 684FB0C228BEB45600C8A6EB /* AuthSignInHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthSignInHelper.swift; sourceTree = ""; }; 684FB0C528BEB84800C8A6EB /* StorageHostApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StorageHostApp.entitlements; sourceTree = ""; }; + 68DAFA792C7796CD00346A43 /* AWSS3StoragePluginMultipleBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginMultipleBucketTests.swift; sourceTree = ""; }; 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginUploadIntegrationTests.swift; sourceTree = ""; }; 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginDownloadIntegrationTests.swift; sourceTree = ""; }; 901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginUploadMetadataTestCase.swift; sourceTree = ""; }; @@ -321,6 +323,7 @@ 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */, 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */, 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */, + 68DAFA792C7796CD00346A43 /* AWSS3StoragePluginMultipleBucketTests.swift */, ); path = AWSS3StoragePluginIntegrationTests; sourceTree = ""; @@ -647,6 +650,7 @@ 21F7630F2BD6B8640048845A /* AsyncTesting.swift in Sources */, 21F763102BD6B8640048845A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */, 21F763112BD6B8640048845A /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */, + 68DAFA7A2C7796CD00346A43 /* AWSS3StoragePluginMultipleBucketTests.swift in Sources */, 21F763122BD6B8640048845A /* AsyncExpectation.swift in Sources */, 21F763132BD6B8640048845A /* AWSS3StoragePluginProgressTests.swift in Sources */, 21F763142BD6B8640048845A /* AWSS3StoragePluginAccessLevelTests.swift in Sources */, diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageStressTests/StorageStressTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageStressTests/StorageStressTests.swift index ffc25e3d2c..7e06693fbd 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageStressTests/StorageStressTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageStressTests/StorageStressTests.swift @@ -230,15 +230,19 @@ final class StorageStressTests: XCTestCase { private func invalidateCurrentSession() { Self.logger.debug("Invalidating URLSession") - guard let plugin = try? Amplify.Storage.getPlugin(for: "awsS3StoragePlugin") as? AWSS3StoragePlugin, - let service = plugin.storageService as? AWSS3StorageService else { - print("Unable to to cast to AWSS3StorageService") + guard let plugin = try? Amplify.Storage.getPlugin(for: "awsS3StoragePlugin") as? AWSS3StoragePlugin else { + print("Unable to to cast to AWSS3StoragePlugin") return } - if let delegate = service.urlSession.delegate as? StorageServiceSessionDelegate { - delegate.storageService = nil + for serviceBehaviour in plugin.storageServicesByBucket.values { + guard let service = serviceBehaviour as? AWSS3StorageService else { + continue + } + if let delegate = service.urlSession.delegate as? StorageServiceSessionDelegate { + delegate.storageService = nil + } + service.urlSession.invalidateAndCancel() } - service.urlSession.invalidateAndCancel() } } diff --git a/api-dump/AWSDataStorePlugin.json b/api-dump/AWSDataStorePlugin.json index 086d183e4d..665dc04908 100644 --- a/api-dump/AWSDataStorePlugin.json +++ b/api-dump/AWSDataStorePlugin.json @@ -8205,7 +8205,7 @@ "-module", "AWSDataStorePlugin", "-o", - "\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.7LdqMpKABA\/AWSDataStorePlugin.json", + "\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.nR3YWolup0\/AWSDataStorePlugin.json", "-I", ".build\/debug", "-sdk-version", diff --git a/api-dump/AWSPluginsCore.json b/api-dump/AWSPluginsCore.json index 9afa93d602..bb212b167e 100644 --- a/api-dump/AWSPluginsCore.json +++ b/api-dump/AWSPluginsCore.json @@ -24463,7 +24463,7 @@ "-module", "AWSPluginsCore", "-o", - "\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.7LdqMpKABA\/AWSPluginsCore.json", + "\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.nR3YWolup0\/AWSPluginsCore.json", "-I", ".build\/debug", "-sdk-version", diff --git a/api-dump/Amplify.json b/api-dump/Amplify.json index 5506628b22..da1656eb7e 100644 --- a/api-dump/Amplify.json +++ b/api-dump/Amplify.json @@ -180399,6 +180399,92 @@ "name": "Optional", "printedName": "Optional", "children": [ + { + "kind": "Function", + "name": "fromOutputs", + "printedName": "fromOutputs(name:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "(any Amplify.StorageBucket)?", + "children": [ + { + "kind": "TypeNominal", + "name": "Paren", + "printedName": "(any Amplify.StorageBucket)", + "children": [ + { + "kind": "TypeNominal", + "name": "StorageBucket", + "printedName": "any Amplify.StorageBucket", + "usr": "s:7Amplify13StorageBucketP" + } + ], + "usr": "s:7Amplify13StorageBucketP" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:Sq7AmplifyAA13StorageBucket_pRszlE11fromOutputs4nameAaB_pSgSS_tFZ", + "mangledName": "$sSq7AmplifyAA13StorageBucket_pRszlE11fromOutputs4nameAaB_pSgSS_tFZ", + "moduleName": "Amplify", + "genericSig": "", + "static": true, + "isFromExtension": true, + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "fromBucketInfo", + "printedName": "fromBucketInfo(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "(any Amplify.StorageBucket)?", + "children": [ + { + "kind": "TypeNominal", + "name": "Paren", + "printedName": "(any Amplify.StorageBucket)", + "children": [ + { + "kind": "TypeNominal", + "name": "StorageBucket", + "printedName": "any Amplify.StorageBucket", + "usr": "s:7Amplify13StorageBucketP" + } + ], + "usr": "s:7Amplify13StorageBucketP" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "BucketInfo", + "printedName": "Amplify.BucketInfo", + "usr": "s:7Amplify10BucketInfoV" + } + ], + "declKind": "Func", + "usr": "s:Sq7AmplifyAA13StorageBucket_pRszlE04fromC4InfoyAaB_pSgAA0cE0VFZ", + "mangledName": "$sSq7AmplifyAA13StorageBucket_pRszlE04fromC4InfoyAaB_pSgAA0cE0VFZ", + "moduleName": "Amplify", + "genericSig": "", + "static": true, + "isFromExtension": true, + "funcSelfKind": "NonMutating" + }, { "kind": "Function", "name": "ifSome", @@ -181411,7 +181497,7 @@ "-module", "Amplify", "-o", - "\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.7LdqMpKABA\/Amplify.json", + "\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.nR3YWolup0\/Amplify.json", "-I", ".build\/debug", "-sdk-version", diff --git a/api-dump/CoreMLPredictionsPlugin.json b/api-dump/CoreMLPredictionsPlugin.json index 71fc795af3..c341d21a5b 100644 --- a/api-dump/CoreMLPredictionsPlugin.json +++ b/api-dump/CoreMLPredictionsPlugin.json @@ -430,7 +430,7 @@ "-module", "CoreMLPredictionsPlugin", "-o", - "\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.7LdqMpKABA\/CoreMLPredictionsPlugin.json", + "\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.nR3YWolup0\/CoreMLPredictionsPlugin.json", "-I", ".build\/debug", "-sdk-version",