diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Adapter.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Adapter.swift index 969fd6475f..5fd65c6ec0 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Adapter.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Adapter.swift @@ -18,10 +18,10 @@ import AWSClientRuntime /// and allows for mocking in unit tests. The methods contain no other logic other than calling the /// same method using the AWSS3 instance. class AWSS3Adapter: AWSS3Behavior { - let awsS3: S3Client + let awsS3: S3ClientProtocol let config: S3Client.S3ClientConfiguration - init(_ awsS3: S3Client, config: S3Client.S3ClientConfiguration) { + init(_ awsS3: S3ClientProtocol, config: S3Client.S3ClientConfiguration) { self.awsS3 = awsS3 self.config = config } @@ -161,7 +161,7 @@ class AWSS3Adapter: AWSS3Behavior { /// Instance of S3 service. /// - Returns: S3 service instance. - func getS3() -> S3Client { + func getS3() -> S3ClientProtocol { return awsS3 } } diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Behavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Behavior.swift index 7319878805..400ca3eb6c 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Behavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Dependency/AWSS3Behavior.swift @@ -35,7 +35,7 @@ protocol AWSS3Behavior { func abortMultipartUpload(_ request: AWSS3AbortMultipartUploadRequest, completion: @escaping (Result) -> Void) // Gets a client for AWS S3 Service. - func getS3() -> S3Client + func getS3() -> S3ClientProtocol } diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift index c26fbc2c79..d31b9e588e 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift @@ -54,6 +54,7 @@ class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { httpClientEngineProxy: HttpClientEngineProxy? = nil, storageConfiguration: StorageConfiguration = .default, storageTransferDatabase: StorageTransferDatabase = .default, + fileSystem: FileSystem = .default, sessionConfiguration: URLSessionConfiguration? = nil, delegateQueue: OperationQueue? = nil, logger: Logger = storageLogger) throws { @@ -97,7 +98,9 @@ class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { self.init(authService: authService, storageConfiguration: storageConfiguration, storageTransferDatabase: storageTransferDatabase, + fileSystem: fileSystem, sessionConfiguration: _sessionConfiguration, + logger: logger, s3Client: s3Client, preSignedURLBuilder: preSignedURLBuilder, awsS3: awsS3, diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadSession.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadSession.swift index d335f31805..fc51016cb9 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadSession.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadSession.swift @@ -44,14 +44,6 @@ class StorageMultipartUploadSession { private let transferTask: StorageTransferTask - private var contentType: String? { - transferTask.contentType - } - - private var requestHeaders: RequestHeaders? { - transferTask.requestHeaders - } - init(client: StorageMultipartUploadClient, bucket: String, key: String, diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageTransferTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageTransferTask.swift index 1cd7e95385..1ecf8894db 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageTransferTask.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageTransferTask.swift @@ -173,10 +173,6 @@ class StorageTransferTask { } } - private var cancelled: Bool { - status == .cancelled - } - var isFailed: Bool { status == .error } @@ -324,7 +320,7 @@ class StorageTransferTask { logger.warn("Unable to complete after cancelled") return } - guard _status == .completed else { + guard _status != .completed else { logger.warn("Task is already completed") return } diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Dependency/AWSS3AdapterTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Dependency/AWSS3AdapterTests.swift new file mode 100644 index 0000000000..031b7d8456 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Dependency/AWSS3AdapterTests.swift @@ -0,0 +1,691 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +//@testable import func AmplifyTestCommon.XCTAssertThrowFatalError +@testable import AWSS3StoragePlugin +import AWSS3 +import XCTest + +class AWSS3AdapterTests: XCTestCase { + private var adapter: AWSS3Adapter! + private var awsS3: S3ClientMock! + + override func setUp() { + awsS3 = S3ClientMock() + adapter = AWSS3Adapter( + awsS3, + config: try! S3Client.S3ClientConfiguration( + region: "us-east-1" + ) + ) + } + + override func tearDown() { + adapter = nil + awsS3 = nil + } + + func testDeleteObject_withSuccess_shouldSucceed() { + let deleteExpectation = expectation(description: "Delete Object") + adapter.deleteObject(.init(bucket: "bucket", key: "key")) { result in + XCTAssertEqual(self.awsS3.deleteObjectCount, 1) + guard case .success = result else { + XCTFail("Expected success") + return + } + deleteExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testDeleteObject_withError_shouldFail() { + let deleteExpectation = expectation(description: "Delete Object") + awsS3.deleteObjectResult = .failure(StorageError.keyNotFound("InvalidKey", "", "", nil)) + adapter.deleteObject(.init(bucket: "bucket", key: "key")) { result in + XCTAssertEqual(self.awsS3.deleteObjectCount, 1) + guard case .failure(let error) = result, + case .keyNotFound(let key, _, _, _) = error else { + XCTFail("Expected StorageError.keyNotFound") + return + } + XCTAssertEqual(key, "InvalidKey") + deleteExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testListObjectsV2_withSuccess_shouldSucceed() { + let listExpectation = expectation(description: "List Objects") + awsS3.listObjectsV2Result = .success(ListObjectsV2OutputResponse( + contents: [ + .init(eTag: "one", key: "key1", lastModified: .init()), + .init(eTag: "two", key: "key2", lastModified: .init()) + ] + )) + adapter.listObjectsV2(.init( + bucket: "bucket", + prefix: "prefix" + )) { result in + XCTAssertEqual(self.awsS3.listObjectsV2Count, 1) + guard case .success = result else { + XCTFail("Expected success") + return + } + listExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testListObjectsV2_withError_shouldFail() { + let listExpectation = expectation(description: "List Objects") + awsS3.listObjectsV2Result = .failure(StorageError.accessDenied("AccessDenied", "", nil)) + adapter.listObjectsV2(.init( + bucket: "bucket", + prefix: "prefix" + )) { result in + XCTAssertEqual(self.awsS3.listObjectsV2Count, 1) + guard case .failure(let error) = result, + case .accessDenied(let description, _, _) = error else { + XCTFail("Expected StorageError.accessDenied") + return + } + XCTAssertEqual(description, "AccessDenied") + listExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testCreateMultipartUpload_withSuccess_shouldSucceed() { + let createMultipartUploadExpectation = expectation(description: "Create Multipart Upload") + awsS3.createMultipartUploadResult = .success(.init( + bucket: "bucket", + key: "key", + uploadId: "uploadId" + )) + adapter.createMultipartUpload(.init(bucket: "bucket", key: "key")) { result in + XCTAssertEqual(self.awsS3.createMultipartUploadCount, 1) + guard case .success = result else { + XCTFail("Expected success") + return + } + createMultipartUploadExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testCreateMultipartUpload_withWrongResponse_shouldFail() { + let createMultipartUploadExpectation = expectation(description: "Create Multipart Upload") + adapter.createMultipartUpload(.init(bucket: "bucket", key: "key")) { result in + XCTAssertEqual(self.awsS3.createMultipartUploadCount, 1) + guard case .failure(let error) = result, + case .unknown(let description, _) = error else { + XCTFail("Expected StorageError.unknown") + return + } + XCTAssertEqual(description, "Invalid response for creating multipart upload") + createMultipartUploadExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testCreateMultipartUpload_withError_shouldFail() { + let createMultipartUploadExpectation = expectation(description: "Create Multipart Upload") + awsS3.createMultipartUploadResult = .failure(StorageError.accessDenied("AccessDenied", "", nil)) + adapter.createMultipartUpload(.init(bucket: "bucket", key: "key")) { result in + XCTAssertEqual(self.awsS3.createMultipartUploadCount, 1) + guard case .failure(let error) = result, + case .accessDenied(let description, _, _) = error else { + XCTFail("Expected StorageError.accessDenied") + return + } + XCTAssertEqual(description, "AccessDenied") + createMultipartUploadExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testListParts_withSuccess_shouldSucceed() { + let listPartsExpectation = expectation(description: "List Parts") + awsS3.listPartsResult = .success(.init( + bucket: "bucket", + key: "key", + parts: [.init(), .init()], + uploadId: "uploadId" + )) + adapter.listParts(bucket: "bucket", key: "key", uploadId: "uploadId") { result in + XCTAssertEqual(self.awsS3.listPartsCount, 1) + guard case .success = result else { + XCTFail("Expected success") + return + } + listPartsExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testListParts_withWrongResponse_shouldFail() { + let listPartsExpectation = expectation(description: "List Parts") + adapter.listParts(bucket: "bucket", key: "key", uploadId: "uploadId") { result in + XCTAssertEqual(self.awsS3.listPartsCount, 1) + guard case .failure(let error) = result, + case .unknown(let description, _) = error else { + XCTFail("Expected StorageError.unknown") + return + } + XCTAssertEqual(description, "ListParts response is invalid") + listPartsExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testListParts_withError_shouldFail() { + let listPartsExpectation = expectation(description: "List Parts") + awsS3.listPartsResult = .failure(StorageError.authError("AuthError", "", nil)) + adapter.listParts(bucket: "bucket", key: "key", uploadId: "uploadId") { result in + XCTAssertEqual(self.awsS3.listPartsCount, 1) + guard case .failure(let error) = result, + case .authError(let description, _, _) = error else { + XCTFail("Expected StorageError.authError") + return + } + XCTAssertEqual(description, "AuthError") + listPartsExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testCompleteMultipartUpload_withSuccess_shouldSucceed() { + let completeMultipartUploadExpectation = expectation(description: "Complete Multipart Upload") + awsS3.completeMultipartUploadResult = .success(.init( + eTag: "eTag" + )) + adapter.completeMultipartUpload(.init( + bucket: "bucket", + key: "key", + uploadId: "uploadId", + parts: [.init(partNumber: 1, eTag: "eTag1"), .init(partNumber: 2, eTag: "eTag2")] + )) { result in + XCTAssertEqual(self.awsS3.completeMultipartUploadCount, 1) + guard case .success = result else { + XCTFail("Expected success") + return + } + completeMultipartUploadExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testCompleteMultipartUpload_withWrongResponse_shouldFail() { + let completeMultipartUploadExpectation = expectation(description: "Complete Multipart Upload") + adapter.completeMultipartUpload(.init(bucket: "bucket", key: "key", uploadId: "uploadId", parts: [])) { result in + XCTAssertEqual(self.awsS3.completeMultipartUploadCount, 1) + guard case .failure(let error) = result, + case .unknown(let description, _) = error else { + XCTFail("Expected StorageError.unknown") + return + } + XCTAssertEqual(description, "Invalid response for completing multipart upload") + completeMultipartUploadExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testCompleteMultipartUpload_withError_shouldFail() { + let completeMultipartUploadExpectation = expectation(description: "Complete Multipart Upload") + awsS3.completeMultipartUploadResult = .failure(StorageError.authError("AuthError", "", nil)) + adapter.completeMultipartUpload(.init(bucket: "bucket", key: "key", uploadId: "uploadId", parts: [])) { result in + XCTAssertEqual(self.awsS3.completeMultipartUploadCount, 1) + guard case .failure(let error) = result, + case .authError(let description, _, _) = error else { + XCTFail("Expected StorageError.authError") + return + } + XCTAssertEqual(description, "AuthError") + completeMultipartUploadExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testAbortMultipartUpload_withSuccess_shouldSucceed() { + let abortExpectation = expectation(description: "Abort Multipart Upload") + adapter.abortMultipartUpload(.init(bucket: "bucket", key: "key", uploadId: "uploadId")) { result in + XCTAssertEqual(self.awsS3.abortMultipartUploadCount, 1) + guard case .success = result else { + XCTFail("Expected success") + return + } + abortExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testAbortMultipartUpload_withError_shouldFail() { + let abortExpectation = expectation(description: "Abort Multipart Upload") + awsS3.abortMultipartUploadResult = .failure(StorageError.keyNotFound("InvalidKey", "", "", nil)) + adapter.abortMultipartUpload(.init(bucket: "bucket", key: "key", uploadId: "uploadId")) { result in + XCTAssertEqual(self.awsS3.abortMultipartUploadCount, 1) + guard case .failure(let error) = result, + case .keyNotFound(let key, _, _, _) = error else { + XCTFail("Expected StorageError.keyNotFound") + return + } + XCTAssertEqual(key, "InvalidKey") + abortExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testGetS3() { + XCTAssertTrue(adapter.getS3() is S3ClientMock) + } +} + +private class S3ClientMock: S3ClientProtocol { + var deleteObjectCount = 0 + var deleteObjectResult: Result = .success(.init()) + func deleteObject(input: AWSS3.DeleteObjectInput) async throws -> AWSS3.DeleteObjectOutputResponse { + deleteObjectCount += 1 + return try deleteObjectResult.get() + } + + var listObjectsV2Count = 0 + var listObjectsV2Result: Result = .success(.init()) + func listObjectsV2(input: AWSS3.ListObjectsV2Input) async throws -> AWSS3.ListObjectsV2OutputResponse { + listObjectsV2Count += 1 + return try listObjectsV2Result.get() + } + + var createMultipartUploadCount = 0 + var createMultipartUploadResult: Result = .success(.init()) + func createMultipartUpload(input: AWSS3.CreateMultipartUploadInput) async throws -> AWSS3.CreateMultipartUploadOutputResponse { + createMultipartUploadCount += 1 + return try createMultipartUploadResult.get() + } + + var listPartsCount = 0 + var listPartsResult: Result = .success(.init()) + func listParts(input: AWSS3.ListPartsInput) async throws -> AWSS3.ListPartsOutputResponse { + listPartsCount += 1 + return try listPartsResult.get() + } + + var completeMultipartUploadCount = 0 + var completeMultipartUploadResult: Result = .success(.init()) + func completeMultipartUpload(input: AWSS3.CompleteMultipartUploadInput) async throws -> AWSS3.CompleteMultipartUploadOutputResponse { + completeMultipartUploadCount += 1 + return try completeMultipartUploadResult.get() + } + + var abortMultipartUploadCount = 0 + var abortMultipartUploadResult: Result = .success(.init()) + func abortMultipartUpload(input: AWSS3.AbortMultipartUploadInput) async throws -> AWSS3.AbortMultipartUploadOutputResponse { + abortMultipartUploadCount += 1 + return try abortMultipartUploadResult.get() + } + + func copyObject(input: AWSS3.CopyObjectInput) async throws -> AWSS3.CopyObjectOutputResponse { + fatalError("Not Implemented") + } + + func createBucket(input: AWSS3.CreateBucketInput) async throws -> AWSS3.CreateBucketOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucket(input: AWSS3.DeleteBucketInput) async throws -> AWSS3.DeleteBucketOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketAnalyticsConfiguration(input: AWSS3.DeleteBucketAnalyticsConfigurationInput) async throws -> AWSS3.DeleteBucketAnalyticsConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketCors(input: AWSS3.DeleteBucketCorsInput) async throws -> AWSS3.DeleteBucketCorsOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketEncryption(input: AWSS3.DeleteBucketEncryptionInput) async throws -> AWSS3.DeleteBucketEncryptionOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketIntelligentTieringConfiguration(input: AWSS3.DeleteBucketIntelligentTieringConfigurationInput) async throws -> AWSS3.DeleteBucketIntelligentTieringConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketInventoryConfiguration(input: AWSS3.DeleteBucketInventoryConfigurationInput) async throws -> AWSS3.DeleteBucketInventoryConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketLifecycle(input: AWSS3.DeleteBucketLifecycleInput) async throws -> AWSS3.DeleteBucketLifecycleOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketMetricsConfiguration(input: AWSS3.DeleteBucketMetricsConfigurationInput) async throws -> AWSS3.DeleteBucketMetricsConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketOwnershipControls(input: AWSS3.DeleteBucketOwnershipControlsInput) async throws -> AWSS3.DeleteBucketOwnershipControlsOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketPolicy(input: AWSS3.DeleteBucketPolicyInput) async throws -> AWSS3.DeleteBucketPolicyOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketReplication(input: AWSS3.DeleteBucketReplicationInput) async throws -> AWSS3.DeleteBucketReplicationOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketTagging(input: AWSS3.DeleteBucketTaggingInput) async throws -> AWSS3.DeleteBucketTaggingOutputResponse { + fatalError("Not Implemented") + } + + func deleteBucketWebsite(input: AWSS3.DeleteBucketWebsiteInput) async throws -> AWSS3.DeleteBucketWebsiteOutputResponse { + fatalError("Not Implemented") + } + + func deleteObjects(input: AWSS3.DeleteObjectsInput) async throws -> AWSS3.DeleteObjectsOutputResponse { + fatalError("Not Implemented") + } + + func deleteObjectTagging(input: AWSS3.DeleteObjectTaggingInput) async throws -> AWSS3.DeleteObjectTaggingOutputResponse { + fatalError("Not Implemented") + } + + func deletePublicAccessBlock(input: AWSS3.DeletePublicAccessBlockInput) async throws -> AWSS3.DeletePublicAccessBlockOutputResponse { + fatalError("Not Implemented") + } + + func getBucketAccelerateConfiguration(input: AWSS3.GetBucketAccelerateConfigurationInput) async throws -> AWSS3.GetBucketAccelerateConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketAcl(input: AWSS3.GetBucketAclInput) async throws -> AWSS3.GetBucketAclOutputResponse { + fatalError("Not Implemented") + } + + func getBucketAnalyticsConfiguration(input: AWSS3.GetBucketAnalyticsConfigurationInput) async throws -> AWSS3.GetBucketAnalyticsConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketCors(input: AWSS3.GetBucketCorsInput) async throws -> AWSS3.GetBucketCorsOutputResponse { + fatalError("Not Implemented") + } + + func getBucketEncryption(input: AWSS3.GetBucketEncryptionInput) async throws -> AWSS3.GetBucketEncryptionOutputResponse { + fatalError("Not Implemented") + } + + func getBucketIntelligentTieringConfiguration(input: AWSS3.GetBucketIntelligentTieringConfigurationInput) async throws -> AWSS3.GetBucketIntelligentTieringConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketInventoryConfiguration(input: AWSS3.GetBucketInventoryConfigurationInput) async throws -> AWSS3.GetBucketInventoryConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketLifecycleConfiguration(input: AWSS3.GetBucketLifecycleConfigurationInput) async throws -> AWSS3.GetBucketLifecycleConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketLocation(input: AWSS3.GetBucketLocationInput) async throws -> AWSS3.GetBucketLocationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketLogging(input: AWSS3.GetBucketLoggingInput) async throws -> AWSS3.GetBucketLoggingOutputResponse { + fatalError("Not Implemented") + } + + func getBucketMetricsConfiguration(input: AWSS3.GetBucketMetricsConfigurationInput) async throws -> AWSS3.GetBucketMetricsConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketNotificationConfiguration(input: AWSS3.GetBucketNotificationConfigurationInput) async throws -> AWSS3.GetBucketNotificationConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketOwnershipControls(input: AWSS3.GetBucketOwnershipControlsInput) async throws -> AWSS3.GetBucketOwnershipControlsOutputResponse { + fatalError("Not Implemented") + } + + func getBucketPolicy(input: AWSS3.GetBucketPolicyInput) async throws -> AWSS3.GetBucketPolicyOutputResponse { + fatalError("Not Implemented") + } + + func getBucketPolicyStatus(input: AWSS3.GetBucketPolicyStatusInput) async throws -> AWSS3.GetBucketPolicyStatusOutputResponse { + fatalError("Not Implemented") + } + + func getBucketReplication(input: AWSS3.GetBucketReplicationInput) async throws -> AWSS3.GetBucketReplicationOutputResponse { + fatalError("Not Implemented") + } + + func getBucketRequestPayment(input: AWSS3.GetBucketRequestPaymentInput) async throws -> AWSS3.GetBucketRequestPaymentOutputResponse { + fatalError("Not Implemented") + } + + func getBucketTagging(input: AWSS3.GetBucketTaggingInput) async throws -> AWSS3.GetBucketTaggingOutputResponse { + fatalError("Not Implemented") + } + + func getBucketVersioning(input: AWSS3.GetBucketVersioningInput) async throws -> AWSS3.GetBucketVersioningOutputResponse { + fatalError("Not Implemented") + } + + func getBucketWebsite(input: AWSS3.GetBucketWebsiteInput) async throws -> AWSS3.GetBucketWebsiteOutputResponse { + fatalError("Not Implemented") + } + + func getObject(input: AWSS3.GetObjectInput) async throws -> AWSS3.GetObjectOutputResponse { + fatalError("Not Implemented") + } + + func getObjectAcl(input: AWSS3.GetObjectAclInput) async throws -> AWSS3.GetObjectAclOutputResponse { + fatalError("Not Implemented") + } + + func getObjectAttributes(input: AWSS3.GetObjectAttributesInput) async throws -> AWSS3.GetObjectAttributesOutputResponse { + fatalError("Not Implemented") + } + + func getObjectLegalHold(input: AWSS3.GetObjectLegalHoldInput) async throws -> AWSS3.GetObjectLegalHoldOutputResponse { + fatalError("Not Implemented") + } + + func getObjectLockConfiguration(input: AWSS3.GetObjectLockConfigurationInput) async throws -> AWSS3.GetObjectLockConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func getObjectRetention(input: AWSS3.GetObjectRetentionInput) async throws -> AWSS3.GetObjectRetentionOutputResponse { + fatalError("Not Implemented") + } + + func getObjectTagging(input: AWSS3.GetObjectTaggingInput) async throws -> AWSS3.GetObjectTaggingOutputResponse { + fatalError("Not Implemented") + } + + func getObjectTorrent(input: AWSS3.GetObjectTorrentInput) async throws -> AWSS3.GetObjectTorrentOutputResponse { + fatalError("Not Implemented") + } + + func getPublicAccessBlock(input: AWSS3.GetPublicAccessBlockInput) async throws -> AWSS3.GetPublicAccessBlockOutputResponse { + fatalError("Not Implemented") + } + + func headBucket(input: AWSS3.HeadBucketInput) async throws -> AWSS3.HeadBucketOutputResponse { + fatalError("Not Implemented") + } + + func headObject(input: AWSS3.HeadObjectInput) async throws -> AWSS3.HeadObjectOutputResponse { + fatalError("Not Implemented") + } + + func listBucketAnalyticsConfigurations(input: AWSS3.ListBucketAnalyticsConfigurationsInput) async throws -> AWSS3.ListBucketAnalyticsConfigurationsOutputResponse { + fatalError("Not Implemented") + } + + func listBucketIntelligentTieringConfigurations(input: AWSS3.ListBucketIntelligentTieringConfigurationsInput) async throws -> AWSS3.ListBucketIntelligentTieringConfigurationsOutputResponse { + fatalError("Not Implemented") + } + + func listBucketInventoryConfigurations(input: AWSS3.ListBucketInventoryConfigurationsInput) async throws -> AWSS3.ListBucketInventoryConfigurationsOutputResponse { + fatalError("Not Implemented") + } + + func listBucketMetricsConfigurations(input: AWSS3.ListBucketMetricsConfigurationsInput) async throws -> AWSS3.ListBucketMetricsConfigurationsOutputResponse { + fatalError("Not Implemented") + } + + func listBuckets(input: AWSS3.ListBucketsInput) async throws -> AWSS3.ListBucketsOutputResponse { + fatalError("Not Implemented") + } + + func listMultipartUploads(input: AWSS3.ListMultipartUploadsInput) async throws -> AWSS3.ListMultipartUploadsOutputResponse { + fatalError("Not Implemented") + } + + func listObjects(input: AWSS3.ListObjectsInput) async throws -> AWSS3.ListObjectsOutputResponse { + fatalError("Not Implemented") + } + + func listObjectVersions(input: AWSS3.ListObjectVersionsInput) async throws -> AWSS3.ListObjectVersionsOutputResponse { + fatalError("Not Implemented") + } + + func putBucketAccelerateConfiguration(input: AWSS3.PutBucketAccelerateConfigurationInput) async throws -> AWSS3.PutBucketAccelerateConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketAcl(input: AWSS3.PutBucketAclInput) async throws -> AWSS3.PutBucketAclOutputResponse { + fatalError("Not Implemented") + } + + func putBucketAnalyticsConfiguration(input: AWSS3.PutBucketAnalyticsConfigurationInput) async throws -> AWSS3.PutBucketAnalyticsConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketCors(input: AWSS3.PutBucketCorsInput) async throws -> AWSS3.PutBucketCorsOutputResponse { + fatalError("Not Implemented") + } + + func putBucketEncryption(input: AWSS3.PutBucketEncryptionInput) async throws -> AWSS3.PutBucketEncryptionOutputResponse { + fatalError("Not Implemented") + } + + func putBucketIntelligentTieringConfiguration(input: AWSS3.PutBucketIntelligentTieringConfigurationInput) async throws -> AWSS3.PutBucketIntelligentTieringConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketInventoryConfiguration(input: AWSS3.PutBucketInventoryConfigurationInput) async throws -> AWSS3.PutBucketInventoryConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketLifecycleConfiguration(input: AWSS3.PutBucketLifecycleConfigurationInput) async throws -> AWSS3.PutBucketLifecycleConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketLogging(input: AWSS3.PutBucketLoggingInput) async throws -> AWSS3.PutBucketLoggingOutputResponse { + fatalError("Not Implemented") + } + + func putBucketMetricsConfiguration(input: AWSS3.PutBucketMetricsConfigurationInput) async throws -> AWSS3.PutBucketMetricsConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketNotificationConfiguration(input: AWSS3.PutBucketNotificationConfigurationInput) async throws -> AWSS3.PutBucketNotificationConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketOwnershipControls(input: AWSS3.PutBucketOwnershipControlsInput) async throws -> AWSS3.PutBucketOwnershipControlsOutputResponse { + fatalError("Not Implemented") + } + + func putBucketPolicy(input: AWSS3.PutBucketPolicyInput) async throws -> AWSS3.PutBucketPolicyOutputResponse { + fatalError("Not Implemented") + } + + func putBucketReplication(input: AWSS3.PutBucketReplicationInput) async throws -> AWSS3.PutBucketReplicationOutputResponse { + fatalError("Not Implemented") + } + + func putBucketRequestPayment(input: AWSS3.PutBucketRequestPaymentInput) async throws -> AWSS3.PutBucketRequestPaymentOutputResponse { + fatalError("Not Implemented") + } + + func putBucketTagging(input: AWSS3.PutBucketTaggingInput) async throws -> AWSS3.PutBucketTaggingOutputResponse { + fatalError("Not Implemented") + } + + func putBucketVersioning(input: AWSS3.PutBucketVersioningInput) async throws -> AWSS3.PutBucketVersioningOutputResponse { + fatalError("Not Implemented") + } + + func putBucketWebsite(input: AWSS3.PutBucketWebsiteInput) async throws -> AWSS3.PutBucketWebsiteOutputResponse { + fatalError("Not Implemented") + } + + func putObject(input: AWSS3.PutObjectInput) async throws -> AWSS3.PutObjectOutputResponse { + fatalError("Not Implemented") + } + + func putObjectAcl(input: AWSS3.PutObjectAclInput) async throws -> AWSS3.PutObjectAclOutputResponse { + fatalError("Not Implemented") + } + + func putObjectLegalHold(input: AWSS3.PutObjectLegalHoldInput) async throws -> AWSS3.PutObjectLegalHoldOutputResponse { + fatalError("Not Implemented") + } + + func putObjectLockConfiguration(input: AWSS3.PutObjectLockConfigurationInput) async throws -> AWSS3.PutObjectLockConfigurationOutputResponse { + fatalError("Not Implemented") + } + + func putObjectRetention(input: AWSS3.PutObjectRetentionInput) async throws -> AWSS3.PutObjectRetentionOutputResponse { + fatalError("Not Implemented") + } + + func putObjectTagging(input: AWSS3.PutObjectTaggingInput) async throws -> AWSS3.PutObjectTaggingOutputResponse { + fatalError("Not Implemented") + } + + func putPublicAccessBlock(input: AWSS3.PutPublicAccessBlockInput) async throws -> AWSS3.PutPublicAccessBlockOutputResponse { + fatalError("Not Implemented") + } + + func restoreObject(input: AWSS3.RestoreObjectInput) async throws -> AWSS3.RestoreObjectOutputResponse { + fatalError("Not Implemented") + } + + func selectObjectContent(input: AWSS3.SelectObjectContentInput) async throws -> AWSS3.SelectObjectContentOutputResponse { + fatalError("Not Implemented") + } + + func uploadPart(input: AWSS3.UploadPartInput) async throws -> AWSS3.UploadPartOutputResponse { + fatalError("Not Implemented") + } + + func uploadPartCopy(input: AWSS3.UploadPartCopyInput) async throws -> AWSS3.UploadPartCopyOutputResponse { + fatalError("Not Implemented") + } + + func writeGetObjectResponse(input: AWSS3.WriteGetObjectResponseInput) async throws -> AWSS3.WriteGetObjectResponseOutputResponse { + fatalError("Not Implemented") + } +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTests.swift new file mode 100644 index 0000000000..03bda7d9e2 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTests.swift @@ -0,0 +1,404 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +@testable import AWSPluginsTestCommon +@testable import AWSS3StoragePlugin +import ClientRuntime +import AWSS3 +import XCTest + +class AWSS3StorageServiceTests: XCTestCase { + private var service: AWSS3StorageService! + private var authService: MockAWSAuthService! + private var database: StorageTransferDatabaseMock! + private var task: StorageTransferTask! + private var fileSystem: MockFileSystem! + + override func setUp() async throws { + authService = MockAWSAuthService() + database = StorageTransferDatabaseMock() + fileSystem = MockFileSystem() + task = StorageTransferTask( + transferType: .download(onEvent: { _ in}), + bucket: "bucket", + key: "key" + ) + task.uploadId = "uploadId" + task.sessionTask = MockStorageSessionTask(taskIdentifier: 1) + database.recoverResult = .success([ + .init(transferTask: task, + multipartUploads: [ + .created( + uploadId: "uploadId", + uploadFile:UploadFile( + fileURL: FileSystem.default.createTemporaryFileURL(), + temporaryFileCreated: true, + size: UInt64(Bytes.megabytes(12).bytes) + ) + ) + ] + ) + ]) + service = try AWSS3StorageService( + authService: authService, + region: "region", + bucket: "bucket", + httpClientEngineProxy: MockHttpClientEngineProxy(), + storageTransferDatabase: database, + fileSystem: fileSystem, + logger: MockLogger() + ) + } + + override func tearDown() { + authService = nil + service = nil + database = nil + task = nil + fileSystem = nil + } + + func testDeinit_shouldUnregisterIdentifier() { + XCTAssertNotNil(StorageBackgroundEventsRegistry.identifier) + service = nil + XCTAssertNil(StorageBackgroundEventsRegistry.identifier) + } + + func testReset_shouldSetValuesToNil() { + service.reset() + XCTAssertNil(service.preSignedURLBuilder) + XCTAssertNil(service.awsS3) + XCTAssertNil(service.region) + XCTAssertNil(service.bucket) + XCTAssertTrue(service.tasks.isEmpty) + XCTAssertTrue(service.multipartUploadSessions.isEmpty) + } + + func testAttachEventHandlers() { + let expectation = self.expectation(description: "Attach Event Handlers") + service.attachEventHandlers( + onUpload: { event in + guard case .completed(_) = event else { + XCTFail("Expected completed") + return + } + expectation.fulfill() + } + ) + XCTAssertNotNil(database.onUploadHandler) + database.onUploadHandler?(.completed(())) + waitForExpectations(timeout: 1) + } + + func testRegisterTask_shouldAddItToTasksDictionary() { + service.register(task: task) + XCTAssertEqual(service.tasks.count, 1) + XCTAssertNotNil(service.tasks[1]) + } + + func testUnregisterTask_shouldRemoveItToTasksDictionary() { + service.tasks = [ + 1: task + ] + service.unregister(task: task) + XCTAssertTrue(service.tasks.isEmpty) + XCTAssertNil(service.tasks[1]) + } + + func testUnregisterTaskIdentifiers_shouldRemoveItToTasksDictionary() { + service.tasks = [ + 1: task, + 2: task + ] + service.unregister(taskIdentifiers: [1]) + XCTAssertEqual(service.tasks.count, 1) + XCTAssertNotNil(service.tasks[2]) + XCTAssertNil(service.tasks[1]) + } + + func testFindTask_shouldReturnTask() { + service.tasks = [ + 1: task + ] + XCTAssertNotNil(service.findTask(taskIdentifier: 1)) + } + + func testValidateParameters_withEmptyBucket_shouldThrowError() { + do { + try service.validateParameters(bucket: "", key: "key", accelerationModeEnabled: true) + XCTFail("Expected error") + } catch { + guard case .validation(let field, let description, let recovery, _) = error as? StorageError else { + XCTFail("Expected StorageError.validation") + return + } + XCTAssertEqual(field, "bucket") + XCTAssertEqual(description, "Invalid bucket specified.") + XCTAssertEqual(recovery, "Please specify a bucket name or configure the bucket property.") + } + } + + func testValidateParameters_withEmptyKey_shouldThrowError() { + do { + try service.validateParameters(bucket: "bucket", key: "", accelerationModeEnabled: true) + XCTFail("Expected error") + } catch { + guard case .validation(let field, let description, let recovery, _) = error as? StorageError else { + XCTFail("Expected StorageError.validation") + return + } + XCTAssertEqual(field, "key") + XCTAssertEqual(description, "Invalid key specified.") + XCTAssertEqual(recovery, "Please specify a key.") + } + } + + func testValidateParameters_withValidParams_shouldNotThrowError() { + do { + try service.validateParameters(bucket: "bucket", key: "key", accelerationModeEnabled: true) + } catch { + XCTFail("Expected success, got \(error)") + } + } + + func testCreateTransferTask_shouldReturnTask() { + let task = service.createTransferTask( + transferType: .upload(onEvent: { event in }), + bucket: "bucket", + key: "key", + requestHeaders: [ + "header": "value" + ] + ) + XCTAssertEqual(task.bucket, "bucket") + XCTAssertEqual(task.key, "key") + XCTAssertEqual(task.requestHeaders?.count, 1) + XCTAssertEqual(task.requestHeaders?["header"], "value") + guard case .upload(_) = task.transferType else { + XCTFail("Expected .upload transferType") + return + } + } + + func testCompleteDownload_shouldReturnData() { + let expectation = self.expectation(description: "Complete Download") + + let downloadTask = StorageTransferTask( + transferType: .download(onEvent: { event in + guard case .completed(let data) = event, + let data = data else { + XCTFail("Expected .completed event with data") + return + } + XCTAssertEqual(String(decoding: data, as: UTF8.self), "someFile") + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + + let sourceUrl = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).txt") + try! "someFile".write(to: sourceUrl, atomically: true, encoding: .utf8) + + service.tasks = [ + 1: downloadTask + ] + + service.completeDownload(taskIdentifier: 1, sourceURL: sourceUrl) + XCTAssertEqual(downloadTask.status, .completed) + waitForExpectations(timeout: 1) + } + + func testCompleteDownload_withLocation_shouldMoveFileToLocation() { + let temporaryDirectory = FileManager.default.temporaryDirectory + let location = temporaryDirectory.appendingPathComponent("\(UUID().uuidString)-newFile.txt") + + let downloadTask = StorageTransferTask( + transferType: .download(onEvent: { _ in }), + bucket: "bucket", + key: "key", + location: location + ) + + let sourceUrl = temporaryDirectory.appendingPathComponent("\(UUID().uuidString)-oldFile.txt") + try! "someFile".write(to: sourceUrl, atomically: true, encoding: .utf8) + + service.tasks = [ + 1: downloadTask + ] + + service.completeDownload(taskIdentifier: 1, sourceURL: sourceUrl) + XCTAssertTrue(FileManager.default.fileExists(atPath: location.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: sourceUrl.path)) + XCTAssertEqual(downloadTask.status, .completed) + } + + func testCompleteDownload_withLocation_andError_shouldFailTask() { + let temporaryDirectory = FileManager.default.temporaryDirectory + let location = temporaryDirectory.appendingPathComponent("\(UUID().uuidString)-newFile.txt") + + let downloadTask = StorageTransferTask( + transferType: .download(onEvent: { _ in }), + bucket: "bucket", + key: "key", + location: location + ) + + let sourceUrl = temporaryDirectory.appendingPathComponent("\(UUID().uuidString)-oldFile.txt") + try! "someFile".write(to: sourceUrl, atomically: true, encoding: .utf8) + + service.tasks = [ + 1: downloadTask + ] + + fileSystem.moveFileError = StorageError.unknown("Unable to move file", nil) + service.completeDownload(taskIdentifier: 1, sourceURL: sourceUrl) + XCTAssertFalse(FileManager.default.fileExists(atPath: location.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: sourceUrl.path)) + XCTAssertEqual(downloadTask.status, .error) + } + + func testCompleteDownload_withNoDownload_shouldDoNothing() { + let expectation = self.expectation(description: "Complete Download") + expectation.isInverted = true + + let uploadTask = StorageTransferTask( + transferType: .upload(onEvent: { event in + XCTFail("Should not report event") + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + + let sourceUrl = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).txt") + try! "someFile".write(to: sourceUrl, atomically: true, encoding: .utf8) + + service.tasks = [ + 1: uploadTask + ] + + service.completeDownload(taskIdentifier: 1, sourceURL: sourceUrl) + XCTAssertNotEqual(uploadTask.status, .completed) + XCTAssertNotEqual(uploadTask.status, .error) + waitForExpectations(timeout: 1) + } + + func testUpload_withoutPreSignedURL_shouldSendFailEvent() { + let data = "someData".data(using: .utf8)! + let expectation = self.expectation(description: "Upload") + service.upload( + serviceKey: "key", + uploadSource: .data(data), + contentType: "application/json", + metadata: [:], + accelerate: true, + onEvent: { event in + guard case .failed(let error) = event, + case .unknown(let description, _) = error else { + XCTFail("Expected .failed event with .unknown error, got \(event)") + return + } + XCTAssertEqual(description, "Failed to get pre-signed URL") + expectation.fulfill() + } + ) + + waitForExpectations(timeout: 1) + } + + func testUpload_withPreSignedURL_shouldSendInitiatedEvent() { + let data = "someData".data(using: .utf8)! + let expectation = self.expectation(description: "Upload") + service.preSignedURLBuilder = MockAWSS3PreSignedURLBuilder() + service.upload( + serviceKey: "key", + uploadSource: .data(data), + contentType: "application/json", + metadata: [:], + accelerate: true, + onEvent: { event in + guard case .initiated(_) = event else { + XCTFail("Expected .initiated event, got \(event)") + return + } + expectation.fulfill() + } + ) + + waitForExpectations(timeout: 1) + } +} + +private class MockHttpClientEngineProxy: HttpClientEngineProxy { + var target: HttpClientEngine? = nil + + var executeCount = 0 + var executeRequest: SdkHttpRequest? + func execute(request: SdkHttpRequest) async throws -> HttpResponse { + executeCount += 1 + executeRequest = request + return .init(body: .empty, statusCode: .accepted) + } +} + +private class StorageTransferDatabaseMock: StorageTransferDatabase { + + func prepareForBackground(completion: (() -> Void)?) { + completion?() + } + + func insertTransferRequest(task: StorageTransferTask) { + + } + + func updateTransferRequest(task: StorageTransferTask) { + + } + + func removeTransferRequest(task: StorageTransferTask) { + + } + + func defaultTransferType(persistableTransferTask: StoragePersistableTransferTask) -> StorageTransferType? { + return nil + } + + var recoverCount = 0 + var recoverResult: Result = .failure(StorageError.unknown("Result not set", nil)) + func recover(urlSession: StorageURLSession, + completionHandler: @escaping (Result) -> Void) { + recoverCount += 1 + completionHandler(recoverResult) + } + + var attachEventHandlersCount = 0 + var onUploadHandler: AWSS3StorageServiceBehavior.StorageServiceUploadEventHandler? = nil + var onDownloadHandler: AWSS3StorageServiceBehavior.StorageServiceDownloadEventHandler? = nil + var onMultipartUploadHandler: AWSS3StorageServiceBehavior.StorageServiceMultiPartUploadEventHandler? = nil + func attachEventHandlers( + onUpload: AWSS3StorageServiceBehavior.StorageServiceUploadEventHandler?, + onDownload: AWSS3StorageServiceBehavior.StorageServiceDownloadEventHandler?, + onMultipartUpload: AWSS3StorageServiceBehavior.StorageServiceMultiPartUploadEventHandler? + ) { + attachEventHandlersCount += 1 + onUploadHandler = onUpload + onDownloadHandler = onDownload + onMultipartUploadHandler = onMultipartUpload + } +} + +private class MockFileSystem: FileSystem { + var moveFileError: Error? = nil + override func moveFile(from sourceFileURL: URL, to destinationURL: URL) throws { + if let moveFileError = moveFileError { + throw moveFileError + } + try super.moveFile(from: sourceFileURL, to: destinationURL) + } +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageMultipartUploadClientTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageMultipartUploadClientTests.swift new file mode 100644 index 0000000000..1cb198e8d2 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageMultipartUploadClientTests.swift @@ -0,0 +1,412 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +@testable import func AmplifyTestCommon.XCTAssertThrowFatalError +@testable import AWSS3StoragePlugin +import AWSS3 +import XCTest + +class DefaultStorageMultipartUploadClientTests: XCTestCase { + private var defaultClient: DefaultStorageMultipartUploadClient! + private var serviceProxy: MockStorageServiceProxy! + private var session: MockStorageMultipartUploadSession! + private var awss3Behavior: MockAWSS3Behavior! + private var uploadFile: UploadFile! + + override func setUp() async throws { + awss3Behavior = MockAWSS3Behavior() + serviceProxy = MockStorageServiceProxy( + awsS3: awss3Behavior + ) + let tempFileURL = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("txt") + try "Hello World".write(to: tempFileURL, atomically: true, encoding: .utf8) + uploadFile = UploadFile( + fileURL: tempFileURL, + temporaryFileCreated: false, + size: 88 + ) + defaultClient = DefaultStorageMultipartUploadClient( + serviceProxy: serviceProxy, + bucket: "bucket", + key: "key", + uploadFile: uploadFile + ) + session = MockStorageMultipartUploadSession( + client: client, + bucket: "bucket", + key: "key", + onEvent: { event in } + ) + client.integrate(session: session) + } + + private var client: StorageMultipartUploadClient! { + defaultClient + } + + override func tearDown() { + defaultClient = nil + serviceProxy = nil + session = nil + awss3Behavior = nil + uploadFile = nil + } + + func testCreateMultipartUpload_withSuccess_shouldSucceed() throws { + awss3Behavior.createMultipartUploadExpectation = expectation(description: "Create Multipart Upload") + awss3Behavior.createMultipartUploadResult = .success(.init( + bucket: "bucket", + key: "key", + uploadId: "uploadId" + )) + try client.createMultipartUpload() + + waitForExpectations(timeout: 1) + XCTAssertEqual(awss3Behavior.createMultipartUploadCount, 1) + XCTAssertEqual(session.handleMultipartUploadCount, 2) + XCTAssertEqual(session.failCount, 0) + if case .created(let uploadFile, let uploadId) = try XCTUnwrap(session.lastMultipartUploadEvent) { + XCTAssertEqual(uploadFile.fileURL, uploadFile.fileURL) + XCTAssertEqual(uploadId, "uploadId") + } + XCTAssertEqual(serviceProxy.registerMultipartUploadSessionCount, 1) + } + + func testCreateMultipartUpload_withError_shouldFail() throws { + awss3Behavior.createMultipartUploadExpectation = expectation(description: "Create Multipart Upload") + awss3Behavior.createMultipartUploadResult = .failure(.unknown("Unknown Error", nil)) + try client.createMultipartUpload() + + waitForExpectations(timeout: 1) + XCTAssertEqual(awss3Behavior.createMultipartUploadCount, 1) + XCTAssertEqual(session.handleMultipartUploadCount, 1) + XCTAssertEqual(session.failCount, 1) + if case .unknown(let description, _) = try XCTUnwrap(session.lastError) as? StorageError { + XCTAssertEqual(description, "Unknown Error") + } + XCTAssertEqual(serviceProxy.registerMultipartUploadSessionCount, 0) + } + + func testCreateMultipartUpload_withoutServiceProxy_shouldThrowFatalError() throws { + serviceProxy = nil + try XCTAssertThrowFatalError { + try? self.client.createMultipartUpload() + } + } + + func testUploadPart_withParts_shouldSucceed() throws { + session.handleUploadPartExpectation = expectation(description: "Upload Part with parts") + + try client.uploadPart( + partNumber: 1, + multipartUpload: .parts( + uploadId: "uploadId", + uploadFile: uploadFile, + partSize: .default, + parts: [ + .pending(bytes: 10), + .pending(bytes: 20) + ] + ), + subTask: .init( + transferType: .upload(onEvent: { event in }), + bucket: "bucket", + key: "key" + ) + ) + + waitForExpectations(timeout: 1) + XCTAssertEqual(session.handleUploadPartCount, 1) + XCTAssertEqual(session.failCount, 0) + if case .started(let partNumber, _) = try XCTUnwrap(session.lastUploadEvent) { + XCTAssertEqual(partNumber, 1) + } + } + + func testUploadPart_withInvalidFile_shouldFail() throws { + session.failExpectation = expectation(description: "Upload Part with invalid file") + + try client.uploadPart( + partNumber: 1, + multipartUpload: .parts( + uploadId: "uploadId", + uploadFile: .init( + fileURL: FileManager.default.temporaryDirectory.appendingPathComponent("noFile.txt"), + temporaryFileCreated: false, + size: 1024), + partSize: .default, + parts: [ + .pending(bytes: 10), + .pending(bytes: 20) + ] + ), + subTask: .init( + transferType: .upload(onEvent: { event in }), + bucket: "bucket", + key: "key" + ) + ) + + waitForExpectations(timeout: 1) + XCTAssertEqual(session.handleUploadPartCount, 0) + XCTAssertEqual(session.failCount, 1) + XCTAssertNil(session.lastUploadEvent) + } + + func testUploadPart_withoutServiceProxy_shouldThrowFatalError() throws { + self.serviceProxy = nil + try XCTAssertThrowFatalError { + try? self.client.uploadPart( + partNumber: 1, + multipartUpload: .parts( + uploadId: "uploadId", + uploadFile: self.uploadFile, + partSize: .default, + parts: [ + .pending(bytes: 10), + .pending(bytes: 20) + ] + ), + subTask: .init( + transferType: .upload(onEvent: { event in }), + bucket: "bucket", + key: "key" + ) + ) + } + } + + func testUploadPart_withoutParts_shouldThrowFatalError() throws { + try XCTAssertThrowFatalError { + try? self.client.uploadPart( + partNumber: 1, + multipartUpload: .created( + uploadId: "uploadId", + uploadFile: self.uploadFile + ), + subTask: .init( + transferType: .upload(onEvent: { event in }), + bucket: "bucket", + key: "key" + ) + ) + } + } + + func testCompleteMultipartUpload_withSuccess_shouldSucceed() throws { + awss3Behavior.completeMultipartUploadExpectation = expectation(description: "Complete Multipart Upload") + awss3Behavior.completeMultipartUploadResult = .success(.init( + bucket: "bucket", + key: "key", + eTag: "eTag" + )) + try client.completeMultipartUpload(uploadId: "uploadId") + + waitForExpectations(timeout: 1) + XCTAssertEqual(awss3Behavior.completeMultipartUploadCount, 1) + XCTAssertEqual(session.handleMultipartUploadCount, 1) + XCTAssertEqual(session.failCount, 0) + if case .completed(let uploadId) = try XCTUnwrap(session.lastMultipartUploadEvent) { + XCTAssertEqual(uploadId, "uploadId") + } + XCTAssertEqual(serviceProxy.unregisterMultipartUploadSessionCount, 1) + } + + func testCompleteMultipartUpload_withError_shouldFail() throws { + awss3Behavior.completeMultipartUploadExpectation = expectation(description: "Complete Multipart Upload") + awss3Behavior.completeMultipartUploadResult = .failure(.unknown("Unknown Error", nil)) + try client.completeMultipartUpload(uploadId: "uploadId") + + waitForExpectations(timeout: 1) + XCTAssertEqual(awss3Behavior.completeMultipartUploadCount, 1) + XCTAssertEqual(session.handleMultipartUploadCount, 0) + XCTAssertEqual(session.failCount, 1) + if case .unknown(let description, _) = try XCTUnwrap(session.lastError) as? StorageError { + XCTAssertEqual(description, "Unknown Error") + } + XCTAssertEqual(serviceProxy.unregisterMultipartUploadSessionCount, 1) + } + + func testCompleteMultipartUpload_withoutServiceProxy_shouldThrowFatalError() throws { + serviceProxy = nil + try XCTAssertThrowFatalError { + try? self.client.completeMultipartUpload(uploadId: "uploadId") + } + } + + func testAbortMultipartUpload_withSuccess_shouldSucceed() throws { + awss3Behavior.abortMultipartUploadExpectation = expectation(description: "Abort Multipart Upload") + awss3Behavior.abortMultipartUploadResult = .success(()) + try client.abortMultipartUpload(uploadId: "uploadId", error: CancellationError()) + + waitForExpectations(timeout: 1) + XCTAssertEqual(awss3Behavior.abortMultipartUploadCount, 1) + XCTAssertEqual(session.handleMultipartUploadCount, 1) + XCTAssertEqual(session.failCount, 0) + if case .aborted(let uploadId, let error) = try XCTUnwrap(session.lastMultipartUploadEvent) { + XCTAssertEqual(uploadId, "uploadId") + XCTAssertTrue(error is CancellationError) + } + XCTAssertEqual(serviceProxy.unregisterMultipartUploadSessionCount, 1) + } + + func testAbortMultipartUpload_withError_shouldFail() throws { + awss3Behavior.abortMultipartUploadExpectation = expectation(description: "Abort Multipart Upload") + awss3Behavior.abortMultipartUploadResult = .failure(.unknown("Unknown Error", nil)) + try client.abortMultipartUpload(uploadId: "uploadId") + + waitForExpectations(timeout: 1) + XCTAssertEqual(awss3Behavior.abortMultipartUploadCount, 1) + XCTAssertEqual(session.handleMultipartUploadCount, 0) + XCTAssertEqual(session.failCount, 1) + if case .unknown(let description, _) = try XCTUnwrap(session.lastError) as? StorageError { + XCTAssertEqual(description, "Unknown Error") + } + XCTAssertEqual(serviceProxy.unregisterMultipartUploadSessionCount, 1) + } + + func testAbortMultipartUpload_withoutServiceProxy_shouldThrowFatalError() throws { + serviceProxy = nil + try XCTAssertThrowFatalError { + try? self.client.abortMultipartUpload(uploadId: "uploadId") + } + } + + func testCancelUploadTasks_shouldSucceed() throws { + let cancelExpectation = expectation(description: "Cancel Upload Tasks") + client.cancelUploadTasks(taskIdentifiers: [0, 1,2], done: { + cancelExpectation.fulfill() + }) + + waitForExpectations(timeout: 1) + XCTAssertEqual(serviceProxy.unregisterTaskIdentifiersCount, 1) + } + + func testFilterRequestHeaders_shouldResultFilteredHeaders() { + let filteredHeaders = defaultClient.filter( + requestHeaders: [ + "validHeader": "validValue", + "x-amz-acl": "invalidValue", + "x-amz-tagging": "invalidValue", + "x-amz-storage-class": "invalidValue", + "x-amz-server-side-encryption": "invalidValue", + "x-amz-meta-invalid_one": "invalidValue", + "x-amz-meta-invalid_two": "invalidValue", + "x-amz-grant-invalid_one": "invalidvalue", + "x-amz-grant-invalid_two": "invalidvalue" + ] + ) + + XCTAssertEqual(filteredHeaders.count, 1) + XCTAssertEqual(filteredHeaders["validHeader"], "validValue") + } +} + +private class MockStorageServiceProxy: StorageServiceProxy { + var preSignedURLBuilder: AWSS3PreSignedURLBuilderBehavior! = MockAWSS3PreSignedURLBuilder() + var awsS3: AWSS3Behavior! + var urlSession = URLSession.shared + var userAgent: String = "" + var urlRequestDelegate: URLRequestDelegate? = nil + + init(awsS3: AWSS3Behavior) { + self.awsS3 = awsS3 + } + + func register(task: StorageTransferTask) {} + + func unregister(task: StorageTransferTask) {} + + var unregisterTaskIdentifiersCount = 0 + func unregister(taskIdentifiers: [TaskIdentifier]) { + unregisterTaskIdentifiersCount += 1 + } + + var registerMultipartUploadSessionCount = 0 + func register(multipartUploadSession: StorageMultipartUploadSession) { + registerMultipartUploadSessionCount += 1 + } + + var unregisterMultipartUploadSessionCount = 0 + func unregister(multipartUploadSession: StorageMultipartUploadSession) { + unregisterMultipartUploadSessionCount += 1 + } +} + +private class MockAWSS3Behavior: AWSS3Behavior { + func deleteObject(_ request: AWSS3DeleteObjectRequest, completion: @escaping (Result) -> Void) {} + + func listObjectsV2(_ request: AWSS3ListObjectsV2Request, completion: @escaping (Result) -> Void) {} + + var createMultipartUploadCount = 0 + var createMultipartUploadResult: Result? = nil + var createMultipartUploadExpectation: XCTestExpectation? = nil + func createMultipartUpload(_ request: CreateMultipartUploadRequest, completion: @escaping (Result) -> Void) { + createMultipartUploadCount += 1 + if let result = createMultipartUploadResult { + completion(result) + } + createMultipartUploadExpectation?.fulfill() + } + + var completeMultipartUploadCount = 0 + var completeMultipartUploadResult: Result? = nil + var completeMultipartUploadExpectation: XCTestExpectation? = nil + func completeMultipartUpload(_ request: AWSS3CompleteMultipartUploadRequest, completion: @escaping (Result) -> Void) { + completeMultipartUploadCount += 1 + if let result = completeMultipartUploadResult { + completion(result) + } + completeMultipartUploadExpectation?.fulfill() + } + + var abortMultipartUploadCount = 0 + var abortMultipartUploadResult: Result? = nil + var abortMultipartUploadExpectation: XCTestExpectation? = nil + func abortMultipartUpload(_ request: AWSS3AbortMultipartUploadRequest, completion: @escaping (Result) -> Void) { + abortMultipartUploadCount += 1 + if let result = abortMultipartUploadResult { + completion(result) + } + abortMultipartUploadExpectation?.fulfill() + } + + func getS3() -> S3ClientProtocol { + return MockS3Client() + } +} + +class MockStorageMultipartUploadSession: StorageMultipartUploadSession { + var handleMultipartUploadCount = 0 + var lastMultipartUploadEvent: StorageMultipartUploadEvent? = nil + override func handle(multipartUploadEvent: StorageMultipartUploadEvent) { + handleMultipartUploadCount += 1 + lastMultipartUploadEvent = multipartUploadEvent + } + + var handleUploadPartCount = 0 + var lastUploadEvent: StorageUploadPartEvent? = nil + var handleUploadPartExpectation: XCTestExpectation? = nil + + override func handle(uploadPartEvent: StorageUploadPartEvent) { + handleUploadPartCount += 1 + lastUploadEvent = uploadPartEvent + handleUploadPartExpectation?.fulfill() + } + + var failCount = 0 + var lastError: Error? = nil + var failExpectation: XCTestExpectation? = nil + override func fail(error: Error) { + failCount += 1 + lastError = error + failExpectation?.fulfill() + } +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift new file mode 100644 index 0000000000..64b5d862be --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift @@ -0,0 +1,223 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +@testable import AWSS3StoragePlugin +import XCTest + +class DefaultStorageTransferDatabaseTests: XCTestCase { + private var database: DefaultStorageTransferDatabase! + private var uploadFile: UploadFile! + private var session: MockStorageSessionTask! + + override func setUp() { + database = DefaultStorageTransferDatabase( + databaseDirectoryURL: FileManager.default.temporaryDirectory, + logger: MockLogger() + ) + uploadFile = UploadFile( + fileURL: FileSystem.default.createTemporaryFileURL(), + temporaryFileCreated: true, + size: UInt64(Bytes.megabytes(12).bytes) + ) + session = MockStorageSessionTask(taskIdentifier: 1) + } + + override func tearDown() { + database = nil + uploadFile = nil + session = nil + } + + func testLinkTasksWithSessions_withMultipartUpload_shouldReturnPairs() { + let transferTask1 = StorageTransferTask( + transferType: .multiPartUpload(onEvent: { _ in }), + bucket: "bucket", + key: "key1" + ) + transferTask1.sessionTask = session + transferTask1.multipartUpload = .created( + uploadId: "uploadId", + uploadFile: uploadFile + ) + + let transferTask2 = StorageTransferTask( + transferType: .multiPartUpload(onEvent: { _ in }), + bucket: "bucket", + key: "key2" + ) + transferTask2.sessionTask = session + transferTask2.multipartUpload = .created( + uploadId: "uploadId", + uploadFile: uploadFile + ) + + let pairs = database.linkTasksWithSessions( + persistableTransferTasks: [ + "taskId1": .init(task: transferTask1), + "taskId2": .init(task: transferTask2) + ], + sessionTasks: [ + session + ] + ) + + XCTAssertEqual(pairs.count, 2) + XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key1" })) + XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key2" })) + } + + func testLinkTasksWithSessions_withMultipartUpload_andNoSession_shouldReturnPairs() { + let transferTask1 = StorageTransferTask( + transferType: .multiPartUpload(onEvent: { _ in }), + bucket: "bucket", + key: "key1" + ) + transferTask1.multipartUpload = .created( + uploadId: "uploadId", + uploadFile: uploadFile + ) + + let transferTask2 = StorageTransferTask( + transferType: .multiPartUpload(onEvent: { _ in }), + bucket: "bucket", + key: "key2" + ) + transferTask2.multipartUpload = .created( + uploadId: "uploadId", + uploadFile: uploadFile + ) + + let pairs = database.linkTasksWithSessions( + persistableTransferTasks: [ + "taskId1": .init(task: transferTask1), + "taskId2": .init(task: transferTask2) + ], + sessionTasks: [ + session + ] + ) + + XCTAssertEqual(pairs.count, 2) + XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key1" })) + XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key2" })) + } + + func testLinkTasksWithSessions_withMultipartUploadPart_shouldReturnPairs() { + let transferTask0 = StorageTransferTask( + transferType: .multiPartUpload(onEvent: { _ in }), + bucket: "bucket", + key: "key1" + ) + transferTask0.sessionTask = session + transferTask0.multipartUpload = .created( + uploadId: "uploadId", + uploadFile: uploadFile + ) + + let transferTask1 = StorageTransferTask( + transferType: .multiPartUploadPart( + uploadId: "uploadId", + partNumber: 1 + ), + bucket: "bucket", + key: "key1" + ) + transferTask1.sessionTask = session + transferTask1.uploadId = "uploadId" + transferTask1.multipartUpload = .parts( + uploadId: "uploadId", + uploadFile: uploadFile, + partSize: try! .init(fileSize: UInt64(Bytes.megabytes(6).bytes)), + parts: [ + .inProgress( + bytes: Bytes.megabytes(6).bytes, + bytesTransferred: Bytes.megabytes(3).bytes, + taskIdentifier: 1 + ), + .completed( + bytes: Bytes.megabytes(6).bytes, + eTag: "eTag") + , + .pending(bytes: Bytes.megabytes(6).bytes) + ] + ) + transferTask1.uploadPart = .completed( + bytes: Bytes.megabytes(6).bytes, + eTag: "eTag" + ) + + let transferTask2 = StorageTransferTask( + transferType: .multiPartUploadPart( + uploadId: "uploadId", + partNumber: 2 + ), + bucket: "bucket", + key: "key1" + ) + transferTask2.sessionTask = session + transferTask2.uploadId = "uploadId" + transferTask2.multipartUpload = .parts( + uploadId: "uploadId", + uploadFile: uploadFile, + partSize: try! .init(fileSize: UInt64(Bytes.megabytes(6).bytes)), + parts: [ + .pending(bytes: Bytes.megabytes(6).bytes), + .pending(bytes: Bytes.megabytes(6).bytes) + ] + ) + transferTask2.uploadPart = .inProgress( + bytes: Bytes.megabytes(6).bytes, + bytesTransferred: Bytes.megabytes(3).bytes, + taskIdentifier: 1 + ) + + let pairs = database.linkTasksWithSessions( + persistableTransferTasks: [ + "taskId0": .init(task: transferTask0), + "taskId1": .init(task: transferTask1), + "taskId2": .init(task: transferTask2) + ], + sessionTasks: [ + session + ] + ) + + XCTAssertEqual(pairs.count, 3) + XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key1" })) + XCTAssertFalse(pairs.contains(where: { $0.transferTask.key == "key2" })) + } + + func testLoadPersistableTasks() { + let urlSession = MockStorageURLSession( + sessionTasks: [ + session + ]) + let expectation = self.expectation(description: "Recover") + database.recover(urlSession: urlSession) { result in + guard case .success(_) = result else { + XCTFail("Expected success") + return + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPrepareForBackground() { + let expectation = self.expectation(description: "Prepare for Background") + database.prepareForBackground() { + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testDefault_shouldReturnDefaultInstance() { + let defaultProtocol: StorageTransferDatabase = .default + XCTAssertTrue(defaultProtocol is DefaultStorageTransferDatabase) + } +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageMultipartUploadSessionTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageMultipartUploadSessionTests.swift index fa808b4ccc..2fa56921ef 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageMultipartUploadSessionTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageMultipartUploadSessionTests.swift @@ -41,6 +41,38 @@ class StorageMultipartUploadSessionTests: XCTestCase { XCTAssertFalse(session.partsFailed) } + func testSessionCreation_withTransferTask() throws { + let client = MockMultipartUploadClient() + let transferType: StorageTransferType = .multiPartUpload(onEvent: {_ in }) + let transferTask = StorageTransferTask( + transferType: transferType, + bucket: "bucket", + key: "key" + ) + + let session = try XCTUnwrap(StorageMultipartUploadSession(client: client, transferTask: transferTask, multipartUpload: .none, logger: MockLogger())) + XCTAssertEqual(session.partsCount, 0) + XCTAssertEqual(session.inProgressCount, 0) + XCTAssertFalse(session.partsCompleted) + XCTAssertFalse(session.partsFailed) + } + + func testSessionCreation_withTransferTask_andInvalidTransferType_shouldReturnNil() throws { + let client = MockMultipartUploadClient() + let transferType: StorageTransferType = .list(onEvent: {_ in }) + let transferTask = StorageTransferTask( + transferType: transferType, + bucket: "bucket", + key: "key" + ) + + XCTAssertNil(StorageMultipartUploadSession( + client: client, + transferTask: transferTask, + multipartUpload: .none + )) + } + func testCompletedMultipartUploadSession() throws { let initiatedExp = expectation(description: "Initiated") let completedExp = expectation(description: "Completed") @@ -105,7 +137,7 @@ class StorageMultipartUploadSessionTests: XCTestCase { let client = MockMultipartUploadClient() // creates an UploadFile for the mock process client.didCompletePartUpload = { (_, partNumber, _, _) in if partNumber == 5 { - closureSession?.handle(multipartUploadEvent: .aborting(error: nil)) + closureSession?.cancel() XCTAssertTrue(closureSession?.isAborted ?? false) } @@ -156,10 +188,10 @@ class StorageMultipartUploadSessionTests: XCTestCase { if pauseCount == 0, partNumber > 5, bytesTransferred > 0 { print("pausing on \(partNumber)") pauseCount += 1 - closureSession?.handle(multipartUploadEvent: .pausing) + closureSession?.pause() XCTAssertTrue(closureSession?.isPaused ?? false) print("resuming on \(partNumber)") - closureSession?.handle(multipartUploadEvent: .resuming) + closureSession?.resume() XCTAssertFalse(closureSession?.isPaused ?? true) } } diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageServiceSessionDelegateTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageServiceSessionDelegateTests.swift new file mode 100644 index 0000000000..02407ee920 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageServiceSessionDelegateTests.swift @@ -0,0 +1,306 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +@testable import AWSPluginsTestCommon +@testable import AWSS3StoragePlugin +import ClientRuntime +import AWSS3 +import XCTest + +class StorageServiceSessionDelegateTests: XCTestCase { + private var delegate: StorageServiceSessionDelegate! + private var service: AWSS3StorageServiceMock! + private var logger: MockLogger! + + override func setUp() { + service = try! AWSS3StorageServiceMock() + logger = MockLogger() + delegate = StorageServiceSessionDelegate( + identifier: "delegateTest", + logger: logger + ) + delegate.storageService = service + } + + override func tearDown() { + logger = nil + service = nil + delegate = nil + } + + func testLogURLSession_withWarningTrue_shouldLogWarning() { + delegate.logURLSessionActivity("message", warning: true) + XCTAssertEqual(logger.warnCount, 1) + XCTAssertEqual(logger.infoCount, 0) + } + + func testLogURLSession_shouldLogInfo() { + delegate.logURLSessionActivity("message") + XCTAssertEqual(logger.warnCount, 0) + XCTAssertEqual(logger.infoCount, 1) + } + + func testDidFinishEvents_withMatchingIdentifiers_shouldRemoveContinuation() async { + let expectation = self.expectation(description: "Did Finish Events") + StorageBackgroundEventsRegistry.register(identifier: "identifier") + Task { + _ = await withCheckedContinuation { continuation in + StorageBackgroundEventsRegistry.handleBackgroundEvents( + identifier: "identifier", + continuation: continuation + ) + expectation.fulfill() + } + } + + await fulfillment(of: [expectation], timeout: 1) + XCTAssertNotNil(StorageBackgroundEventsRegistry.continuation) + delegate.urlSessionDidFinishEvents(forBackgroundURLSession: .shared) + XCTAssertNil(StorageBackgroundEventsRegistry.continuation) + } + + func testDidFinishEvents_withNonMatchingIdentifiers_shouldRemoveContinuation() async { + let expectation = self.expectation(description: "Did Finish Events") + StorageBackgroundEventsRegistry.register(identifier: "identifier2") + Task { + _ = await withCheckedContinuation { continuation in + StorageBackgroundEventsRegistry.handleBackgroundEvents( + identifier: "identifier2", + continuation: continuation + ) + expectation.fulfill() + } + } + + await fulfillment(of: [expectation], timeout: 1) + XCTAssertNotNil(StorageBackgroundEventsRegistry.continuation) + delegate.urlSessionDidFinishEvents(forBackgroundURLSession: .shared) + XCTAssertNotNil(StorageBackgroundEventsRegistry.continuation) + } + + func testDidBecomeInvalid_withError_shouldResetURLSession() { + delegate.urlSession(.shared, didBecomeInvalidWithError: StorageError.accessDenied("", "", nil)) + XCTAssertEqual(service.resetURLSessionCount, 1) + } + + func testDidBecomeInvalid_withNilError_shouldResetURLSession() { + delegate.urlSession(.shared, didBecomeInvalidWithError: nil) + XCTAssertEqual(service.resetURLSessionCount, 1) + } + + func testDidComplete_withNSURLErrorCancelled_shouldNotCompleteTask() { + let task = URLSession.shared.dataTask(with: FileManager.default.temporaryDirectory) + let reasons = [ + NSURLErrorCancelledReasonBackgroundUpdatesDisabled, + NSURLErrorCancelledReasonInsufficientSystemResources, + NSURLErrorCancelledReasonUserForceQuitApplication, + NSURLErrorCancelled + ] + + for reason in reasons { + let expectation = self.expectation(description: "Did Complete With Error Reason \(reason)") + expectation.isInverted = true + let storageTask = StorageTransferTask( + transferType: .upload(onEvent: { _ in + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + service.mockedTask = storageTask + let error: Error = NSError( + domain: NSURLErrorDomain, + code: NSURLErrorCancelled, + userInfo: [ + NSURLErrorBackgroundTaskCancelledReasonKey: reason + ] + ) + + delegate.urlSession(.shared, task: task, didCompleteWithError: error) + waitForExpectations(timeout: 1) + XCTAssertEqual(storageTask.status, .unknown) + XCTAssertEqual(service.unregisterCount, 0) + } + } + + func testDidComplete_withError_shouldFailTask() { + let task = URLSession.shared.dataTask(with: FileManager.default.temporaryDirectory) + let expectation = self.expectation(description: "Did Complete With Error") + let storageTask = StorageTransferTask( + transferType: .upload(onEvent: { _ in + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + service.mockedTask = storageTask + + delegate.urlSession(.shared, task: task, didCompleteWithError: StorageError.accessDenied("", "", nil)) + waitForExpectations(timeout: 1) + XCTAssertEqual(storageTask.status, .error) + XCTAssertEqual(service.unregisterCount, 1) + } + + func testDidSendBodyData_upload_shouldSendInProcessEvent() { + let task = URLSession.shared.dataTask(with: FileManager.default.temporaryDirectory) + let expectation = self.expectation(description: "Did Send Body Data") + let storageTask = StorageTransferTask( + transferType: .upload(onEvent: { event in + guard case .inProcess(let progress) = event else { + XCTFail("Expected .inProcess event, got \(event)") + return + } + XCTAssertEqual(progress.totalUnitCount, 120) + XCTAssertEqual(progress.completedUnitCount, 100) + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + service.mockedTask = storageTask + + delegate.urlSession( + .shared, + task: task, + didSendBodyData: 10, + totalBytesSent: 100, + totalBytesExpectedToSend: 120 + ) + + waitForExpectations(timeout: 1) + } + + func testDidSendBodyData_multiPartUploadPart_shouldSendInProcessEvent() { + let task = URLSession.shared.dataTask(with: FileManager.default.temporaryDirectory) + let storageTask = StorageTransferTask( + transferType: .multiPartUploadPart( + uploadId: "uploadId", + partNumber: 3 + ), + bucket: "bucket", + key: "key" + ) + service.mockedTask = storageTask + let multipartSession = MockStorageMultipartUploadSession( + client: MockMultipartUploadClient(), + bucket: "bucket", + key: "key", + onEvent: { event in } + ) + service.mockedMultipartUploadSession = multipartSession + + delegate.urlSession( + .shared, + task: task, + didSendBodyData: 10, + totalBytesSent: 100, + totalBytesExpectedToSend: 120 + ) + XCTAssertEqual(multipartSession.handleUploadPartCount, 1) + guard case .progressUpdated(let partNumber, let bytesTransferred, let taskIdentifier) = multipartSession.lastUploadEvent else { + XCTFail("Expected .progressUpdated event") + return + } + + XCTAssertEqual(partNumber, 3) + XCTAssertEqual(bytesTransferred, 10) + XCTAssertEqual(taskIdentifier, task.taskIdentifier) + } + + func testDidWriteData_shouldNotifyProgress() { + let task = URLSession.shared.downloadTask(with: FileManager.default.temporaryDirectory) + let expectation = self.expectation(description: "Did Write Data") + let storageTask = StorageTransferTask( + transferType: .download(onEvent: { event in + guard case .inProcess(let progress) = event else { + XCTFail("Expected .inProcess event, got \(event)") + return + } + XCTAssertEqual(progress.totalUnitCount, 300) + XCTAssertEqual(progress.completedUnitCount, 200) + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + service.mockedTask = storageTask + + delegate.urlSession( + .shared, + downloadTask: task, + didWriteData: 15, + totalBytesWritten: 200, + totalBytesExpectedToWrite: 300 + ) + + waitForExpectations(timeout: 1) + } + + func testDiFinishDownloading_withError_shouldNotCompleteDownload() { + let task = URLSession.shared.downloadTask(with: FileManager.default.temporaryDirectory) + let expectation = self.expectation(description: "Did Finish Downloading") + expectation.isInverted = true + let storageTask = StorageTransferTask( + transferType: .download(onEvent: { _ in + expectation.fulfill() + }), + bucket: "bucket", + key: "key" + ) + service.mockedTask = storageTask + + delegate.urlSession( + .shared, + downloadTask: task, + didFinishDownloadingTo: FileManager.default.temporaryDirectory + ) + + waitForExpectations(timeout: 1) + XCTAssertEqual(service.completeDownloadCount, 0) + } +} + +private class AWSS3StorageServiceMock: AWSS3StorageService { + convenience init() throws { + try self.init( + authService: MockAWSAuthService(), + region: "region", + bucket: "bucket", + storageTransferDatabase: MockStorageTransferDatabase() + ) + } + + override var identifier: String { + return "identifier" + } + + var mockedTask: StorageTransferTask? = nil + override func findTask(taskIdentifier: TaskIdentifier) -> StorageTransferTask? { + return mockedTask + } + + var resetURLSessionCount = 0 + override func resetURLSession() { + resetURLSessionCount += 1 + } + + var unregisterCount = 0 + override func unregister(task: StorageTransferTask) { + unregisterCount += 1 + } + + var mockedMultipartUploadSession: StorageMultipartUploadSession? = nil + override func findMultipartUploadSession(uploadId: UploadID) -> StorageMultipartUploadSession? { + return mockedMultipartUploadSession + } + + var completeDownloadCount = 0 + override func completeDownload(taskIdentifier: TaskIdentifier, sourceURL: URL) { + completeDownloadCount += 1 + } +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageTransferTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageTransferTaskTests.swift new file mode 100644 index 0000000000..5749a75b66 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageTransferTaskTests.swift @@ -0,0 +1,576 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import AWSS3StoragePlugin +import XCTest + +class StorageTransferTaskTests: XCTestCase { + + // MARK: - Resume tests + func testResume_withSessionTask_shouldCallResume_andReportInitiatedEvent() { + let expectation = expectation(description: ".initiated event received on resume with only sessionTask") + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { event in + guard case .initiated(_) = event else { + XCTFail("Expected .initiated, got \(event)") + return + } + expectation.fulfill() + }), + sessionTask: sessionTask, + proxyStorageTask: nil + ) + XCTAssertEqual(task.status, .paused) + + task.resume() + waitForExpectations(timeout: 0.5) + + XCTAssertEqual(sessionTask.resumeCount, 1) + XCTAssertEqual(task.status, .inProgress) + } + + func testResume_withProxyStorageTask_shouldCallResume_andReportInitiatedEvent() { + let expectation = expectation(description: ".initiated event received on resume with only proxyStorageTask") + let sessionTask = MockSessionTask() + let storageTask = MockStorageTask() + let task = createTask( + transferType: .download(onEvent: { event in + guard case .initiated(_) = event else { + XCTFail("Expected .initiated, got \(event)") + return + } + expectation.fulfill() + }), + sessionTask: sessionTask, // Set the sessioTask to set task.status = .paused + proxyStorageTask: storageTask + ) + task.sessionTask = nil // Remove the session task + XCTAssertEqual(task.status, .paused) + + task.resume() + waitForExpectations(timeout: 0.5) + + XCTAssertEqual(sessionTask.resumeCount, 0) + XCTAssertEqual(storageTask.resumeCount, 1) + XCTAssertEqual(task.status, .inProgress) + } + + func testResume_withSessionTask_andProxyStorageTask_shouldCallResume_andReportInitiatedEvent() { + let expectation = expectation(description: ".initiated event received on resume with sessionTask and proxyStorageTask") + let sessionTask = MockSessionTask() + let storageTask = MockStorageTask() + let task = createTask( + transferType: .multiPartUpload(onEvent: { event in + guard case .initiated(_) = event else { + XCTFail("Expected .initiated, got \(event)") + return + } + expectation.fulfill() + }), + sessionTask: sessionTask, + proxyStorageTask: storageTask + ) + XCTAssertEqual(task.status, .paused) + + task.resume() + waitForExpectations(timeout: 0.5) + + XCTAssertEqual(sessionTask.resumeCount, 1) + XCTAssertEqual(storageTask.resumeCount, 0) + XCTAssertEqual(task.status, .inProgress) + } + + func testResume_withoutSessionTask_withoutProxyStorateTask_shouldNotCallResume_andNotReportEvent() { + let expectation = expectation(description: "no event is received on resume when no sessionTask nor proxyStorageTask") + expectation.isInverted = true + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .multiPartUpload(onEvent: { event in + XCTFail("No event expected, got \(event)") + expectation.fulfill() + }), + sessionTask: sessionTask, // Set the sessionTask to set task.status = .paused + proxyStorageTask: nil + ) + task.sessionTask = nil // Remove the sessionTask + XCTAssertEqual(task.status, .paused) + + task.resume() + waitForExpectations(timeout: 0.5) + + XCTAssertEqual(sessionTask.resumeCount, 0) + XCTAssertEqual(task.status, .paused) + } + + func testResume_withTaskNotPaused_shouldNotCallResume_andNotReportEvent() { + let expectation = expectation(description: "no event is received on resume when the session is not paused") + expectation.isInverted = true + let task = createTask( + transferType: .multiPartUpload(onEvent: { event in + XCTFail("No event expected, got \(event)") + expectation.fulfill() + }), + sessionTask: nil, // Do not set session task so task.status = .unknown + proxyStorageTask: nil + ) + XCTAssertEqual(task.status, .unknown) + + task.resume() + waitForExpectations(timeout: 0.5) + + XCTAssertEqual(task.status, .unknown) + } + + // MARK: - Suspend Tests + func testSuspend_withSessionTask_shouldCallSuspend() { + let sessionTask = MockSessionTask(state: .running) + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: nil + ) + // Set the task to inProgress by setting a multiPartUpload.creating + task.multipartUpload = .creating + XCTAssertEqual(task.status, .inProgress) + + task.suspend() + + XCTAssertEqual(sessionTask.suspendCount, 1) + XCTAssertEqual(task.status, .paused) + } + + func testSuspend_withProxyStorageTask_shouldCallPause() { + let storageTask = MockStorageTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: nil, + proxyStorageTask: storageTask + ) + // Set the task to inProgress by setting a multiPartUpload.creating + task.multipartUpload = .creating + XCTAssertEqual(task.status, .inProgress) + + task.suspend() + + XCTAssertEqual(storageTask.pauseCount, 1) + XCTAssertEqual(task.status, .paused) + } + + func testSuspend_withSessionTask_andProxyStorageTask_shouldCallSuspend() { + let sessionTask = MockSessionTask(state: .running) + let storageTask = MockStorageTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: storageTask + ) + // Set the task to inProgress by setting a multiPartUpload.creating + task.multipartUpload = .creating + XCTAssertEqual(task.status, .inProgress) + + task.suspend() + + XCTAssertEqual(sessionTask.suspendCount, 1) + XCTAssertEqual(storageTask.pauseCount, 0) + XCTAssertEqual(task.status, .paused) + } + + func testSuspend_withoutSessionTask_andWithoutProxyStorageTask_shouldDoNothing() { + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: nil, + proxyStorageTask: nil + ) + // Set the task to inProgress by setting a multiPartUpload.creating + task.multipartUpload = .creating + XCTAssertEqual(task.status, .inProgress) + + task.suspend() + + XCTAssertEqual(task.status, .inProgress) + } + + func testSuspend_withTaskNotInProgress_shouldDoNothing() { + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: nil + ) + // Set the task to completed by setting a multiPartUpload.completed + task.multipartUpload = .completed(uploadId: "") + XCTAssertEqual(task.status, .completed) + + task.suspend() + + XCTAssertEqual(sessionTask.suspendCount, 0) + XCTAssertEqual(task.status, .completed) + } + + func testPause_shouldCallSuspend() { + let sessionTask = MockSessionTask(state: .running) + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: nil + ) + // Set the task to inProgress by setting a multiPartUpload.creating + task.multipartUpload = .creating + XCTAssertEqual(task.status, .inProgress) + + task.pause() + + XCTAssertEqual(sessionTask.suspendCount, 1) + XCTAssertEqual(task.status, .paused) + } + + // MARK: - Cancel Tests + func testCancel_withSessionTask_shouldCancel() { + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: MockStorageTask() + ) + + // Set the task to completed by setting a multiPartUpload.completed + XCTAssertNotEqual(task.status, .completed) + + task.cancel() + + XCTAssertEqual(task.status, .cancelled) + XCTAssertEqual(sessionTask.cancelCount, 1) + XCTAssertNil(task.proxyStorageTask) + } + + func testCancel_withProxyStorageTask_shouldCancel() { + let storageTask = MockStorageTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: nil, + proxyStorageTask: storageTask + ) + + task.cancel() + XCTAssertEqual(task.status, .cancelled) + XCTAssertEqual(storageTask.cancelCount, 1) + XCTAssertNil(task.proxyStorageTask) + } + + func testCancel_withoutSessionTask_withoutProxyStorageTask_shouldDoNothing() { + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: nil, + proxyStorageTask: nil + ) + + task.cancel() + XCTAssertNotEqual(task.status, .cancelled) + } + + func testCancel_withTaskCompleted_shouldDoNothing() { + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: MockStorageTask() + ) + // Set the task to completed by setting a multiPartUpload.completed + task.multipartUpload = .completed(uploadId: "") + XCTAssertEqual(task.status, .completed) + + task.cancel() + XCTAssertEqual(sessionTask.cancelCount, 0) + XCTAssertNotNil(task.proxyStorageTask) + } + + // MARK: - Complete Tests + func testComplete_withSessionTask_shouldComplete() { + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: MockStorageTask() + ) + + task.complete() + XCTAssertEqual(task.status, .completed) + XCTAssertNil(task.proxyStorageTask) + } + + func testComplete_withTaskCancelled_shouldDoNothing() { + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: nil + ) + task.cancel() + XCTAssertEqual(task.status, .cancelled) + + task.complete() + XCTAssertEqual(task.status, .cancelled) + } + + func testComplete_withTaskCompleted_shouldDoNothing() { + let sessionTask = MockSessionTask() + let task = createTask( + transferType: .upload(onEvent: { _ in }), + sessionTask: sessionTask, + proxyStorageTask: MockStorageTask() + ) + // Set the task to completed by setting a multiPartUpload.completed + task.multipartUpload = .completed(uploadId: "") + XCTAssertEqual(task.status, .completed) + + task.complete() + + XCTAssertNotNil(task.proxyStorageTask) + } + + // MARK: - Fail Tests + func testFail_shouldReportFailEvent() { + let expectation = expectation(description: ".failed event received on fail") + let task = createTask( + transferType: .upload(onEvent: { event in + guard case .failed(_) = event else { + XCTFail("Expected .failed, got \(event)") + return + } + expectation.fulfill() + }), + sessionTask: MockSessionTask(), + proxyStorageTask: MockStorageTask() + ) + task.fail(error: CancellationError()) + + waitForExpectations(timeout: 0.5) + XCTAssertEqual(task.status, .error) + XCTAssertTrue(task.isFailed) + XCTAssertNil(task.proxyStorageTask) + } + + func testFail_withFailedTask_shouldNotReportEvent() { + let expectation = expectation(description: "event received on fail for failed task") + expectation.isInverted = true + let task = createTask( + transferType: .upload(onEvent: { event in + XCTFail("No event expected, got \(event)") + expectation.fulfill() + }), + sessionTask: MockSessionTask(), + proxyStorageTask: MockStorageTask() + ) + + // Set the task to error by setting a multiPartUpload.failed + task.multipartUpload = .failed(uploadId: "", parts: nil, error: CancellationError()) + XCTAssertEqual(task.status, .error) + task.fail(error: CancellationError()) + + waitForExpectations(timeout: 0.5) + XCTAssertNotNil(task.proxyStorageTask) + } + + // MARK: - Response Tests + func testResponseText_withValidData_shouldReturnText() { + let task = createTask( + transferType: .upload(onEvent: { _ in}), + sessionTask: nil, + proxyStorageTask: nil + ) + task.responseData = "Test".data(using: .utf8) + + XCTAssertEqual(task.responseText, "Test") + } + + func testResponseText_withInvalidData_shouldReturnNil() { + let task = createTask( + transferType: .upload(onEvent: { _ in}), + sessionTask: nil, + proxyStorageTask: nil + ) + task.responseData = Data(count: 9999) + + XCTAssertNil(task.responseText) + } + + func testResponseText_withoutData_shouldReturnNil() { + let task = createTask( + transferType: .upload(onEvent: { _ in}), + sessionTask: nil, + proxyStorageTask: nil + ) + task.responseData = nil + + XCTAssertNil(task.responseText) + } + + // MARK: - PartNumber Tests + func testPartNumber_withMultipartUpload_shouldReturnPartNumber() { + let partNumber: PartNumber = 5 + let task = createTask( + transferType: .multiPartUploadPart(uploadId: "", partNumber: partNumber), + sessionTask: nil, + proxyStorageTask: nil + ) + + XCTAssertEqual(task.partNumber, partNumber) + } + + func testPartNumber_withOtherTransferType_shouldReturnNil() { + let task = createTask( + transferType: .upload(onEvent: { _ in}), + sessionTask: nil, + proxyStorageTask: nil + ) + + XCTAssertNil(task.partNumber) + } + + // MARK: - HTTPRequestHeaders Tests + func testHTTPRequestHeaders_shouldSetValues() { + let task = createTask( + transferType: .upload(onEvent: { _ in}), + sessionTask: nil, + proxyStorageTask: nil, + requestHeaders: [ + "header1": "value1", + "header2": "value2" + ] + ) + + var request = URLRequest(url: FileManager.default.temporaryDirectory) + XCTAssertNil(request.allHTTPHeaderFields) + + request.setHTTPRequestHeaders(transferTask: task) + XCTAssertEqual(request.allHTTPHeaderFields?.count, 2) + XCTAssertEqual(request.allHTTPHeaderFields?["header1"], "value1") + XCTAssertEqual(request.allHTTPHeaderFields?["header2"], "value2") + } + + func testHTTPRequestHeaders_withoutHeaders_shouldDoNothing() { + let task = createTask( + transferType: .upload(onEvent: { _ in}), + sessionTask: nil, + proxyStorageTask: nil, + requestHeaders: nil + ) + + var request = URLRequest(url: FileManager.default.temporaryDirectory) + XCTAssertNil(request.allHTTPHeaderFields) + + request.setHTTPRequestHeaders(transferTask: task) + XCTAssertNil(request.allHTTPHeaderFields) + } +} + +extension StorageTransferTaskTests { + private func createTask( + transferType: StorageTransferType, + sessionTask: StorageSessionTask?, + proxyStorageTask: StorageTask?, + requestHeaders: [String: String]? = nil + ) -> StorageTransferTask { + let transferID = UUID().uuidString + let bucket = "BUCKET" + let key = UUID().uuidString + let task = StorageTransferTask( + transferID: transferID, + transferType: transferType, + bucket: bucket, + key: key, + location: nil, + contentType: nil, + requestHeaders: requestHeaders, + storageTransferDatabase: MockStorageTransferDatabase(), + logger: MockLogger() + ) + task.sessionTask = sessionTask + task.proxyStorageTask = proxyStorageTask + return task + } +} + + +private class MockStorageTask: StorageTask { + var pauseCount = 0 + func pause() { + pauseCount += 1 + } + + var resumeCount = 0 + func resume() { + resumeCount += 1 + } + + var cancelCount = 0 + func cancel() { + cancelCount += 1 + } +} + +private class MockSessionTask: StorageSessionTask { + let taskIdentifier: TaskIdentifier + let state: URLSessionTask.State + + init( + taskIdentifier: TaskIdentifier = 1, + state: URLSessionTask.State = .suspended + ) { + self.taskIdentifier = taskIdentifier + self.state = state + } + + var resumeCount = 0 + func resume() { + resumeCount += 1 + } + + var suspendCount = 0 + func suspend() { + suspendCount += 1 + } + + var cancelCount = 0 + func cancel() { + cancelCount += 1 + } +} + +class MockLogger: Logger { + var logLevel: LogLevel = .verbose + + func error(_ message: @autoclosure () -> String) { + print(message()) + } + + func error(error: Error) { + print(error) + } + + var warnCount = 0 + func warn(_ message: @autoclosure () -> String) { + print(message()) + warnCount += 1 + } + + var infoCount = 0 + func info(_ message: @autoclosure () -> String) { + print(message()) + infoCount += 1 + } + + func debug(_ message: @autoclosure () -> String) { + print(message()) + } + + func verbose(_ message: @autoclosure () -> String) { + print(message()) + } +}