diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+DownloadBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+DownloadBehavior.swift index 35c1c404be..9cf9cd5549 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+DownloadBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+DownloadBehavior.swift @@ -52,7 +52,7 @@ extension AWSS3StorageService { request.setHTTPRequestHeaders(transferTask: transferTask) - let downloadTask = urlSession.downloadTask(with: request) + let downloadTask = backgroundUrlSession.downloadTask(with: request) transferTask.sessionTask = downloadTask // log task identifier? diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+UploadBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+UploadBehavior.swift index fc9c16c542..88e106fe61 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+UploadBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+UploadBehavior.swift @@ -66,7 +66,7 @@ extension AWSS3StorageService { request.setHTTPRequestHeaders(transferTask: transferTask) - let uploadTask = urlSession.uploadTask(with: request, fromFile: fileURL) + let uploadTask = foregroundUrlSession.uploadTask(with: request, fromFile: fileURL) transferTask.sessionTask = uploadTask // log task identifier? diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift index 3aa970b7f3..a8906ccb1a 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift @@ -11,6 +11,11 @@ import AWSS3 import Amplify import AWSPluginsCore +/// Represents a concrete implementation of the +/// [AWSS3StorageServiceBehaviour](x-source-tag://AWSS3StorageServiceBehaviour) +/// protocol. +/// +/// - Tag: AWSS3StorageService class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { // resettable values @@ -24,9 +29,10 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { var s3Client: S3Client! let storageConfiguration: StorageConfiguration - let sessionConfiguration: URLSessionConfiguration - var delegateQueue: OperationQueue? - var urlSession: URLSession + let backgroundSessionController: StorageServiceSessionController + var backgroundUrlSession: URLSession { backgroundSessionController.session } + let foregroundSessionController: StorageServiceSessionController + var foregroundUrlSession: URLSession { foregroundSessionController.session } let storageTransferDatabase: StorageTransferDatabase let fileSystem: FileSystem @@ -76,7 +82,7 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { self.init(authService: authService, storageConfiguration: storageConfiguration, storageTransferDatabase: storageTransferDatabase, - sessionConfiguration: _sessionConfiguration, + backgroundSessionConfiguration: _sessionConfiguration, s3Client: s3Client, preSignedURLBuilder: preSignedURLBuilder, awsS3: awsS3, @@ -87,7 +93,8 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { storageConfiguration: StorageConfiguration = .default, storageTransferDatabase: StorageTransferDatabase = .default, fileSystem: FileSystem = .default, - sessionConfiguration: URLSessionConfiguration, + backgroundSessionConfiguration: URLSessionConfiguration, + foregroundSessionConfiguration: URLSessionConfiguration = .default, delegateQueue: OperationQueue? = nil, logger: Logger = storageLogger, s3Client: S3Client, @@ -97,11 +104,15 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { self.storageConfiguration = storageConfiguration self.storageTransferDatabase = storageTransferDatabase self.fileSystem = fileSystem - self.sessionConfiguration = sessionConfiguration - let delegate = StorageServiceSessionDelegate(identifier: storageConfiguration.sessionIdentifier, logger: logger) - self.delegateQueue = delegateQueue - self.urlSession = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: delegateQueue) + self.backgroundSessionController = StorageServiceSessionController(identifier: storageConfiguration.sessionIdentifier, + configuration: backgroundSessionConfiguration, + logger: logger, + delegateQueue: delegateQueue) + self.foregroundSessionController = StorageServiceSessionController(identifier: storageConfiguration.sessionIdentifier, + configuration: foregroundSessionConfiguration, + logger: logger, + delegateQueue: delegateQueue) self.logger = logger self.s3Client = s3Client @@ -111,17 +122,15 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { StorageBackgroundEventsRegistry.register(identifier: identifier) - delegate.storageService = self - - storageTransferDatabase.recover(urlSession: urlSession) { [weak self] result in + self.backgroundSessionController.delegate = self + self.foregroundSessionController.delegate = self + storageTransferDatabase.recover(urlSession: backgroundUrlSession) { [weak self] result in guard let self = self else { fatalError() } - switch result { - case .success(let pairs): - logger.info("Recovery completed: [pairs = \(pairs.count)]") - self.processTransferTaskPairs(pairs: pairs) - case .failure(let error): - logger.error(error: error) - } + self.didRecover(tasks: result) + } + storageTransferDatabase.recover(urlSession: foregroundUrlSession) { [weak self] result in + guard let self = self else { fatalError() } + self.didRecover(tasks: result) } } @@ -129,6 +138,16 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { StorageBackgroundEventsRegistry.unregister(identifier: identifier) } + private func didRecover(tasks result: Result) { + switch result { + case .success(let pairs): + logger.info("Recovery completed: [pairs = \(pairs.count)]") + self.processTransferTaskPairs(pairs: pairs) + case .failure(let error): + logger.error(error: error) + } + } + func reset() { authService = nil preSignedURLBuilder = nil @@ -139,11 +158,6 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { multipartUploadSessions.removeAll() } - func resetURLSession() { - let delegate = StorageServiceSessionDelegate(identifier: storageConfiguration.sessionIdentifier, logger: logger) - self.urlSession = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: delegateQueue) - } - func attachEventHandlers(onUpload: AWSS3StorageServiceBehaviour.StorageServiceUploadEventHandler? = nil, onDownload: AWSS3StorageServiceBehaviour.StorageServiceDownloadEventHandler? = nil, onMultipartUpload: AWSS3StorageServiceBehaviour.StorageServiceMultiPartUploadEventHandler? = nil) { @@ -282,3 +296,5 @@ class AWSS3StorageService: AWSS3StorageServiceBehaviour, StorageServiceProxy { } } + +extension AWSS3StorageService: StorageServiceSessionControllerDelegate {} diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadClient.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadClient.swift index 9dc34a3dbd..71706fe7bb 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadClient.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageMultipartUploadClient.swift @@ -106,7 +106,7 @@ class DefaultStorageMultipartUploadClient: StorageMultipartUploadClient { request.setValue(userAgent, forHTTPHeaderField: "User-Agent") */ - let uploadTask = serviceProxy.urlSession.uploadTask(with: request, fromFile: partialFileURL) + let uploadTask = serviceProxy.backgroundUrlSession.uploadTask(with: request, fromFile: partialFileURL) subTask.sessionTask = uploadTask subTask.uploadPart = multipartUpload.part(for: partNumber) @@ -180,7 +180,7 @@ class DefaultStorageMultipartUploadClient: StorageMultipartUploadClient { func cancelUploadTasks(taskIdentifiers: [TaskIdentifier], done: @escaping () -> Void) { guard let service = serviceProxy else { return } service.unregister(taskIdentifiers: taskIdentifiers) - service.urlSession.getActiveTasks { tasks in + service.backgroundUrlSession.getActiveTasks { tasks in for task in tasks { if taskIdentifiers.contains(task.taskIdentifier) { task.cancel() diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceProxy.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceProxy.swift index 8b2044745b..e044345b22 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceProxy.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceProxy.swift @@ -11,7 +11,8 @@ import Amplify protocol StorageServiceProxy: AnyObject { var preSignedURLBuilder: AWSS3PreSignedURLBuilderBehavior! { get } var awsS3: AWSS3Behavior! { get } - var urlSession: URLSession { get } + var backgroundUrlSession: URLSession { get } + var foregroundUrlSession: URLSession { get } func register(task: StorageTransferTask) func unregister(task: StorageTransferTask) diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceSessionDelegate.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceSessionController.swift similarity index 82% rename from AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceSessionDelegate.swift rename to AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceSessionController.swift index 03da3cc4fe..565ac53bfd 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceSessionDelegate.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StorageServiceSessionController.swift @@ -10,16 +10,45 @@ import Foundation import Amplify import AWSPluginsCore -// MARK: - StorageServiceSessionDelegate - +/// - Tag: StorageServiceSessionControllerDelegate +protocol StorageServiceSessionControllerDelegate: AnyObject { + var identifier: String { get } + func unregister(task: StorageTransferTask) + func findTask(taskIdentifier: TaskIdentifier) -> StorageTransferTask? + func findMultipartUploadSession(uploadId: UploadID) -> StorageMultipartUploadSession? + func completeDownload(taskIdentifier: TaskIdentifier, sourceURL: URL) +} + +/// Represents glue code between a `URLSession` and its +/// [StorageServiceSessionControllerDelegate](x-source-tag://StorageServiceSessionControllerDelegate) +/// which at the time of this writing is a +/// [AWSS3StorageService](x-source-tag://AWSS3StorageService). +/// +/// - Tag: StorageServiceSessionController +class StorageServiceSessionController: NSObject { + + weak var delegate: StorageServiceSessionControllerDelegate? -class StorageServiceSessionDelegate: NSObject { let identifier: String + let configuration: URLSessionConfiguration + let delegateQueue: OperationQueue? let logger: Logger - weak var storageService: AWSS3StorageService? + var session: URLSession = .shared - init(identifier: String, logger: Logger = storageLogger) { + init(identifier: String, + configuration: URLSessionConfiguration, + logger: Logger = storageLogger, + delegateQueue: OperationQueue? = nil) { self.identifier = identifier + self.configuration = configuration self.logger = logger + self.delegateQueue = delegateQueue + super.init() + self.resetURLSession() + } + + private func resetURLSession() { + self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue) } // Set a Symbolic Breakpoint in Xcode to monitor these messages @@ -32,7 +61,7 @@ class StorageServiceSessionDelegate: NSObject { } private func findTransferTask(for taskIdentifier: TaskIdentifier) -> StorageTransferTask? { - guard let storageService = storageService, + guard let storageService = delegate, let transferTask = storageService.findTask(taskIdentifier: taskIdentifier) else { logger.debug("Did not find transfer task: \(taskIdentifier)") return nil @@ -48,12 +77,12 @@ public extension Notification.Name { // MARK: - URLSessionDelegate - -extension StorageServiceSessionDelegate: URLSessionDelegate { +extension StorageServiceSessionController: URLSessionDelegate { func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { logURLSessionActivity("Session did finish background events") - if let identifier = storageService?.identifier, + if let identifier = delegate?.identifier, let continuation = StorageBackgroundEventsRegistry.getContinuation(for: identifier) { // Must be run on main thread as covered by Apple Developer docs. Task { @MainActor in @@ -74,13 +103,13 @@ extension StorageServiceSessionDelegate: URLSessionDelegate { NotificationCenter.default.post(name: Notification.Name.StorageURLSessionDidBecomeInvalidNotification, object: session) // Reset URLSession since the current one has become invalid. - storageService?.resetURLSession() + resetURLSession() } } // MARK: - URLSessionTaskDelegate - -extension StorageServiceSessionDelegate: URLSessionTaskDelegate { +extension StorageServiceSessionController: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { @@ -106,7 +135,7 @@ extension StorageServiceSessionDelegate: URLSessionTaskDelegate { logURLSessionActivity("Session task did complete: \(task.taskIdentifier)") } - guard let storageService = storageService, + guard let storageService = delegate, let transferTask = findTransferTask(for: task.taskIdentifier) else { logURLSessionActivity("Session task not handled: \(task.taskIdentifier)") return @@ -155,7 +184,7 @@ extension StorageServiceSessionDelegate: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { logURLSessionActivity("Session task update: [bytesSent: \(bytesSent)], [totalBytesSent: \(totalBytesSent)], [totalBytesExpectedToSend: \(totalBytesExpectedToSend)]") - guard let storageService = storageService, + guard let storageService = delegate, let transferTask = findTransferTask(for: task.taskIdentifier) else { return } switch transferTask.transferType { @@ -180,7 +209,7 @@ extension StorageServiceSessionDelegate: URLSessionTaskDelegate { // MARK: - URLSessionDownloadDelegate - -extension StorageServiceSessionDelegate: URLSessionDownloadDelegate { +extension StorageServiceSessionController: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { logURLSessionActivity("Session download task [\(downloadTask.taskIdentifier)] did write [\(bytesWritten)], [totalBytesWritten \(totalBytesWritten)], [totalBytesExpectedToWrite: \(totalBytesExpectedToWrite)]") @@ -195,7 +224,7 @@ extension StorageServiceSessionDelegate: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { logURLSessionActivity("Session download task [\(downloadTask.taskIdentifier)] did finish downloading to \(location.path)") - guard let storageService = storageService, + guard let storageService = delegate, let transferTask = findTransferTask(for: downloadTask.taskIdentifier) else { return } let response = StorageTransferResponse(task: downloadTask, error: nil, transferTask: transferTask) diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSAuthServiceBehavior.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSAuthServiceBehavior.swift new file mode 100644 index 0000000000..8b0ad1792b --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSAuthServiceBehavior.swift @@ -0,0 +1,53 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import AWSPluginsCore +@testable import AWSS3StoragePlugin + +import AWSClientRuntime +import AWSS3 +import Amplify +import Foundation + +/// Test-friendly implementation of a +/// [AWSAuthServiceBehavior](x-source-tag://AWSAuthServiceBehavior) protocol. +/// +/// - Tag: MockAWSAuthServiceBehavior +final class MockAWSAuthServiceBehavior { + var interactions: [String] = [] + var credentialsProvider = MockCredentialsProvider() + var tokenClaimsByToken: [String: [String : AnyObject]] = [:] + var identityID = UUID().uuidString + var userPoolAccessToken = UUID().uuidString +} + +extension MockAWSAuthServiceBehavior: AWSAuthServiceBehavior { + + func getCredentialsProvider() -> CredentialsProvider { + interactions.append(#function) + return credentialsProvider + } + + func getTokenClaims(tokenString: String) -> Result<[String : AnyObject], AuthError> { + interactions.append(#function) + if let claims = tokenClaimsByToken[tokenString] { + return .success(claims) + } + return .failure(.unknown(tokenString)) + } + + func getIdentityID() async throws -> String { + interactions.append(#function) + return identityID + } + + func getUserPoolAccessToken() async throws -> String { + interactions.append(#function) + return userPoolAccessToken + } + +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3PreSignedURLBuilder.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3PreSignedURLBuilder.swift index 40fb5d282f..3aff2eba48 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3PreSignedURLBuilder.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3PreSignedURLBuilder.swift @@ -5,15 +5,28 @@ // SPDX-License-Identifier: Apache-2.0 // -/* -import Foundation @testable import AWSS3StoragePlugin + import AWSS3 +import Foundation -public class MockAWSS3PreSignedURLBuilder: AWSS3PreSignedURLBuilderBehavior { - public func getPreSignedURL(_ getPreSignedURLRequest: AWSS3GetPreSignedURLRequest) -> AWSTask { - return AWSTask() - } +/// Test-friendly implementation of a +/// [AWSS3PreSignedURLBuilderBehavior](x-source-tag://AWSS3PreSignedURLBuilderBehavior) +/// protocol. +/// +/// - Tag: MockAWSS3PreSignedURLBuilder +final class MockAWSS3PreSignedURLBuilder { + var interactions: [String] = [] + var defaultURL = URL(fileURLWithPath: NSTemporaryDirectory().appendingPathComponent(UUID().uuidString)) + var preSignedURLs: [String: URL] = [:] +} +extension MockAWSS3PreSignedURLBuilder: AWSS3PreSignedURLBuilderBehavior { + func getPreSignedURL(key: String, signingOperation: AWSS3SigningOperation, expires: Int64?) async throws -> URL { + interactions.append(#function) + if let url = preSignedURLs[key] { + return url + } + return defaultURL + } } -*/ diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockCredentialsProvider.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockCredentialsProvider.swift new file mode 100644 index 0000000000..1e389b8cd6 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockCredentialsProvider.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AWSClientRuntime +import Foundation + +/// Test-friendly implementation of a +/// [CredentialsProvider](x-source-tag://CredentialsProvider) protocol. +/// +/// - Tag: MockCredentialsProvider +final class MockCredentialsProvider { + var interactions: [String] = [] + var credentials = AWSCredentials(accessKey: UUID().uuidString, + secret: UUID().uuidString, + expirationTimeout: UInt64.random(in: 1..<100), + sessionToken: UUID().uuidString) +} + +extension MockCredentialsProvider: CredentialsProvider { + func getCredentials() async throws -> AWSClientRuntime.AWSCredentials { + return credentials + } +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockLogger.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockLogger.swift new file mode 100644 index 0000000000..4218e69dbb --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockLogger.swift @@ -0,0 +1,50 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify + +/// Test-friendly implementation of the Amplify +/// [Logger](x-source-tag://Logger) +/// protocol. +/// +/// - Tag: MockLogger +final class MockLogger { + struct Entry: Equatable { + var level: Amplify.LogLevel + var message: String + } + var logLevel: Amplify.LogLevel = .debug + var entries: [Entry] = [] +} + +extension MockLogger: Logger { + + func error(_ message: @autoclosure () -> String) { + entries.append(.init(level: .error, message: message())) + } + + func error(error: Error) { + entries.append(.init(level: .error, message: "\(error)")) + } + + func warn(_ message: @autoclosure () -> String) { + entries.append(.init(level: .warn, message: message())) + } + + func info(_ message: @autoclosure () -> String) { + entries.append(.init(level: .info, message: message())) + } + + func debug(_ message: @autoclosure () -> String) { + entries.append(.init(level: .debug, message: message())) + } + + func verbose(_ message: @autoclosure () -> String) { + entries.append(.init(level: .verbose, message: message())) + } + +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageServiceSessionControllerTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageServiceSessionControllerTests.swift new file mode 100644 index 0000000000..017ab74946 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/StorageServiceSessionControllerTests.swift @@ -0,0 +1,143 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import AWSS3StoragePlugin + +import Amplify +import XCTest + +final class StorageServiceSessionControllerTests: XCTestCase { + + enum TestError: Error { + case urlSessionError + } + + var systemUnderTest: StorageServiceSessionController! + var serviceIdentifier: String! + var logger: MockLogger! + var tasksByIdentifier: [TaskIdentifier: StorageTransferTask]! + var delegateInteractions: [String]! + var notifications: [Notification]! + var fileURL: URL! + + override func setUp() async throws { + self.serviceIdentifier = "StorageServiceSessionControllerTests.\(UUID().uuidString)" + self.logger = MockLogger() + self.tasksByIdentifier = [:] + self.delegateInteractions = [] + self.notifications = [] + self.systemUnderTest = StorageServiceSessionController(identifier: UUID().uuidString, + configuration: URLSessionConfiguration.ephemeral, + logger: self.logger) + self.systemUnderTest.delegate = self + self.fileURL = URL(fileURLWithPath: NSTemporaryDirectory().appendingPathComponent(UUID().uuidString)) + let data = try XCTUnwrap(UUID().uuidString.data(using: .utf8)) + try data.write(to: fileURL, options: .atomic) + } + + override func tearDown() async throws { + NotificationCenter.default.removeObserver(self) + try FileManager.default.removeItem(at: self.fileURL) + self.systemUnderTest = nil + self.serviceIdentifier = nil + self.logger = nil + self.tasksByIdentifier = nil + self.delegateInteractions = nil + self.notifications = nil + self.fileURL = nil + } + + func testurlSessionDidFinishEvents() throws { + systemUnderTest.urlSessionDidFinishEvents(forBackgroundURLSession: systemUnderTest.session) + XCTAssertEqual(self.logger.entries, [.init(level: .info, message: "[URLSession] Session did finish background events")]) + } + + func testUrlSessionDidBecomeInvalidWithError() throws { + NotificationCenter.default.addObserver(forName: Notification.Name.StorageURLSessionDidBecomeInvalidNotification, object: systemUnderTest.session, queue: nil) { [weak self] notification in + self?.notifications.append(notification) + } + + XCTAssertEqual(self.notifications, []) + let originalSession = systemUnderTest.session + let originalSessionObjectIdentifier = ObjectIdentifier(originalSession) + + systemUnderTest.urlSession(systemUnderTest.session, didBecomeInvalidWithError: TestError.urlSessionError) + + let updatedSession = systemUnderTest.session + let updatedSessionObjectIdentifier = ObjectIdentifier(updatedSession) + XCTAssertNotEqual(originalSessionObjectIdentifier, updatedSessionObjectIdentifier) + XCTAssertEqual(self.notifications.map { $0.name }, [Notification.Name.StorageURLSessionDidBecomeInvalidNotification]) + XCTAssertEqual(self.logger.entries, [.init(level: .warn, message: "[URLSession] Session did become invalid: \(systemUnderTest.identifier) [urlSessionError]")]) + } + + func testUrlSessionDidCompleteButNotHttp() throws { + let request = URLRequest(url: self.fileURL) + let task = systemUnderTest.session.downloadTask(with: request) + self.tasksByIdentifier[task.taskIdentifier] = StorageTransferTask(transferID: "\(task.taskIdentifier)", + transferType: .download(onEvent: { _ in }), + bucket: UUID().uuidString, + key: UUID().uuidString) + + systemUnderTest.urlSession(systemUnderTest.session, task: task, didCompleteWithError: nil) + XCTAssertEqual(self.logger.entries.count, 2) + XCTAssertEqual(self.logger.entries[0], .init(level: .info, message: "[URLSession] Session task did complete: \(task.taskIdentifier)")) + XCTAssertEqual(self.logger.entries[1].level, .warn) + XCTAssertTrue(self.logger.entries[1].message.hasPrefix("[URLSession] Failed with error: StorageError: Unexpected error occurred with message: Response is not an HTTP response"), self.logger.entries[1].message) + } + + func testUrlSessionDidCompleteWithoutRegisteredTask() throws { + let request = URLRequest(url: self.fileURL) + let task = systemUnderTest.session.downloadTask(with: request) + systemUnderTest.urlSession(systemUnderTest.session, task: task, didCompleteWithError: nil) + XCTAssertEqual(self.logger.entries, [ + .init(level: .info, message: "[URLSession] Session task did complete: \(task.taskIdentifier)"), + .init(level: .debug, message: "Did not find transfer task: \(task.taskIdentifier)"), + .init(level: .info, message: "[URLSession] Session task not handled: \(task.taskIdentifier)"), + ]) + } + + func testUrlSessionDidCompleteWithErrorWithoutRegisteredTask() throws { + let request = URLRequest(url: self.fileURL) + let task = systemUnderTest.session.downloadTask(with: request) + systemUnderTest.urlSession(systemUnderTest.session, task: task, didCompleteWithError: TestError.urlSessionError) + XCTAssertEqual(self.logger.entries, [ + .init(level: .warn, message: "[URLSession] Session task did complete with error: \(task.taskIdentifier) [urlSessionError]"), + .init(level: .debug, message: "Did not find transfer task: \(task.taskIdentifier)"), + .init(level: .info, message: "[URLSession] Session task not handled: \(task.taskIdentifier)"), + ]) + } + +} + +extension StorageServiceSessionControllerTests: StorageServiceSessionControllerDelegate { + + var identifier: String { + delegateInteractions.append(#function) + return self.serviceIdentifier + } + + func unregister(task: StorageTransferTask) { + delegateInteractions.append(#function) + guard let taskIdentifier = task.taskIdentifier else { return } + self.tasksByIdentifier[taskIdentifier] = task + } + + func findTask(taskIdentifier: TaskIdentifier) -> StorageTransferTask? { + delegateInteractions.append(#function) + return self.tasksByIdentifier[taskIdentifier] + } + + func findMultipartUploadSession(uploadId: UploadID) -> StorageMultipartUploadSession? { + delegateInteractions.append(#function) + return nil + } + + func completeDownload(taskIdentifier: TaskIdentifier, sourceURL: URL) { + delegateInteractions.append(#function) + } + +}