From 4e7509579ef17804113deb7331414d136503a93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar=20Dias?= Date: Wed, 6 Dec 2023 15:05:58 +0100 Subject: [PATCH] feat(monkeys): Interact with "external" users from the test [WPB-5129] (#2219) --- monkeys/schema.json | 122 ++++++++++++-- .../wire/kalium/monkeys/ActionScheduler.kt | 7 +- .../wire/kalium/monkeys/MonkeyApplication.kt | 20 +-- .../com/wire/kalium/monkeys/actions/Action.kt | 2 + .../actions/HandleExternalRequestAction.kt | 65 ++++++++ .../actions/LeaveConversationAction.kt | 2 +- .../actions/SendExternalRequestAction.kt | 32 ++++ .../monkeys/actions/SendMessageAction.kt | 96 ++++++++++- .../kalium/monkeys/conversation/Monkey.kt | 115 +++++++++---- .../conversation/MonkeyConversation.kt | 2 +- .../com/wire/kalium/monkeys/homeDirectory.kt | 5 +- .../wire/kalium/monkeys/importer/TestData.kt | 28 +++- .../monkeys/importer/TestDataImporter.kt | 155 +++++++++--------- .../wire/kalium/monkeys/importer/UserData.kt | 33 +++- .../kalium/monkeys/pool/ConversationPool.kt | 8 +- .../wire/kalium/monkeys/pool/MonkeyPool.kt | 62 +++++-- .../AddUserToConversationActionTest.kt | 41 ----- .../actions/LeaveConversationActionTest.kt | 61 ------- .../kalium/monkeys/actions/LoginActionTest.kt | 43 ----- .../monkeys/actions/SendMessageActionTest.kt | 105 ------------ .../monkeys/actions/SendRequestActionTest.kt | 51 ------ 21 files changed, 578 insertions(+), 477 deletions(-) create mode 100644 monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/HandleExternalRequestAction.kt create mode 100644 monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendExternalRequestAction.kt delete mode 100644 monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/AddUserToConversationActionTest.kt delete mode 100644 monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationActionTest.kt delete mode 100644 monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LoginActionTest.kt delete mode 100644 monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendMessageActionTest.kt delete mode 100644 monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendRequestActionTest.kt diff --git a/monkeys/schema.json b/monkeys/schema.json index 2511922f4eb..44cce233a07 100644 --- a/monkeys/schema.json +++ b/monkeys/schema.json @@ -145,23 +145,56 @@ "description": "Should the application dump the users created into a file", "type": "boolean" }, - "users": { - "description": "All users that are going to be used in the test. If set the parameters to create users will be ignored", - "type": "array", - "items": { - "type": "object", - "required": [ - "email", - "id" - ], - "properties": { - "email": { - "description": "Email of the user", - "type": "string" - }, - "id": { - "description": "UUID of the user", - "type": "string" + "presetTeam": { + "description": "Team configuration and its users to use instead of creating them", + "type": "object", + "required": [ + "id", + "owner", + "users" + ], + "properties": { + "id": { + "description": "The team id", + "type": "string" + }, + "owner": { + "description": "The creator of the group. It will be used to fetch the team's users, so it must have permission in the API to do so", + "type": "object", + "required": [ + "email", + "id" + ], + "properties": { + "email": { + "description": "Email of the user", + "type": "string" + }, + "id": { + "description": "UUID of the user", + "type": "string" + } + } + }, + "users": { + "description": "All users that are going to be used in the test. If set the parameters to create users will be ignored", + "type": "array", + "items": { + "type": "object", + "required": [ + "email", + "id" + ], + "properties": { + "email": { + "description": "Email of the user", + "type": "string" + }, + "id": { + "description": "UUID of the user", + "type": "string" + } + } } } } @@ -225,7 +258,9 @@ "ADD_USERS_TO_CONVERSATION", "LEAVE_CONVERSATION", "DESTROY_CONVERSATION", - "SEND_REQUEST" + "SEND_REQUEST", + "HANDLE_EXTERNAL_REQUEST", + "SEND_EXTERNAL_REQUEST" ] } }, @@ -254,6 +289,12 @@ }, { "$ref": "#/$defs/SendRequest" + }, + { + "$ref": "#/$defs/SendExternalRequest" + }, + { + "$ref": "#/$defs/HandleExternalRequest" } ] } @@ -426,6 +467,51 @@ "type": "boolean" } } + }, + "SendExternalRequest": { + "description": "Picks a random user and send a connection request to an user not on the scope of the monkeys", + "type": "object", + "required": [ + "userCount", + "originTeam", + "targetTeam" + ], + "properties": { + "userCount": { + "description": "How many users should send requests", + "$ref": "#/$defs/UserCount" + }, + "originTeam": { + "description": "Users from which team should the requests originate", + "type": "string" + }, + "targetTeam": { + "description": "Users from which team should be targeted", + "type": "string" + } + } + }, + "HandleExternalRequest": { + "description": "Searches for a monkey with pending connection requests and accepts or rejects it. At this moment it considers also connection requests from other monkeys", + "type": "object", + "required": [ + "userCount", + "shouldAccept" + ], + "properties": { + "userCount": { + "description": "How many users should handle requests", + "$ref": "#/$defs/UserCount" + }, + "shouldAccept": { + "description": "If the request should be accepted or not", + "type": "boolean" + }, + "greetMessage": { + "description": "If accepted, this custom message will be sent to the other user. If not informed a random message will be sent.", + "type": "string" + } + } } } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/ActionScheduler.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/ActionScheduler.kt index e42560b3948..9e6a39d230c 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/ActionScheduler.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/ActionScheduler.kt @@ -37,8 +37,8 @@ object ActionScheduler { suspend fun start(testCase: String, actions: List, coreLogic: CoreLogic, monkeyPool: MonkeyPool) { actions.forEach { actionConfig -> CoroutineScope(Dispatchers.Default).launch { - while (this.isActive) { - val actionName = actionConfig.type::class.serializer().descriptor.serialName + val actionName = actionConfig.type::class.serializer().descriptor.serialName + do { val tags = listOf(Tag.of("testCase", testCase)) try { logger.i("Running action $actionName: ${actionConfig.description} ${actionConfig.count} times") @@ -58,7 +58,8 @@ object ActionScheduler { MetricsCollector.count("c_errors", tags.plusElement(Tag.of("action", actionName))) } delay(actionConfig.repeatInterval.toLong()) - } + } while (this.isActive && actionConfig.repeatInterval > 0u) + logger.i("Task for action $actionName finished") } } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/MonkeyApplication.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/MonkeyApplication.kt index 5c46102d01b..d8fe8ea6a4b 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/MonkeyApplication.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/MonkeyApplication.kt @@ -62,7 +62,7 @@ class MonkeyApplication : CliktCommand(allowMultipleSubcommands = true) { if (logOutputFile != null) { CoreLogger.init(KaliumLogger.Config(logLevel, listOf(fileLogger))) } else { - CoreLogger.init(KaliumLogger.Config(logLevel, emptyList())) + CoreLogger.init(KaliumLogger.Config(logLevel)) } MonkeyLogger.init(KaliumLogger.Config(logLevel, listOf(monkeyFileLogger))) logger.i("Initializing Metrics Endpoint") @@ -81,22 +81,20 @@ class MonkeyApplication : CliktCommand(allowMultipleSubcommands = true) { private suspend fun runMonkeys( testData: TestData - ) = with(testData) { + ) { val users = TestDataImporter.generateUserData(testData) - testData.testCases.forEachIndexed { index, testCase -> - val monkeyPool = MonkeyPool(users, testCase.name) + return testData.testCases.forEachIndexed { index, testCase -> val coreLogic = coreLogic("$HOME_DIRECTORY/.kalium/${testCase.name.replace(' ', '_')}") - // the first one creates the preset groups + logger.i("Logging in and out all users to create key packages") + val monkeyPool = MonkeyPool(users, testCase.name) + // the first one creates the preset groups and logs everyone in so keypackages are created if (index == 0) { + logger.i("Creating initial key packages for clients (logging everyone in and out). This can take a while...") + monkeyPool.warmUp(coreLogic) logger.i("Creating prefixed groups") testData.conversationDistribution.forEach { (prefix, config) -> ConversationPool.createPrefixedConversations( - coreLogic, - prefix, - config.groupCount, - config.userCount, - config.protocol, - monkeyPool + coreLogic, prefix, config.groupCount, config.userCount, config.protocol, monkeyPool ) } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/Action.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/Action.kt index 5c9f450abe6..da7373967a6 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/Action.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/Action.kt @@ -34,6 +34,8 @@ abstract class Action { is ActionType.Reconnect -> ReconnectAction(config.type) is ActionType.SendMessage -> SendMessageAction(config.type) is ActionType.SendRequest -> SendRequestAction(config.type) + is ActionType.HandleExternalRequest -> HandleExternalRequestAction(config.type) + is ActionType.SendExternalRequest -> SendExternalRequestAction(config.type) } } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/HandleExternalRequestAction.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/HandleExternalRequestAction.kt new file mode 100644 index 00000000000..677196260e5 --- /dev/null +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/HandleExternalRequestAction.kt @@ -0,0 +1,65 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.monkeys.actions + +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.monkeys.conversation.Monkey +import com.wire.kalium.monkeys.importer.ActionType +import com.wire.kalium.monkeys.pool.MonkeyPool + +private val DIRECT_MESSAGES = arrayOf( + """ + Hey there, + + I hope you're doing well. I've got a bit of a craving for bananas, and I was wondering if you might be able to share a few with me? + It would mean a lot. 😊 + + Thanks a bunch, + A friendly monkey 🍌🐡 + """.trimIndent(), """ + Yo, + + I'm in need of some bananas, my friend. Can you hook me up? I'd appreciate it big time. + + Respect, + A neutral monkey 🍌 + """.trimIndent(), """ + Listen up, + + I ain't messin' around. I want them bananas, and I want 'em now. You better deliver or there'll be consequences. + + No games, + An evil monkey πŸŒπŸ‘ΏπŸ’€ + """.trimIndent() +) + +class HandleExternalRequestAction(val config: ActionType.HandleExternalRequest) : Action() { + override suspend fun execute(coreLogic: CoreLogic, monkeyPool: MonkeyPool) { + val monkeys = monkeyPool.randomMonkeysWithConnectionRequests(config.userCount) + monkeys.forEach { (monkey, pendingConnections) -> + if (config.shouldAccept) { + val otherUser = + Monkey.external(pendingConnections.random().otherUser?.id ?: error("Cannot get other user id from connection request")) + monkey.acceptRequest(otherUser) + monkey.sendDirectMessageTo(otherUser, config.greetMessage.ifBlank { DIRECT_MESSAGES.random() }) + } else { + monkey.rejectRequest(Monkey.external(pendingConnections.random().connection.qualifiedToId)) + } + } + } +} diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationAction.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationAction.kt index f35f532a0e6..a469b00ea5b 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationAction.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationAction.kt @@ -28,7 +28,7 @@ class LeaveConversationAction(val config: ActionType.LeaveConversation) : Action targets.forEach { conv -> val leavers = conv.randomMonkeys(this.config.userCount) // conversation admin should never leave the group - leavers.filter { it.user != conv.creator.user }.forEach { + leavers.filter { it.monkeyType.userData() != conv.creator.monkeyType.userData() }.forEach { it.leaveConversation(conv.conversation.id) } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendExternalRequestAction.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendExternalRequestAction.kt new file mode 100644 index 00000000000..c1bcd31010f --- /dev/null +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendExternalRequestAction.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.monkeys.actions + +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.monkeys.importer.ActionType +import com.wire.kalium.monkeys.pool.MonkeyPool + +class SendExternalRequestAction(val config: ActionType.SendExternalRequest) : Action() { + override suspend fun execute(coreLogic: CoreLogic, monkeyPool: MonkeyPool) { + val monkeys = monkeyPool.randomLoggedInMonkeysFromTeam(config.originTeam, config.userCount) + val usersFromTeam = monkeyPool.externalUsersFromTeam(config.targetTeam) + monkeys.forEach { monkey -> + monkey.sendRequest(usersFromTeam.random()) + } + } +} diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendMessageAction.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendMessageAction.kt index 33fc874a51b..03346529822 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendMessageAction.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/actions/SendMessageAction.kt @@ -27,10 +27,87 @@ import com.wire.kalium.monkeys.pool.MonkeyPool private const val ONE_2_1: String = "One21" private val EMOJI: List = listOf( - "πŸ‘€", "🦭", "πŸ˜΅β€πŸ’«", "πŸ‘¨β€πŸ³", - "🍌", "πŸ†", "πŸ‘¨β€πŸŒΎ", "πŸ„β€", - "πŸ₯Ά", "🀀", "πŸ™ˆ", "πŸ™Š", - "πŸ’", "πŸ™‰", "🦍", "🐡" + "πŸ‘€", "🦭", "πŸ˜΅β€πŸ’«", "πŸ‘¨β€πŸ³", "🍌", "πŸ†", "πŸ‘¨β€πŸŒΎ", "πŸ„β€", "πŸ₯Ά", "🀀", "πŸ™ˆ", "πŸ™Š", "πŸ’", "πŸ™‰", "🦍", "🐡" +) +private val MESSAGES = listOf( + """ + Hey there, + + I'm really in the mood for some bananas, and I was wondering if you could lend a hand. I'd truly appreciate it. Bananas have a + special place in my heart, and your help would brighten my day. + + Thanks for thinking it over. + """, + """ + Hey, + + I've got a hankering for some bananas. Any chance you could help me out? + + Appreciate it! + """, + """ + Hello, + + I'm on the hunt for bananas, and I'm reaching out to see if you or your group could assist. Your help would mean a lot to me. + + Thanks in advance. + """, + """ + Hey, + + I've got this banana craving that won't quit. Can you come to the rescue? + + Much appreciated! + """, + """ + Hi there, + + I'm in need of some bananas, and I'm wondering if you or your group could lend a hand. Your support would make my day. + + Thanks a bunch! + """, + """ + Hello, + + I'm really feeling the banana vibe right now. Could you or your group be the banana heroes I'm looking for? + + Thanks in advance. + """, + """ + Hey, + + Bananas are calling my name. Any chance you can help satisfy my fruity desires? + + Thanks a bunch! + """, + """ + Hi, + + I'm on a mission to find bananas, and I'm hoping you or your group can assist. Your kindness would be greatly appreciated. + + Thanks for considering. + """, + """ + Hey there, + + I've got a strong craving for bananas. Could you lend a hand in satisfying it? + + Many thanks! + """, + """ + Hello, + + I'm in the mood for some bananas. Can you or your group help me out? It would mean a lot. + + Thanks in advance! + """, + """ + Hey, + + I've got this banana hankering that won't quit. Can you be the banana hero I need? + + Much appreciated! + """, ) class SendMessageAction(val config: ActionType.SendMessage) : Action() { @@ -42,7 +119,7 @@ class SendMessageAction(val config: ActionType.SendMessage) : Action() { val monkeys = monkeyPool.randomLoggedInMonkeys(this.config.userCount) monkeys.forEach { monkey -> val targetMonkey = monkey.randomPeer(monkeyPool) - monkey.sendDirectMessageTo(targetMonkey, randomMessage(targetMonkey.user.email, i)) + monkey.sendDirectMessageTo(targetMonkey, randomMessage()) } } else { ConversationPool.getFromPrefixed(target).forEach { conv -> @@ -66,11 +143,14 @@ private suspend fun MonkeyConversation.sendMessage(userCount: UserCount, i: Int) logger.d("No monkey is logged in in the picked conversation") } monkeys.forEach { monkey -> - val message = randomMessage(this.conversation.name ?: "fellow stranger", i) + val message = randomMessage() monkey.sendMessageTo(this.conversation.id, message) } } -private fun randomMessage(target: String, i: Int): String { - return "Hello everyone from $target. Give me ${i + 1} banana(s). ${EMOJI.random()}" +private fun randomMessage(): String { + return """ + ${MESSAGES.random()} + ${EMOJI.random()} + """.trimIndent() } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/Monkey.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/Monkey.kt index cca0705c531..b36e4e599c5 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/Monkey.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/Monkey.kt @@ -20,9 +20,12 @@ package com.wire.kalium.monkeys.conversation import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.client.ClientType +import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationOptions import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.data.sync.SyncState +import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase @@ -37,18 +40,44 @@ import com.wire.kalium.logic.feature.publicuser.GetAllContactsResult import com.wire.kalium.monkeys.importer.Backend import com.wire.kalium.monkeys.importer.UserCount import com.wire.kalium.monkeys.importer.UserData +import com.wire.kalium.monkeys.logger import com.wire.kalium.monkeys.pool.ConversationPool import com.wire.kalium.monkeys.pool.MonkeyPool import com.wire.kalium.monkeys.pool.resolveUserCount import kotlinx.coroutines.flow.first +sealed class MonkeyType { + data class Internal(val user: UserData) : MonkeyType() + data class External(val userId: UserId) : MonkeyType() + + fun userId(): UserId = when (this) { + is External -> this.userId + is Internal -> this.user.userId + } + + /** + * Ensures that the monkey type is internal and return its user data + */ + fun userData(): UserData = when (this) { + is External -> error("This is an external Monkey and can't perform this operation") + is Internal -> this.user + } +} + /** * A monkey is a user puppeteered by the test framework. - * It contains the basic [user] data and provides + * It contains the basic user data and provides * the [monkeyState] which we can use to perform actions. */ @Suppress("TooManyFunctions") -class Monkey(val user: UserData) { +class Monkey(val monkeyType: MonkeyType) { + companion object { + // this means there are users within the team not managed by IM + // We can still send messages and add them to groups but not act on their behalf + fun external(userId: UserId) = Monkey(MonkeyType.External(userId)) + fun internal(user: UserData) = Monkey(MonkeyType.Internal(user)) + } + private var monkeyState: MonkeyState = MonkeyState.NotReady fun isSessionActive(): Boolean { @@ -57,22 +86,22 @@ class Monkey(val user: UserData) { override fun equals(other: Any?): Boolean { return other != null && when (other) { - is Monkey -> other.user.userId == this.user.userId + is Monkey -> other.monkeyType.userId() == this.monkeyType.userId() else -> false } } override fun hashCode(): Int { - return this.user.userId.hashCode() + return this.monkeyType.userId().hashCode() } /** * Logs user in and register client (if not registered) */ suspend fun login(coreLogic: CoreLogic, callback: (Monkey) -> Unit) { - val authScope = getAuthScope(coreLogic, this.user.backend) - val email = this.user.email - val password = this.user.password + val authScope = getAuthScope(coreLogic, this.monkeyType.userData().team.backend) + val email = this.monkeyType.userData().email + val password = this.monkeyType.userData().password val loginResult = authScope.login(email, password, false) if (loginResult !is AuthenticationResult.Success) { error("User creds didn't work ($email, $password)") @@ -90,18 +119,33 @@ class Monkey(val user: UserData) { error("Failed to store user. $storeResult") } } - this.monkeyState = MonkeyState.Ready(coreLogic.getSessionScope(loginResult.authData.userId)) + val sessionScope = coreLogic.getSessionScope(loginResult.authData.userId) val registerClientParam = RegisterClientUseCase.RegisterClientParam( - password = this.user.password, - capabilities = emptyList(), - clientType = ClientType.Temporary + password = this.monkeyType.userData().password, capabilities = emptyList(), clientType = ClientType.Temporary ) - val registerResult = this.monkeyState.readyThen { client.getOrRegister(registerClientParam) } + val registerResult = sessionScope.client.getOrRegister(registerClientParam) if (registerResult is RegisterClientResult.Failure) { this.monkeyState = MonkeyState.NotReady - error("Failed registering client of monkey ${this.user.email}: $registerResult") + error("Failed registering client of monkey ${this.monkeyType.userData().email}: $registerResult") } - callback(this) + var isFinished: Boolean + do { + val state = sessionScope.observeSyncState().first { it !is SyncState.Waiting && it !is SyncState.SlowSync } + when (state) { + is SyncState.Failed -> { + this.monkeyState = MonkeyState.NotReady + logger.w("Failed logging in: ${state.cause}. Retrying? ${state.cause.isRetryable}") + isFinished = !state.cause.isRetryable + } + is SyncState.GatheringPendingEvents, + is SyncState.Live -> { + this.monkeyState = MonkeyState.Ready(sessionScope) + callback(this) + isFinished = true + } + else -> error("This should have been done") + } + } while (!isFinished) } private suspend fun connectedMonkeys(): List { @@ -111,18 +155,19 @@ class Monkey(val user: UserData) { if (connectedUsersResult is GetAllContactsResult.Success) { connectedUsersResult.allContacts.map { it.id } } else { - error("Failed getting connected users of monkey ${self.user.email}: $connectedUsersResult") + error("Failed getting connected users of monkey ${self.monkeyType.userId()}: $connectedUsersResult") } } } suspend fun logout(callback: (Monkey) -> Unit) { this.monkeyState.readyThen { logout(LogoutReason.SELF_SOFT_LOGOUT) } + this.monkeyState = MonkeyState.NotReady callback(this) } suspend fun randomPeer(monkeyPool: MonkeyPool): Monkey { - return monkeyPool.get(this.connectedMonkeys().randomOrNull() ?: error("Monkey ${this.user.email} not connected to anyone")) + return monkeyPool.get(this.connectedMonkeys().randomOrNull() ?: error("Monkey ${this.monkeyType.userId()} not connected to anyone")) } suspend fun randomPeers(userCount: UserCount, monkeyPool: MonkeyPool, filterOut: List = listOf()): List { @@ -133,19 +178,25 @@ class Monkey(val user: UserData) { suspend fun sendRequest(anotherMonkey: Monkey) { this.monkeyState.readyThen { - connection.sendConnectionRequest(anotherMonkey.user.userId) + connection.sendConnectionRequest(anotherMonkey.monkeyType.userId()) } } suspend fun acceptRequest(anotherMonkey: Monkey) { this.monkeyState.readyThen { - connection.acceptConnectionRequest(anotherMonkey.user.userId) + connection.acceptConnectionRequest(anotherMonkey.monkeyType.userId()) } } suspend fun rejectRequest(anotherMonkey: Monkey) { this.monkeyState.readyThen { - connection.ignoreConnectionRequest(anotherMonkey.user.userId) + connection.ignoreConnectionRequest(anotherMonkey.monkeyType.userId()) + } + } + + suspend fun pendingConnectionRequests(): List { + return this.monkeyState.readyThen { + conversations.observePendingConnectionRequests().first().filter { it.connection.status == ConnectionState.PENDING } } } @@ -165,19 +216,22 @@ class Monkey(val user: UserData) { val self = this return this.monkeyState.readyThen { val result = conversations.createGroupConversation( - name, monkeyList.map { it.user.userId }, - ConversationOptions(protocol = protocol) + name, monkeyList.map { it.monkeyType.userId() }, ConversationOptions(protocol = protocol) ) if (result is CreateGroupConversationUseCase.Result.Success) { MonkeyConversation(self, result.conversation, isDestroyable, monkeyList) } else { - error("${self.user.email} could not create group $name: $result") + if (result is CreateGroupConversationUseCase.Result.UnknownFailure) { + error("${self.monkeyType.userId()} could not create group $name: ${result.cause}") + } else { + error("${self.monkeyType.userId()} could not create group $name: $result") + } } } } suspend fun leaveConversation(conversationId: ConversationId) { - if (this.user.userId == ConversationPool.conversationCreator(conversationId)?.user?.userId) { + if (this.monkeyType.userId() == ConversationPool.conversationCreator(conversationId)?.monkeyType?.userId()) { error("Creator of the group can't leave") } this.monkeyState.readyThen { @@ -186,7 +240,7 @@ class Monkey(val user: UserData) { } suspend fun destroyConversation(conversationId: ConversationId) { - if (this.user.userId != ConversationPool.conversationCreator(conversationId)?.user?.userId) { + if (this.monkeyType.userId() != ConversationPool.conversationCreator(conversationId)?.monkeyType?.userId()) { error("Only the creator can destroy a group") } @@ -197,29 +251,26 @@ class Monkey(val user: UserData) { suspend fun addMonkeysToConversation(conversationId: ConversationId, monkeys: List) { this.monkeyState.readyThen { - conversations.addMemberToConversationUseCase( - conversationId, - monkeys.map { it.user.userId }) + conversations.addMemberToConversationUseCase(conversationId, monkeys.map { it.monkeyType.userId() }) } } suspend fun removeMonkeyFromConversation(id: ConversationId, monkey: Monkey) { this.monkeyState.readyThen { conversations.removeMemberFromConversation( - id, - monkey.user.userId + id, monkey.monkeyType.userId() ) } } suspend fun sendDirectMessageTo(anotherMonkey: Monkey, message: String) { - val self = this.user.email + val self = this.monkeyType.userId() this.monkeyState.readyThen { - val result = conversations.getOrCreateOneToOneConversationUseCase(anotherMonkey.user.userId) + val result = conversations.getOrCreateOneToOneConversationUseCase(anotherMonkey.monkeyType.userId()) if (result is CreateConversationResult.Success) { messages.sendTextMessage(result.conversation.id, message) } else { - error("$self failed contacting ${anotherMonkey.user.email}: $result") + error("$self failed contacting ${anotherMonkey.monkeyType.userId()}: $result") } } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/MonkeyConversation.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/MonkeyConversation.kt index 8e05c88a449..ffa8e721ebf 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/MonkeyConversation.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/conversation/MonkeyConversation.kt @@ -64,6 +64,6 @@ class MonkeyConversation(val creator: Monkey, val conversation: Conversation, va } fun membersIds(): List { - return this.participants.map { it.user.userId } + return this.participants.map { it.monkeyType.userId() } } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt index f0c76cd4db1..cf2ab5c5ead 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt @@ -29,14 +29,13 @@ fun coreLogic( rootPath: String, ): CoreLogic { val coreLogic = CoreLogic( - rootPath, - kaliumConfigs = KaliumConfigs( + rootPath, kaliumConfigs = KaliumConfigs( developmentApiEnabled = true, encryptProteusStorage = true, isMLSSupportEnabled = true, wipeOnDeviceRemoval = true, fetchAllTeamMembersEagerly = true, - ), "Wire Infinite Monkeys" + ), userAgent = "Wire Infinite Monkeys", useInMemoryStorage = true ) coreLogic.updateApiVersionsScheduler.scheduleImmediateApiVersionUpdate() return coreLogic diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestData.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestData.kt index 7e231af217b..f7195c9af05 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestData.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestData.kt @@ -127,6 +127,22 @@ sealed class ActionType { @SerialName("delayResponse") val delayResponse: ULong = 0u, @SerialName("shouldAccept") val shouldAccept: Boolean = true ) : ActionType() + + @Serializable + @SerialName("HANDLE_EXTERNAL_REQUEST") + data class HandleExternalRequest( + @SerialName("userCount") val userCount: UserCount, + @SerialName("shouldAccept") val shouldAccept: Boolean, + @SerialName("greetMessage") val greetMessage: String = "" + ) : ActionType() + + @Serializable + @SerialName("SEND_EXTERNAL_REQUEST") + data class SendExternalRequest( + @SerialName("userCount") val userCount: UserCount, + @SerialName("originTeam") val originTeam: String, + @SerialName("targetTeam") val targetTeam: String + ) : ActionType() } @Serializable @@ -145,7 +161,17 @@ data class BackendConfig( @SerialName("authPassword") val authPassword: String, @SerialName("userCount") val userCount: ULong, @SerialName("dumpUsers") val dumpUsers: Boolean = false, - @SerialName("users") val users: List = listOf() + @SerialName("presetTeam") val presetTeam: TeamConfig? = null +) + +@Serializable +data class TeamConfig( + @SerialName("id") + val id: String, + @SerialName("owner") + val owner: UserAccount, + @SerialName("users") + val users: List ) @Serializable diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestDataImporter.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestDataImporter.kt index d45a0bbd893..e5442afa49b 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestDataImporter.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/TestDataImporter.kt @@ -21,7 +21,6 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.monkeys.logger import com.wire.kalium.network.KaliumKtorCustomLogging import com.wire.kalium.network.tools.KtxSerializer -import com.wire.kalium.util.serialization.toJsonArray import com.wire.kalium.util.serialization.toJsonObject import io.github.serpro69.kfaker.Faker import io.ktor.client.HttpClient @@ -48,6 +47,8 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import java.io.File import java.net.URLEncoder @@ -55,9 +56,7 @@ import java.util.UUID private const val TEAM_ROLE: String = "member" -private val tokenStorage = mutableListOf() - -private data class Team(val id: String, val backend: Backend, val owner: UserData) +private var token: BearerTokens? = null object TestDataImporter { @OptIn(ExperimentalSerializationApi::class) @@ -75,54 +74,44 @@ object TestDataImporter { } } - private fun getUserData(backendConfig: BackendConfig): List { - val backend = with(backendConfig) { - Backend( - api, - accounts, - webSocket, - blackList, - teams, - website, - title, - domain, - teamName - ) - } - return backendConfig.users.map { user -> - UserData( - user.email, - backendConfig.passwordForUsers, - UserId(user.unqualifiedId, backendConfig.domain), - backend - ) - } - } - - private fun dumpUsers(teamName: String, users: List) { - val json = users.map { + private fun dumpUsers(team: Team, users: List) { + val json = mapOf("id" to team.id, "owner" to mapOf( + "email" to team.owner.email, "id" to team.owner.userId.value + ), "users" to users.map { mapOf( - "email" to it.email, - "id" to it.userId.value + "email" to it.email, "id" to it.userId.value ) - }.toJsonArray() - File("$teamName.json").writeText(jsonSerializer.encodeToString(json)) + }).toJsonObject() + File("${team.name}.json").writeText(jsonSerializer.encodeToString(json)) } suspend fun generateUserData(testData: TestData): List { return testData.backends.flatMap { backendConfig -> - if (backendConfig.users.isNotEmpty()) { - getUserData(backendConfig) + val httpClient = basicHttpClient(backendConfig) + if (backendConfig.presetTeam != null) { + val backend = Backend.fromConfig(backendConfig) + val team = Team( + backendConfig.teamName, + backendConfig.presetTeam.id, + backend, + backendConfig.presetTeam.owner.email, + backendConfig.passwordForUsers, + UserId(backendConfig.presetTeam.owner.unqualifiedId, backendConfig.domain), + httpClient + ) + backendConfig.presetTeam.users.map { user -> + UserData( + user.email, backendConfig.passwordForUsers, UserId(user.unqualifiedId, backendConfig.domain), team + ) + } } else { - val httpClient = basicHttpClient(backendConfig) val team = httpClient.createTeam(backendConfig) - val users = (1..backendConfig.userCount.toInt()) - .map { httpClient.createUser(it, team, backendConfig.passwordForUsers) } - .plus(team.owner) - .also { httpClient.close() } + val users = (1..backendConfig.userCount.toInt()).map { httpClient.createUser(it, team, backendConfig.passwordForUsers) } + .plus(team.owner).also { httpClient.close() } if (backendConfig.dumpUsers) { - dumpUsers(backendConfig.teamName, users) + dumpUsers(team, users) } + users.forEach { setUserHandle(backendConfig, it) } users } } @@ -137,20 +126,13 @@ private suspend fun HttpClient.createTeam(backendConfig: BackendConfig): Team { val email = "$ownerId@wire.com" post("activate/send") { setBody(mapOf("email" to email)) } - val code = - get("i/users/activation-code?email=${URLEncoder.encode(email, "utf-8")}").body()["code"]?.jsonPrimitive?.content - ?: error("Failed to get activation code for owner") + val code = get("i/users/activation-code?email=${URLEncoder.encode(email, "utf-8")}").body()["code"]?.jsonPrimitive?.content + ?: error("Failed to get activation code for owner") val user = post("register") { setBody( mapOf( - "email" to email, - "name" to ownerName, - "password" to backendConfig.passwordForUsers, - "email_code" to code, - "team" to mapOf( - "name" to backendConfig.teamName, - "icon" to "default", - "binding" to true + "email" to email, "name" to ownerName, "password" to backendConfig.passwordForUsers, "email_code" to code, "team" to mapOf( + "name" to backendConfig.teamName, "icon" to "default", "binding" to true ) ).toJsonObject() ) @@ -165,63 +147,86 @@ private suspend fun HttpClient.createTeam(backendConfig: BackendConfig): Team { "defaultProtocol" to "proteus", "protocolToggleUsers" to listOf(), "supportedProtocols" to listOf("mls", "proteus") - ), - "status" to "enabled" + ), "status" to "enabled" ).toJsonObject() ) } val backend = Backend.fromConfig(backendConfig) val userId = user["id"]?.jsonPrimitive?.content ?: error("Could not register user") - val team = Team(teamId, backend, UserData(email, backendConfig.passwordForUsers, UserId(userId, backend.domain), backend)) - ownerLogin(team) + val team = Team( + name = backendConfig.teamName, + id = teamId, + backend = backend, + ownerEmail = email, + ownerPassword = backendConfig.passwordForUsers, + ownerId = UserId(userId, backend.domain), + client = this + ) + login(team.owner.email, team.owner.password) put("self/handle") { setBody(mapOf("handle" to ownerId).toJsonObject()) } logger.i("Owner $email (id $userId) of team ${backendConfig.teamName} (id: $teamId) in backend ${backendConfig.domain}") return team } +private suspend fun setUserHandle(backendConfig: BackendConfig, userData: UserData) { + lateinit var token: BearerTokens + val httpClient = basicHttpClient(backendConfig) { token } + httpClient.login(userData.email, userData.password) { accessToken -> + token = BearerTokens(accessToken, "") + } + httpClient.put("self/handle") { setBody(mapOf("handle" to "monkey-${userData.userId.value}").toJsonObject()) } +} + private suspend fun HttpClient.createUser(i: Int, team: Team, userPassword: String): UserData { val faker = Faker() val userName = faker.name.name() - val email = "monkey-user-$i-${team.id}@wire.com" + val email = "monkey-user-$i-${team.id}@${team.name}.wire.com" val invitationCode = invite(team.id, email, userName) val response = post("register") { setBody( mapOf( - "email" to email, - "name" to userName, - "password" to userPassword, - "team_code" to invitationCode + "email" to email, "name" to userName, "password" to userPassword, "team_code" to invitationCode ).toJsonObject() ) }.body() val userId = response["id"]?.jsonPrimitive?.content ?: error("Could not register user in team") - logger.d("Created user $email (id $userId) in team ${team.backend.teamName}") - return UserData(email, userPassword, UserId(userId, team.backend.domain), team.backend) + logger.d("Created user $email (id $userId) in team ${team.name}") + return UserData(email, userPassword, UserId(userId, team.backend.domain), team) } -private suspend fun HttpClient.ownerLogin(team: Team) { +private suspend fun HttpClient.login( + email: String, + password: String, + tokenProvider: (accessToken: String) -> Unit = { accessToken -> + token = BearerTokens(accessToken, "") + } +) { val response = post("login") { setBody( mapOf( - "email" to team.owner.email, - "password" to team.owner.password, - "label" to "" + "email" to email, "password" to password, "label" to "" ).toJsonObject() ) }.body() val accessToken = response["access_token"]?.jsonPrimitive?.content ?: error("Could not login") - tokenStorage.add(BearerTokens(accessToken, "")) + tokenProvider(accessToken) +} + +internal suspend fun HttpClient.teamParticipants(team: Team): List { + this.login(team.owner.email, team.owner.password) + val members = get("teams/${team.id}/members").body() + return members["members"]!!.jsonArray.map { member -> + UserId(member.jsonObject["user"]!!.jsonPrimitive.content, team.backend.domain) + } } private suspend fun HttpClient.invite(teamId: String, email: String, name: String): String { val invitationId = post("teams/$teamId/invitations") { setBody( mapOf( - "email" to email, - "role" to TEAM_ROLE, - "inviter_name" to name + "email" to email, "role" to TEAM_ROLE, "inviter_name" to name ).toJsonObject() ) }.body()["id"]?.jsonPrimitive?.content ?: error("Could not invite new user") @@ -229,7 +234,7 @@ private suspend fun HttpClient.invite(teamId: String, email: String, name: Strin ?: error("Could not retrieve user invitation") } -private fun basicHttpClient(backendConfig: BackendConfig) = HttpClient(OkHttp.create()) { +private fun basicHttpClient(backendConfig: BackendConfig, tokenProvider: () -> BearerTokens? = { token }) = HttpClient(OkHttp.create()) { val excludedPaths = listOf("register", "login", "activate") defaultRequest { url(backendConfig.api) @@ -251,9 +256,7 @@ private fun basicHttpClient(backendConfig: BackendConfig) = HttpClient(OkHttp.cr sendWithoutRequest { it.url.pathSegments.isNotEmpty() && it.url.pathSegments[0] != "i" && !excludedPaths.contains(it.url.pathSegments[0]) } - loadTokens { - tokenStorage.last() - } + loadTokens(tokenProvider) } basic { sendWithoutRequest { diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/UserData.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/UserData.kt index 707642c13c1..aeefb197161 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/UserData.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/importer/UserData.kt @@ -18,14 +18,42 @@ package com.wire.kalium.monkeys.importer import com.wire.kalium.logic.data.user.UserId +import io.ktor.client.HttpClient data class UserData( val email: String, val password: String, val userId: UserId, - val backend: Backend + val team: Team, ) +@Suppress("LongParameterList") +class Team( + val name: String, + val id: String, + val backend: Backend, + ownerEmail: String, + ownerPassword: String, + ownerId: UserId, + private val client: HttpClient +) { + val owner: UserData + + init { + this.owner = UserData(ownerEmail, ownerPassword, ownerId, this) + } + + suspend fun usersFromTeam(): List = this.client.teamParticipants(this) + + override fun equals(other: Any?): Boolean { + return other != null && other is Team && other.id == this.id + } + + override fun hashCode(): Int { + return id.hashCode() + } +} + data class Backend( val api: String, val accounts: String, @@ -35,11 +63,10 @@ data class Backend( val website: String, val title: String, val domain: String, - val teamName: String ) { companion object { fun fromConfig(config: BackendConfig): Backend = with(config) { - Backend(api, accounts, webSocket, blackList, teams, website, title, domain, teamName) + Backend(api, accounts, webSocket, blackList, teams, website, title, domain) } } } diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/ConversationPool.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/ConversationPool.kt index f8ac6457f4c..fad6bf2a80e 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/ConversationPool.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/ConversationPool.kt @@ -75,7 +75,7 @@ object ConversationPool { protocol: ConversationOptions.Protocol, monkeyPool: MonkeyPool ) { - val name = "By monkey ${creator.user.email} - $protocol - ${Random.nextUInt()}" + val name = "By monkey ${creator.monkeyType.userId()} - $protocol - ${Random.nextUInt()}" val monkeyList = creator.randomPeers(userCount, monkeyPool) val conversation = creator.createConversation(name, monkeyList, protocol) this.addToPool(conversation) @@ -86,7 +86,7 @@ object ConversationPool { protocol: ConversationOptions.Protocol, monkeyPool: MonkeyPool ) { - val creator = monkeyPool.randomMonkeys(UserCount.single())[0] + val creator = monkeyPool.randomLoggedInMonkeys(UserCount.single())[0] this.createDynamicConversation(creator, userCount, protocol, monkeyPool) } @@ -96,7 +96,7 @@ object ConversationPool { protocol: ConversationOptions.Protocol, monkeyPool: MonkeyPool ) { - val creator = monkeyPool.randomMonkeysFromTeam(team, UserCount.single())[0] + val creator = monkeyPool.randomLoggedInMonkeysFromTeam(team, UserCount.single())[0] this.createDynamicConversation(creator, userCount, protocol, monkeyPool) } @@ -112,7 +112,7 @@ object ConversationPool { ) { repeat(count.toInt()) { groupIndex -> val creator = monkeyPool.randomMonkeys(UserCount.single())[0] - val name = "Prefixed $prefix by monkey ${creator.user.email} - $protocol - $groupIndex" + val name = "Prefixed $prefix by monkey ${creator.monkeyType.userId()} - $protocol - $groupIndex" val conversation = creator.makeReadyThen(coreLogic, monkeyPool) { val participants = creator.randomPeers(userCount, monkeyPool) createConversation(name, participants, protocol, false) diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/MonkeyPool.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/MonkeyPool.kt index 057a146c039..ec6c540c962 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/MonkeyPool.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/pool/MonkeyPool.kt @@ -17,17 +17,26 @@ */ package com.wire.kalium.monkeys.pool +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.monkeys.MetricsCollector import com.wire.kalium.monkeys.conversation.Monkey +import com.wire.kalium.monkeys.importer.Team import com.wire.kalium.monkeys.importer.UserCount import com.wire.kalium.monkeys.importer.UserData import io.micrometer.core.instrument.Tag +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import java.util.concurrent.ConcurrentHashMap import kotlin.math.roundToInt class MonkeyPool(users: List, testCase: String) { - // a map of monkeys per domain + // all the teams by name + private val teams: ConcurrentHashMap = ConcurrentHashMap() + + // a map of monkeys per team private val pool: ConcurrentHashMap> = ConcurrentHashMap() // a map of monkeys per UserId @@ -41,29 +50,36 @@ class MonkeyPool(users: List, testCase: String) { init { users.forEach { - val monkey = Monkey(it) - this.pool.getOrPut(it.backend.teamName) { mutableListOf() }.add(monkey) - this.poolLoggedOut.getOrPut(it.backend.teamName) { ConcurrentHashMap() }[it.userId] = monkey + val monkey = Monkey.internal(it) + this.pool.getOrPut(it.team.name) { mutableListOf() }.add(monkey) + this.poolLoggedOut.getOrPut(it.team.name) { ConcurrentHashMap() }[it.userId] = monkey this.poolById[it.userId] = monkey + this.teams.putIfAbsent(it.team.name, it.team) } this.poolLoggedOut.forEach { (domain, usersById) -> MetricsCollector.gaugeMap( - "g_loggedOutUsers", - listOf(Tag.of("domain", domain), Tag.of("testCase", testCase)), - usersById + "g_loggedOutUsers", listOf(Tag.of("domain", domain), Tag.of("testCase", testCase)), usersById ) // init so we can have metrics for it this.poolLoggedIn[domain] = ConcurrentHashMap() } this.poolLoggedIn.forEach { (domain, usersById) -> MetricsCollector.gaugeMap( - "g_loggedInUsers", - listOf(Tag.of("domain", domain), Tag.of("testCase", testCase)), - usersById + "g_loggedInUsers", listOf(Tag.of("domain", domain), Tag.of("testCase", testCase)), usersById ) } } + suspend fun warmUp(core: CoreLogic) = coroutineScope { + // this is needed to create key packages for clients at least once + poolById.values.map { + async { + it.login(core) {} + it.logout {} + } + }.awaitAll() + } + fun randomMonkeysFromTeam(team: String, userCount: UserCount): List { val count = resolveUserCount(userCount) val backendUsers = this.pool[team]?.shuffled() ?: error("Team $team doesn't exist or there are no monkeys in the team") @@ -86,6 +102,14 @@ class MonkeyPool(users: List, testCase: String) { return backendUsers.take(count.toInt()) } + suspend fun randomMonkeysWithConnectionRequests(userCount: UserCount): Map> { + val monkeysWithPendingRequests = this.poolLoggedIn.values.flatMap { idToMonkey -> + idToMonkey.values.map { it to it.pendingConnectionRequests() }.filter { it.second.isNotEmpty() } + }.shuffled() + val count = resolveUserCount(userCount, monkeysWithPendingRequests.count().toUInt()) + return monkeysWithPendingRequests.take(count.toInt()).toMap() + } + /** * This is costly depending on the size. Use with caution */ @@ -104,14 +128,22 @@ class MonkeyPool(users: List, testCase: String) { return allUsers.take(count.toInt()) } + suspend fun externalUsersFromTeam(teamName: String): List { + val team = this.teams[teamName] ?: error("Backend $teamName not found") + val monkeysFromTeam = this.pool[teamName] ?: error("Monkeys from $teamName not found") + return team.usersFromTeam().filter { u -> monkeysFromTeam.none { u == it.monkeyType.userId() } }.map(Monkey::external) + } + fun loggedIn(monkey: Monkey) { - this.poolLoggedIn.getOrPut(monkey.user.backend.teamName) { ConcurrentHashMap() }[monkey.user.userId] = monkey - this.poolLoggedOut[monkey.user.backend.teamName]?.remove(monkey.user.userId) + this.poolLoggedIn.getOrPut(monkey.monkeyType.userData().team.name) { ConcurrentHashMap() }[monkey.monkeyType.userData().userId] = + monkey + this.poolLoggedOut[monkey.monkeyType.userData().team.name]?.remove(monkey.monkeyType.userData().userId) } fun loggedOut(monkey: Monkey) { - this.poolLoggedIn[monkey.user.backend.teamName]?.remove(monkey.user.userId) - this.poolLoggedOut.getOrPut(monkey.user.backend.teamName) { ConcurrentHashMap() }[monkey.user.userId] = monkey + this.poolLoggedIn[monkey.monkeyType.userData().team.name]?.remove(monkey.monkeyType.userData().userId) + this.poolLoggedOut.getOrPut(monkey.monkeyType.userData().team.name) { ConcurrentHashMap() }[monkey.monkeyType.userData().userId] = + monkey } private fun resolveUserCount(userCount: UserCount): UInt { @@ -125,7 +157,7 @@ class MonkeyPool(users: List, testCase: String) { } fun get(userId: UserId): Monkey { - return this.poolById[userId] ?: error("Monkey with id $userId not found.") + return this.poolById[userId] ?: Monkey.external(userId) } } diff --git a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/AddUserToConversationActionTest.kt b/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/AddUserToConversationActionTest.kt deleted file mode 100644 index 734c6d948c6..00000000000 --- a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/AddUserToConversationActionTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.wire.kalium.monkeys.actions - -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.monkeys.conversation.Monkey -import com.wire.kalium.monkeys.conversation.MonkeyConversation -import com.wire.kalium.monkeys.importer.ActionType -import com.wire.kalium.monkeys.importer.UserCount -import com.wire.kalium.monkeys.pool.ConversationPool -import com.wire.kalium.monkeys.pool.MonkeyPool -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.confirmVerified -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.Ignore -import org.junit.Test - -class AddUserToConversationActionTest { - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenAddUserConfig_newUsersShouldBeAdded() = runTest { - val config = ActionType.AddUsersToConversation(1u, UserCount.single()) - mockkObject(ConversationPool) - val monkeyPool = mockk() - val monkey = mockk(relaxed = true) - val conversation = mockk(relaxed = true) - val creator = mockk() - val coreLogic = mockk() - every { ConversationPool.randomDynamicConversations(config.countGroups.toInt()) } returns listOf(conversation) - every { conversation.creator } returns creator - coEvery { creator.randomPeers(config.userCount, any()) } returns listOf(monkey) - AddUserToConversationAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { conversation.addMonkeys(listOf(monkey)) } - verify(exactly = 1) { conversation.creator } - verify(exactly = 1) { conversation.membersIds() } - confirmVerified(conversation) - } -} diff --git a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationActionTest.kt b/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationActionTest.kt deleted file mode 100644 index 6351dd62edf..00000000000 --- a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LeaveConversationActionTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.wire.kalium.monkeys.actions - -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.monkeys.conversation.Monkey -import com.wire.kalium.monkeys.conversation.MonkeyConversation -import com.wire.kalium.monkeys.importer.ActionType -import com.wire.kalium.monkeys.importer.UserCount -import com.wire.kalium.monkeys.pool.ConversationPool -import com.wire.kalium.monkeys.pool.MonkeyPool -import io.mockk.coVerify -import io.mockk.confirmVerified -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.Ignore -import org.junit.Test - -class LeaveConversationActionTest { - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenOnlyOneUser_noUserShouldLeave() = runTest { - val config = ActionType.LeaveConversation(1u, UserCount.single()) - mockkObject(ConversationPool) - val monkeyPool = mockk() - val conversation = mockk(relaxed = true) - val creator = mockk(relaxed = true) - val coreLogic = mockk() - every { ConversationPool.randomDynamicConversations(config.countGroups.toInt()) } returns listOf(conversation) - every { conversation.creator } returns creator - every { conversation.randomMonkeys(config.userCount) } returns listOf(creator) - LeaveConversationAction(config).execute(coreLogic, monkeyPool) - coVerify(inverse = true) { creator.leaveConversation(any()) } - coVerify(exactly = 2) { creator.user } - confirmVerified(creator) - } - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenMultipleUsers_oneShouldLeave() = runTest { - val config = ActionType.LeaveConversation(1u, UserCount.fixed(2u)) - mockkObject(ConversationPool) - val monkeyPool = mockk() - val monkey = mockk(relaxed = true) - val conversation = mockk(relaxed = true) - val creator = mockk(relaxed = true) - val coreLogic = mockk() - every { ConversationPool.randomDynamicConversations(config.countGroups.toInt()) } returns listOf(conversation) - every { conversation.creator } returns creator - every { conversation.randomMonkeys(config.userCount) } returns listOf(creator, monkey) - LeaveConversationAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.leaveConversation(conversation.conversation.id) } - coVerify(inverse = true) { creator.leaveConversation(any()) } - verify(exactly = 1) { monkey.user } - verify(exactly = 3) { creator.user } - confirmVerified(creator) - confirmVerified(monkey) - } -} diff --git a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LoginActionTest.kt b/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LoginActionTest.kt deleted file mode 100644 index b3c962bd27a..00000000000 --- a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/LoginActionTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.wire.kalium.monkeys.actions - -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.monkeys.conversation.Monkey -import com.wire.kalium.monkeys.importer.ActionType -import com.wire.kalium.monkeys.importer.UserCount -import com.wire.kalium.monkeys.pool.MonkeyPool -import io.mockk.coVerify -import io.mockk.confirmVerified -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Ignore -import org.junit.Test - -class LoginActionTest { - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenLoginConfigProvided_thenShouldLogin() = runTest { - val monkeyPool = mockk() - val monkey = mockk(relaxed = true) - val coreLogic = mockk() - every { monkeyPool.randomLoggedOutMonkeys(UserCount.single()) } returns listOf(monkey) - LoginAction(ActionType.Login(UserCount.single())).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.login(coreLogic, monkeyPool::loggedIn) } - coVerify(exactly = 0) { monkey.logout(monkeyPool::loggedOut) } - confirmVerified(monkey) - } - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenLoginConfigWithDurationProvided_thenShouldLoginAndLogout() = runTest { - val monkeyPool = mockk() - val monkey = mockk(relaxed = true) - val coreLogic = mockk() - every { monkeyPool.randomLoggedOutMonkeys(UserCount.single()) } returns listOf(monkey) - LoginAction(ActionType.Login(UserCount.single(), 10u)).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.login(coreLogic, monkeyPool::loggedIn) } - coVerify(exactly = 1) { monkey.logout(monkeyPool::loggedOut) } - confirmVerified(monkey) - } -} diff --git a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendMessageActionTest.kt b/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendMessageActionTest.kt deleted file mode 100644 index 5e43d6b8484..00000000000 --- a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendMessageActionTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.wire.kalium.monkeys.actions - -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.monkeys.conversation.Monkey -import com.wire.kalium.monkeys.conversation.MonkeyConversation -import com.wire.kalium.monkeys.importer.ActionType -import com.wire.kalium.monkeys.importer.UserCount -import com.wire.kalium.monkeys.pool.ConversationPool -import com.wire.kalium.monkeys.pool.MonkeyPool -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.confirmVerified -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.Ignore -import org.junit.Test - -class SendMessageActionTest { - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenEmptyTargets_randomConversationsShouldBePicked() = runTest { - val config = ActionType.SendMessage(UserCount.single(), 1u, 1u, listOf()) - val monkeyPool = mockk() - mockkObject(ConversationPool) - val monkey = mockk(relaxed = true) - val conversation = mockk(relaxed = true) - val coreLogic = mockk() - every { ConversationPool.randomConversations(config.countGroups) } returns listOf(conversation) - every { conversation.randomMonkeys(config.userCount) } returns listOf(monkey) - SendMessageAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.sendMessageTo(any(), any()) } - verify { conversation.randomMonkeys(config.userCount) } - verify { conversation.conversation } - verify { conversation.conversation.name } - verify { conversation.conversation.id } - confirmVerified(monkey, conversation) - } - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenTargets_PrefixedConversationShouldBePicked() = runTest { - val config = ActionType.SendMessage(UserCount.single(), 1u, 1u, listOf("group1")) - val monkeyPool = mockk() - mockkObject(ConversationPool) - val monkey = mockk(relaxed = true) - val conversation = mockk(relaxed = true) - val coreLogic = mockk() - every { ConversationPool.getFromPrefixed("group1") } returns listOf(conversation) - every { conversation.randomMonkeys(config.userCount) } returns listOf(monkey) - SendMessageAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.sendMessageTo(any(), any()) } - verify { conversation.randomMonkeys(config.userCount) } - verify { conversation.conversation } - verify { conversation.conversation.name } - verify { conversation.conversation.id } - confirmVerified(monkey, conversation) - } - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenOne21Informed_directMessageShouldBeSent() = runTest { - val config = ActionType.SendMessage(UserCount.single(), 1u, 1u, listOf("One21")) - val monkeyPool = mockk() - mockkObject(ConversationPool) - val monkey = mockk(relaxed = true) - val targetMonkey = mockk(relaxed = true) - val coreLogic = mockk() - every { monkeyPool.randomLoggedInMonkeys(config.userCount) } returns listOf(monkey) - coEvery { monkey.randomPeer(monkeyPool) } returns targetMonkey - SendMessageAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.sendDirectMessageTo(targetMonkey, any()) } - coVerify(exactly = 1) { monkey.randomPeer(monkeyPool) } - confirmVerified(monkey) - } - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenOne21AndTargetInformed_multipleMessagesShouldBeSent() = runTest { - val config = ActionType.SendMessage(UserCount.single(), 1u, 1u, listOf("One21", "group1")) - val monkeyPool = mockk() - mockkObject(ConversationPool) - val monkey = mockk(relaxed = true) - val targetMonkey = mockk(relaxed = true) - val conversation = mockk(relaxed = true) - val coreLogic = mockk() - every { ConversationPool.getFromPrefixed("group1") } returns listOf(conversation) - every { conversation.randomMonkeys(config.userCount) } returns listOf(monkey) - - every { monkeyPool.randomLoggedInMonkeys(config.userCount) } returns listOf(monkey) - coEvery { monkey.randomPeer(monkeyPool) } returns targetMonkey - SendMessageAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkey.sendDirectMessageTo(targetMonkey, any()) } - coVerify(exactly = 1) { monkey.sendMessageTo(any(), any()) } - coVerify(exactly = 1) { monkey.randomPeer(monkeyPool) } - verify { conversation.randomMonkeys(config.userCount) } - verify { conversation.conversation } - verify { conversation.conversation.name } - verify { conversation.conversation.id } - confirmVerified(monkey, conversation) - } -} diff --git a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendRequestActionTest.kt b/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendRequestActionTest.kt deleted file mode 100644 index 23e8bcfef23..00000000000 --- a/monkeys/src/test/kotlin/com/wire/kalium/monkeys/actions/SendRequestActionTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.wire.kalium.monkeys.actions - -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.monkeys.conversation.Monkey -import com.wire.kalium.monkeys.importer.ActionType -import com.wire.kalium.monkeys.importer.UserCount -import com.wire.kalium.monkeys.pool.MonkeyPool -import io.mockk.coVerify -import io.mockk.confirmVerified -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Ignore -import org.junit.Test - -class SendRequestActionTest { - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenAcceptRequestConfig_shouldAcceptRequest() = runTest { - val config = ActionType.SendRequest(UserCount.single(), UserCount.single(), "wire.com", "wearezeta.com", 0u) - val monkeyPool = mockk() - val monkeyOrigin = mockk(relaxed = true) - val monkeyTarget = mockk(relaxed = true) - val coreLogic = mockk() - every { monkeyPool.randomLoggedInMonkeysFromTeam(config.originTeam, UserCount.single()) } returns listOf(monkeyOrigin) - every { monkeyPool.randomLoggedInMonkeysFromTeam(config.targetTeam, UserCount.single()) } returns listOf(monkeyTarget) - SendRequestAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkeyOrigin.sendRequest(monkeyTarget) } - coVerify(exactly = 1) { monkeyTarget.acceptRequest(monkeyOrigin) } - confirmVerified(monkeyOrigin) - confirmVerified(monkeyTarget) - } - - @Test - @Ignore("For some reason this is failing when merged to develop") - fun givenRejectRequestConfig_shouldRejectRequest() = runTest { - val config = ActionType.SendRequest(UserCount.single(), UserCount.single(), "wire.com", "wearezeta.com", 0u, false) - val monkeyPool = mockk() - val monkeyOrigin = mockk(relaxed = true) - val monkeyTarget = mockk(relaxed = true) - val coreLogic = mockk() - every { monkeyPool.randomLoggedInMonkeysFromTeam(config.originTeam, UserCount.single()) } returns listOf(monkeyOrigin) - every { monkeyPool.randomLoggedInMonkeysFromTeam(config.targetTeam, UserCount.single()) } returns listOf(monkeyTarget) - SendRequestAction(config).execute(coreLogic, monkeyPool) - coVerify(exactly = 1) { monkeyOrigin.sendRequest(monkeyTarget) } - coVerify(exactly = 1) { monkeyTarget.rejectRequest(monkeyOrigin) } - confirmVerified(monkeyOrigin) - confirmVerified(monkeyTarget) - } -}