Add mock tests

This commit is contained in:
Erik C. Thauvin 2025-05-09 10:54:14 -07:00
parent 1ec08e732b
commit 1ad447d661
Signed by: erik
GPG key ID: 776702A6A2DA330E
34 changed files with 816 additions and 226 deletions

View file

@ -125,7 +125,12 @@ public class MobibotBuild extends Project {
.include(dependency("net.thauvin.erik", "pinboard-poster", "1.2.1-SNAPSHOT"))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.6.0"));
scope(test)
// Mockito
.include(dependency("net.bytebuddy", "byte-buddy", version(1, 17, 5)))
.include(dependency("org.mockito.kotlin", "mockito-kotlin", version(5, 4, 0)))
// AssertK
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1)))
// JUnit
.include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2)))

View file

@ -14,12 +14,12 @@ import java.time.ZoneId
*/
object ReleaseInfo {
const val PROJECT = "mobibot"
const val VERSION = "0.8.0-rc+20250507140607"
const val VERSION = "0.8.0-rc+20250509075545"
@JvmField
@Suppress("MagicNumber")
val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(1746651967388L), ZoneId.systemDefault()
Instant.ofEpochMilli(1746802545281L), ZoneId.systemDefault()
)
const val WEBSITE = "https://mobitopia.org/mobibot/"

View file

@ -73,11 +73,11 @@ class Calc : AbstractModule() {
if (args.isNotBlank()) {
try {
event.respond(calculate(args))
} catch (e: IllegalArgumentException) {
if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e)
event.respond("No idea. This is the kind of math I don't get.")
} catch (e: UnknownFunctionOrVariableException) {
if (logger.isWarnEnabled) logger.warn("Unable to calculate: $args", e)
event.respond("No idea. This is the kind of math I don't get.")
} catch (e: IllegalArgumentException) {
if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e)
event.respond("No idea. I must've some form of Dyscalculia.")
}
} else {

View file

@ -50,47 +50,6 @@ class CryptoPrices : AbstractModule() {
override val name = "CryptoPrices"
/**
* Returns the cryptocurrency market price from
* [Coinbase](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price).
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (CURRENCIES.isEmpty()) {
try {
loadCurrencies()
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
}
}
val debugMessage = "crypto($cmd $args)"
if (args == CODES_KEYWORD) {
event.sendMessage("The supported currencies are:")
event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true)
} else if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try {
val price = currentPrice(args.split(' '))
val amount = try {
price.toCurrency()
} catch (_: IllegalArgumentException) {
price.amount
}
event.respond("${price.base} current price is $amount [${CURRENCIES[price.currency]}]")
} catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
e.message?.let {
event.respond(it)
}
} catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e)
event.respond("An IO error has occurred while retrieving the cryptocurrency market price.")
}
} else {
helpResponse(event)
}
}
companion object {
// Crypto command
private const val CRYPTO_CMD = "crypto"
@ -101,6 +60,9 @@ class CryptoPrices : AbstractModule() {
// Currency codes keyword
private const val CODES_KEYWORD = "codes"
// Default error message
const val DEFAULT_ERROR_MESSAGE = "An error has occurred while retrieving the cryptocurrency market price"
/**
* Get the current market price.
*/
@ -156,4 +118,47 @@ class CryptoPrices : AbstractModule() {
}
loadCurrencies()
}
/**
* Returns the cryptocurrency market price from
* [Coinbase](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price).
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (CURRENCIES.isEmpty()) {
try {
loadCurrencies()
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
}
}
val debugMessage = "crypto($cmd $args)"
if (args == CODES_KEYWORD) {
event.sendMessage("The supported currencies are:")
event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true)
} else if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try {
val price = currentPrice(args.split(' '))
val amount = try {
price.toCurrency()
} catch (_: IllegalArgumentException) {
price.amount
}
event.respond("${price.base} current price is $amount [${CURRENCIES[price.currency]}]")
} catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
if (e.message != null) {
event.respond("$DEFAULT_ERROR_MESSAGE: ${e.message}")
} else {
event.respond("$DEFAULT_ERROR_MESSAGE.")
}
} catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e)
event.respond("$DEFAULT_ERROR_MESSAGE: ${e.message}")
}
} else {
helpResponse(event)
}
}
}

View file

@ -48,7 +48,6 @@ import java.net.URL
import java.text.DecimalFormat
import java.util.*
/**
* Converts between currencies.
*/
@ -79,13 +78,19 @@ class CurrencyConverter : AbstractModule() {
// Currency symbols
private val SYMBOLS: TreeMap<String, String> = TreeMap()
/**
* No API key error message.
*/
const val ERROR_MESSAGE_NO_API_KEY = "No Exchange Rate API key specified."
/**
* Converts from a currency to another.
*/
@JvmStatic
fun convertCurrency(apiKey: String?, query: String): Message {
if (apiKey.isNullOrEmpty()) {
throw ModuleException("${CURRENCY_CMD}($query)", "No Exchange Rate API key specified.")
throw ModuleException("${CURRENCY_CMD}($query)", ERROR_MESSAGE_NO_API_KEY)
}
val cmds = query.split(" ")
@ -175,17 +180,22 @@ class CurrencyConverter : AbstractModule() {
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
reload(properties[API_KEY_PROP])
when {
SYMBOLS.isEmpty() -> {
event.respond(EMPTY_SYMBOLS_TABLE)
}
args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ (to|in) [a-zA-Z]{3}+".toRegex()) -> {
val msg = convertCurrency(properties[API_KEY_PROP], args)
event.respond(msg.msg)
if (msg.isError) {
helpResponse(event)
try {
val msg = convertCurrency(properties[API_KEY_PROP], args)
if (msg.isError) {
helpResponse(event)
} else {
event.respond(msg.msg)
}
} catch (e: ModuleException) {
if (LOGGER.isWarnEnabled) LOGGER.warn(e.debugMessage, e)
event.respond(e.message)
}
}

View file

@ -130,6 +130,9 @@ class Gemini2 : AbstractModule() {
e.message?.let {
event.respond(it)
}
} catch (e: NumberFormatException) {
if (logger.isErrorEnabled) logger.error("Invalid $MAX_TOKENS_PROP property.", e)
event.respond("The $name module is misconfigured.")
}
} else {
helpResponse(event)

View file

@ -31,7 +31,6 @@
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.ReleaseInfo
import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
@ -56,15 +55,24 @@ import java.net.URL
class GoogleSearch : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java)
override val name = "GoogleSearch"
override val name = SERVICE_NAME
companion object {
// Google API Key property
/**
* API Key property
*/
const val API_KEY_PROP = "google-api-key"
// Google Custom Search Engine ID property
/**
* Google Custom Search Engine ID property
*/
const val CSE_KEY_PROP = "google-cse-cx"
/**
* The service name
*/
const val SERVICE_NAME = "GoogleSearch"
// Google command
private const val GOOGLE_CMD = "google"
@ -82,7 +90,7 @@ class GoogleSearch : AbstractModule() {
if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) {
throw ModuleException(
"${GoogleSearch::class.java.name} is disabled.",
"${GOOGLE_CMD.capitalize()} is disabled. The API keys are missing."
"$SERVICE_NAME is disabled. The API keys are missing."
)
}
val results = mutableListOf<Message>()

View file

@ -89,16 +89,14 @@ class Joke : AbstractModule() {
* Returns a random joke from [JokeAPI](https://v2.jokeapi.dev/).
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
try {
randomJoke().forEach {
sendIRC().notice(channel, it.msg.colorize(it.color))
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
try {
randomJoke().forEach {
event.bot().sendIRC().notice(channel, it.msg.colorize(it.color))
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
}

View file

@ -74,11 +74,18 @@ class Mastodon : SocialModule() {
*/
@JvmStatic
@Throws(ModuleException::class)
fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
fun toot(accessToken: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
if (accessToken.isNullOrBlank()) {
throw ModuleException("Missing access token", "The access token is missing.")
} else if (instance.isNullOrBlank()) {
throw ModuleException("Missing instance", "The Mastodon instance is missing.")
} else if (isDm && handle.isNullOrBlank()) {
throw ModuleException("Missing handle", "The Mastodon handle is missing.")
}
val request = HttpRequest.newBuilder()
.uri(URI.create("https://$instance/api/v1/statuses"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $apiKey")
.header("Authorization", "Bearer $accessToken")
.POST(
HttpRequest.BodyPublishers.ofString(
JSONWriter.valueToString(
@ -89,8 +96,7 @@ class Mastodon : SocialModule() {
}
)
)
)
.build()
).build()
try {
val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
@ -105,7 +111,7 @@ class Mastodon : SocialModule() {
throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
}
} else {
throw IOException("Status Code: " + response.statusCode())
throw IOException("HTTP Status Code: " + response.statusCode())
}
} catch (e: IOException) {
throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
@ -142,7 +148,7 @@ class Mastodon : SocialModule() {
@Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String {
return toot(
apiKey = properties[ACCESS_TOKEN_PROP],
accessToken = properties[ACCESS_TOKEN_PROP],
instance = properties[INSTANCE_PROP],
handle = handle,
message = message,

View file

@ -96,7 +96,7 @@ class RockPaperScissors : AbstractModule() {
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
val hand = Hands.valueOf(cmd.uppercase())
val botHand = Hands.entries[(0..Hands.entries.size).random()]
val botHand = Hands.entries[(0..Hands.entries.size - 1).random()]
when {
hand == botHand -> {
event.respond("${hand.name} vs. ${botHand.name} » You ${"tie".bold()}.")

View file

@ -30,7 +30,6 @@
*/
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.reader
@ -54,7 +53,7 @@ import java.net.URL
class StockQuote : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java)
override val name = "StockQuote"
override val name = SERVICE_NAME
companion object {
/**
@ -67,6 +66,11 @@ class StockQuote : AbstractModule() {
*/
const val INVALID_SYMBOL = "Invalid symbol."
/**
* The service name.
*/
const val SERVICE_NAME = "StockQuote"
// API URL
private const val API_URL = "https://www.alphavantage.co/query?function="
@ -111,8 +115,8 @@ class StockQuote : AbstractModule() {
fun getQuote(symbol: String, apiKey: String?): List<Message> {
if (apiKey.isNullOrBlank()) {
throw ModuleException(
"${StockQuote::class.java.name} is disabled.",
"${STOCK_CMD.capitalize()} is disabled. The API key is missing."
"$SERVICE_NAME is disabled.",
"$SERVICE_NAME is disabled. The API key is missing."
)
}
val messages = mutableListOf<Message>()

View file

@ -56,7 +56,7 @@ import kotlin.math.roundToInt
class Weather2 : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java)
override val name = "Weather"
override val name = WEATHER_NAME
companion object {
/**
@ -64,6 +64,11 @@ class Weather2 : AbstractModule() {
*/
const val API_KEY_PROP = "owm-api-key"
/**
* The service name.
*/
const val WEATHER_NAME = "Weather"
// Weather command
private const val WEATHER_CMD = "weather"

View file

@ -42,25 +42,40 @@ import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.URL
/**
* Allows user to query Wolfram Alpha.
*/
class WolframAlpha : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(WolframAlpha::class.java)
override val name = "WolframAlpha"
override val name = SERVICE_NAME
companion object {
/**
* The Wolfram Alpha API Key property.
* The Wolfram Alpha AppID property.
*/
const val APPID_KEY_PROP = "wolfram-appid"
/**
* Metric unit
*/
const val METRIC = "metric"
/**
* Imperial unit
*/
const val IMPERIAL = "imperial"
/**
* The service name.
*/
const val SERVICE_NAME = "WolframAlpha"
/**
* The Wolfram units properties
*/
const val UNITS_PROP = "wolfram-units"
const val METRIC = "metric"
const val IMPERIAL = "imperial"
// Wolfram command
private const val WOLFRAM_CMD = "wolfram"
@ -79,17 +94,17 @@ class WolframAlpha : AbstractModule() {
throw ModuleException(
"wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ",
urlReader.body.ifEmpty {
"Looks like Wolfram Alpha isn't able to answer that. (${urlReader.responseCode})"
"Looks like $SERVICE_NAME isn't able to answer that. (${urlReader.responseCode})"
}
)
}
} catch (ioe: IOException) {
throw ModuleException(
"wolfram($query): IOE", "An IO Error occurred while querying Wolfram Alpha.", ioe
"wolfram($query): IOE", "An IO Error occurred while querying $SERVICE_NAME.", ioe
)
}
} else {
throw ModuleException("wolfram($query): No API Key", "No Wolfram Alpha API key specified.")
throw ModuleException("wolfram($query): No API Key", "No $SERVICE_NAME AppID specified.")
}
}
}

View file

@ -46,7 +46,9 @@ class PinboardTest : LocalProperties() {
private val pinboard = Pinboard().apply { setApiToken(apiToken) }
private fun newEntry(): EntryLink {
return EntryLink(randomUrl(), "Test Example", "ErikT", "", "#mobitopia", listOf("test"))
return EntryLink(
randomUrl(), "Test Example", "ErikT", "", "#mobitopia", listOf("test")
)
}
private fun randomUrl(): String {
@ -55,7 +57,9 @@ class PinboardTest : LocalProperties() {
private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean {
val response =
URL("https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()).reader().body
URL(
"https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()
).reader().body
matches.forEach {
if (!response.contains(it)) {

View file

@ -162,6 +162,7 @@ class UtilsTest {
assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
}
@Test
fun `Convert property to int using default value`() {
assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
@ -177,6 +178,7 @@ class UtilsTest {
val two = listOf("1", "2")
assertThat(two.lastOrEmpty(), "lastOrEmpty(1,2)").isEqualTo("2")
}
@Test
fun `Return empty if list only has one item`() {
val one = listOf("1")
@ -258,6 +260,7 @@ class UtilsTest {
fun `Convert string to int`() {
assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
}
@Test
fun `Convert string to int using default value`() {
assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
@ -314,11 +317,13 @@ class UtilsTest {
fun `Replace occurrences not found in string`() {
assertThat(test.replaceEach(search, replace), "replaceEach(nothing)").isEqualTo(test)
}
@Test
fun `Replace and remove occurrences in string`() {
assertThat(test.replaceEach(arrayOf("t", "e"), arrayOf("", "E")), "replaceEach($test)")
.isEqualTo(test.replace("t", "").replace("e", "E"))
}
@Test
fun `Replace empty occurrences in string`() {
assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")

View file

@ -37,37 +37,70 @@ import assertk.assertions.isInstanceOf
import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.modules.Calc.Companion.calculate
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class CalcTest {
@Test
fun `Calculate basic addition`() {
assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${2.bold()}")
@Nested
@DisplayName("Calculate Tests")
inner class CalculateTests {
@Test
fun `Calculate basic addition`() {
assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${2.bold()}")
}
@Test
fun `Calculate basic subtraction`() {
assertThat(calculate("1 -3"), "calculate(1-3)").isEqualTo("1-3 = ${(-2).bold()}")
}
@Test
fun `Calculate mathematical constants`() {
assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)").isEqualTo("pi+π+e+φ = ${"10.62".bold()}")
}
@Test
fun `Calculate scientific notations`() {
assertThat(calculate("3e2 - 3e1"), "calculate(3e2-3e1 )").isEqualTo("3e2-3e1 = ${"270".bold()}")
}
@Test
fun `Calculate trigonometric functions`() {
assertThat(calculate("3*sin(10)-cos(2)"), "calculate(3*sin(10)-cos(2)")
.isEqualTo("3*sin(10)-cos(2) = ${"-1.22".bold()}")
}
@Test
fun `Empty calculation should throw exception`() {
assertFailure { calculate(" ") }.isInstanceOf(IllegalArgumentException::class.java)
}
@Test
fun `Invalid calculation should throw exception`() {
assertFailure { calculate("a + b = c") }.isInstanceOf(UnknownFunctionOrVariableException::class.java)
}
}
@Test
fun `Calculate basic subtraction`() {
assertThat(calculate("1 -3"), "calculate(1-3)").isEqualTo("1-3 = ${(-2).bold()}")
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `Basic calculation`() {
val calc = Calc()
val event = Mockito.mock(GenericMessageEvent::class.java)
calc.commandResponse("channel", "calc", "1 + 1 * 2", event)
Mockito.verify(event, Mockito.times(1)).respond("1+1*2 = ${"3".bold()}")
}
@Test
fun `Calculate mathematical constants`() {
assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)").isEqualTo("pi+π+e+φ = ${"10.62".bold()}")
}
@Test
fun `Calculate scientific notations`() {
assertThat(calculate("3e2 - 3e1"), "calculate(3e2-3e1 )").isEqualTo("3e2-3e1 = ${"270".bold()}")
}
@Test
fun `Calculate trigonometric functions`() {
assertThat(calculate("3*sin(10)-cos(2)"), "calculate(3*sin(10)-cos(2)")
.isEqualTo("3*sin(10)-cos(2) = ${"-1.22".bold()}")
}
@Test
fun `Invalid calculation show throw exception`() {
assertFailure { calculate("one + one") }.isInstanceOf(UnknownFunctionOrVariableException::class.java)
@Test
fun `Invalid calculation`() {
val calc = Calc()
val event = Mockito.mock(GenericMessageEvent::class.java)
calc.commandResponse("channel", "calc", "two + two", event)
Mockito.verify(event, Mockito.times(1)).respond("No idea. This is the kind of math I don't get.")
}
}
}

View file

@ -32,26 +32,45 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.hasNoCause
import assertk.assertions.isInstanceOf
import assertk.assertions.contains
import assertk.assertions.hasNoCause
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.LocalProperties
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class ChatGpt2Test : LocalProperties() {
@Test
fun apiKey() {
assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) }
.isInstanceOf(ModuleException::class.java)
.hasNoCause()
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun moduleMisconfigured() {
val chatGpt2 = ChatGpt2()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
chatGpt2.commandResponse("channel", "chatgpt", "1 liter to gallon", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("The ${ChatGpt2.CHATGPT_NAME} module is misconfigured.")
}
}
@Nested
@DisplayName("Chat Tests")
inner class ChatTests {
@Test
fun apiKey() {
assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) }
.isInstanceOf(ModuleException::class.java)
.hasNoCause()
}
private val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
@Test

View file

@ -32,9 +32,7 @@ package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan
import assertk.assertions.prop
import assertk.assertions.*
import net.thauvin.erik.crypto.CryptoPrice
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.getCurrencyName
@ -42,15 +40,14 @@ import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.loadCurrencies
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import java.util.logging.ConsoleHandler
import java.util.logging.Level
import kotlin.test.Test
class CryptoPricesTest {
init {
loadCurrencies()
}
companion object {
@JvmStatic
@BeforeAll
@ -63,12 +60,71 @@ class CryptoPricesTest {
}
}
init {
loadCurrencies()
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `Current price for BTC`() {
val cryptoPrices = CryptoPrices()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
cryptoPrices.commandResponse("channel", "crypto", "btc", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).startsWith("BTC current price is $")
}
@Test
fun `Current price for BTC in EUR`() {
val cryptoPrices = CryptoPrices()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
cryptoPrices.commandResponse("channel", "crypto", "eth eur", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).matches(Regex("ETH current price is €.* \\[Euro]"))
}
@Test
fun `Invalid crypto symbol`() {
val cryptoPrices = CryptoPrices()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
cryptoPrices.commandResponse("channel", "crypto", "foo", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value)
.isEqualTo("${CryptoPrices.DEFAULT_ERROR_MESSAGE}: not found")
}
}
@Nested
@DisplayName("Currency Name Tests")
inner class CurrencyNameTests {
@Test
fun `Currency name for USD`() {
assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
}
@Test
fun `Currency name for EUR`() {
assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
}
}
@Nested
@DisplayName("Current Price Tests")
inner class CurrentPriceTests {
@Test
@Throws(ModuleException::class)
fun currentPriceBitcoin() {
fun `Current price for Bitcoin`() {
val price = currentPrice(listOf("BTC"))
assertThat(price, "currentPrice(BTC)").all {
prop(CryptoPrice::base).isEqualTo("BTC")
@ -79,7 +135,7 @@ class CryptoPricesTest {
@Test
@Throws(ModuleException::class)
fun currentPriceEthereum() {
fun `Current price for Ethereum in Euro`() {
val price = currentPrice(listOf("ETH", "EUR"))
assertThat(price, "currentPrice(ETH, EUR)").all {
prop(CryptoPrice::base).isEqualTo("ETH")
@ -88,18 +144,4 @@ class CryptoPricesTest {
}
}
}
@Nested
@DisplayName("Currency Name Tests")
inner class CurrencyNameTests {
@Test
fun getCurrencyNameUsd() {
assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
}
@Test
fun getCurrencyNameEur() {
assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
}
}
}

View file

@ -32,10 +32,7 @@ package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isInstanceOf
import assertk.assertions.matches
import assertk.assertions.prop
import assertk.assertions.*
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols
@ -44,6 +41,9 @@ import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.PublicMessage
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class CurrencyConverterTest : LocalProperties() {
@ -52,6 +52,37 @@ class CurrencyConverterTest : LocalProperties() {
loadSymbols(apiKey)
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `USD to CAD`() {
val currencyConverter = CurrencyConverter()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
currencyConverter.properties.put(
CurrencyConverter.API_KEY_PROP, getProperty(CurrencyConverter.API_KEY_PROP)
)
currencyConverter.commandResponse("channel", "currency", "1 USD to CAD", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).matches("1 United States Dollar = \\d+\\.\\d{2,3} Canadian Dollar".toRegex())
}
@Test
fun `API Key is not specified`() {
val currencyConverter = CurrencyConverter()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
currencyConverter.commandResponse("channel", "currency", "1 USD to CAD", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo(CurrencyConverter.ERROR_MESSAGE_NO_API_KEY)
}
}
@Nested
@DisplayName("Currency Converter Tests")
inner class CurrencyConverterTests {

View file

@ -35,42 +35,87 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.matches
import assertk.assertions.startsWith
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.RepeatedTest
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random
import kotlin.test.Test
class DiceTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `Roll die`() {
val dice = Dice()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
dice.commandResponse("channel", "dice", "", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).startsWith("you rolled")
}
@RepeatedTest(3)
fun `Roll die with 9 sides`() {
val dice = Dice()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
dice.commandResponse("channel", "dice", "1d9", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).matches("you rolled \u0002[1-9]\u0002".toRegex())
}
@RepeatedTest(3)
fun `Roll dice`() {
val dice = Dice()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
dice.commandResponse("channel", "dice", "2d6", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value)
.matches("you rolled \u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002\\d{1,2}\\u0002".toRegex())
}
}
@Nested
@DisplayName("Roll Tests")
inner class RollTests {
@Test
fun `Roll 1 die with 1 side`() {
assertThat(Dice.roll(1, 1)).isEqualTo("\u00021\u0002")
}
@Test
fun `Roll 1 die with 6 sides`() {
fun `Roll die`() {
assertThat(Dice.roll(1, 6)).matches("\u0002[1-6]\u0002".toRegex())
}
@Test
fun `Roll die with 1 side`() {
assertThat(Dice.roll(1, 1)).isEqualTo("\u00021\u0002")
}
@RepeatedTest(5)
fun `Roll 1 die with random sides`() {
fun `Roll die with random sides`() {
assertThat(Dice.roll(1, Random.nextInt(1, 11))).matches("\u0002([1-9]|10)\u0002".toRegex())
}
@Test
fun `Roll 2 dice`() {
assertThat(Dice.roll(2, 6))
.matches("\u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002[1-9][0-2]?\u0002".toRegex())
}
@Test
fun `Roll 2 dice with 1 side`() {
assertThat(Dice.roll(2, 1)).isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002")
}
@Test
fun `Roll 2 dice with 6 sides`() {
assertThat(Dice.roll(2, 6))
.matches("\u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002[1-9][0-2]?\u0002".toRegex())
}
@Test
fun `Roll 3 dice with 1 side`() {
assertThat(Dice.roll(4, 1))

View file

@ -37,6 +37,9 @@ import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.LocalProperties
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class Gemini2Test : LocalProperties() {
@ -67,6 +70,22 @@ class Gemini2Test : LocalProperties() {
}
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun moduleMisconfigured() {
val gemini2 = Gemini2()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
gemini2.commandResponse("channel", "gemini", "1 liter to gallon", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("The ${Gemini2.GEMINI_NAME} module is misconfigured.")
}
}
@Nested
@DisplayName("API Keys Test")
inner class ApiKeysTest {

View file

@ -42,9 +42,16 @@ import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.mockito.kotlin.whenever
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class GoogleSearchTest : LocalProperties() {
private val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
private val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
@Throws(ModuleException::class)
fun sanitizedSearch(query: String, apiKey: String, cseKey: String): List<Message> {
try {
@ -59,6 +66,47 @@ class GoogleSearchTest : LocalProperties() {
}
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `API Keys are missing`() {
val googleSearch = GoogleSearch()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
val user = Mockito.mock(org.pircbotx.User::class.java)
whenever(event.user).thenReturn(user)
whenever(user.nick).thenReturn("mock")
googleSearch.commandResponse("channel", "google", "seattle seahawks", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.isEqualTo("${GoogleSearch.SERVICE_NAME} is disabled. The API keys are missing.")
}
@Test
fun `No results found`() {
val googleSearch = GoogleSearch()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
val user = Mockito.mock(org.pircbotx.User::class.java)
whenever(event.user).thenReturn(user)
whenever(user.nick).thenReturn("mock")
googleSearch.properties.put(GoogleSearch.API_KEY_PROP, apiKey)
googleSearch.properties.put(GoogleSearch.CSE_KEY_PROP, cseKey)
googleSearch.commandResponse("channel", "google", "\"foobarbarfoofoobarblahblah\"", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.isEqualTo("\u000304No results found.\u000F")
}
}
@Nested
@DisplayName("API Keys Test")
inner class ApiKeysTest {
@ -85,9 +133,6 @@ class GoogleSearchTest : LocalProperties() {
@Nested
@DisplayName("Search Tests")
inner class SearchTests {
private val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
private val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
@Test
fun `Query should not be empty`() {
assertThat(sanitizedSearch("", apiKey, cseKey).first()).isInstanceOf(ErrorMessage::class.java)

View file

@ -33,13 +33,59 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.any
import assertk.assertions.contains
import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup
import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class LookupTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun lookupByHostname() {
val lookup = Lookup()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
lookup.commandResponse("channel", "lookup", "ec2-54-234-237-183.compute-1.amazonaws.com", event)
Mockito.verify(event, Mockito.times(1)).respondWith(captor.capture())
assertThat(captor.value).contains("54.234.237.183")
}
@Test
fun lookupByAddress() {
val lookup = Lookup()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
lookup.commandResponse("channel", "lookup", "54.234.237.183", event)
Mockito.verify(event, Mockito.times(1)).respondWith(captor.capture())
assertThat(captor.value).contains("ec2-54-234-237-183.compute-1.amazonaws.com")
}
@Test
fun lookupUnknownHostname() {
val lookup = Lookup()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
lookup.commandResponse("channel", "lookup", "foobar", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).isEqualTo("Unknown host.")
}
}
@Nested
@DisplayName("Lookup Tests")
inner class LookupTests {

View file

@ -30,25 +30,104 @@
*/
package net.thauvin.erik.mobibot.modules
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.mockito.kotlin.whenever
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class MastodonTest : LocalProperties() {
@Test
@Throws(ModuleException::class)
fun `Toot on Mastodon`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertThat(
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
getProperty(Mastodon.INSTANCE_PROP),
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
).contains(msg)
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `API Key is not specified`() {
val mastodon = Mastodon()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
val user = Mockito.mock(org.pircbotx.User::class.java)
whenever(event.user).thenReturn(user)
whenever(user.nick).thenReturn("mock")
mastodon.commandResponse("channel", "toot", "This is a test.", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("The access token is missing.")
}
}
@Nested
@DisplayName("Toot Tests")
inner class TootTests {
@Test
@Throws(ModuleException::class)
fun `Empty Access Token should throw exception`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertFailure {
toot(
"",
getProperty(Mastodon.INSTANCE_PROP),
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
}.isInstanceOf(ModuleException::class.java).hasMessage("The access token is missing.")
}
@Test
@Throws(ModuleException::class)
fun `Empty Handle should throw exception`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertFailure {
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
getProperty(Mastodon.INSTANCE_PROP),
"",
msg,
true
)
}.isInstanceOf(ModuleException::class.java).hasMessage("The Mastodon handle is missing.")
}
@Test
@Throws(ModuleException::class)
fun `Empty Instance should throw exception`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertFailure {
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
"",
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
}.isInstanceOf(ModuleException::class.java).hasMessage("The Mastodon instance is missing.")
}
@Test
@Throws(ModuleException::class)
fun `Toot on Mastodon`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertThat(
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
getProperty(Mastodon.INSTANCE_PROP),
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
).contains(msg)
}
}
}

View file

@ -74,7 +74,7 @@ class ModuleExceptionTest {
@Test
fun sanitizeMessage() {
val apiKey = "1234567890"
var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me"))
var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL https://foo.com?apiKey=$apiKey&userID=me"))
assertThat(
e.sanitize(apiKey, "", "me").message, "ModuleException(debugMessage, message, IOException(url))"
).isNotNull().all {

View file

@ -38,7 +38,7 @@ import kotlin.test.Test
class PingTest {
@Test
fun `Get a radon ping`() {
fun `Get a random ping`() {
for (i in 0..9) {
assertThat(Ping.PINGS, "Ping.PINGS[$i]").contains(randomPing())
}

View file

@ -33,12 +33,36 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.matches
import net.thauvin.erik.mobibot.modules.RockPaperScissors.Companion.winLoseOrDraw
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.RepeatedTest
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class RockPaperScissorsTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@RepeatedTest(3)
fun `Play Rock Paper Scissors`() {
val rockPaperScissors = RockPaperScissors()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
rockPaperScissors.commandResponse("channel", "rock", "", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.matches(
".* (vs\\.|crushes|covers|cuts) (ROCK|PAPER|SCISSORS) » You \u0002(tie|win|lose)\u0002.".toRegex()
)
}
}
@Nested
@DisplayName("Win, Lose or Draw Tests")
inner class WinLoseOrDrawTests {

View file

@ -39,6 +39,11 @@ import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.StockQuote.Companion.getQuote
import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class StockQuoteTest : LocalProperties() {
@ -61,55 +66,75 @@ class StockQuoteTest : LocalProperties() {
}
}
@Test
@Throws(ModuleException::class)
fun `API key should not be empty`() {
assertFailure { getSanitizedQuote("test", "") }.isInstanceOf(ModuleException::class.java)
.messageContains("disabled")
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `API Key is missing`() {
val stockQuote = StockQuote()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
@Test
@Throws(ModuleException::class)
fun `Symbol should not be empty`() {
assertThat(getSanitizedQuote("", "apikey").first(), "getQuote(empty)").all {
isInstanceOf(ErrorMessage::class.java)
prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
stockQuote.commandResponse("channel", "stock", "goog", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("${StockQuote.SERVICE_NAME} is disabled. The API key is missing.")
}
}
@Test
@Throws(ModuleException::class)
fun `Get stock quote for Apple`() {
val symbol = "apple inc"
val messages = getSanitizedQuote(symbol, apiKey)
assertThat(messages, "response not empty").isNotEmpty()
assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg)
.matches("Symbol: AAPL .*".toRegex())
assertThat(messages, "getQuote($symbol)").index(1).prop(Message::msg)
.matches(buildMatch("Price").toRegex())
assertThat(messages, "getQuote($symbol)").index(2).prop(Message::msg)
.matches(buildMatch("Previous").toRegex())
assertThat(messages, "getQuote($symbol)").index(3).prop(Message::msg)
.matches(buildMatch("Open").toRegex())
}
@Nested
@DisplayName("Get Quote Tests")
inner class GetQuoteTests {
@Test
@Throws(ModuleException::class)
fun `API key should not be empty`() {
assertFailure { getSanitizedQuote("test", "") }.isInstanceOf(ModuleException::class.java)
.messageContains("disabled")
}
@Test
@Throws(ModuleException::class)
fun `Get stock quote for Google`() {
val symbol = "goog"
val messages = getSanitizedQuote(symbol, apiKey)
assertThat(messages, "response not empty").isNotEmpty()
assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg)
.matches("Symbol: GOOG .*".toRegex())
}
@Test
@Throws(ModuleException::class)
fun `Symbol should not be empty`() {
assertThat(getSanitizedQuote("", "apikey").first(), "getQuote(empty)").all {
isInstanceOf(ErrorMessage::class.java)
prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
}
}
@Test
@Throws(ModuleException::class)
fun `Invalid symbol should throw exception`() {
val symbol = "foobar"
assertThat(getSanitizedQuote(symbol, apiKey).first(), "getQuote($symbol)").all {
isInstanceOf(ErrorMessage::class.java)
prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
@Test
@Throws(ModuleException::class)
fun `Get stock quote for Apple`() {
val symbol = "apple inc"
val messages = getSanitizedQuote(symbol, apiKey)
assertThat(messages, "response not empty").isNotEmpty()
assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg)
.matches("Symbol: AAPL .*".toRegex())
assertThat(messages, "getQuote($symbol)").index(1).prop(Message::msg)
.matches(buildMatch("Price").toRegex())
assertThat(messages, "getQuote($symbol)").index(2).prop(Message::msg)
.matches(buildMatch("Previous").toRegex())
assertThat(messages, "getQuote($symbol)").index(3).prop(Message::msg)
.matches(buildMatch("Open").toRegex())
}
@Test
@Throws(ModuleException::class)
fun `Get stock quote for Google`() {
val symbol = "goog"
val messages = getSanitizedQuote(symbol, apiKey)
assertThat(messages, "response not empty").isNotEmpty()
assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg)
.matches("Symbol: GOOG .*".toRegex())
}
@Test
@Throws(ModuleException::class)
fun `Invalid symbol should throw exception`() {
val symbol = "foobar"
assertThat(getSanitizedQuote(symbol, apiKey).first(), "getQuote($symbol)").all {
isInstanceOf(ErrorMessage::class.java)
prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
}
}
}
}

View file

@ -0,0 +1,54 @@
/*
* WarTest.kt
*
* Copyright 2004-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.matches
import org.junit.jupiter.api.RepeatedTest
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
class WarTest {
@RepeatedTest(3)
fun `Play war`() {
val war = War()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
war.commandResponse("channel", "war", "", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.matches("[\uD83C\uDCA0-\uD83C\uDCDF] {2}[\uD83C\uDCA0-\uD83C\uDCDF] {2}» .+(win|lose|tie).+!".toRegex())
}
}

View file

@ -45,9 +45,28 @@ import net.thauvin.erik.mobibot.modules.Weather2.Companion.mphToKmh
import net.thauvin.erik.mobibot.msg.Message
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class Weather2Test : LocalProperties() {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `API Key is missing`() {
val weather2 = Weather2()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
weather2.commandResponse("channel", "weather", "seattle, us", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("${Weather2.WEATHER_NAME} is disabled. The API key is missing.")
}
}
@Nested
@DisplayName("API Key Tests")
inner class ApiKeyTests {

View file

@ -35,6 +35,7 @@ import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
@ -45,10 +46,12 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import java.util.stream.Stream
import kotlin.test.Test
class WolframAlphaTest : LocalProperties() {
companion object {
@JvmStatic
@ -61,6 +64,22 @@ class WolframAlphaTest : LocalProperties() {
}
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun appIdNotSpecified() {
val wolframAlpha = WolframAlpha()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
wolframAlpha.commandResponse("channel", "wolfram", "1 liter to gallon", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("No ${WolframAlpha.SERVICE_NAME} AppID specified.")
}
}
@Nested
@DisplayName("App ID Tests")
inner class AppIdTests {

View file

@ -40,11 +40,31 @@ import net.thauvin.erik.mobibot.modules.WorldTime.Companion.COUNTRIES_MAP
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.time
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.Colors
import org.pircbotx.hooks.types.GenericMessageEvent
import java.time.ZoneId
import kotlin.test.Test
class WordTimeTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `Time in Tokyo`() {
val worldTime = WorldTime()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
worldTime.commandResponse("channel", "time", "jp", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.matches("The time is \u0002([01]\\d|2[0-3]):([0-5]\\d)\u0002 on .+ in \u0002Tokyo\u0002".toRegex())
}
}
@Nested
@DisplayName("Time Tests")
inner class TimeTests {