Skip to content

Commit

Permalink
Feature/mtsdk 171 clear/delete conversation (#207)
Browse files Browse the repository at this point in the history
Additions:
- Implement Conversation Clear feature.

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 Authenticated Messaging on Android and iOS.
- Add/Update unit tests.

---------

Co-authored-by: Matthew Morehouse <94386514+mmorehou@users.noreply.github.com>
Co-authored-by: betsyknoke <37122956+betsyknoke@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 3, 2023
1 parent a75f9ce commit 4d5378b
Show file tree
Hide file tree
Showing 28 changed files with 569 additions and 29 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
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 @@ -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 4d5378b

Please sign in to comment.