Improve and extend command tests
- Added param converter tests - Refactored test functions to have more descriptive names and have documentation and to make their behaviour more predictable (at a glance) - Implement separation of user error in the command system and make tests for it more specific - Improve handling of testing messages received from commands (like the help text)
This commit is contained in:
parent
2787c280f5
commit
702655cf9f
11 changed files with 131 additions and 40 deletions
|
@ -1,6 +1,7 @@
|
||||||
package buttondevteam.lib
|
package buttondevteam.lib
|
||||||
|
|
||||||
import buttondevteam.core.MainPlugin
|
import buttondevteam.core.MainPlugin
|
||||||
|
import buttondevteam.lib.test.TestException
|
||||||
import org.bukkit.Bukkit
|
import org.bukkit.Bukkit
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
|
@ -90,7 +91,7 @@ object ChromaUtils {
|
||||||
fun throwWhenTested(exception: Throwable, message: String) {
|
fun throwWhenTested(exception: Throwable, message: String) {
|
||||||
if (isTest) {
|
if (isTest) {
|
||||||
// Propagate exception back to the tests
|
// Propagate exception back to the tests
|
||||||
throw Exception(message, exception)
|
throw TestException(message, exception)
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we don't run the code directly, so we need to handle this here
|
// Otherwise we don't run the code directly, so we need to handle this here
|
||||||
TBMCCoreAPI.SendException(message, exception, MainPlugin.instance)
|
TBMCCoreAPI.SendException(message, exception, MainPlugin.instance)
|
||||||
|
|
|
@ -101,6 +101,8 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
*
|
*
|
||||||
* @param sender The sender who sent the command
|
* @param sender The sender who sent the command
|
||||||
* @param commandline The command line, including the leading command char
|
* @param commandline The command line, including the leading command char
|
||||||
|
*
|
||||||
|
* @return Whether the main command exists
|
||||||
*/
|
*/
|
||||||
open fun handleCommand(sender: TP, commandline: String): Boolean {
|
open fun handleCommand(sender: TP, commandline: String): Boolean {
|
||||||
val results = dispatcher.parse(commandline.removePrefix("/"), sender)
|
val results = dispatcher.parse(commandline.removePrefix("/"), sender)
|
||||||
|
@ -348,7 +350,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
val sender = context.source
|
val sender = context.source
|
||||||
|
|
||||||
if (!sd.hasPermission(sender)) {
|
if (!sd.hasPermission(sender)) {
|
||||||
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command")
|
CommandUtils.reportUserError(sender, "${ChatColor.RED}You don't have permission to use this command")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
// TODO: WIP
|
// TODO: WIP
|
||||||
|
@ -357,7 +359,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
if (convertedSender == null) {
|
if (convertedSender == null) {
|
||||||
//TODO: Should have a prettier display of Command2 classes here
|
//TODO: Should have a prettier display of Command2 classes here
|
||||||
val type = sd.senderType.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch }.trim()
|
val type = sd.senderType.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch }.trim()
|
||||||
sender.sendMessage("${ChatColor.RED}You need to be a $type to use this command.")
|
CommandUtils.reportUserError(sender, "${ChatColor.RED}You need to be a $type to use this command.")
|
||||||
executeHelpText(context) //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only
|
executeHelpText(context) //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -384,7 +386,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
?: error("No suitable converter found for ${argument.type} ${argument.name}")
|
?: error("No suitable converter found for ${argument.type} ${argument.name}")
|
||||||
val result = converter.converter.apply(userArgument)
|
val result = converter.converter.apply(userArgument)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
sender.sendMessage("${ChatColor.RED}Error: ${converter.errormsg}")
|
CommandUtils.reportUserError(sender, "${ChatColor.RED}Error: ${converter.errormsg}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
params.add(result)
|
params.add(result)
|
||||||
|
|
|
@ -137,14 +137,11 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterCommands(plugin: ButtonPlugin) {
|
fun unregisterCommands(plugin: ButtonPlugin) {
|
||||||
unregisterCommandIf({ node -> Optional.ofNullable(node.data.command).map { obj -> obj.plugin }.map { obj -> plugin == obj }.orElse(false) }, true)
|
unregisterCommandIf({ node -> node.data.command.plugin == plugin }, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterCommands(component: Component<*>) {
|
fun unregisterCommands(component: Component<*>) {
|
||||||
unregisterCommandIf({ node ->
|
unregisterCommandIf({ node -> node.data.command.component == component }, true)
|
||||||
Optional.ofNullable(node.data.command).map { obj: ICommand2MC -> obj.plugin }
|
|
||||||
.map { comp: ButtonPlugin -> component.javaClass.simpleName == comp.javaClass.simpleName }.orElse(false)
|
|
||||||
}, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleCommand(sender: Command2MCSender, commandline: String): Boolean {
|
override fun handleCommand(sender: Command2MCSender, commandline: String): Boolean {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package buttondevteam.lib.chat.commands
|
package buttondevteam.lib.chat.commands
|
||||||
|
|
||||||
|
import buttondevteam.lib.ChromaUtils
|
||||||
import buttondevteam.lib.chat.*
|
import buttondevteam.lib.chat.*
|
||||||
|
import buttondevteam.lib.test.TestCommandFailedException
|
||||||
import com.google.common.base.Defaults
|
import com.google.common.base.Defaults
|
||||||
import com.google.common.primitives.Primitives
|
import com.google.common.primitives.Primitives
|
||||||
import com.mojang.brigadier.builder.ArgumentBuilder
|
import com.mojang.brigadier.builder.ArgumentBuilder
|
||||||
|
@ -20,6 +22,14 @@ object CommandUtils {
|
||||||
.lowercase(Locale.getDefault())
|
.lowercase(Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the given message (with no additional formatting) to the given sender or throws a special exception during testing.
|
||||||
|
*/
|
||||||
|
fun reportUserError(sender: Command2Sender, message: String) {
|
||||||
|
if (ChromaUtils.isTest) throw TestCommandFailedException(message)
|
||||||
|
else sender.sendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the given action on the given node and all of its nodes recursively and creates new nodes.
|
* Performs the given action on the given node and all of its nodes recursively and creates new nodes.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package buttondevteam.lib.test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the command execution failed for some reason. Note that this doesn't necessarily mean the test failed.
|
||||||
|
*/
|
||||||
|
class TestCommandFailedException(message: String) : Exception(message)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package buttondevteam.lib.test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception that occurs just because we're running tests. It's used to test for various error cases.
|
||||||
|
*/
|
||||||
|
class TestException(message: String, cause: Throwable) : Exception(message, cause)
|
|
@ -122,6 +122,16 @@ abstract class Command2MCCommands {
|
||||||
@CommandClass
|
@CommandClass
|
||||||
object TestEmptyCommand : ICommand2MC()
|
object TestEmptyCommand : ICommand2MC()
|
||||||
|
|
||||||
|
@CommandClass
|
||||||
|
object TestParamConverterCommand : ICommand2MC(), ITestCommand2MC {
|
||||||
|
override var testCommandReceived: String? = null
|
||||||
|
|
||||||
|
@Command2.Subcommand
|
||||||
|
fun def(sender: Command2MCSender, something: TestConvertedParameter) {
|
||||||
|
testCommandReceived = something.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface ITestCommand2MC {
|
interface ITestCommand2MC {
|
||||||
var testCommandReceived: String?
|
var testCommandReceived: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,11 +57,21 @@ class Command2MCTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Order(5)
|
||||||
fun testHasPermission() {
|
fun testHasPermission() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Order(4)
|
||||||
fun testAddParamConverter() {
|
fun testAddParamConverter() {
|
||||||
|
TestParamConverterCommand.register()
|
||||||
|
ButtonPlugin.command2MC.addParamConverter(TestConvertedParameter::class.java, {
|
||||||
|
if (it == "test") null
|
||||||
|
else TestConvertedParameter(it)
|
||||||
|
}, "Failed to convert test param!") { arrayOf("test1", "test2").asIterable() }
|
||||||
|
val sender = createSender()
|
||||||
|
sender.assertCommand("/testparamconverter hmm", TestParamConverterCommand, "hmm")
|
||||||
|
sender.assertCommandUserError("/testparamconverter test", "§cError: §cFailed to convert test param!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -74,10 +84,9 @@ class Command2MCTest {
|
||||||
@Test
|
@Test
|
||||||
@Order(3)
|
@Order(3)
|
||||||
fun testHandleCommand() {
|
fun testHandleCommand() {
|
||||||
val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java)
|
val sender = createSender()
|
||||||
user.playerName = "TestPlayer"
|
sender.assertFailingCommand("/missingtest")
|
||||||
val sender = TestCommand2MCSender(user)
|
sender.assertFailingCommand("/erroringtest") // Tests completely missing the sender parameter
|
||||||
sender.runFailingCommand("/erroringtest") // Tests completely missing the sender parameter
|
|
||||||
testTestCommand(sender)
|
testTestCommand(sender)
|
||||||
testNoArgTestCommand(sender)
|
testNoArgTestCommand(sender)
|
||||||
testMultiArgTestCommand(sender)
|
testMultiArgTestCommand(sender)
|
||||||
|
@ -101,15 +110,21 @@ class Command2MCTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createSender(): TestCommand2MCSender {
|
||||||
|
val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java)
|
||||||
|
user.playerName = "TestPlayer"
|
||||||
|
return TestCommand2MCSender(user)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests parameter conversion, help text and errors.
|
* Tests parameter conversion, help text and errors.
|
||||||
*/
|
*/
|
||||||
private fun testTestCommand(sender: TestCommand2MCSender) {
|
private fun testTestCommand(sender: TestCommand2MCSender) {
|
||||||
sender.runCommand("/test hmm", TestCommand, "hmm")
|
sender.assertCommand("/test hmm", TestCommand, "hmm")
|
||||||
sender.runCommand("/test plugin Chroma-Core", TestCommand, "Chroma-Core")
|
sender.assertCommand("/test plugin Chroma-Core", TestCommand, "Chroma-Core")
|
||||||
sender.runCrashingCommand("/test playerfail TestPlayer") { it.cause?.message == "No suitable converter found for class buttondevteam.lib.player.TBMCPlayer param1" }
|
sender.assertCrashingCommand("/test playerfail TestPlayer") { it.cause?.message == "No suitable converter found for class buttondevteam.lib.player.TBMCPlayer param1" }
|
||||||
assertEquals("§cError: §cNo Chroma plugin found by that name.", sender.runCommandWithReceive("/test plugin asd"))
|
sender.assertCommandUserError("/test plugin asd", "§cError: §cNo Chroma plugin found by that name.")
|
||||||
sender.runCrashingCommand("/test errortest") { it.cause?.cause?.message === "Hmm" }
|
sender.assertCrashingCommand("/test errortest") { it.cause?.cause?.message === "Hmm" }
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Test command\n" +
|
"Test command\n" +
|
||||||
"Used for testing\n" +
|
"Used for testing\n" +
|
||||||
|
@ -124,29 +139,29 @@ class Command2MCTest {
|
||||||
* Tests having no arguments for the command and different sender types.
|
* Tests having no arguments for the command and different sender types.
|
||||||
*/
|
*/
|
||||||
private fun testNoArgTestCommand(sender: TestCommand2MCSender) {
|
private fun testNoArgTestCommand(sender: TestCommand2MCSender) {
|
||||||
sender.runCommand("/noargtest", NoArgTestCommand, "TestPlayer")
|
sender.assertCommand("/noargtest", NoArgTestCommand, "TestPlayer")
|
||||||
sender.runCrashingCommand("/noargtest failing") { it.cause?.cause is IllegalStateException }
|
sender.assertCommandReceiveMessage("/noargtest failing", "") // Help text
|
||||||
sender.runCommand("/noargtest sendertest", NoArgTestCommand, "TestPlayer")
|
sender.assertCommand("/noargtest sendertest", NoArgTestCommand, "TestPlayer")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests parameter type conversion with multiple (optional) parameters.
|
* Tests parameter type conversion with multiple (optional) parameters.
|
||||||
*/
|
*/
|
||||||
private fun testMultiArgTestCommand(sender: TestCommand2MCSender) {
|
private fun testMultiArgTestCommand(sender: TestCommand2MCSender) {
|
||||||
sender.runCommand("/multiargtest test hmm mhm", MultiArgTestCommand, "hmmmhm")
|
sender.assertCommand("/multiargtest test hmm mhm", MultiArgTestCommand, "hmmmhm")
|
||||||
sender.runCommand("/multiargtest test2 true 19", MultiArgTestCommand, "true 19")
|
sender.assertCommand("/multiargtest test2 true 19", MultiArgTestCommand, "true 19")
|
||||||
sender.runCommand("/multiargtest testoptional", MultiArgTestCommand, "false")
|
sender.assertCommand("/multiargtest testoptional", MultiArgTestCommand, "false")
|
||||||
sender.runCommand("/multiargtest testoptional true", MultiArgTestCommand, "true")
|
sender.assertCommand("/multiargtest testoptional true", MultiArgTestCommand, "true")
|
||||||
sender.runCommand("/multiargtest testoptionalmulti true teszt", MultiArgTestCommand, "true teszt")
|
sender.assertCommand("/multiargtest testoptionalmulti true teszt", MultiArgTestCommand, "true teszt")
|
||||||
sender.runCommand("/multiargtest testoptionalmulti true", MultiArgTestCommand, "true null")
|
sender.assertCommand("/multiargtest testoptionalmulti true", MultiArgTestCommand, "true null")
|
||||||
sender.runCommand("/multiargtest testoptionalmulti", MultiArgTestCommand, "false null")
|
sender.assertCommand("/multiargtest testoptionalmulti", MultiArgTestCommand, "false null")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests more type of parameters and wrong param type.
|
* Tests more type of parameters and wrong param type.
|
||||||
*/
|
*/
|
||||||
private fun testTestParamsCommand(sender: TestCommand2MCSender) {
|
private fun testTestParamsCommand(sender: TestCommand2MCSender) {
|
||||||
sender.runCommand("/testparams 12 34 56 78", TestParamsCommand, "12 34 56.0 78.0 Player0")
|
sender.assertCommand("/testparams 12 34 56 78", TestParamsCommand, "12 34 56.0 78.0 Player0")
|
||||||
assertEquals("§cExpected integer at position 11: ...estparams <--[HERE]", sender.runCommandWithReceive("/testparams asd 34 56 78"))
|
assertEquals("§cExpected integer at position 11: ...estparams <--[HERE]", sender.runCommandWithReceive("/testparams asd 34 56 78"))
|
||||||
// TODO: Change test when usage help is added
|
// TODO: Change test when usage help is added
|
||||||
}
|
}
|
||||||
|
@ -155,8 +170,8 @@ class Command2MCTest {
|
||||||
* Tests a command that has no default handler.
|
* Tests a command that has no default handler.
|
||||||
*/
|
*/
|
||||||
private fun testSomeCommand(sender: TestCommand2MCSender) {
|
private fun testSomeCommand(sender: TestCommand2MCSender) {
|
||||||
sender.runCommand("/some test cmd", TestNoMainCommand1, "TestPlayer")
|
sender.assertCommand("/some test cmd", TestNoMainCommand1, "TestPlayer")
|
||||||
sender.runCommand("/some another cmd", TestNoMainCommand2, "TestPlayer")
|
sender.assertCommand("/some another cmd", TestNoMainCommand2, "TestPlayer")
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"§6---- Subcommands ----\n" +
|
"§6---- Subcommands ----\n" +
|
||||||
"/some another cmd\n" +
|
"/some another cmd\n" +
|
||||||
|
|
|
@ -4,6 +4,7 @@ import buttondevteam.core.component.channel.Channel
|
||||||
import buttondevteam.lib.architecture.ButtonPlugin
|
import buttondevteam.lib.architecture.ButtonPlugin
|
||||||
import buttondevteam.lib.chat.Command2MCSender
|
import buttondevteam.lib.chat.Command2MCSender
|
||||||
import buttondevteam.lib.player.TBMCPlayer
|
import buttondevteam.lib.player.TBMCPlayer
|
||||||
|
import buttondevteam.lib.test.TestCommandFailedException
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFails
|
import kotlin.test.assertFails
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ class TestCommand2MCSender(user: TBMCPlayer) : Command2MCSender(user, Channel.gl
|
||||||
if (allowMessageReceive) {
|
if (allowMessageReceive) {
|
||||||
messageReceived += message + "\n"
|
messageReceived += message + "\n"
|
||||||
} else {
|
} else {
|
||||||
error(message)
|
error("Received unexpected message during test: $message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,25 +27,62 @@ class TestCommand2MCSender(user: TBMCPlayer) : Command2MCSender(user, Channel.gl
|
||||||
private fun withMessageReceive(action: () -> Unit): String {
|
private fun withMessageReceive(action: () -> Unit): String {
|
||||||
messageReceived = ""
|
messageReceived = ""
|
||||||
allowMessageReceive = true
|
allowMessageReceive = true
|
||||||
action()
|
val result = kotlin.runCatching { action() }
|
||||||
allowMessageReceive = false
|
allowMessageReceive = false
|
||||||
|
result.getOrThrow()
|
||||||
return messageReceived.trim()
|
return messageReceived.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runCommand(command: String, obj: Command2MCCommands.ITestCommand2MC, expected: String) {
|
private fun runCommand(command: String): Boolean {
|
||||||
assert(ButtonPlugin.command2MC.handleCommand(this, command)) { "Could not find command $command" }
|
return ButtonPlugin.command2MC.handleCommand(this, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertCommand(command: String) {
|
||||||
|
assert(runCommand(command)) { "Could not find command $command" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the command could be found and the command sets the appropriate property to the expected value.
|
||||||
|
*/
|
||||||
|
fun assertCommand(command: String, obj: Command2MCCommands.ITestCommand2MC, expected: String) {
|
||||||
|
assertCommand(command)
|
||||||
assertEquals(expected, obj.testCommandReceived)
|
assertEquals(expected, obj.testCommandReceived)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the command and returns any value sent to the sender.
|
||||||
|
*/
|
||||||
|
@Deprecated("This isn't an assertion function, use the assertion ones instead.")
|
||||||
fun runCommandWithReceive(command: String): String {
|
fun runCommandWithReceive(command: String): String {
|
||||||
return withMessageReceive { ButtonPlugin.command2MC.handleCommand(this, command) }
|
return withMessageReceive { runCommand(command) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runFailingCommand(command: String) {
|
/**
|
||||||
assert(!ButtonPlugin.command2MC.handleCommand(this, command)) { "Could execute command $command that shouldn't work" }
|
* Tests for when the command either runs successfully and sends back some text or displays help text.
|
||||||
|
*/
|
||||||
|
fun assertCommandReceiveMessage(command: String, expectedMessage: String) {
|
||||||
|
assertEquals(expectedMessage, withMessageReceive { assertCommand(command) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runCrashingCommand(command: String, errorCheck: (Throwable) -> Boolean) {
|
/**
|
||||||
assert(errorCheck(assertFails { ButtonPlugin.command2MC.handleCommand(this, command) })) { "Command exception failed test!" }
|
* Tests for when the command cannot be executed at all.
|
||||||
|
*/
|
||||||
|
fun assertFailingCommand(command: String) {
|
||||||
|
assert(!runCommand(command)) { "Could execute command $command that shouldn't work" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for when the command execution encounters an exception. This includes test exceptions as well.
|
||||||
|
*/
|
||||||
|
fun assertCrashingCommand(command: String, errorCheck: (Throwable) -> Boolean) {
|
||||||
|
val ex = assertFails { runCommand(command) }
|
||||||
|
assert(errorCheck(ex)) { "Command exception failed test! Exception: ${ex.stackTraceToString()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for expected user errors. Anything that results in the command system (and not the command itself) sending anything to the user.
|
||||||
|
*/
|
||||||
|
fun assertCommandUserError(command: String, message: String) {
|
||||||
|
assertCrashingCommand(command) { ex -> ex.cause?.let { it is TestCommandFailedException && it.message == message } ?: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package buttondevteam.lib.chat.test
|
||||||
|
|
||||||
|
class TestConvertedParameter(val value: String)
|
|
@ -0,0 +1,3 @@
|
||||||
|
package buttondevteam.lib.chat.test
|
||||||
|
|
||||||
|
class TestConvertedUser(val name: String)
|
Loading…
Reference in a new issue