From 16eb6f72360717a11d7056f441b96488e1057e8f Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:58:12 -0400 Subject: [PATCH] feat(Storage): Adding integration tests for getURL, remove and list (#3584) --- ...3StoragePluginGetURLIntegrationTests.swift | 85 ++++++++ ...agePluginListObjectsIntegrationTests.swift | 186 ++++++++++++++++++ ...3StoragePluginRemoveIntegrationTests.swift | 166 ++++++++++++++++ .../StorageHostApp.xcodeproj/project.pbxproj | 12 ++ 4 files changed, 449 insertions(+) create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift new file mode 100644 index 0000000000..d8a4496e82 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift @@ -0,0 +1,85 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify + +import AWSS3StoragePlugin +import ClientRuntime +import AWSClientRuntime +import CryptoKit +import XCTest +import AWSS3 + +class AWSS3StoragePluginGetURLIntegrationTests: AWSS3StoragePluginTestBase { + + /// Given: An object in storage + /// When: Call the getURL API + /// Then: The operation completes successfully with the URL retrieved + func testGetRemoteURL() async throws { + let key = "public/" + UUID().uuidString + try await uploadData(key: key, dataString: key) + _ = try await Amplify.Storage.uploadData( + path: .fromString(key), + data: Data(key.utf8), + options: .init()) + + let remoteURL = try await Amplify.Storage.getURL(path: .fromString(key)) + + // The presigned URL generation does not result in an SDK or HTTP call. + XCTAssertEqual(requestRecorder.sdkRequests.map { $0.method} , []) + + 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) + + _ = try await Amplify.Storage.remove(path: .fromString(key)) + } + + /// - Given: A key for a non-existent S3 object + /// - When: A pre-signed URL is requested for that key with `validateObjectExistence = true` + /// - Then: A StorageError.keyNotFound error is thrown + func testGetURLForUnknownKeyWithValidation() async throws { + let unknownKey = "public/" + UUID().uuidString + do { + let url = try await Amplify.Storage.getURL( + path: .fromString(unknownKey), + options: .init( + pluginOptions: AWSStorageGetURLOptions(validateObjectExistence: true) + ) + ) + XCTFail("Expecting failure but got url: \(url)") + } catch StorageError.keyNotFound(let key, _, _, _) { + XCTAssertTrue(key.contains(unknownKey)) + } + + // A S3 HeadObject call is expected + XCTAssert(requestRecorder.sdkRequests.map(\.method).allSatisfy { $0 == .head }) + + XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, []) + } + + /// - Given: A key for a non-existent S3 object + /// - When: A pre-signed URL is requested for that key with `validateObjectExistence = false` + /// - Then: A pre-signed URL is returned + func testGetURLForUnknownKeyWithoutValidation() async throws { + let unknownKey = UUID().uuidString + let url = try await Amplify.Storage.getURL( + path: .fromString(unknownKey), + options: .init( + pluginOptions: AWSStorageGetURLOptions(validateObjectExistence: false) + ) + ) + XCTAssertNotNil(url) + + // No SDK or URLRequest calls expected + XCTAssertEqual(requestRecorder.sdkRequests.map { $0.method} , []) + XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, []) + } +} diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift new file mode 100644 index 0000000000..6b05278a11 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift @@ -0,0 +1,186 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify + +import AWSS3StoragePlugin +import ClientRuntime +import AWSClientRuntime +import CryptoKit +import XCTest +import AWSS3 + +class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase { + + /// Given: Multiple data object which is uploaded to a public path + /// When: `Amplify.Storage.list` is run + /// Then: The API should execute successfully and list objects for path + func testListObjectsUploadedPublicData() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + let uniqueStringPath = "public/\(key)" + + _ = try await Amplify.Storage.uploadData(path: .fromString(uniqueStringPath + "/test1"), data: data, options: nil).value + + let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(firstListResult.items.filter({ $0.path == uniqueStringPath}).count, 1) + + _ = try await Amplify.Storage.uploadData(path: .fromString(uniqueStringPath + "/test2"), data: data, options: nil).value + + let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(secondListResult.items.filter({ $0.path == uniqueStringPath}).count, 2) + + // Clean up + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "/test1")) + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "/test2")) + } + + /// Given: Multiple data object which is uploaded to a protected path + /// When: `Amplify.Storage.list` is run + /// Then: The API should execute successfully and list objects for path + func testListObjectsUploadedProtectedData() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + var uniqueStringPath = "" + + // Sign in + _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password) + + _ = try await Amplify.Storage.uploadData( + path: .fromIdentityID({ identityId in + uniqueStringPath = "protected/\(identityId)/\(key)" + return uniqueStringPath + "test1" + }), + data: data, + options: nil).value + + let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(firstListResult.items.filter({ $0.path == uniqueStringPath}).count, 1) + + _ = try await Amplify.Storage.uploadData( + path: .fromIdentityID({ identityId in + uniqueStringPath = "protected/\(identityId)/\(key)" + return uniqueStringPath + "test2" + }), + data: data, + options: nil).value + + let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(secondListResult.items.filter({ $0.path == uniqueStringPath}).count, 2) + + // clean up + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test1")) + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test2")) + + } + + /// Given: Multiple data object which is uploaded to a private path + /// When: `Amplify.Storage.list` is run + /// Then: The API should execute successfully and list objects for path + func testListObjectsUploadedPrivateData() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + var uniqueStringPath = "" + + // Sign in + _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password) + + _ = try await Amplify.Storage.uploadData( + path: .fromIdentityID({ identityId in + uniqueStringPath = "private/\(identityId)/\(key)" + return uniqueStringPath + "test1" + }), + data: data, + options: nil).value + + let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(firstListResult.items.filter({ $0.path == uniqueStringPath}).count, 1) + + _ = try await Amplify.Storage.uploadData( + path: .fromIdentityID({ identityId in + uniqueStringPath = "private/\(identityId)/\(key)" + return uniqueStringPath + "test2" + }), + data: data, + options: nil).value + + let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(secondListResult.items.filter({ $0.path == uniqueStringPath}).count, 2) + + // clean up + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test1")) + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test2")) + + } + + /// Given: Give a unique key that does not exist + /// When: `Amplify.Storage.list` is run + /// Then: The API should execute and throw an error + func testRemoveKeyDoesNotExist() async throws { + let key = UUID().uuidString + let uniqueStringPath = "public/\(key)" + + do { + _ = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + } + catch { + guard let storageError = error as? StorageError else { + XCTFail("Error should be of type StorageError but got \(error)") + return + } + guard case .keyNotFound(_, _, _, let underlyingError) = storageError else { + XCTFail("Error should be of type keyNotFound but got \(error)") + return + } + + guard underlyingError is AWSS3.NotFound else { + XCTFail("Underlying error should be of type AWSS3.NotFound but got \(error)") + return + } + } + } + + /// Given: Give a unique key where is user is NOT logged in + /// When: `Amplify.Storage.list` is run + /// Then: The API should execute and throw an error + func testRemoveKeyWhenNotSignedInForPrivateKey() async throws { + let key = UUID().uuidString + let uniqueStringPath = "private/\(key)" + + do { + _ = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + } + catch { + guard let storageError = error as? StorageError else { + XCTFail("Error should be of type StorageError but got \(error)") + return + } + guard case .accessDenied(_, _, let underlyingError) = storageError else { + XCTFail("Error should be of type keyNotFound but got \(error)") + return + } + + guard underlyingError is UnknownAWSHTTPServiceError else { + XCTFail("Underlying error should be of type UnknownAWSHTTPServiceError but got \(error)") + return + } + } + } + +} diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift new file mode 100644 index 0000000000..561c1504ba --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift @@ -0,0 +1,166 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify + +import AWSS3StoragePlugin +import ClientRuntime +import AWSClientRuntime +import CryptoKit +import XCTest +import AWSS3 + +class AWSS3StoragePluginRemoveIntegrationTests: AWSS3StoragePluginTestBase { + + /// Given: A data object which is uploaded to a public path + /// When: `Amplify.Storage.remove` is run + /// Then: The API should execute successfully and remove the object + func testRemoveUploadedPublicData() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + let uniqueStringPath = "public/\(key)" + + _ = try await Amplify.Storage.uploadData(path: .fromString(uniqueStringPath), data: data, options: nil).value + + let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(firstListResult.items.filter({ $0.key == uniqueStringPath}).count, 1) + + // Validate + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath)) + + let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(secondListResult.items.filter({ $0.key == uniqueStringPath}).count, 0) + + } + + /// Given: A data object which is uploaded to a protected path + /// When: `Amplify.Storage.remove` is run + /// Then: The API should execute successfully and remove the object + func testRemoveUploadedProtectedData() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + var uniqueStringPath = "" + + // Sign in + _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password) + + _ = try await Amplify.Storage.uploadData( + path: .fromIdentityID({ identityId in + uniqueStringPath = "protected/\(identityId)/\(key)" + return uniqueStringPath + }), + data: data, + options: nil).value + + let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(firstListResult.items.filter({ $0.key == uniqueStringPath}).count, 1) + + // Validate + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath)) + + let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(secondListResult.items.filter({ $0.key == uniqueStringPath}).count, 0) + + } + + /// Given: A data object which is uploaded to a private path + /// When: `Amplify.Storage.remove` is run + /// Then: The API should execute successfully and remove the object + func testRemoveUploadedPrivateData() async throws { + let key = UUID().uuidString + let data = Data(key.utf8) + var uniqueStringPath = "" + + // Sign in + _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password) + + _ = try await Amplify.Storage.uploadData( + path: .fromIdentityID({ identityId in + uniqueStringPath = "private/\(identityId)/\(key)" + return uniqueStringPath + }), + data: data, + options: nil).value + + let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(firstListResult.items.filter({ $0.key == uniqueStringPath}).count, 1) + + // Validate + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath)) + + let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) + + // Validate the item was uploaded. + XCTAssertEqual(secondListResult.items.filter({ $0.key == uniqueStringPath}).count, 0) + + } + + /// Given: Give a unique key that does not exist + /// When: `Amplify.Storage.remove` is run + /// Then: The API should execute and throw an error + func testRemoveKeyDoesNotExist() async throws { + let key = UUID().uuidString + let uniqueStringPath = "public/\(key)" + + do { + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath)) + } + catch { + guard let storageError = error as? StorageError else { + XCTFail("Error should be of type StorageError but got \(error)") + return + } + guard case .keyNotFound(_, _, _, let underlyingError) = storageError else { + XCTFail("Error should be of type keyNotFound but got \(error)") + return + } + + guard underlyingError is AWSS3.NotFound else { + XCTFail("Underlying error should be of type AWSS3.NotFound but got \(error)") + return + } + } + } + + /// Given: Give a unique key where is user is NOT logged in + /// When: `Amplify.Storage.remove` is run + /// Then: The API should execute and throw an error + func testRemoveKeyWhenNotSignedInForPrivateKey() async throws { + let key = UUID().uuidString + let uniqueStringPath = "private/\(key)" + + do { + _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath)) + } + catch { + guard let storageError = error as? StorageError else { + XCTFail("Error should be of type StorageError but got \(error)") + return + } + guard case .accessDenied(_, _, let underlyingError) = storageError else { + XCTFail("Error should be of type keyNotFound but got \(error)") + return + } + + guard underlyingError is UnknownAWSHTTPServiceError else { + XCTFail("Underlying error should be of type UnknownAWSHTTPServiceError but got \(error)") + return + } + } + } + +} diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj index 2fcf520255..fa8368a9a5 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 031BC3F328EC9B2C0047B2E8 /* AppIcon.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 031BC3F228EC9B2C0047B2E8 /* AppIcon.xcassets */; }; 21D165C32BBEF329001E3D4B /* amplify_outputs.json in Resources */ = {isa = PBXBuildFile; fileRef = 21D165C22BBEF329001E3D4B /* amplify_outputs.json */; }; 21D165C42BBEF329001E3D4B /* amplify_outputs.json in Resources */ = {isa = PBXBuildFile; fileRef = 21D165C22BBEF329001E3D4B /* amplify_outputs.json */; }; + 488C2A732BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */; }; + 488C2A752BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */; }; + 488C2A772BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */; }; 56043E9329FC4D33003E3424 /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = D5C0382101A0E23943FDF4CB /* amplifyconfiguration.json */; }; 562B9AA42A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; 562B9AA52A0D734E00A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; @@ -108,6 +111,9 @@ 21D165C02BBEDF0A001E3D4B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 21D165C12BBEEFE8001E3D4B /* AWSS3StoragePluginGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AWSS3StoragePluginGen2IntegrationTests.xctestplan; sourceTree = ""; }; 21D165C22BBEF329001E3D4B /* amplify_outputs.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = amplify_outputs.json; sourceTree = ""; }; + 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginRemoveIntegrationTests.swift; sourceTree = ""; }; + 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginListObjectsIntegrationTests.swift; sourceTree = ""; }; + 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginGetURLIntegrationTests.swift; sourceTree = ""; }; 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginRequestRecorder.swift; sourceTree = ""; }; 565DF16F2953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginAccelerateIntegrationTests.swift; sourceTree = ""; }; 681D7D392A42637700F7C310 /* StorageWatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StorageWatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -282,6 +288,9 @@ 684FB08728BEAF8E00C8A6EB /* ResumabilityTests */, 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */, 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */, + 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */, + 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */, + 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */, ); path = AWSS3StoragePluginIntegrationTests; sourceTree = ""; @@ -623,18 +632,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 488C2A772BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift in Sources */, 565DF1702953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift in Sources */, 684FB0C328BEB45600C8A6EB /* AuthSignInHelper.swift in Sources */, 681DFEB228E748270000C36A /* AsyncTesting.swift in Sources */, 68828E4828C2AAA6006E7C0A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */, 901AB3E92AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */, 681DFEB328E748270000C36A /* AsyncExpectation.swift in Sources */, + 488C2A732BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift in Sources */, 68828E4628C2736C006E7C0A /* AWSS3StoragePluginProgressTests.swift in Sources */, 684FB0B528BEB08900C8A6EB /* AWSS3StoragePluginAccessLevelTests.swift in Sources */, 68828E4028C1549E006E7C0A /* AWSS3StoragePluginDownloadFileResumabilityTests.swift in Sources */, 734605242BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift in Sources */, 68828E4528C26D2D006E7C0A /* AWSS3StoragePluginPrefixKeyResolverTests.swift in Sources */, 684FB0B328BEB08900C8A6EB /* AWSS3StoragePluginTestBase.swift in Sources */, + 488C2A752BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift in Sources */, 68828E3F28C1549B006E7C0A /* AWSS3StoragePluginUploadFileResumabilityTests.swift in Sources */, 562B9AA42A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */, 68828E3E28C1546F006E7C0A /* AWSS3StoragePluginConfigurationTests.swift in Sources */,