diff --git a/AmazonConnectChatIOS/.gitignore b/.gitignore similarity index 93% rename from AmazonConnectChatIOS/.gitignore rename to .gitignore index af80e6e..198180d 100644 --- a/AmazonConnectChatIOS/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /Packages/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +contents.xcworkspacedata # Xcode files xcuserdata/ diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOS.xcodeproj/project.pbxproj b/AmazonConnectChatIOS/AmazonConnectChatIOS.xcodeproj/project.pbxproj index ce4ced9..cec1cfb 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOS.xcodeproj/project.pbxproj +++ b/AmazonConnectChatIOS/AmazonConnectChatIOS.xcodeproj/project.pbxproj @@ -48,12 +48,17 @@ F74726442BF418F0002B278E /* MetricsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74726432BF418F0002B278E /* MetricsClient.swift */; }; F74726472BF41948002B278E /* MetricsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74726462BF41948002B278E /* MetricsManager.swift */; }; F74726522BF6F018002B278E /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74726512BF6F018002B278E /* Config.swift */; }; + F76BB94D2C1898BE0005C3EC /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76BB94C2C1898BE0005C3EC /* APIClient.swift */; }; + F76BB9532C18F84D0005C3EC /* MockAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76BB9522C18F84D0005C3EC /* MockAPIClient.swift */; }; + F76BB9562C1900480005C3EC /* APIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76BB9552C1900480005C3EC /* APIClientTests.swift */; }; F7B0747C2BDB298400E8B805 /* HeartbeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B0747B2BDB298400E8B805 /* HeartbeatManager.swift */; }; - F7B0747D2BDB298400E8B805 /* HeartbeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B0747B2BDB298400E8B805 /* HeartbeatManager.swift */; }; F7B0747F2BDB2A0700E8B805 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B0747E2BDB2A0700E8B805 /* Notifications.swift */; }; - F7B074802BDB2A0700E8B805 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B0747E2BDB2A0700E8B805 /* Notifications.swift */; }; F7B074822BDB2D3A00E8B805 /* NetworkConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B074812BDB2D3A00E8B805 /* NetworkConnectionManager.swift */; }; - F7B074832BDB2D3A00E8B805 /* NetworkConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B074812BDB2D3A00E8B805 /* NetworkConnectionManager.swift */; }; + F7B4A1282C1157B0005C7921 /* AttachmentTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B4A1272C1157B0005C7921 /* AttachmentTypes.swift */; }; + F7B4A1362C14FDA9005C7921 /* MockAttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B4A1352C14FDA9005C7921 /* MockAttachmentManager.swift */; }; + F7B4A1382C15649B005C7921 /* MockHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B4A1372C15649B005C7921 /* MockHttpClient.swift */; }; + F7B4A13A2C17AE43005C7921 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B4A1392C17AE43005C7921 /* TestUtils.swift */; }; + F7B4A13C2C17E3C3005C7921 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B4A13B2C17E3C3005C7921 /* MockURLSession.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -111,9 +116,17 @@ F74726432BF418F0002B278E /* MetricsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsClient.swift; sourceTree = ""; }; F74726462BF41948002B278E /* MetricsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsManager.swift; sourceTree = ""; }; F74726512BF6F018002B278E /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + F76BB94C2C1898BE0005C3EC /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; + F76BB9522C18F84D0005C3EC /* MockAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIClient.swift; sourceTree = ""; }; + F76BB9552C1900480005C3EC /* APIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientTests.swift; sourceTree = ""; }; F7B0747B2BDB298400E8B805 /* HeartbeatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatManager.swift; sourceTree = ""; }; F7B0747E2BDB2A0700E8B805 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; F7B074812BDB2D3A00E8B805 /* NetworkConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectionManager.swift; sourceTree = ""; }; + F7B4A1272C1157B0005C7921 /* AttachmentTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentTypes.swift; sourceTree = ""; }; + F7B4A1352C14FDA9005C7921 /* MockAttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAttachmentManager.swift; sourceTree = ""; }; + F7B4A1372C15649B005C7921 /* MockHttpClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; + F7B4A1392C17AE43005C7921 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; + F7B4A13B2C17E3C3005C7921 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; FA5FA5BCFCE22047DCA91060 /* Pods-AmazonConnectChatIOS-AmazonConnectChatIOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AmazonConnectChatIOS-AmazonConnectChatIOSTests.debug.xcconfig"; path = "Target Support Files/Pods-AmazonConnectChatIOS-AmazonConnectChatIOSTests/Pods-AmazonConnectChatIOS-AmazonConnectChatIOSTests.debug.xcconfig"; sourceTree = ""; }; FCB565490D66D6F8C4BB3C09 /* Pods-AmazonConnectChatIOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AmazonConnectChatIOS.release.xcconfig"; path = "Target Support Files/Pods-AmazonConnectChatIOS/Pods-AmazonConnectChatIOS.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -160,10 +173,9 @@ 7E2336E12BFE7D7400E6C4B7 /* Network */ = { isa = PBXGroup; children = ( + F76BB9542C19000F0005C3EC /* Mocks */, 7E2336E22BFE7D7400E6C4B7 /* AWSClientTests.swift */, - 7E2336E32BFE7D7400E6C4B7 /* MockAWSClient.swift */, - 7E2336E42BFE7D7400E6C4B7 /* MockWebSocketManager.swift */, - 7E2336E52BFE7D7400E6C4B7 /* MockAWSConnectParticipant.swift */, + F76BB9552C1900480005C3EC /* APIClientTests.swift */, ); path = Network; sourceTree = ""; @@ -172,6 +184,8 @@ isa = PBXGroup; children = ( 7E2336E72BFE7D7400E6C4B7 /* MockConnectionDetailsProvider.swift */, + F7B4A1352C14FDA9005C7921 /* MockAttachmentManager.swift */, + F7B4A1392C17AE43005C7921 /* TestUtils.swift */, ); path = utils; sourceTree = ""; @@ -249,6 +263,7 @@ 7ED5DEC12BEAF933001693FC /* TranscriptItem.swift */, 7ED5DEC32BEAFD42001693FC /* Event.swift */, 7ED5DEC52BEB38B8001693FC /* Metadata.swift */, + F7B4A1272C1157B0005C7921 /* AttachmentTypes.swift */, ); path = Models; sourceTree = ""; @@ -263,6 +278,7 @@ F74726432BF418F0002B278E /* MetricsClient.swift */, F74726462BF41948002B278E /* MetricsManager.swift */, 7EF438492BFF056400E7F8E9 /* AWSConnectParticipantAdapter.swift */, + F76BB94C2C1898BE0005C3EC /* APIClient.swift */, ); path = Network; sourceTree = ""; @@ -330,6 +346,19 @@ path = Pods; sourceTree = ""; }; + F76BB9542C19000F0005C3EC /* Mocks */ = { + isa = PBXGroup; + children = ( + 7E2336E52BFE7D7400E6C4B7 /* MockAWSConnectParticipant.swift */, + 7E2336E42BFE7D7400E6C4B7 /* MockWebSocketManager.swift */, + 7E2336E32BFE7D7400E6C4B7 /* MockAWSClient.swift */, + F76BB9522C18F84D0005C3EC /* MockAPIClient.swift */, + F7B4A1372C15649B005C7921 /* MockHttpClient.swift */, + F7B4A13B2C17E3C3005C7921 /* MockURLSession.swift */, + ); + path = Mocks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -507,6 +536,7 @@ buildActionMask = 2147483647; files = ( 7E57F6522BC7758C00B25623 /* ConnectionDetails.swift in Sources */, + F76BB94D2C1898BE0005C3EC /* APIClient.swift in Sources */, 7E58A6092BBB460A00965327 /* DefaultHttpClient.swift in Sources */, F74726522BF6F018002B278E /* Config.swift in Sources */, 7E91597B2BC0A92E00821C05 /* Constants.swift in Sources */, @@ -521,6 +551,7 @@ 7E58A60A2BBB460A00965327 /* HttpMethod.swift in Sources */, F7B0747C2BDB298400E8B805 /* HeartbeatManager.swift in Sources */, 7ED5DEC22BEAF933001693FC /* TranscriptItem.swift in Sources */, + F7B4A1282C1157B0005C7921 /* AttachmentTypes.swift in Sources */, 7ED5DEC62BEB38B8001693FC /* Metadata.swift in Sources */, 7E58A6112BBB484D00965327 /* SDKLoggerProtocol.swift in Sources */, 7E58A60C2BBB460A00965327 /* HttpClient.swift in Sources */, @@ -545,16 +576,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F7B4A13C2C17E3C3005C7921 /* MockURLSession.swift in Sources */, + F7B4A1362C14FDA9005C7921 /* MockAttachmentManager.swift in Sources */, + F76BB9532C18F84D0005C3EC /* MockAPIClient.swift in Sources */, + F76BB9562C1900480005C3EC /* APIClientTests.swift in Sources */, + F7B4A13A2C17AE43005C7921 /* TestUtils.swift in Sources */, 7E2336EF2BFE7D7400E6C4B7 /* MockAWSConnectParticipant.swift in Sources */, 7E2336F22BFE7D7400E6C4B7 /* ChatServiceTests.swift in Sources */, - F7B074802BDB2A0700E8B805 /* Notifications.swift in Sources */, 7E2336F32BFE7D7400E6C4B7 /* MockChatService.swift in Sources */, - F7B0747D2BDB298400E8B805 /* HeartbeatManager.swift in Sources */, 7E2336EE2BFE7D7400E6C4B7 /* MockWebSocketManager.swift in Sources */, 7E2336EC2BFE7D7400E6C4B7 /* AWSClientTests.swift in Sources */, 7E2336F12BFE7D7400E6C4B7 /* ChatSessionTests.swift in Sources */, - F7B074832BDB2D3A00E8B805 /* NetworkConnectionManager.swift in Sources */, 7E2336F02BFE7D7400E6C4B7 /* MockConnectionDetailsProvider.swift in Sources */, + F7B4A1382C15649B005C7921 /* MockHttpClient.swift in Sources */, 7E2336ED2BFE7D7400E6C4B7 /* MockAWSClient.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOS.xcworkspace/contents.xcworkspacedata b/AmazonConnectChatIOS/AmazonConnectChatIOS.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 8a28388..0000000 --- a/AmazonConnectChatIOS/AmazonConnectChatIOS.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/APIClientTests.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/APIClientTests.swift new file mode 100644 index 0000000..b678de2 --- /dev/null +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/APIClientTests.swift @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import XCTest +import AWSConnectParticipant +@testable import AmazonConnectChatIOS + +class APIClientTests: XCTestCase { + var apiClient: APIClient! + var testFileUrl = FileManager.default.temporaryDirectory.appendingPathComponent("sample.txt") + + override func setUp() { + super.setUp() + apiClient = APIClient(httpClient: MockHttpClient()) + } + + override func tearDown() { + super.tearDown() + } + + func testUploadAttachment_Success() { + let expectation = self.expectation(description: "UploadAttachment succeeds") + + let startAttachmentUploadResponse = AWSConnectParticipantStartAttachmentUploadResponse() + startAttachmentUploadResponse?.uploadMetadata = AWSConnectParticipantUploadMetadata() + + let testUrl = "https://www.test-endpoint.com" + + startAttachmentUploadResponse?.uploadMetadata?.headersToInclude = TestConstants.sampleAttachmentHeaders + + startAttachmentUploadResponse?.uploadMetadata?.url = testUrl + + + TestUtils.writeSampleTextToUrl(url: testFileUrl) + + apiClient.uploadAttachment(file: testFileUrl, response: startAttachmentUploadResponse!) { success, error in + if success { + let mockHttpClient = self.apiClient.httpClient as! MockHttpClient + XCTAssertEqual(mockHttpClient.urlString, testUrl) + XCTAssertEqual(mockHttpClient.headers, TestConstants.sampleAttachmentHttpHeaders) + XCTAssertNotNil(mockHttpClient.body) + expectation.fulfill() + } else if error != nil { + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } else { + XCTFail("Expected success, got unexpected failure") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testUploadAttachment_NoDataFailure() { + let expectation = self.expectation(description: "UploadAttachment fails due to no data") + + let startAttachmentUploadResponse = AWSConnectParticipantStartAttachmentUploadResponse() + + startAttachmentUploadResponse?.uploadMetadata = AWSConnectParticipantUploadMetadata() + + let testUrl = "https://www.test-endpoint.com" + + startAttachmentUploadResponse?.uploadMetadata?.headersToInclude = TestConstants.sampleAttachmentHeaders + + startAttachmentUploadResponse?.uploadMetadata?.url = testUrl + + apiClient.uploadAttachment(file: testFileUrl, response: startAttachmentUploadResponse!) { success, error in + if success { + XCTFail("Expected failure, got unexpected success") + } else if error != nil { + XCTAssertEqual(error?.localizedDescription, "Unable to read file data") + expectation.fulfill() + } else { + XCTFail("Expected failure with error") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testUploadAttachment_NoHeadersFailure() { + let expectation = self.expectation(description: "UploadAttachment fails due to missing headers") + + let startAttachmentUploadResponse = AWSConnectParticipantStartAttachmentUploadResponse() + + startAttachmentUploadResponse?.uploadMetadata = AWSConnectParticipantUploadMetadata() + + let testUrl = "https://www.test-endpoint.com" + + startAttachmentUploadResponse?.uploadMetadata?.url = testUrl + + + TestUtils.writeSampleTextToUrl(url: testFileUrl) + + apiClient.uploadAttachment(file: testFileUrl, response: startAttachmentUploadResponse!) { success, error in + if success { + XCTFail("Expected failure, got unexpected success") + } else if error != nil { + XCTAssertEqual(error?.localizedDescription, "Missing upload metadata headers") + expectation.fulfill() + } else { + XCTFail("Expected failure with error") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } +} diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/AWSClientTests.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/AWSClientTests.swift index 7af0cc9..e4911f5 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/AWSClientTests.swift +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/AWSClientTests.swift @@ -79,7 +79,7 @@ class AWSClientTests: XCTestCase { } // Test Create Participant Connection Success - func testCreateParticipantConnectionSuccess() { + func testCreateParticipantConnection_Success() { let response = AWSConnectParticipantCreateParticipantConnectionResponse()! response.websocket = AWSConnectParticipantWebsocket() response.websocket?.url = "wss://example.com" @@ -89,12 +89,12 @@ class AWSClientTests: XCTestCase { } // Test Create Participant Connection Failure - func testCreateParticipantConnectionFailure() { + func testCreateParticipantConnection_Failure() { performCreateParticipantConnectionTest(expectedResult: .failure(MockAWSConnectParticipant.MockError.unexpected)) } // Test Create Participant Connection Request Failure - func testCreateParticipantConnectionRequestCreationFailure() { + func testCreateParticipantConnection_RequestCreationFailure() { performCreateParticipantConnectionTest(expectedResult: .failure(AWSClient.AWSClientError.requestCreationFailed), simulateRequestFailure: true) } @@ -124,17 +124,17 @@ class AWSClientTests: XCTestCase { } // Test Disconnect Participant Connection Success - func testDisconnectParticipantConnectionSuccess() { + func testDisconnectParticipantConnection_Success() { performDisconnectParticipantConnectionTest(expectedResult: .success(nil)) } // Test Disconnect Participant Connection Failure - func testDisconnectParticipantConnectionFailure() { + func testDisconnectParticipantConnection_Failure() { performDisconnectParticipantConnectionTest(expectedResult: .failure(MockAWSConnectParticipant.MockError.unexpected)) } // Test Disconnect Participant Connection Request Failure - func testDisconnectParticipantConnectionRequestCreationFailure() { + func testDisconnectParticipantConnection_RequestCreationFailure() { performDisconnectParticipantConnectionTest(expectedResult: .failure(AWSClient.AWSClientError.requestCreationFailed), simulateRequestFailure: true) } @@ -164,17 +164,17 @@ class AWSClientTests: XCTestCase { } // Test Send Message Success - func testSendMessageSuccess() { + func testSendMessage_Success() { performSendMessageTest(expectedResult: .success(nil)) } // Test Send Message Failure - func testSendMessageFailure() { + func testSendMessage_Failure() { performSendMessageTest(expectedResult: .failure(MockAWSConnectParticipant.MockError.unexpected)) } // Test Send Message Request Failure - func testSendMessageRequestCreationFailure() { + func testSendMessage_RequestCreationFailure() { performSendMessageTest(expectedResult: .failure(AWSClient.AWSClientError.requestCreationFailed), simulateRequestFailure: true) } @@ -204,17 +204,17 @@ class AWSClientTests: XCTestCase { } // Test Send Event Success - func testSendEventSuccess() { + func testSendEvent_Success() { performSendEventTest(expectedResult: .success(nil)) } // Test Send Event Failure - func testSendEventFailure() { + func testSendEvent_Failure() { performSendEventTest(expectedResult: .failure(MockAWSConnectParticipant.MockError.unexpected)) } // Test Send Event Request Failure - func testSendEventRequestCreationFailure() { + func testSendEvent_RequestCreationFailure() { performSendEventTest(expectedResult: .failure(AWSClient.AWSClientError.requestCreationFailed), simulateRequestFailure: true) } @@ -241,7 +241,7 @@ class AWSClientTests: XCTestCase { } // Test Get Transcript Success - func testGetTranscriptSuccess() { + func testGetTranscript_Success() { let response = AWSConnectParticipantGetTranscriptResponse()! let item = AWSConnectParticipantItem()! item.content = "Hello" @@ -250,19 +250,19 @@ class AWSClientTests: XCTestCase { } // Test Get Transcript Failure - func testGetTranscriptFailure() { + func testGetTranscript_Failure() { performGetTranscriptTest(expectedResult: .failure(MockAWSConnectParticipant.MockError.unexpected)) } // Test Get Transcript No Result - func testGetTranscriptNoResult() { + func testGetTranscript_NoResultFailure() { mockClient.getTranscriptResult = .success(AWSConnectParticipantGetTranscriptResponse()) let expectation = self.expectation(description: "GetTranscriptNoResult") awsClient.getTranscript(getTranscriptArgs: AWSConnectParticipantGetTranscriptRequest()) { result in switch result { - case .success: - XCTFail("Expected failure, got success") + case .success(let response): + XCTFail("Expected failure, got unexpected success: \(String(describing: response))") case .failure(let error as NSError): XCTAssertEqual(error.domain, "aws.amazon.com") XCTAssertEqual(error.code, 1001) @@ -275,7 +275,7 @@ class AWSClientTests: XCTestCase { } // Test Get Transcript Incorrect Type - func testGetTranscriptIncorrectType() { + func testGetTranscript_IncorrectTypeFailure() { let response = AWSConnectParticipantGetTranscriptResponse()! response.transcript = nil // Simulate incorrect type mockClient.getTranscriptResult = .success(response) @@ -283,8 +283,8 @@ class AWSClientTests: XCTestCase { awsClient.getTranscript(getTranscriptArgs: AWSConnectParticipantGetTranscriptRequest()) { result in switch result { - case .success: - XCTFail("Expected failure, got success") + case .success(let response): + XCTFail("Expected failure, got unexpected success: \(String(describing: response))") case .failure(let error as NSError): XCTAssertEqual(error.domain, "aws.amazon.com") XCTAssertEqual(error.code, 1001) @@ -295,4 +295,103 @@ class AWSClientTests: XCTestCase { waitForExpectations(timeout: timeout, handler: nil) } + + func testStartAttachmentUpload_Success() { + let response = AWSConnectParticipantStartAttachmentUploadResponse()! + response.attachmentId = "12345" + mockClient.startAttachmentUploadResult = .success(response) + let expectation = self.expectation(description: "StartAttachmentUpload succeeds") + + awsClient.startAttachmentUpload(connectionToken: dummyToken, contentType: "text/plain", attachmentName: "sample.txt", attachmentSizeInBytes: 1000) { result in + switch result { + case .success(let expectedResponse): + XCTAssertEqual(expectedResponse.attachmentId, response.attachmentId) + expectation.fulfill() + case .failure(let error as NSError): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + waitForExpectations(timeout: timeout, handler: nil) + } + + func testStartAttachmentUpload_Failure() { + mockClient.startAttachmentUploadResult = .failure(MockAWSConnectParticipant.MockError.unexpected) + let expectation = self.expectation(description: "StartAttachmentUpload fails") + + awsClient.startAttachmentUpload(connectionToken: dummyToken, contentType: "text/plain", attachmentName: "sample.txt", attachmentSizeInBytes: 1000) { result in + switch result { + case .success(let response): + XCTFail("Expected failure, got unexpected success: \(String(describing: response))") + case .failure(let error): + XCTAssertEqual(error as NSError, MockAWSConnectParticipant.MockError.unexpected as NSError) + expectation.fulfill() + } + } + waitForExpectations(timeout: timeout, handler: nil) + } + + func testCompleteAttachmentUpload_Success() { + let response = AWSConnectParticipantCompleteAttachmentUploadResponse()! + mockClient.completeAttachmnetUploadResult = .success(response) + let expectation = self.expectation(description: "CompleteAttachmentUpload succeeds") + + awsClient.completeAttachmentUpload(connectionToken: dummyToken, attachmentIds: ["12345"]) { result in + switch result { + case .success(let expectedResponse): + expectation.fulfill() + case .failure(let error as NSError): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + waitForExpectations(timeout: timeout, handler: nil) + } + + func testCompleteAttachmentUpload_Failure() { + mockClient.completeAttachmnetUploadResult = .failure(MockAWSConnectParticipant.MockError.unexpected) + let expectation = self.expectation(description: "CompleteAttachmentUpload fails") + + awsClient.completeAttachmentUpload(connectionToken: dummyToken, attachmentIds: ["12345"]) { result in + switch result { + case .success(let response): + XCTFail("Expected failure, got unexpected success: \(String(describing: response))") + case .failure(let error as NSError): + XCTAssertEqual(error as NSError, MockAWSConnectParticipant.MockError.unexpected as NSError) + expectation.fulfill() + } + } + waitForExpectations(timeout: timeout, handler: nil) + } + + func testGetAttachment_Success() { + let response = AWSConnectParticipantGetAttachmentResponse()! + response.url = "https://www.example-s3-url.com" + mockClient.getAttachmentResult = .success(response) + let expectation = self.expectation(description: "GetAttachment succeeds") + + awsClient.getAttachment(connectionToken: dummyToken, attachmentId: "12345") { result in + switch result { + case .success(let expectedResponse): + XCTAssertEqual(expectedResponse.url, response.url) + expectation.fulfill() + case .failure(let error as NSError): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + waitForExpectations(timeout: timeout, handler: nil) + } + + func testGetAttachment_Failure() { + mockClient.getAttachmentResult = .failure(MockAWSConnectParticipant.MockError.unexpected) + let expectation = self.expectation(description: "GetAttachment fails") + + awsClient.getAttachment(connectionToken: dummyToken, attachmentId: "12345") { result in + switch result { + case .success(let response): + XCTFail("Expected failure, got unexpected success: \(String(describing: response))") + case .failure(let error as NSError): + XCTAssertEqual(error as NSError, MockAWSConnectParticipant.MockError.unexpected as NSError) + expectation.fulfill() } + } + waitForExpectations(timeout: timeout, handler: nil) + } } diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAPIClient.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAPIClient.swift new file mode 100644 index 0000000..8c4c556 --- /dev/null +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAPIClient.swift @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import Foundation +import AWSConnectParticipant +@testable import AmazonConnectChatIOS + +class MockAPIClient:APIClientProtocol { + let httpClient: HttpClient + var mockUploadAttachment = true + var mockSendMetrics = true + + var uploadAttachmentCalled = false + var sendMetricsCalled = false + + init(httpClient: HttpClient = DefaultHttpClient()) { + self.httpClient = httpClient + } + + func uploadAttachment(file: URL, response: AWSConnectParticipantStartAttachmentUploadResponse, completion: @escaping (Bool, Error?) -> Void) { + if mockUploadAttachment { + uploadAttachmentCalled = true + completion(true, nil) + } + } + + func sendMetrics(metricsEndpoint: String, metricList: [Metric], completion: @escaping (Result) -> Void) -> Void { + + } +} diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockAWSClient.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAWSClient.swift similarity index 60% rename from AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockAWSClient.swift rename to AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAWSClient.swift index bdd331d..69cc28e 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockAWSClient.swift +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAWSClient.swift @@ -11,7 +11,10 @@ class MockAWSClient: AWSClientProtocol { var sendMessageResult: Result? var sendEventResult: Result? var getTranscriptResult: Result? - + var startAttachmentUploadResult: Result? + var completeAttachmentUploadResult: Result? + var getAttachmentResult: Result? + func createParticipantConnection(participantToken: String, completion: @escaping (Result) -> Void) { if let result = createParticipantConnectionResult { completion(result) @@ -41,5 +44,23 @@ class MockAWSClient: AWSClientProtocol { completion(result) } } + + func startAttachmentUpload(connectionToken: String, contentType: String, attachmentName: String, attachmentSizeInBytes: Int, completion: @escaping (Result) -> Void) { + if let result = startAttachmentUploadResult { + completion(result) + } + } + + func completeAttachmentUpload(connectionToken: String, attachmentIds: [String], completion: @escaping (Result) -> Void) { + if let result = completeAttachmentUploadResult { + completion(result) + } + } + + func getAttachment(connectionToken: String, attachmentId: String, completion: @escaping (Result) -> Void) { + if let result = getAttachmentResult { + completion(result) + } + } } diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockAWSConnectParticipant.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAWSConnectParticipant.swift similarity index 62% rename from AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockAWSConnectParticipant.swift rename to AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAWSConnectParticipant.swift index 55c53c5..cd65300 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockAWSConnectParticipant.swift +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockAWSConnectParticipant.swift @@ -11,6 +11,9 @@ class MockAWSConnectParticipant: AWSConnectParticipantProtocol { var sendMessageResult: Result? var sendEventResult: Result? var getTranscriptResult: Result? + var startAttachmentUploadResult: Result? + var completeAttachmnetUploadResult: Result? + var getAttachmentResult: Result? func createParticipantConnection(_ request: AWSConnectParticipantCreateParticipantConnectionRequest?) -> AWSTask { let taskCompletionSource = AWSTaskCompletionSource() @@ -97,6 +100,57 @@ class MockAWSConnectParticipant: AWSConnectParticipantProtocol { return taskCompletionSource.task } + func startAttachmentUpload(_ request: AWSConnectParticipantStartAttachmentUploadRequest?) -> AWSTask { + let taskCompletionSource = AWSTaskCompletionSource() + if let result = startAttachmentUploadResult { + switch result { + case .success(let response): + taskCompletionSource.set(result: response) + case .failure(let error): + taskCompletionSource.set(error: error) + } + } else if request == nil { + taskCompletionSource.set(error: AWSClient.AWSClientError.requestCreationFailed) + } else { + taskCompletionSource.set(error: MockError.unexpected) + } + return taskCompletionSource.task + } + + func completeAttachmentUpload(_ request: AWSConnectParticipantCompleteAttachmentUploadRequest?) -> AWSTask { + let taskCompletionSource = AWSTaskCompletionSource() + if let result = completeAttachmnetUploadResult { + switch result { + case .success(let response): + taskCompletionSource.set(result: response) + case .failure(let error): + taskCompletionSource.set(error: error) + } + } else if request == nil { + taskCompletionSource.set(error: AWSClient.AWSClientError.requestCreationFailed) + } else { + taskCompletionSource.set(error: MockError.unexpected) + } + return taskCompletionSource.task + } + + func getAttachment(_ request: AWSConnectParticipantGetAttachmentRequest?) -> AWSTask { + let taskCompletionSource = AWSTaskCompletionSource() + if let result = getAttachmentResult { + switch result { + case .success(let response): + taskCompletionSource.set(result: response) + case .failure(let error): + taskCompletionSource.set(error: error) + } + } else if request == nil { + taskCompletionSource.set(error: AWSClient.AWSClientError.requestCreationFailed) + } else { + taskCompletionSource.set(error: MockError.unexpected) + } + return taskCompletionSource.task + } + enum MockError: Error { case unexpected } diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockHttpClient.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockHttpClient.swift new file mode 100644 index 0000000..b06731a --- /dev/null +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockHttpClient.swift @@ -0,0 +1,28 @@ +// +// MockHttpClient.swift +// AmazonConnectChatIOSTests +// + +import XCTest +import UniformTypeIdentifiers +import AWSConnectParticipant + +@testable import AmazonConnectChatIOS + +class MockHttpClient: DefaultHttpClient { + var urlString: String? + var headers: HttpHeaders? + var body: Encodable? + + override func putJson(_ urlString: String, + _ headers: HttpHeaders?, + _ body: B, + _ onSuccess: @escaping () -> Void, + _ onFailure: @escaping (_ error: Error) -> Void) { + self.urlString = urlString + self.headers = headers + self.body = body + onSuccess() + return + } +} diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockURLSession.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockURLSession.swift new file mode 100644 index 0000000..e7f9311 --- /dev/null +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockURLSession.swift @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import UniformTypeIdentifiers +import AWSConnectParticipant + +@testable import AmazonConnectChatIOS + + +class MockURLSession: URLSession { + var downloadTaskCalled = false + var mockUrlResult: URL? + var mockUrlResponse: URLResponse? + var mockError: (any Error)? + + override func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, (any Error)?) -> Void) -> URLSessionDownloadTask { + completionHandler(mockUrlResult, mockUrlResponse, mockError) + return MockURLSessionDownloadTask() + } +} + + +class MockURLSessionDownloadTask: URLSessionDownloadTask { + override func resume() { + // No-op: Do nothing + } +} diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockWebSocketManager.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockWebSocketManager.swift similarity index 100% rename from AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/MockWebSocketManager.swift rename to AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Network/Mocks/MockWebSocketManager.swift diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatServiceTests.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatServiceTests.swift index 258c06d..b6211db 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatServiceTests.swift +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatServiceTests.swift @@ -20,6 +20,7 @@ class ChatServiceTests: XCTestCase { override func tearDown() { tearDownMocks() + tearDownTempFile() super.tearDown() } @@ -44,6 +45,18 @@ class ChatServiceTests: XCTestCase { mockWebsocketManager = nil } + private func tearDownTempFile(url: URL? = nil) { + let deleteUrl = url ?? TestConstants.testFileUrl + do { + if FileManager.default.fileExists(atPath: deleteUrl.path) { + try FileManager.default.removeItem(at: deleteUrl) + print("Temp file successfully cleared") + } + } catch { + print("Failed to remove test file: \(error.localizedDescription)") + } + } + // Helper method to create chat details private func createChatDetails() -> ChatDetails { return ChatDetails(contactId: "testContactId", participantId: "testParticipantId", participantToken: "testParticipantToken") @@ -421,6 +434,358 @@ class ChatServiceTests: XCTestCase { waitForExpectations(timeout: 1) } + + func testSendAttachment_Success() { + let expectation = self.expectation(description: "SendAttachment succeeds") + TestUtils.writeSampleTextToUrl(url: TestConstants.testFileUrl) + + let mockAttachmentManager = MockAttachmentManager() + let mockAPIClient = MockAPIClient() + mockAttachmentManager.apiClient = mockAPIClient + mockAttachmentManager.sendAttachment(file: TestConstants.testFileUrl) { success, error in + XCTAssertTrue(success) + XCTAssertNil(error) + XCTAssertTrue(mockAttachmentManager.startAttachmentUploadCalled) + XCTAssertTrue(mockAPIClient.uploadAttachmentCalled) + XCTAssertTrue(mockAttachmentManager.completeAttachmentUploadCalled) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testSendAttachment_InvalidMimeFailure() { + let expectation = self.expectation(description: "SendAttachment fails due to invalid mime type") + let fileUrl = FileManager.default.temporaryDirectory.appendingPathComponent("sample.xyz") + let fileContents = "Sample text file contents" + + do { + try fileContents.write(to: fileUrl, atomically: true, encoding: .utf8) + print("File created successfully at: \(TestConstants.testFileUrl.path)") + } catch { + print("Failed to create file: \(error.localizedDescription)") + return + } + let mockAttachmentManager = MockAttachmentManager() + let mockAPIClient = MockAPIClient() + mockAttachmentManager.apiClient = mockAPIClient + mockAttachmentManager.sendAttachment(file: fileUrl) { success, error in + self.tearDownTempFile(url: fileUrl) + XCTAssertFalse(success) + XCTAssertEqual(error?.localizedDescription, "Could not parse MIME type from file URL") + XCTAssertFalse(mockAttachmentManager.startAttachmentUploadCalled) + XCTAssertFalse(mockAPIClient.uploadAttachmentCalled) + XCTAssertFalse(mockAttachmentManager.completeAttachmentUploadCalled) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testSendAttachment_UnsupportedMimeFailure() { + let expectation = self.expectation(description: "Send Attachment fails due to unsupported mime type") + let fileUrl = FileManager.default.temporaryDirectory.appendingPathComponent("sample.webp") + let fileContents = "Sample text file contents" + + do { + try fileContents.write(to: fileUrl, atomically: true, encoding: .utf8) + print("File created successfully at: \(TestConstants.testFileUrl.path)") + } catch { + print("Failed to create file: \(error.localizedDescription)") + return + } + let mockAttachmentManager = MockAttachmentManager() + let mockAPIClient = MockAPIClient() + mockAttachmentManager.apiClient = mockAPIClient + mockAttachmentManager.sendAttachment(file: fileUrl) { success, error in + self.tearDownTempFile(url: fileUrl) + XCTAssertFalse(success) + XCTAssertEqual(error?.localizedDescription, "image/webp is not a supported file type") + XCTAssertFalse(mockAttachmentManager.startAttachmentUploadCalled) + XCTAssertFalse(mockAPIClient.uploadAttachmentCalled) + XCTAssertFalse(mockAttachmentManager.completeAttachmentUploadCalled) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testSendAttachment_InvalidFileSizeFailure() { + let expectation = self.expectation(description: "SendAttachment fails due to invalid file size") + + let mockAttachmentManager = MockAttachmentManager() + let mockAPIClient = MockAPIClient() + mockAttachmentManager.apiClient = mockAPIClient + mockAttachmentManager.sendAttachment(file: TestConstants.testFileUrl) { success, error in + XCTAssertFalse(success) + XCTAssertEqual(error?.localizedDescription, "Could not get valid file size") + XCTAssertFalse(mockAttachmentManager.startAttachmentUploadCalled) + XCTAssertFalse(mockAPIClient.uploadAttachmentCalled) + XCTAssertFalse(mockAttachmentManager.completeAttachmentUploadCalled) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testStartAttachmentUpload_Success() { + let expectation = self.expectation(description: "StartAttachmentUpload succeeds") + let response = AWSConnectParticipantStartAttachmentUploadResponse() + mockAWSClient.startAttachmentUploadResult = .success(response!) + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + + chatService.startAttachmentUpload(contentType: "text/plain", attachmentName: "sample.txt", attachmentSizeInBytes: 1000) { result in + switch result { + case .success(let startAttachmentResponse): + XCTAssertEqual(startAttachmentResponse, response) + expectation.fulfill() + case .failure(let error): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testStartAttachmentUpload_Failure() { + let expectation = self.expectation(description: "StartAttachmentUpload fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + mockAWSClient.startAttachmentUploadResult = .failure(expectedError) + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + + chatService.startAttachmentUpload(contentType: "text/plain", attachmentName: "sample.txt", attachmentSizeInBytes: 1000) { result in + switch result { + case .success(let startAttachmentResponse): + XCTFail("Expected failure, got unexpected success: \(String(describing: startAttachmentResponse))") + case .failure(let error): + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testCompleteAttachmentUpload_Success() { + let expectation = self.expectation(description: "CompleteAttachmentUpload succeeds") + let response = AWSConnectParticipantCompleteAttachmentUploadResponse() + mockAWSClient.completeAttachmentUploadResult = .success(response!) + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + + chatService.completeAttachmentUpload(attachmentIds: ["12345"]) { success, error in + if success { + expectation.fulfill() + } else if error != nil { + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } else { + XCTFail("Expected success, got unexpected result") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testCompleteAttachmentUpload_Failure() { + let expectation = self.expectation(description: "CompleteAttachmentUpload fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + mockAWSClient.completeAttachmentUploadResult = .failure(expectedError) + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + + chatService.completeAttachmentUpload(attachmentIds: ["12345"]) { success, error in + if success { + XCTFail("Expected success, got unexpected success") + } else if error != nil { + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } else { + XCTFail("Expected success, got unexpected result") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testGetAttachmentDownloadUrl_Success() { + let expectation = self.expectation(description: "GetAttachmentDownloadUrl succeeds") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + guard let response = AWSConnectParticipantGetAttachmentResponse() else { + XCTFail("AWSConnectParticipantGetAttachmentResponse returned nil") + return + } + + response.url = "https://www.test-endpoint.com" + mockAWSClient.getAttachmentResult = .success(response) + + + chatService.getAttachmentDownloadUrl(attachmentId: "12345") { result in + switch result { + case .success(let url): + XCTAssertEqual(url.absoluteString, response.url) + expectation.fulfill() + case .failure(let error): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + + waitForExpectations(timeout: 1) + } + + func testGetAttachmentDownloadUrl_Failure() { + let expectation = self.expectation(description: "GetAttachmentDownloadUrl fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + guard let response = AWSConnectParticipantGetAttachmentResponse() else { + XCTFail("AWSConnectParticipantGetAttachmentResponse returned nil") + return + } + + response.url = "https://www.test-endpoint.com" + mockAWSClient.getAttachmentResult = .failure(expectedError) + + + chatService.getAttachmentDownloadUrl(attachmentId: "12345") { result in + switch result { + case .success(let url): + XCTFail("Expected failure, got unexpected success: \(url.absoluteString)") + case .failure(let error): + XCTAssertEqual(error as NSError, expectedError) + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1) + } + + func testDownloadAttachment_Success() { + let expectation = self.expectation(description: "StartAttachmentUpload succeeds") + guard let response = AWSConnectParticipantGetAttachmentResponse() else { + XCTFail("AWSConnectParticipantGetAttachmentResponse returned nil") + return + } + + response.url = "https://www.test-endpoint.com" + mockAWSClient.getAttachmentResult = .success(response) + + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + + let mockAttachmentManager = MockAttachmentManager( + awsClient: mockAWSClient, + connectionDetailsProvider: mockConnectionDetailsProvider, + websocketManagerFactory: { _ in self.mockWebsocketManager } + ) + + mockAttachmentManager.downloadAttachment(attachmentId: "12345", filename: "sample.txt") { result in + switch result { + case .success(let url): + XCTAssertEqual(url.absoluteString, response.url) + expectation.fulfill() + case .failure(let error): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadAttachment_Failure() { + let expectation = self.expectation(description: "StartAttachmentUpload fails") + + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + mockAWSClient.getAttachmentResult = .failure(expectedError) + + let connectionDetails = createConnectionDetails() + mockConnectionDetailsProvider.mockConnectionDetails = connectionDetails + + + chatService.downloadAttachment(attachmentId: "12345", filename: "sample.txt") { result in + switch result { + case .success(let url): + XCTFail("Expected failure, got unexpected success: \(url.absoluteString)") + case .failure(let error): + XCTAssertEqual(error as NSError, expectedError) + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadFile_Success() { + let expectation = self.expectation(description: "DownloadFile succeeds") + + TestUtils.writeSampleTextToUrl(url: TestConstants.testFileUrl) + + let mockUrlSession = MockURLSession() + mockUrlSession.mockUrlResult = TestConstants.testFileUrl + chatService.urlSession = mockUrlSession + + var filename = "sample2.txt" + + var expectedLocalUrl = FileManager.default.temporaryDirectory.appendingPathComponent(filename) + + chatService.downloadFile(url: TestConstants.testFileUrl, filename: filename) { (localUrl, error) in + if let localUrl = localUrl { + XCTAssertEqual(localUrl, expectedLocalUrl) + expectation.fulfill() + } else if let error = error { + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadFile_ErrorFailure() { + let expectation = self.expectation(description: "DownloadFile fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + + TestUtils.writeSampleTextToUrl(url: TestConstants.testFileUrl) + + let mockUrlSession = MockURLSession() + mockUrlSession.mockError = expectedError + chatService.urlSession = mockUrlSession + + chatService.downloadFile(url: TestConstants.testFileUrl, filename: "sample2.txt") { (localUrl, error) in + if let localUrl = localUrl { + XCTFail("Expected failure, got unexpected success: \(localUrl.absoluteString)") + expectation.fulfill() + } else if let error = error { + XCTAssertEqual(error as NSError, expectedError) + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadFile_NoFileFailure() { + let expectation = self.expectation(description: "DownloadFile fails due to no file") + + let mockUrlSession = MockURLSession() + chatService.urlSession = mockUrlSession + + chatService.downloadFile(url: TestConstants.testFileUrl, filename: "sample2.txt") { (localUrl, error) in + if let localUrl = localUrl { + XCTFail("Expected failure, got unexpected success: \(localUrl.absoluteString)") + expectation.fulfill() + } else if let error = error { + XCTAssertEqual(error.localizedDescription, "No file found at URL") + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1.0, handler: nil) + } } extension ChatServiceTests { diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatSessionTests.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatSessionTests.swift index 52fba76..d281384 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatSessionTests.swift +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/ChatSessionTests.swift @@ -8,6 +8,7 @@ import AWSConnectParticipant class ChatSessionTests: XCTestCase { var chatSession: ChatSession! var mockChatService: MockChatService! + var testFileUrl = FileManager.default.temporaryDirectory.appendingPathComponent("sample.txt") override func setUp() { super.setUp() @@ -257,4 +258,99 @@ class ChatSessionTests: XCTestCase { let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) performGetTranscriptTest(scanDirection: .backward, sortOrder: .ascending, maxResults: 15, nextToken: nil, startPosition: nil, expectedResult: .failure(expectedError)) } + + func testSendAttachment_Success() { + let expectation = self.expectation(description: "SendAttachment succeeds") + + mockChatService.sendAttachmentResult = .success(()) + chatSession.sendAttachment(file: testFileUrl) { result in + switch result { + case .success(): + expectation.fulfill() + case .failure(let error): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testSendAttachment_Failure() { + let expectation = self.expectation(description: "SendAttachment fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + mockChatService.sendAttachmentResult = .failure(expectedError) + chatSession.sendAttachment(file: testFileUrl) { result in + switch result { + case .success(): + XCTFail("Expected failure, got unexpected success") + case .failure(let error): + XCTAssertEqual(error as NSError, expectedError) + expectation.fulfill() + } + } + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadAttachment_Success() { + let expectation = self.expectation(description: "DownloadAttachment succeeds") + + mockChatService.downloadAttachmentResult = .success(testFileUrl) + chatSession.downloadAttachment(attachmentId: "12345", filename: "sample.txt") { result in + switch result { + case .success(let url): + XCTAssertEqual(url, self.testFileUrl) + expectation.fulfill() + case .failure(let error): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadAttachment_Failure() { + let expectation = self.expectation(description: "DownloadAttachment fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + mockChatService.downloadAttachmentResult = .failure(expectedError) + chatSession.downloadAttachment(attachmentId: "12345", filename: "sample.txt") { result in + switch result { + case .success(let url): + XCTFail("Expected failure, got unexpected success: \(String(describing: url))") + case .failure(let error): + XCTAssertEqual(error as NSError, expectedError) + expectation.fulfill() + } + } + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testGetAttachmentDownloadUrl_Success() { + let expectation = self.expectation(description: "GetAttachmentDownloadUrl succeeds") + + mockChatService.getAttachmentDownloadUrlResult = .success(testFileUrl) + chatSession.getAttachmentDownloadUrl(attachmentId: "12345") { result in + switch result { + case .success(let url): + XCTAssertEqual(url, self.testFileUrl) + expectation.fulfill() + case .failure(let error): + XCTFail("Expected success, got unexpected failure: \(String(describing: error))") + } + } + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testGetAttachmentDownloadUrl_Failure() { + let expectation = self.expectation(description: "GetAttachmentDownloadUrl fails") + let expectedError = NSError(domain: "TestDomain", code: 1, userInfo: nil) + mockChatService.getAttachmentDownloadUrlResult = .failure(expectedError) + chatSession.getAttachmentDownloadUrl(attachmentId: "12345") { result in + switch result { + case .success(let url): + XCTFail("Expected failure, got unexpected success: \(String(describing: url))") + case .failure(let error): + XCTAssertEqual(error as NSError, expectedError) + expectation.fulfill() + } + } + waitForExpectations(timeout: 1.0, handler: nil) + } } diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/MockChatService.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/MockChatService.swift index 63c901b..39d89b7 100644 --- a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/MockChatService.swift +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/Service/MockChatService.swift @@ -7,11 +7,13 @@ import AWSConnectParticipant @testable import AmazonConnectChatIOS class MockChatService: ChatServiceProtocol { - var createChatSessionResult: Result? var disconnectChatSessionResult: Result? var sendMessageResult: Result? var sendEventResult: Result? + var sendAttachmentResult: Result? + var downloadAttachmentResult: Result? + var getAttachmentDownloadUrlResult: Result? var getTranscriptResult: Result? var eventPublisher = PassthroughSubject() var transcriptItemPublisher = PassthroughSubject() @@ -72,6 +74,45 @@ class MockChatService: ChatServiceProtocol { } } } + + func sendAttachment(file: URL, completion: @escaping (Bool, (any Error)?) -> Void) { + if let result = sendAttachmentResult { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + switch result { + case .success: + completion(true, nil) + case .failure(let error): + completion(false, error) + } + } + } + } + + func downloadAttachment(attachmentId: String, filename: String, completion: @escaping (Result) -> Void) { + if let result = downloadAttachmentResult { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + switch result { + case .success(let url): + completion(.success(url)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + + func getAttachmentDownloadUrl(attachmentId: String, completion: @escaping (Result) -> Void) { + if let result = getAttachmentDownloadUrlResult { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + switch result { + case .success(let url): + completion(.success(url)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } func subscribeToEvents(handleEvent: @escaping (ChatEvent) -> Void) -> AnyCancellable { return eventPublisher.sink(receiveValue: handleEvent) diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/utils/MockAttachmentManager.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/utils/MockAttachmentManager.swift new file mode 100644 index 0000000..cf81991 --- /dev/null +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/utils/MockAttachmentManager.swift @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import XCTest +import UniformTypeIdentifiers +import AWSConnectParticipant + +@testable import AmazonConnectChatIOS + +class MockAttachmentManager: ChatService { + var startAttachmentUploadCalled = false + var completeAttachmentUploadCalled = false + var downloadFileCalled = false + + var mockStartAttachmentUpload = true + var mockCompleteAttachmentUpload = true + var mockDownloadFile = true + + override func startAttachmentUpload(contentType: String, attachmentName: String, attachmentSizeInBytes: Int, completion: @escaping (Result) -> Void) { + if mockStartAttachmentUpload { + startAttachmentUploadCalled = true + let mockResponse = AWSConnectParticipantStartAttachmentUploadResponse() + mockResponse!.attachmentId = "mockAttachmentId" + completion(.success(mockResponse!)) + } else { + super.startAttachmentUpload(contentType: contentType, attachmentName: attachmentName, attachmentSizeInBytes: attachmentSizeInBytes, completion: completion) + } + } + + override func completeAttachmentUpload(attachmentIds: [String], completion: @escaping (Bool, Error?) -> Void) { + if mockCompleteAttachmentUpload { + completeAttachmentUploadCalled = true + completion(true, nil) + } else { + super.completeAttachmentUpload(attachmentIds: attachmentIds, completion: completion) + } + } + + override func downloadFile(url: URL, filename: String, completion: @escaping (URL?, Error?) -> Void) { + if mockDownloadFile { + completeAttachmentUploadCalled = true + completion(url, nil) + } else { + super.downloadFile(url: url, filename: filename, completion: completion) + } + } +} diff --git a/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/utils/TestUtils.swift b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/utils/TestUtils.swift new file mode 100644 index 0000000..637290c --- /dev/null +++ b/AmazonConnectChatIOS/AmazonConnectChatIOSTests/Core/utils/TestUtils.swift @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +@testable import AmazonConnectChatIOS + +struct TestUtils { + static func writeSampleTextToUrl(url: URL) { + let fileContents = "Sample text file contents" + do { + try fileContents.write(to: url, atomically: true, encoding: .utf8) + print("File created successfully at: \(url.path)") + } catch { + print("Failed to create file: \(error.localizedDescription)") + return + } + } +} + +struct TestConstants { + static let sampleAttachmentHeaders = [ + "x-amz-meta-contact_id": "12345", + "x-amz-meta-initial_contact_id": "12345", + "x-amz-acl": "bucket-owner-full-control", + "Content-Disposition": "attachment; filename=\"sample.txt\"", + "x-amz-meta-account_id": "12345", + "Content-Length": "12345", + "Content-Type": "text/plain", + "x-amz-meta-organization_id": "12345" + ] + + static let sampleAttachmentHttpHeaders: HttpHeaders = [ + HttpHeaders.Key.amzMetaContactId: "12345", + HttpHeaders.Key.amzMetaInitialContactId: "12345", + HttpHeaders.Key.amzAcl: "bucket-owner-full-control", + HttpHeaders.Key.contentDisposition: "attachment; filename=\"sample.txt\"", + HttpHeaders.Key.amzMetaAccountId: "12345", + HttpHeaders.Key.contentLength: "12345", + HttpHeaders.Key.contentType: "text/plain", + HttpHeaders.Key.amzMetaOrganizationId: "12345" + ] + + static let testFileUrl = FileManager.default.temporaryDirectory.appendingPathComponent("sample.txt") +} diff --git a/AmazonConnectChatIOS/Podfile.lock b/AmazonConnectChatIOS/Podfile.lock index c3c2b6b..a67ff91 100644 --- a/AmazonConnectChatIOS/Podfile.lock +++ b/AmazonConnectChatIOS/Podfile.lock @@ -18,4 +18,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 8ce922eea6873625e870ccb0e15742075629a4f8 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/AmazonConnectChatIOS/Sources/Core/Models/AttachmentTypes.swift b/AmazonConnectChatIOS/Sources/Core/Models/AttachmentTypes.swift new file mode 100644 index 0000000..d0dd248 --- /dev/null +++ b/AmazonConnectChatIOS/Sources/Core/Models/AttachmentTypes.swift @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + + +import Foundation + +public enum AttachmentTypes: String { + case csv = "text/csv" + case doc = "application/msword" + case docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + case heic = "image/heic" + case jpg = "image/jpeg" + case mov = "video/quicktime" + case mp4 = "video/mp4" + case pdf = "application/pdf" + case png = "image/png" + case ppt = "application/vnd.ms-powerpoint" + case pptx = "application/vnd.openxmlformats-officedocument.presentationml.presentation" + case rtf = "application/rtf" + case txt = "text/plain" + case wav = "audio/wav" + case xls = "application/vnd.ms-excel" + case xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" +} diff --git a/AmazonConnectChatIOS/Sources/Core/Models/ChatEvent.swift b/AmazonConnectChatIOS/Sources/Core/Models/ChatEvent.swift index e870535..fb7b624 100644 --- a/AmazonConnectChatIOS/Sources/Core/Models/ChatEvent.swift +++ b/AmazonConnectChatIOS/Sources/Core/Models/ChatEvent.swift @@ -18,6 +18,13 @@ public enum ContentType: String { case interactiveText = "application/vnd.amazonaws.connect.message.interactive" } +public enum WebSocketMessageType: String { + case message = "MESSAGE" + case event = "EVENT" + case attachment = "ATTACHMENT" + case messageMetadata = "MESSAGEMETADATA" +} + enum ChatEvent { case connectionEstablished diff --git a/AmazonConnectChatIOS/Sources/Core/Models/Message.swift b/AmazonConnectChatIOS/Sources/Core/Models/Message.swift index 3165646..7312938 100644 --- a/AmazonConnectChatIOS/Sources/Core/Models/Message.swift +++ b/AmazonConnectChatIOS/Sources/Core/Models/Message.swift @@ -13,7 +13,7 @@ public protocol MessageProtocol: TranscriptItemProtocol { var participant: String { get set } var text: String { get set } var contentType: String { get set } - var messageID: String? { get set } + var messageId: String? { get set } var displayName: String? { get set } var messageDirection: MessageDirection? { get set } var metadata: (any MetadataProtocol)? { get set } @@ -23,18 +23,20 @@ public class Message: TranscriptItem, MessageProtocol { public var participant: String public var text: String public var messageDirection: MessageDirection? - public var messageID: String? + public var messageId: String? + public var attachmentId: String? public var displayName: String? @Published public var metadata: (any MetadataProtocol)? - public init(participant: String, text: String, contentType: String, messageDirection: MessageDirection? = nil, timeStamp: String, messageID: String? = nil, + public init(participant: String, text: String, contentType: String, messageDirection: MessageDirection? = nil, timeStamp: String, attachmentId: String? = nil, messageID: String? = nil, displayName: String? = nil, serializedContent: [String: Any], metadata: (any MetadataProtocol)? = nil) { self.participant = participant self.text = text self.messageDirection = messageDirection - self.messageID = messageID + self.messageId = messageID self.metadata = metadata self.displayName = displayName + self.attachmentId = attachmentId super.init(timeStamp: timeStamp, contentType: contentType, serializedContent: serializedContent) } diff --git a/AmazonConnectChatIOS/Sources/Core/Network/APIClient.swift b/AmazonConnectChatIOS/Sources/Core/Network/APIClient.swift new file mode 100644 index 0000000..63dbf58 --- /dev/null +++ b/AmazonConnectChatIOS/Sources/Core/Network/APIClient.swift @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import Foundation +import AWSConnectParticipant + +protocol APIClientProtocol { + func uploadAttachment(file: URL, response: AWSConnectParticipantStartAttachmentUploadResponse, completion: @escaping (Bool, Error?) -> Void) + func sendMetrics(metricsEndpoint: String, metricList: [Metric], completion: @escaping (Result) -> Void) +} + + +class APIClient:APIClientProtocol { + static let shared: APIClient = APIClient(httpClient: DefaultHttpClient()) + let httpClient: HttpClient + + init(httpClient: HttpClient = DefaultHttpClient()) { + self.httpClient = httpClient + } + + func uploadAttachment(file: URL, response: AWSConnectParticipantStartAttachmentUploadResponse, completion: @escaping (Bool, Error?) -> Void) { + guard let fileData = try? Data(contentsOf: file) else { + completion(false, NSError(domain: "ChatService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unable to read file data"])) + return + } + + guard let headers = response.uploadMetadata?.headersToInclude else { + completion(false, NSError(domain: "ChatService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing upload metadata headers"])) + return + } + + let headersToInclude: HttpHeaders = headers.reduce(into: HttpHeaders()) { result, pair in + if let key = HttpHeader.Key(rawValue: pair.key) { + result[key] = pair.value + } + } + + self.httpClient.putJson((response.uploadMetadata?.url)!, headersToInclude, fileData) { + completion(true, nil) + } _: { error in + completion(false, error) + } + } + + func sendMetrics(metricsEndpoint: String, metricList: [Metric], completion: @escaping (Result) -> Void) -> Void { + let body = CreateMetricRequestBody(metricList: metricList, metricNamespace: "chat-widget") + self.httpClient.postJson(metricsEndpoint, nil, body) { (data : PutMetricsResponse) in + completion(.success(data)) + } _: { error in + completion(.failure(error)) + } + } +} diff --git a/AmazonConnectChatIOS/Sources/Core/Network/AWSClient.swift b/AmazonConnectChatIOS/Sources/Core/Network/AWSClient.swift index fefbb4c..872158c 100644 --- a/AmazonConnectChatIOS/Sources/Core/Network/AWSClient.swift +++ b/AmazonConnectChatIOS/Sources/Core/Network/AWSClient.swift @@ -35,6 +35,34 @@ protocol AWSClientProtocol { /// - completion: Completion handler to handle the success status or error. func sendEvent(connectionToken: String, contentType: ContentType, content: String, completion: @escaping (Result) -> Void) + /// Requests a pre-signed S3 URL with authentication headers used to upload a given file to S3. + /// - Parameters: + /// - connectionToken: The token for the connection through which the event is sent. + /// - contentType: Describes the MIME file type of the attachment. + /// - attachmentName: A case-sensitive name of the attachment being uploaded. + /// - attachmentSizeInBytes: The size of the attachment in bytes. + /// - completion: Completion handler to handle the success status or error. + func startAttachmentUpload(connectionToken: String, contentType: String, attachmentName: String, attachmentSizeInBytes: Int, completion: @escaping (Result) -> Void) + + /// Communicates with the Connect Participant backend to signal that the file has been uploaded successfully. + /// - Parameters: + /// - connectionToken: The token for the connection through which the event is sent. + /// - attachmentIds: A list of unique identifiers for the attachments. + /// - completion: Completion handler to handle the success status or error. + func completeAttachmentUpload(connectionToken: String, attachmentIds: [String], completion: @escaping (Result) -> Void) + + /// Retrieves a download URL for the attachment defined by the attachmentId. + /// - Parameters: + /// - connectionToken: The token for the connection through which the event is sent. + /// - attachmentId: A unique identifier for the attachment. + /// - completion: Completion handler to handle the success status or error. + func getAttachment(connectionToken: String, attachmentId: String, completion: @escaping (Result) -> Void) + + /// Requests the chat transcript. + /// - Parameters: + /// - connectionToken: The token for the connection through which the event is sent. + /// - attachmentIds: A list of unique identifiers for the attachments. + /// - completion: Completion handler to handle the success status or error. func getTranscript(getTranscriptArgs: AWSConnectParticipantGetTranscriptRequest, completion: @escaping (Result) -> Void) } @@ -63,6 +91,18 @@ class AWSClient: AWSClientProtocol { AWSConnectParticipantSendEventRequest() } + var startAttachmentUploadRequest: () -> AWSConnectParticipantStartAttachmentUploadRequest? = { + AWSConnectParticipantStartAttachmentUploadRequest() + } + + var completeAttachmentUploadRequest: () -> AWSConnectParticipantCompleteAttachmentUploadRequest? = { + AWSConnectParticipantCompleteAttachmentUploadRequest() + } + + var getAttachmentRequest: () -> AWSConnectParticipantGetAttachmentRequest? = { + AWSConnectParticipantGetAttachmentRequest() + } + private init() {} @@ -168,6 +208,71 @@ class AWSClient: AWSClientProtocol { }) } + func startAttachmentUpload(connectionToken: String, contentType: String, attachmentName: String, attachmentSizeInBytes: Int, completion: @escaping (Result) -> Void) { + guard let request = AWSConnectParticipantStartAttachmentUploadRequest() else { + completion(.failure(AWSClientError.requestCreationFailed)) + return + } + + request.connectionToken = connectionToken + request.contentType = contentType + request.attachmentName = attachmentName + request.attachmentSizeInBytes = NSNumber(value: attachmentSizeInBytes) + + connectParticipantClient?.startAttachmentUpload(request).continueWith(executor: AWSExecutor.mainThread(), block: { (task: AWSTask) -> AnyObject? in + if let error = task.error { + completion(.failure(error)) + } else if let result = task.result { + completion(.success(result)) + } else { + completion(.failure(AWSClientError.unknownError)) + } + return nil + }) + } + + func completeAttachmentUpload(connectionToken: String, attachmentIds: [String], completion: @escaping (Result) -> Void) { + guard let request = AWSConnectParticipantCompleteAttachmentUploadRequest() else { + completion(.failure(AWSClientError.requestCreationFailed)) + return + } + + request.connectionToken = connectionToken + request.attachmentIds = attachmentIds + + connectParticipantClient?.completeAttachmentUpload(request).continueWith(executor: AWSExecutor.mainThread(), block: { (task: AWSTask) -> AnyObject? in + if let error = task.error { + completion(.failure(error)) + } else if let result = task.result { + completion(.success(result)) + } else { + completion(.failure(AWSClientError.unknownError)) + } + return nil + }) + } + + func getAttachment(connectionToken: String, attachmentId: String, completion: @escaping (Result) -> Void) { + guard let request = AWSConnectParticipantGetAttachmentRequest() else { + completion(.failure(AWSClientError.requestCreationFailed)) + return + } + + request.connectionToken = connectionToken + request.attachmentId = attachmentId + + connectParticipantClient?.getAttachment(request).continueWith(executor: AWSExecutor.mainThread(), block: { (task: AWSTask) -> AnyObject? in + if let error = task.error { + completion(.failure(error)) + } else if let result = task.result { + completion(.success(result)) + } else { + completion(.failure(AWSClientError.unknownError)) + } + return nil + }) + } + func getTranscript(getTranscriptArgs: AWSConnectParticipantGetTranscriptRequest, completion: @escaping (Result) -> Void) { connectParticipantClient?.getTranscript(getTranscriptArgs).continueWith { (task) -> AnyObject? in if let error = task.error { diff --git a/AmazonConnectChatIOS/Sources/Core/Network/AWSConnectParticipantAdapter.swift b/AmazonConnectChatIOS/Sources/Core/Network/AWSConnectParticipantAdapter.swift index 950748e..472f587 100644 --- a/AmazonConnectChatIOS/Sources/Core/Network/AWSConnectParticipantAdapter.swift +++ b/AmazonConnectChatIOS/Sources/Core/Network/AWSConnectParticipantAdapter.swift @@ -10,6 +10,9 @@ protocol AWSConnectParticipantProtocol { func sendMessage(_ request: AWSConnectParticipantSendMessageRequest?) -> AWSTask func sendEvent(_ request: AWSConnectParticipantSendEventRequest?) -> AWSTask func getTranscript(_ request: AWSConnectParticipantGetTranscriptRequest?) -> AWSTask + func startAttachmentUpload(_ request: AWSConnectParticipantStartAttachmentUploadRequest?) -> AWSTask + func completeAttachmentUpload(_ request: AWSConnectParticipantCompleteAttachmentUploadRequest?) -> AWSTask + func getAttachment(_ request: AWSConnectParticipantGetAttachmentRequest?) -> AWSTask } class AWSConnectParticipantAdapter: AWSConnectParticipantProtocol { @@ -68,4 +71,25 @@ class AWSConnectParticipantAdapter: AWSConnectParticipantProtocol { } return participant.getTranscript(request) } + + func startAttachmentUpload(_ request: AWSConnectParticipantStartAttachmentUploadRequest?) -> AWSTask { + guard let request = request else { + return AWSTask(error: NSError(domain: "AWSConnectParticipantAdapter", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Invalid request"])) + } + return participant.startAttachmentUpload(request) + } + + func completeAttachmentUpload(_ request: AWSConnectParticipantCompleteAttachmentUploadRequest?) -> AWSTask { + guard let request = request else { + return AWSTask(error: NSError(domain: "AWSConnectParticipantAdapter", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Invalid request"])) + } + return participant.completeAttachmentUpload(request) + } + + func getAttachment(_ request: AWSConnectParticipantGetAttachmentRequest?) -> AWSTask { + guard let request = request else { + return AWSTask(error: NSError(domain: "AWSConnectParticipantAdapter", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Invalid request"])) + } + return participant.getAttachment(request) + } } diff --git a/AmazonConnectChatIOS/Sources/Core/Network/MetricsManager.swift b/AmazonConnectChatIOS/Sources/Core/Network/MetricsManager.swift index f9bf719..974f207 100644 --- a/AmazonConnectChatIOS/Sources/Core/Network/MetricsManager.swift +++ b/AmazonConnectChatIOS/Sources/Core/Network/MetricsManager.swift @@ -4,7 +4,7 @@ import Foundation class MetricsManager { - private let httpClient = DefaultHttpClient() + var apiClient: APIClientProtocol = APIClient.shared private let endpointUrl: String private var timer: Timer? private var metricList: [Metric] @@ -31,7 +31,7 @@ class MetricsManager { DispatchQueue.main.async { self.timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in if !self.metricList.isEmpty { - self.sendMetrics() { result in + self.apiClient.sendMetrics(metricsEndpoint: self.endpointUrl, metricList: self.metricList) { result in switch result { case .success: self.metricList = [] @@ -56,15 +56,6 @@ class MetricsManager { } } - func sendMetrics(completion: @escaping (Result) -> Void) -> Void { - let body = CreateMetricRequestBody(metricList: self.metricList, metricNamespace: "chat-widget") - self.httpClient.postJson(self.endpointUrl, nil, body) { (data : PutMetricsResponse) in - completion(.success(data)) - } _: { error in - completion(.failure(error)) - } - } - private func getCountMetricDimensions() -> [Dimension] { let countMetricDimensions = [ Dimension(name: "WidgetType", value: "MobileChatSDK"), diff --git a/AmazonConnectChatIOS/Sources/Core/Network/WebSocketManager.swift b/AmazonConnectChatIOS/Sources/Core/Network/WebSocketManager.swift index 91420be..81dedd8 100644 --- a/AmazonConnectChatIOS/Sources/Core/Network/WebSocketManager.swift +++ b/AmazonConnectChatIOS/Sources/Core/Network/WebSocketManager.swift @@ -163,26 +163,48 @@ class WebsocketManager: NSObject, WebsocketManagerProtocol { } func processJsonContentAndGetItem(_ json: [String: Any]) -> TranscriptItem? { - guard let content = json["content"] as? String, - let innerJson = try? JSONSerialization.jsonObject(with: Data(content.utf8), options: []) as? [String: Any] else { - return nil - } + let content = json["content"] as? String + + if let stringContent = content, + let innerJson = try? JSONSerialization.jsonObject(with: Data(stringContent.utf8), options: []) as? [String: Any] { + guard let typeString = innerJson["Type"] as? String, let type = WebSocketMessageType(rawValue: typeString) else { + print("Unknown websocket message type: \(String(describing: innerJson["Type"]))") + return nil + } + let time = CommonUtils().formatTime(innerJson["AbsoluteTime"] as! String)! + switch type { + case .message: + return self.handleMessage(innerJson, json) + case .event: + guard let eventTypeString = innerJson["ContentType"] as? String, let eventType = ContentType(rawValue: eventTypeString) else { + print("Unknown event type \(String(describing: innerJson["ContentType"]))") + return nil + } - let type = innerJson["Type"] as! String // MESSAGE, EVENT - if type == "MESSAGE" { - return handleMessage(innerJson, json) - } else if innerJson["ContentType"] as! String == ContentType.joined.rawValue { - return handleParticipantJoined(innerJson, json) - } else if innerJson["ContentType"] as! String == ContentType.left.rawValue { - return handleParticipantLeft(innerJson, json) - } else if innerJson["ContentType"] as! String == ContentType.typing.rawValue { - return handleTyping(innerJson, json) - } else if innerJson["ContentType"] as! String == ContentType.ended.rawValue { - return handleChatEnded(innerJson, json) - } else if innerJson["ContentType"] as! String == ContentType.metaData.rawValue { - return handleMetadata(innerJson, json) + switch eventType { + case .joined: + // Handle participant joined event + return handleParticipantJoined(innerJson, json) + case .left: + // Handle participant left event + return handleParticipantLeft(innerJson, json) + case .typing: + // Handle typing event + return handleTyping(innerJson, json) + case .ended: + // Handle chat ended event + return handleChatEnded(innerJson, json) + default: + print("Unknown event: \(String(describing: eventType))") + } + case .attachment: + return handleAttachment(innerJson, json) + case .messageMetadata: + return handleMetadata(innerJson, json) + } + + return nil } - return nil } @@ -396,6 +418,39 @@ extension WebsocketManager { ) } + func handleAttachment(_ innerJson: [String: Any], _ serializedContent: [String: Any]) -> TranscriptItem? { + let participantRole = innerJson["ParticipantRole"] as! String + let messageId = innerJson["Id"] as! String + let time = CommonUtils().formatTime(innerJson["AbsoluteTime"] as! String)! + + var attachmentName: String? = nil + var contentType: String? = nil + var attachmentId: String? = nil + if let attachmentsArray = innerJson["Attachments"] as? [[String: Any]], + let firstAttachment = attachmentsArray.first, + let firstAttachmentName = firstAttachment["AttachmentName"] as? String, + let firstAttachmentContentType = firstAttachment["ContentType"] as? String, + let firstAttachmentId = firstAttachment["AttachmentId"] as? String { + attachmentName = firstAttachmentName + contentType = firstAttachmentContentType + attachmentId = firstAttachmentId + } else { + print("Failed to access attachments") + return nil + } + + let message = Message( + participant: participantRole, + text: attachmentName!, + contentType: contentType!, + timeStamp: time, + attachmentId: attachmentId, + messageID: messageId, + serializedContent: serializedContent + ) + return message + } + func handleParticipantJoined(_ innerJson: [String: Any], _ serializedContent: [String: Any]) -> TranscriptItem? { let participantRole = innerJson["ParticipantRole"] as! String let time = CommonUtils().formatTime(innerJson["AbsoluteTime"] as! String)! diff --git a/AmazonConnectChatIOS/Sources/Core/Service/ChatService.swift b/AmazonConnectChatIOS/Sources/Core/Service/ChatService.swift index b474913..85cf80a 100644 --- a/AmazonConnectChatIOS/Sources/Core/Service/ChatService.swift +++ b/AmazonConnectChatIOS/Sources/Core/Service/ChatService.swift @@ -4,12 +4,16 @@ import Foundation import Combine import AWSConnectParticipant +import UniformTypeIdentifiers protocol ChatServiceProtocol { func createChatSession(chatDetails: ChatDetails, completion: @escaping (Bool, Error?) -> Void) func disconnectChatSession(completion: @escaping (Bool, Error?) -> Void) func sendMessage(contentType: ContentType, message: String, completion: @escaping (Bool, Error?) -> Void) func sendEvent(event: ContentType, content: String?, completion: @escaping (Bool, Error?) -> Void) + func sendAttachment(file: URL, completion: @escaping (Bool, Error?) -> Void) + func downloadAttachment(attachmentId: String, filename: String, completion: @escaping (Result) -> Void) + func getAttachmentDownloadUrl(attachmentId: String, completion: @escaping (Result) -> Void) func subscribeToEvents(handleEvent: @escaping (ChatEvent) -> Void) -> AnyCancellable func subscribeToTranscriptItem(handleTranscriptItem: @escaping (TranscriptItem) -> Void) -> AnyCancellable func subscribeToTranscriptList(handleTranscriptList: @escaping ([TranscriptItem]) -> Void) -> AnyCancellable @@ -20,6 +24,8 @@ class ChatService : ChatServiceProtocol { var eventPublisher = PassthroughSubject() var transcriptItemPublisher = PassthroughSubject() var transcriptListPublisher = CurrentValueSubject<[TranscriptItem], Never>([]) + var urlSession = URLSession(configuration: .default) + var apiClient: APIClientProtocol = APIClient.shared private var eventCancellables = Set() private var transcriptItemCancellables = Set() private var transcriptListCancellables = Set() @@ -27,17 +33,16 @@ class ChatService : ChatServiceProtocol { private var awsClient: AWSClientProtocol private var websocketManager: WebsocketManagerProtocol? private var websocketManagerFactory: (URL) -> WebsocketManagerProtocol - + init(awsClient: AWSClientProtocol = AWSClient.shared, - connectionDetailsProvider: ConnectionDetailsProviderProtocol = ConnectionDetailsProvider.shared, - websocketManagerFactory: @escaping (URL) -> WebsocketManagerProtocol = { WebsocketManager(wsUrl: $0) }) { + connectionDetailsProvider: ConnectionDetailsProviderProtocol = ConnectionDetailsProvider.shared, + websocketManagerFactory: @escaping (URL) -> WebsocketManagerProtocol = { WebsocketManager(wsUrl: $0) }) { self.awsClient = awsClient self.connectionDetailsProvider = connectionDetailsProvider self.websocketManagerFactory = websocketManagerFactory self.registerNotificationListeners() } - - + func createChatSession(chatDetails: ChatDetails, completion: @escaping (Bool, Error?) -> Void) { self.connectionDetailsProvider.updateChatDetails(newDetails: chatDetails) awsClient.createParticipantConnection(participantToken: chatDetails.participantToken) { result in @@ -170,6 +175,172 @@ class ChatService : ChatServiceProtocol { } } + func sendAttachment(file: URL, completion: @escaping(Bool, Error?) -> Void) { + var mimeType: String? + var fileSize: Int? + + if let typeIdentifier = UTType(filenameExtension: file.pathExtension), + let mime = typeIdentifier.preferredMIMEType { + if AttachmentTypes(rawValue: mime) != nil { + mimeType = mime + } else { + let error = NSError(domain: "ChatService", code: -1, userInfo: [NSLocalizedDescriptionKey: "\(mime) is not a supported file type"]) + completion(false, error) + return + } + } else { + let error = NSError(domain: "ChatService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not parse MIME type from file URL"]) + completion(false, error) + return + } + + let fileName = file.lastPathComponent + + if let fileSizeValue = try? file.resourceValues(forKeys: [.fileSizeKey]).fileSize { + fileSize = fileSizeValue + } else { + let error = NSError(domain: "ChatService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not get valid file size"]) + completion(false, error) + return + } + + self.startAttachmentUpload(contentType: mimeType!, attachmentName: fileName, attachmentSizeInBytes: fileSize!) { result in + switch result { + case .success(let response): + self.apiClient.uploadAttachment(file: file, response: response) { success, error in + if success { + self.completeAttachmentUpload(attachmentIds: [response.attachmentId!]) { success, error in + if success { + completion(true, nil) + } else { + completion(false, error) + } + } + } else if error != nil { + print("Attachment upload failed: \(String(describing: error))") + } else { + print("Attachment upload failed") + } + } + case .failure(let error): + completion(false, error) + } + } + } + + func startAttachmentUpload(contentType: String, attachmentName: String, attachmentSizeInBytes: Int, completion: @escaping (Result) -> Void) { + guard let connectionDetails = connectionDetailsProvider.getConnectionDetails() else { + completion(.failure(NSError())) + return + } + + awsClient.startAttachmentUpload(connectionToken: connectionDetails.connectionToken!, contentType: contentType, attachmentName: attachmentName, attachmentSizeInBytes: attachmentSizeInBytes) { result in + switch result { + case .success(let response): + completion(.success(response)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func completeAttachmentUpload(attachmentIds: [String], completion: @escaping (Bool, Error?) -> Void) { + guard let connectionDetails = connectionDetailsProvider.getConnectionDetails() else { + completion(false, NSError()) + return + } + + awsClient.completeAttachmentUpload(connectionToken: connectionDetails.connectionToken!, attachmentIds: attachmentIds) { result in + switch result { + case .success(_): + completion(true, nil) + case .failure(let error): + print("Complete attachmentUpload failed: \(String(describing: error))") + completion(false, error) + } + } + } + + func getAttachmentDownloadUrl(attachmentId: String, completion: @escaping (Result) -> Void) { + guard let connectionDetails = connectionDetailsProvider.getConnectionDetails() else { + completion(.failure(NSError())) + return + } + + awsClient.getAttachment(connectionToken: connectionDetails.connectionToken!, attachmentId: attachmentId) { result in + switch result { + case .success(let response): + if let url = URL(string: response.url!) { + completion(.success(url)) + } else { + completion(.failure(NSError())) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func downloadAttachment(attachmentId: String, filename: String, completion: @escaping (Result) -> Void) { + getAttachmentDownloadUrl(attachmentId: attachmentId) { result in + switch result { + case .success(let url): + self.downloadFile(url: url, filename: filename) { (localUrl, error) in + if let localUrl = localUrl { + print("File successfully downloaded to temporary directory") + completion(.success(localUrl)) + } else if let error = error { + print("Failed to download file: \(error.localizedDescription)") + completion(.failure(error)) + } + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func downloadFile(url: URL, filename: String, completion: @escaping (URL?, Error?) -> Void) { + let downloadTask = urlSession.downloadTask(with: url) { (tempLocalUrl, response, error) in + if let error = error { + print("Download error: \(error.localizedDescription)") + completion(nil, error) + return + } + + guard let tempLocalUrl = tempLocalUrl else { + print("No file found at URL") + completion(nil, NSError(domain: "ChatService", code: -1, userInfo: [NSLocalizedDescriptionKey: "No file found at URL"])) + return + } + + do { + let tempDirectory = FileManager.default.temporaryDirectory + let tempFilePathUrl = tempDirectory.appendingPathComponent(filename) + + + if FileManager.default.fileExists(atPath: tempFilePathUrl.path) { + do { + // Delete the existing file + try FileManager.default.removeItem(at: tempFilePathUrl) + print("Existing file deleted successfully.") + } catch { + print("Error deleting existing file: \(error)") + completion(nil, error) + return + } + } + + try FileManager.default.moveItem(at: tempLocalUrl, to: tempFilePathUrl) + completion(tempFilePathUrl, nil) + } catch let error { + completion(nil, error) + } + } + + downloadTask.resume() + } + func getTranscript( scanDirection: AWSConnectParticipantScanDirection? = nil, sortOrder: AWSConnectParticipantSortKey? = nil, diff --git a/AmazonConnectChatIOS/Sources/Core/Service/ChatSession.swift b/AmazonConnectChatIOS/Sources/Core/Service/ChatSession.swift index 830179b..a5d422d 100644 --- a/AmazonConnectChatIOS/Sources/Core/Service/ChatSession.swift +++ b/AmazonConnectChatIOS/Sources/Core/Service/ChatSession.swift @@ -14,6 +14,9 @@ public protocol ChatSessionProtocol { func sendEvent(event: ContentType, content: String, completion: @escaping (Result) -> Void) func sendReadReceipt(event: ContentType, messageId: String, completion: @escaping (Result) -> Void) func getTranscript(scanDirection: AWSConnectParticipantScanDirection?, sortOrder: AWSConnectParticipantSortKey?, maxResults: NSNumber?, nextToken: String?, startPosition: AWSConnectParticipantStartPosition?, completion: @escaping (Result) -> Void) + func sendAttachment(file: URL, completion: @escaping (Result) -> Void) + func downloadAttachment(attachmentId: String, filename: String, completion: @escaping (Result) -> Void) + func getAttachmentDownloadUrl(attachmentId: String, completion: @escaping (Result) -> Void) var onConnectionEstablished: (() -> Void)? { get set } var onConnectionBroken: (() -> Void)? { get set } @@ -87,7 +90,7 @@ public class ChatSession: ChatSessionProtocol { /// Attempts to connect to a chat session with the given details. public func connect(chatDetails: ChatDetails, completion: @escaping (Result) -> Void) { reestablishSubscriptions() // Re-establish subscriptions whenever a new chat session is initiated - chatService.createChatSession(chatDetails: chatDetails) { [weak self] success, error in + chatService.createChatSession(chatDetails: chatDetails) { success, error in DispatchQueue.main.async { if success { SDKLogger.logger.logDebug("Chat session successfully created.") @@ -180,6 +183,48 @@ public class ChatSession: ChatSessionProtocol { } } + /// Sends an attachment within the chat session. + public func sendAttachment(file: URL, completion: @escaping (Result) -> Void) { + chatService.sendAttachment(file: file) { success, error in + DispatchQueue.main.async { + if let error = error { + SDKLogger.logger.logError("Error sending attachment: \(error.localizedDescription )") + completion(.failure(error)) + } else { + completion(.success(())) + } + } + } + } + + /// Downloads an attachment to the app's temporary directory given an attachment ID. + public func downloadAttachment(attachmentId: String, filename: String, completion: @escaping (Result) -> Void) { + chatService.downloadAttachment(attachmentId: attachmentId, filename: filename) { result in + switch result { + case .success(let localUrl): + SDKLogger.logger.logDebug("File successfully downloaded to temporary directory") + completion(.success(localUrl)) + case .failure(let error): + SDKLogger.logger.logError("Failed to download attachment: \(error.localizedDescription )") + completion(.failure(error)) + } + } + } + + /// Returns the download URL link for the given attachment ID + public func getAttachmentDownloadUrl(attachmentId: String, completion: @escaping (Result) -> Void) { + chatService.getAttachmentDownloadUrl(attachmentId: attachmentId) { result in + switch result { + case .success(let localUrl): + SDKLogger.logger.logDebug("Attachment download url successfully retrieved") + completion(.success(localUrl)) + case .failure(let error): + SDKLogger.logger.logError("Failed to download attachment \(error.localizedDescription)") + completion(.failure(error)) + } + } + } + deinit { eventSubscription?.cancel() messageSubscription?.cancel() diff --git a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/DefaultHttpClient.swift b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/DefaultHttpClient.swift index 65fd73c..b198f90 100644 --- a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/DefaultHttpClient.swift +++ b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/DefaultHttpClient.swift @@ -5,7 +5,8 @@ import Foundation import os class DefaultHttpClient: HttpClient { - + static var shared: any HttpClient = DefaultHttpClient() + // Retryable http codes: https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html private let retryableHttpCodes: Set = [400, 403, 408, 429, 500, 502, 503, 504, 509] private let maxRetry = 2 @@ -63,6 +64,20 @@ class DefaultHttpClient: HttpClient { send(request, 0, onSuccess, onFailure) } + func putJson(_ urlString: String, + _ headers: HttpHeaders?, + _ body: B, + _ onSuccess: @escaping () -> Void, + _ onFailure: @escaping (_ error: Error) -> Void) { + + guard let request = createPutRequest(urlString, headers, body as? Data) else { + let error = NSError(domain: "aws.amazon.com", code: 400) + onFailure(error) + return + } + send(request, 0, onSuccess, onFailure) + } + func send(_ request: URLRequest, _ retryCount: Int, _ onSuccess: @escaping () -> Void, @@ -73,7 +88,7 @@ class DefaultHttpClient: HttpClient { onFailure(error) } } - + func send(_ request: URLRequest, _ retryCount: Int, _ onSuccess: @escaping (_ data: R) -> Void, @@ -91,8 +106,14 @@ class DefaultHttpClient: HttpClient { } else { // Fallback on earlier versions } - let decodedData = try decoder.decode(R.self, from: data) - onSuccess(decodedData) + if data.isEmpty { + let jsonData = "{}".data(using: .utf8)! + let emptyObject = try decoder.decode(R.self, from: jsonData) + onSuccess(emptyObject) + } else { + let decodedData = try decoder.decode(R.self, from: data) + onSuccess(decodedData) + } } catch { onFailure(error) } @@ -129,6 +150,7 @@ class DefaultHttpClient: HttpClient { onFailure(error) return } + print(data.base64EncodedString()) onSuccess(data) }.resume() } @@ -162,6 +184,7 @@ class DefaultHttpClient: HttpClient { } } + // TODO: Remove request printing // Print the entire request for debugging if let requestData = try? JSONSerialization.data(withJSONObject: request.allHTTPHeaderFields ?? [:], options: .prettyPrinted), let requestBody = String(data: requestData, encoding: .utf8) { @@ -179,6 +202,35 @@ class DefaultHttpClient: HttpClient { return request } + + + private func createPutRequest(_ urlString: String, + _ headers: HttpHeaders?, + _ body: Data?) -> URLRequest? { + guard let serviceUrl = URL(string: urlString) else { return nil } + + var request = URLRequest(url: serviceUrl) + request.httpMethod = HttpMethod.put.rawValue + + if let headers = headers { + for (headerKey, headerValue) in headers { + request.setValue(headerValue, forHTTPHeaderField: headerKey.rawValue) + } + } + + // TODO: Remove request printing + // Print the entire request for debugging + if let requestData = try? JSONSerialization.data(withJSONObject: request.allHTTPHeaderFields ?? [:], options: .prettyPrinted), + let requestBody = String(data: requestData, encoding: .utf8) { + print("Raw Request:") + print("URL: \(request.url?.absoluteString ?? "N/A")") + print("HTTP Method: \(request.httpMethod ?? "N/A")") + print("Headers:\n\(requestBody)") + } + + request.httpBody = body + return request + } } private extension Formatter { diff --git a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpClient.swift b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpClient.swift index 37b075e..d386182 100644 --- a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpClient.swift +++ b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpClient.swift @@ -6,7 +6,6 @@ import Foundation typealias HttpHeaders = [HttpHeader.Key: String] protocol HttpClient { - func getJson(_ urlString: String, _ onSuccess: @escaping (_ data: R) -> Void, _ onFailure: @escaping (_ error: Error) -> Void) @@ -28,4 +27,9 @@ protocol HttpClient { _ body: B, _ onSuccess: @escaping (_ data: R) -> Void, _ onFailure: @escaping (_ error: Error) -> Void) + func putJson(_ urlString: String, + _ headers: HttpHeaders?, + _ body: B, + _ onSuccess: @escaping () -> Void, + _ onFailure: @escaping (_ error: Error) -> Void) } diff --git a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpHeader.swift b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpHeader.swift index ff7acb1..93e504a 100644 --- a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpHeader.swift +++ b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpHeader.swift @@ -13,6 +13,13 @@ extension HttpHeader { case contentType = "Content-Type" case wafToken = "x-aws-waf-token" case amzBearer = "X-Amz-Bearer" + case amzMetaInitialContactId = "x-amz-meta-initial_contact_id" + case amzMetaContactId = "x-amz-meta-contact_id" + case amzMetaOrganizationId = "x-amz-meta-organization_id" + case amzAcl = "x-amz-acl" + case contentLength = "Content-Length" + case contentDisposition = "Content-Disposition" + case amzMetaAccountId = "x-amz-meta-account_id" } } diff --git a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpMethod.swift b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpMethod.swift index af0884d..85716c2 100644 --- a/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpMethod.swift +++ b/AmazonConnectChatIOS/Sources/Core/Utils/HttpClient/HttpMethod.swift @@ -4,5 +4,5 @@ import Foundation enum HttpMethod: String { - case get, post + case get, post, put }