Skip to content

Commit

Permalink
Merge pull request #209 from MyPureCloud/develop
Browse files Browse the repository at this point in the history
develop into main 2.5.0
### Additions:
- Implement Conversation Clear feature.
- Add application parameter to ws request.
- Add user agent with Platform and OS version details.
### Bug fix:
- Do not check DeploymentConfig.ConversationDisconnect status when Disconnect event received.
- Reorder sequence of handling Event.ConversationDisconnect. First do state transition and only then dispatch event.
### Documentation:
- Add/Update KDoc.
- SDK Documentation published in Developer Center
- Add Clear Conversation related diagrams.
### Test applications:
- Implement Clear Conversation on Android and iOS Testbed Applications.
### Testing:
- Add automation tests for Clear Conversation on Android and iOS.
- Add/Update unit tests.
- Refactor MessagingClientImplTest.kt
  • Loading branch information
AfanasievAnton authored Aug 3, 2023
2 parents 000263b + afeea80 commit cc6fcd9
Show file tree
Hide file tree
Showing 50 changed files with 2,351 additions and 1,686 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,19 @@ data class CallDetails(
fun API.waitForConversation(): Conversation? {
for (x in 0..60) {
val conversations = getAllConversations()
if (conversations.firstOrNull() != null) {
return conversations[0]
if (conversations != null) {
conversations.forEach { conversation ->
Log.i(TAG, "conversationId: $conversation.id")
val callDetails = conversation.getParticipantFromPurpose("agent")?.messages
if (callDetails != null) {
callDetails.forEach { callDetail ->
Log.i(TAG, "call detail state: $callDetail.state")
if (callDetail.isAlerting()) {
return conversation
}
}
}
}
}
sleep(1000)
}
Expand Down Expand Up @@ -162,3 +173,8 @@ fun API.disconnectAllConversations() {
sendConnectOrDisconnect(conversation)
}
}

fun API.checkForConversationMessages(conversationId: String) {
val listOfMessages = getConversationInfo(conversationId).getParticipantFromPurpose("agent")?.messages?.toList()
if (listOfMessages != null) AssertionError("Conversation still has messages associated with it but should not")
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.genesys.cloud.messenger.androidcomposeprototype.ui.testbed.TestBedVie
import com.genesys.cloud.messenger.transport.util.DefaultVault
import com.genesys.cloud.messenger.uitest.support.ApiHelper.API
import com.genesys.cloud.messenger.uitest.support.ApiHelper.answerNewConversation
import com.genesys.cloud.messenger.uitest.support.ApiHelper.checkForConversationMessages
import com.genesys.cloud.messenger.uitest.support.ApiHelper.disconnectAllConversations
import com.genesys.cloud.messenger.uitest.support.ApiHelper.sendConnectOrDisconnect
import com.genesys.cloud.messenger.uitest.support.ApiHelper.sendOutboundMessageFromAgentToUser
Expand Down Expand Up @@ -75,6 +76,9 @@ class ComposePrototypeUITest : BaseTests() {
private val fakeAuthUserName = "daffy.duck@looneytunes.com"
private val fakeAuthPassword = "xxxxxxxxxx"
private val TAG = TestBedViewModel::class.simpleName
private val clearConversation = "clearConversation"
private val connectionClosedMessage = "Connection Closed Normally"
private val connectionClosedCode = "1000"

fun enterDeploymentInfo(deploymentId: String) {
opening {
Expand Down Expand Up @@ -252,6 +256,15 @@ class ComposePrototypeUITest : BaseTests() {
}
}

fun clearConversation() {
messenger {
verifyPageIsVisible()
enterCommand(clearConversation)
waitForProperResponse(connectionClosedMessage)
waitForProperResponse(connectionClosedCode)
}
}

@Test
fun testSendTypingIndicator() {
apiHelper.disconnectAllConversations()
Expand Down Expand Up @@ -514,4 +527,38 @@ class ComposePrototypeUITest : BaseTests() {
oktaSignInWithPKCE(fakeAuthUserName, fakeAuthPassword, false)
verifyNotAuthenticated(notAuthenticateText)
}

@Test
fun testConversationClear() {
apiHelper.disconnectAllConversations()
enterDeploymentInfo(testConfig.deploymentId)
connect()
sendMsg(helloText)
val conversationInfo = apiHelper.answerNewConversation()
if (conversationInfo == null) AssertionError("Unable to answer conversation.")
else {
Log.i(TAG, "Conversation started successfully.")
// Test case 1: Send clear conversation command and check for connection closed and conversation cleared
clearConversation()
// Test case 2: After clearing conversation and disconnecting, connect again and check if conversation is a new session and conversation ids are the same
connect()
// Since the ConversationCleared event does not appear long enough in the Compose Prototype, we will check to verify there are no messages for the cleared conversation
apiHelper.checkForConversationMessages(conversationInfo.id)
verifyResponse(autoStartEnabledText)
sendMsg(helloText)
val conversationInfo2 = apiHelper.answerNewConversation()
if (conversationInfo2 == null) AssertionError("Unable to answer second conversation.")
else {
Log.i(TAG, "Second Conversation started successfully.")
if (conversationInfo.id == conversationInfo2.id) AssertionError("The conversation ids are the same after a conversation clear but should not be.")
apiHelper.sendConnectOrDisconnect(conversationInfo2)
// wait for agent to disconnect
apiHelper.waitForParticipantToConnectOrDisconnect(conversationInfo2.id)
}
apiHelper.sendConnectOrDisconnect(conversationInfo)
// wait for agent to disconnect
apiHelper.waitForParticipantToConnectOrDisconnect(conversationInfo.id)
}
bye()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fun TestBedContent(
style = typography.h5
)
Text(
"Commands: oktaSignIn, oktaSignInWithPKCE, oktaLogout, connect, connectAuthenticated, newChat, send <msg>, history, clearConversation, attach, detach, delete <attachmentId> , deployment, bye, healthCheck, addAttribute <key> <value>, typing, authorize",
"Commands: oktaSignIn, oktaSignInWithPKCE, oktaLogout, connect, connectAuthenticated, newChat, send <msg>, history, invalidateConversationCache, attach, detach, delete <attachmentId> , deployment, bye, healthCheck, addAttribute <key> <value>, typing, authorize, clearConversation",
style = typography.caption,
softWrap = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,15 @@ class TestBedViewModel : ViewModel(), CoroutineScope {
"attach" -> doAttach()
"detach" -> doDetach(input)
"deployment" -> doDeployment()
"clearConversation" -> doClearConversation()
"invalidateConversationCache" -> doInvalidateConversationCache()
"addAttribute" -> doAddCustomAttributes(input)
"typing" -> doIndicateTyping()
"newChat" -> doStartNewChat()
"oktaSignIn" -> doOktaSignIn(false)
"oktaSignInWithPKCE" -> doOktaSignIn(true)
"oktaLogout" -> logoutFromOktaSession()
"authorize" -> doAuthorize()
"clearConversation" -> doClearConversation()
else -> {
Log.e(TAG, "Invalid command")
commandWaiting = false
Expand Down Expand Up @@ -257,6 +258,14 @@ class TestBedViewModel : ViewModel(), CoroutineScope {
}

private fun doClearConversation() {
try {
client.clearConversation()
} catch (t: Throwable) {
handleException(t, "clearConversation")
}
}

private fun doInvalidateConversationCache() {
client.invalidateConversationCache()
clearCommand()
}
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ plugins {
}

// CocoaPods requires the podspec to have a `version`.
val buildVersion = "2.4.1"
val buildVersion = "2.5.0"
val snapshot = System.getenv("SNAPSHOT_BUILD") ?: ""
version = "${buildVersion}${snapshot}"
group = "cloud.genesys"
Expand Down
4 changes: 2 additions & 2 deletions iosApp/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- transport (2.4.1)
- transport (2.5.0)

DEPENDENCIES:
- transport (from `../transport`)
Expand All @@ -9,7 +9,7 @@ EXTERNAL SOURCES:
:path: "../transport"

SPEC CHECKSUMS:
transport: 0068e821988a5eb502bc2a39ed2d0d66d9df0fd3
transport: 400f4907b202f3b56ec71ced91bf59b4d2bacbe8

PODFILE CHECKSUM: 10743e43aeaf72bb49673a0e57360c028906ace5

Expand Down
13 changes: 11 additions & 2 deletions iosApp/iosApp/MessengerInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ final class MessengerInteractor {
func fetchDeployment(completion: @escaping (DeploymentConfig?, Error?) -> Void) {
messengerTransport.fetchDeploymentConfig(completionHandler: completion)
}

func clearConversation() {
func invalidateConversationCache() {
messagingClient.invalidateConversationCache()
}

Expand All @@ -156,4 +156,13 @@ final class MessengerInteractor {
throw error
}
}

func clearConversation() throws {
do {
try messagingClient.clearConversation()
} catch {
print("clearConversation() failed. \(error.localizedDescription)")
throw error
}
}
}
9 changes: 6 additions & 3 deletions iosApp/iosApp/TestbedViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ class TestbedViewController: UIViewController {
case deployment
case bye
case healthCheck
case clearConversation
case invalidateConversationCache
case addAttribute
case typing
case authorize
case clearConversation

var helpDescription: String {
switch self {
Expand Down Expand Up @@ -418,8 +419,8 @@ extension TestbedViewController : UITextFieldDelegate {
}
self.info.text = "<\(deploymentConfig?.description() ?? "Unknown deployment config")>"
}
case (.clearConversation, _):
messenger.clearConversation()
case (.invalidateConversationCache, _):
messenger.invalidateConversationCache()
case(.addAttribute, let msg?):
let segments = segmentUserInput(msg)
if let key = segments.0, !key.isEmpty {
Expand Down Expand Up @@ -452,6 +453,8 @@ extension TestbedViewController : UITextFieldDelegate {
}

messenger.authorize(authCode: self.authCode ?? "", redirectUri: signInRedirectURI, codeVerifier: codeVerifier)
case (.clearConversation, _):
try messenger.clearConversation()
default:
self.info.text = "Invalid command"
}
Expand Down
19 changes: 19 additions & 0 deletions iosApp/iosAppTests/Support/MessengerInteractorTester.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class MessengerInteractorTester {
var errorExpectation: XCTestExpectation?
var disconnectedSession: XCTestExpectation?
var connectionClosed: XCTestExpectation?
var closedStateChange: XCTestExpectation?
var conversationCleared: XCTestExpectation?
var authExpectation: XCTestExpectation?
var receivedMessageText: String? = nil
var receivedDownloadUrl: String? = nil
Expand Down Expand Up @@ -50,6 +52,7 @@ class MessengerInteractorTester {
self?.readOnlyStateExpectation?.fulfill()
case _ as MessagingClientState.Closed:
self?.testExpectation?.fulfill()
self?.closedStateChange?.fulfill()
case let error as MessagingClientState.Error:
print("Socket <error>. code: <\(error.code.description)> , message: <\(error.message ?? "No message")>")
self?.errorExpectation?.fulfill()
Expand Down Expand Up @@ -137,6 +140,9 @@ class MessengerInteractorTester {
print("Auth event: \(loggedOut.description)")
self?.authState = AuthState.loggedOut
self?.authExpectation?.fulfill()
case let cleared as Event.ConversationCleared:
print("Conversation cleared event: \(cleared.description)")
self?.conversationCleared?.fulfill()
case let error as Event.Error:
print("Error Event: \(error.description())")
self?.errorExpectation?.fulfill()
Expand Down Expand Up @@ -373,6 +379,19 @@ class MessengerInteractorTester {
receivedMessageText = nil
}

func clearConversation() {
connectionClosed = XCTestExpectation(description: "Wait for connection to be closed.")
closedStateChange = XCTestExpectation(description: "Wait for the connection state to be closed.")
conversationCleared = XCTestExpectation(description: "Wait for the conversation cleared event to be received.")
do {
try messenger.clearConversation()
} catch {
XCTFail(error.localizedDescription)
}
let result = XCTWaiter().wait(for: [connectionClosed!, closedStateChange!, conversationCleared!], timeout: 30)
XCTAssertTrue(result == .completed, "The Clear Conversation command may have had an error, or the expected state changes didn't happen.")
}

}

public enum AuthState {
Expand Down
48 changes: 47 additions & 1 deletion iosApp/iosAppTests/iosAppTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class iosAppTests: XCTestCase {
return
}
let deployment = try! Deployment()
let testController = MessengerInteractorTester(deployment: Deployment(deploymentId: config.authDeploymentId, domain: deployment.domain))
var testController = MessengerInteractorTester(deployment: Deployment(deploymentId: config.authDeploymentId, domain: deployment.domain))
testController.authorize(config: config, authCode: config.authCode)
testController.startNewMessengerConnection(authorized: true)
testController.sendText(text: "Testing from E2E test.")
Expand All @@ -65,6 +65,12 @@ class iosAppTests: XCTestCase {
// Disconnect. Ensure that auth logs out correctly.
testController.authLogout()

// Temporary fix for MTSDK-222.
let newToken = UUID().uuidString
testController.messenger.tokenVault.store(key: "token", value: newToken)
print("New token: \(newToken)")
testController = MessengerInteractorTester(deployment: Deployment(deploymentId: config.authDeploymentId, domain: deployment.domain))

// With the same test controller, authenticate with a different user's auth code.
// Ensure that we can answer a new conversation.
testController.authorize(config: config, authCode: config.authCode2)
Expand Down Expand Up @@ -222,6 +228,46 @@ class iosAppTests: XCTestCase {
}
}

func testConversationClear() {
// Setup the session. Send a message.
guard let messengerTester = messengerTester else {
XCTFail("Failed to setup the Messenger tester.")
return
}

messengerTester.startNewMessengerConnection()
messengerTester.sendText(text: "Testing from E2E test.")

// Use the public API to answer the new Messenger conversation.
guard let conversationInfo = ApiHelper.shared.answerNewConversation() else {
XCTFail("The message we sent may not have connected to an agent.")
return
}

// Test case 1: After sending Conversation Clear from client. Ensure we receive Event.ConversationCleared and Event.ConnectionClosed from Transport.
// Client state should also be set to Closed.
messengerTester.clearConversation()
XCTAssertEqual(messengerTester.currentClientState, .Closed(code: 1000, reason: "The user has closed the connection."), "The client state did not close for the expected reason.")

// Test case 2: While disconnected after a cleared conversation. Start a new conversation. Ensure that this conversation is considered a new session.
messengerTester.startNewMessengerConnection()
if let configuredState = messengerTester.currentClientState as? MessagingClientState.Configured {
XCTAssertTrue(configuredState.newSession, "The configured session is not considered a new session.")
} else {
XCTFail("Unable to confirm details about the configured state \(messengerTester.currentClientState?.description ?? "N/A").")
}
guard let conversationInfo2 = ApiHelper.shared.answerNewConversation() else {
XCTFail("The message we sent may not have connected to an agent.")
return
}
XCTAssertNotEqual(conversationInfo.conversationId, conversationInfo2.conversationId, "The new conversationID is the same as the previous conversation's. This conversation should be new after the previous one was cleared.")

// Disconnect agent from all conversations.
ApiHelper.shared.sendConnectOrDisconnect(conversationInfo: conversationInfo, connecting: false, wrapup: true)
ApiHelper.shared.sendConnectOrDisconnect(conversationInfo: conversationInfo2, connecting: false, wrapup: true)
messengerTester.disconnectMessenger()
}

func testDisconnectAgent_ReadOnly() {
// Setup the session. Send a message.
guard let messengerTester = messengerTester else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.genesys.cloud.messenger.transport.network

import com.genesys.cloud.messenger.transport.core.ErrorCode
import com.genesys.cloud.messenger.transport.core.ErrorMessage
import com.genesys.cloud.messenger.transport.util.Platform
import com.genesys.cloud.messenger.transport.util.logs.Log
import com.genesys.cloud.messenger.transport.util.logs.okHttpLogger
import io.ktor.http.Url
Expand All @@ -23,7 +24,11 @@ internal actual class PlatformSocket actual constructor(
actual fun openSocket(listener: PlatformSocketListener) {
this.listener = listener
val socketRequest =
Request.Builder().url(url.toString()).header(name = "Origin", value = url.host).build()
Request.Builder()
.url(url.toString())
.header(name = "Origin", value = url.host)
.header(name = "User-Agent", Platform().platform)
.build()
val webClient = OkHttpClient()
.newBuilder()
.pingInterval(pingInterval.toLong(), TimeUnit.SECONDS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal class ErrorCodeTest {
assertTrue(ErrorCode.mapFrom(6002) is ErrorCode.AuthLogoutFailed)
assertTrue(ErrorCode.mapFrom(6003) is ErrorCode.RefreshAuthTokenFailure)
assertTrue(ErrorCode.mapFrom(6004) is ErrorCode.HistoryFetchFailure)
assertTrue(ErrorCode.mapFrom(6005) is ErrorCode.ClearConversationFailure)
val randomIn300Range = Random.nextInt(300, 400)
assertEquals(
ErrorCode.mapFrom(randomIn300Range),
Expand Down
Loading

0 comments on commit cc6fcd9

Please sign in to comment.