Added httpError function

Renamed apiCall to getRawJoke
Added HTTP Error Exception class
Added models and exceptions
This commit is contained in:
Erik C. Thauvin 2022-09-23 00:44:58 -07:00
parent 5ac9e751c3
commit c27321d31a
13 changed files with 288 additions and 108 deletions

View file

@ -26,3 +26,71 @@ data class Joke(
val language: Language val language: Language
) )
``` ```
If an error occurs, a `JokeException` is thrown:
```kotlin
class JokeException(
val error: Boolean,
val internalError: Boolean,
val code: Int,
message: String,
val causedBy: List<String>,
val additionalInfo: String,
val timestamp: Long,
cause: Throwable? = null
) : Exception(message, cause)
```
If an HTTP error occurs an `HttpErrorException` is thrown, with its message and cause matching the [JokeAPI status codes](https://sv443.net/jokeapi/v2/#status-codes):
```kotlin
class HttpErrorException(
val statusCode: Int,
message: String,
cause: Throwable? = null
) : IOException(message, cause)
```
## Gradle, Maven, etc.
To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
```gradle
dependencies {
implementation("net.thauvin.erik:jokeapi:0.9-SNAPSHOT")
}
```
Instructions for using with Maven, Ivy, etc. can be found on Maven Central.
## Raw Joke
You can also retrieve a raw joke in all [supported formats](https://jokeapi.dev/#format-param).
For example for YAML:
```kotlin
var joke = getRawJoke(format = Format.YAML, idRange = IdRange(22))
println(joke)
```
```yaml
error: false
category: "Programming"
type: "single"
joke: "If Bill Gates had a dime for every time Windows crashed ... Oh wait, he does."
flags:
nsfw: false
religious: false
political: false
racist: false
sexist: false
explicit: false
id: 22
safe: true
lang: "en"
```

View file

@ -2,10 +2,9 @@
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues>
<ID>ComplexMethod:JokeApi.kt$JokeApi.Companion$@JvmStatic @JvmOverloads @Throws(IOException::class) fun apiCall( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Set&lt;Type> = emptySet(), format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ): String</ID> <ID>ComplexMethod:JokeApi.kt$JokeApi.Companion$@JvmStatic @JvmOverloads @Throws(HttpErrorException::class, IOException::class) fun getRawJoke( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ): String</ID>
<ID>LongMethod:JokeApi.kt$JokeApi.Companion$@Throws(JokeException::class, IOException::class) private fun fetchUrl(url: String) : String</ID> <ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, )</ID>
<ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Set&lt;Type> = emptySet(), format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, )</ID> <ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = true )</ID>
<ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Set&lt;Type> = emptySet(), search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false )</ID>
<ID>LongParameterList:JokeException.kt$JokeException$( val error: Boolean, val internalError: Boolean, val code: Int, message: String, val causedBy: List&lt;String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID> <ID>LongParameterList:JokeException.kt$JokeException$( val error: Boolean, val internalError: Boolean, val code: Int, message: String, val causedBy: List&lt;String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$10</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$10</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$200</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$200</ID>
@ -14,10 +13,10 @@
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$403</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$403</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$404</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$404</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$413</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$413</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$428</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$414</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$429</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$500</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$500</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$523</ID> <ID>MagicNumber:JokeApi.kt$JokeApi.Companion$523</ID>
<ID>ThrowsCount:JokeApi.kt$JokeApi.Companion$@Throws(JokeException::class, IOException::class) private fun fetchUrl(url: String) : String</ID>
<ID>UtilityClassWithPublicConstructor:JokeApi.kt$JokeApi</ID> <ID>UtilityClassWithPublicConstructor:JokeApi.kt$JokeApi</ID>
</CurrentIssues> </CurrentIssues>
</SmellBaseline> </SmellBaseline>

View file

@ -32,6 +32,15 @@
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
@ -45,12 +54,14 @@ import java.util.stream.Collectors
class JokeApi { class JokeApi {
companion object { companion object {
private const val API_URL = "https://v2.jokeapi.dev/joke/" private const val API_URL = "https://v2.jokeapi.dev/joke/"
@JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) } val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@Throws(IOException::class) @Throws(HttpErrorException::class, IOException::class)
fun apiCall( fun getRawJoke(
categories: Set<Category> = setOf(Category.ANY), categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH, language: Language = Language.ENGLISH,
flags: Set<Flag> = emptySet(), flags: Set<Flag> = emptySet(),
@ -135,8 +146,8 @@ class JokeApi {
return fetchUrl(urlBuilder.toString()) return fetchUrl(urlBuilder.toString())
} }
@Throws(JokeException::class, IOException::class) @Throws(HttpErrorException::class, IOException::class)
private fun fetchUrl(url: String): String { internal fun fetchUrl(url: String): String {
logger.log(Level.FINE, url) logger.log(Level.FINE, url)
val connection = URL(url).openConnection() as HttpURLConnection val connection = URL(url).openConnection() as HttpURLConnection
@ -151,54 +162,72 @@ class JokeApi {
} }
return body return body
} else { } else {
when (connection.responseCode) { throw httpError(connection.responseCode)
400 -> throw IOException(
"400: Bad Request", IOException(
"The request you have sent to JokeAPI is formatted incorrectly and cannot be processed"
)
)
403 -> throw IOException(
"4o3: Forbidden", IOException(
"You have been added to the blacklist due to malicious behavior and are not allowed" + " to send requests to JokeAPI anymore"
)
)
404 -> throw IOException(
"404: Not Found", IOException("The URL you have requested couldn't be found")
)
413 -> throw IOException(
"413: Payload Too Large",
IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes")
)
428 -> throw IOException(
"429: Too Many Requests", IOException(
"You have exceeded the limit of 120 requests per minute and have to wait a bit" + " until you are allowed to send requests again"
)
)
500 -> throw IOException(
"500: Internal Server Error", IOException(
"There was a general internal error within JokeAPI. You can get more info from" + " the properties in the response text"
)
)
523 -> throw IOException(
"523: Origin Unreachable", IOException(
"The server is temporarily offline due to maintenance or a dynamic IP update." + " Please be patient in this case."
)
)
else -> throw IOException("${connection.responseCode}: Unknown Error")
}
} }
} }
private fun httpError(responseCode: Int): HttpErrorException {
val httpException: HttpErrorException
when (responseCode) {
400 -> httpException = HttpErrorException(
responseCode, "Bad Request", IOException(
"The request you have sent to JokeAPI is formatted incorrectly and cannot be processed."
)
)
403 -> httpException = HttpErrorException(
responseCode, "Forbidden", IOException(
"You have been added to the blacklist due to malicious behavior and are not allowed"
+ " to send requests to JokeAPI anymore."
)
)
404 -> httpException = HttpErrorException(
responseCode, "Not Found", IOException("The URL you have requested couldn't be found.")
)
413 -> httpException = HttpErrorException(
responseCode,
"URI Too Long",
IOException("The URL exceeds the maximum length of 250 characters.")
)
414 -> httpException = HttpErrorException(
responseCode,
"Payload Too Large",
IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes.")
)
429 -> httpException = HttpErrorException(
responseCode, "Too Many Requests", IOException(
"You have exceeded the limit of 120 requests per minute and have to wait a bit" +
" until you are allowed to send requests again."
)
)
500 -> httpException = HttpErrorException(
responseCode, "Internal Server Error", IOException(
"There was a general internal error within JokeAPI. You can get more info from" +
" the properties in the response text."
)
)
523 -> httpException = HttpErrorException(
responseCode, "Origin Unreachable", IOException(
"The server is temporarily offline due to maintenance or a dynamic IP update." +
" Please be patient in this case."
)
)
else -> httpException = HttpErrorException(responseCode, "Unknown HTTP Error")
}
return httpException
}
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@Throws(JokeException::class, IOException::class) @Throws(JokeException::class, HttpErrorException::class, IOException::class)
fun getJoke( fun getJoke(
categories: Set<Category> = setOf(Category.ANY), categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH, language: Language = Language.ENGLISH,
@ -210,7 +239,17 @@ class JokeApi {
splitNewLine: Boolean = true splitNewLine: Boolean = true
): Joke { ): Joke {
val json = val json =
JSONObject(apiCall(categories, language, flags, type, search = search, idRange = idRange, safe = safe)) JSONObject(
getRawJoke(
categories,
language,
flags,
type,
search = search,
idRange = idRange,
safe = safe
)
)
if (json.getBoolean("error")) { if (json.getBoolean("error")) {
val causedBy = json.getJSONArray("causedBy") val causedBy = json.getJSONArray("causedBy")
val causes = MutableList<String>(causedBy.length()) { i -> causedBy.getString(i) } val causes = MutableList<String>(causedBy.length()) { i -> causedBy.getString(i) }

View file

@ -0,0 +1,45 @@
/*
* HttpErrorException.kt
*
* Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* 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.jokeapi.exceptions
import java.io.IOException
class HttpErrorException @JvmOverloads constructor(
val statusCode: Int,
message: String,
cause: Throwable? = null
) : IOException(message, cause) {
companion object {
private const val serialVersionUID = 1L
}
}

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.exceptions
class JokeException @JvmOverloads constructor( class JokeException @JvmOverloads constructor(
val error: Boolean, val error: Boolean,
@ -47,8 +47,7 @@ class JokeException @JvmOverloads constructor(
} }
fun debug(): String { fun debug(): String {
return "JokeException(message=$message, error=$error, internalError=$internalError, code=$code, causedBy=$causedBy, additionalInfo='$additionalInfo', timestamp=$timestamp)" return "JokeException(message=$message, error=$error, internalError=$internalError, code=$code," +
" causedBy=$causedBy, additionalInfo='$additionalInfo', timestamp=$timestamp)"
} }
} }

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
/** /**
* Categories and aliases. * Categories and aliases.

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
/** /**
* Blacklist flags. * Blacklist flags.

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
/** /**
* Response formats. * Response formats.

View file

@ -30,6 +30,6 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
data class IdRange(val start: Int = -1, val end: Int = -1) data class IdRange(val start: Int = -1, val end: Int = -1)

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
data class Joke( data class Joke(
val error: Boolean, val error: Boolean,

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
/** /**
* Supported languages. * Supported languages.

View file

@ -30,7 +30,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi.models
/** /**
* Joke types. * Joke types.

View file

@ -32,9 +32,18 @@
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.JokeApi.Companion.apiCall import net.thauvin.erik.jokeapi.JokeApi.Companion.fetchUrl
import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
import net.thauvin.erik.jokeapi.JokeApi.Companion.getRawJoke
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
@ -42,41 +51,12 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.util.logging.ConsoleHandler import java.util.logging.ConsoleHandler
import java.util.logging.Level import java.util.logging.Level
internal class JokeApiTest { internal class JokeApiTest {
@Test
fun `API call with TXT`() {
val response = apiCall(format = Format.TEXT)
logger.log(Level.FINE, response)
assertAll("plain text",
{ assertTrue(response.isNotEmpty()) { "should be not empty" } },
{ assertFalse(response.startsWith("Error ")) { "should not be an error" } }
)
}
@Test
fun `API Call with invalid ID Range`() {
val response = apiCall(format = Format.TXT, idRange = IdRange(0, 30000))
logger.log(Level.FINE, response)
assertTrue(response.startsWith("Error ")) { "should be an error" }
}
@Test
fun `API Call with XML`() {
val response = apiCall(format = Format.XML)
logger.log(Level.FINE, response)
assertTrue(response.startsWith("<?xml version='1.0'?>\n<data>\n <error>false</error>")) { "should be xml" }
}
@Test
fun `API Call with YAML`() {
val response = apiCall(format = Format.YAML)
logger.log(Level.FINE, response)
assertTrue(response.startsWith("error: false")) { "should be yaml" }
}
@Test @Test
fun `Get Joke`() { fun `Get Joke`() {
val joke = getJoke() val joke = getJoke()
@ -86,8 +66,7 @@ internal class JokeApiTest {
{ assertTrue(joke.joke.isNotEmpty()) { "joke should not be empty" } }, { assertTrue(joke.joke.isNotEmpty()) { "joke should not be empty" } },
{ assertTrue(joke.type == Type.TWOPART || joke.type == Type.SINGLE) { "type should validate" } }, { assertTrue(joke.type == Type.TWOPART || joke.type == Type.SINGLE) { "type should validate" } },
{ assertTrue(joke.id >= 0) { "id should be >= 0" } }, { assertTrue(joke.id >= 0) { "id should be >= 0" } },
{ assertEquals(Language.EN, joke.language) { "language should be english" } } { assertEquals(Language.EN, joke.language) { "language should be english" } })
)
} }
@Test @Test
@ -98,8 +77,7 @@ internal class JokeApiTest {
assertAll("joke by id", assertAll("joke by id",
{ assertTrue(joke.flags.contains(Flag.NSFW) && joke.flags.contains(Flag.EXPLICIT)) { "nsfw & explicit" } }, { assertTrue(joke.flags.contains(Flag.NSFW) && joke.flags.contains(Flag.EXPLICIT)) { "nsfw & explicit" } },
{ assertEquals(172, joke.id) { "id is $id" } }, { assertEquals(172, joke.id) { "id is $id" } },
{ assertEquals(Category.PUN, joke.category) { "category should be pun" } } { assertEquals(Category.PUN, joke.category) { "category should be pun" } })
)
} }
@Test @Test
@ -141,8 +119,7 @@ internal class JokeApiTest {
logger.log(Level.FINE, joke.toString()) logger.log(Level.FINE, joke.toString())
assertAll("safe joke", assertAll("safe joke",
{ assertTrue(joke.safe) { "should be safe" } }, { assertTrue(joke.safe) { "should be safe" } },
{ assertTrue(joke.flags.isEmpty()) { "flags should be empty" } } { assertTrue(joke.flags.isEmpty()) { "flags should be empty" } })
)
} }
@Test @Test
@ -158,8 +135,7 @@ internal class JokeApiTest {
logger.log(Level.FINE, joke.toString()) logger.log(Level.FINE, joke.toString())
assertAll("two-part joke", assertAll("two-part joke",
{ assertEquals(Type.TWOPART, joke.type) { "type should be two-part" } }, { assertEquals(Type.TWOPART, joke.type) { "type should be two-part" } },
{ assertTrue(joke.joke.size > 1) { "should have multiple lines" } } { assertTrue(joke.joke.size > 1) { "should have multiple lines" } })
)
} }
@Test @Test
@ -170,13 +146,55 @@ internal class JokeApiTest {
assertEquals(id, joke.id) { "id should be 1" } assertEquals(id, joke.id) { "id should be 1" }
} }
@Test
fun `Get Raw Joke with TXT`() {
val response = getRawJoke(format = Format.TEXT)
logger.log(Level.FINE, response)
assertAll("plain text",
{ assertTrue(response.isNotEmpty()) { "should be not empty" } },
{ assertFalse(response.startsWith("Error ")) { "should not be an error" } })
}
@Test
fun `Get Raw Joke with invalid ID Range`() {
val response = getRawJoke(format = Format.TXT, idRange = IdRange(0, 30000))
logger.log(Level.FINE, response)
assertTrue(response.startsWith("Error ")) { "should be an error" }
}
@Test
fun `Get Raw Joke with XML`() {
val response = getRawJoke(format = Format.XML)
logger.log(Level.FINE, response)
assertTrue(response.startsWith("<?xml version='1.0'?>\n<data>\n <error>false</error>")) { "should be xml" }
}
@Test
fun `Get Raw Joke with YAML`() {
val response = getRawJoke(format = Format.YAML)
logger.log(Level.FINE, response)
assertTrue(response.startsWith("error: false")) { "should be yaml" }
}
@Test
fun `Fetch Invalid URL`() {
val statusCode = 999
val e = assertThrows<HttpErrorException> {
fetchUrl("https://httpstat.us/$statusCode")
}
assertAll("JokeException validation",
{ assertEquals(statusCode, e.statusCode) { "status code should be $statusCode" } },
{ assertTrue(e.message!!.isNotEmpty()) { "message should not be empty" } },
{ assertTrue(e.cause == null) { "cause should be null" } })
}
@Test @Test
fun `Validate Joke Exception`() { fun `Validate Joke Exception`() {
val e = assertThrows<JokeException> { val e = assertThrows<JokeException> {
getJoke(categories = setOf(Category.CHRISTMAS), search = "foo") getJoke(categories = setOf(Category.CHRISTMAS), search = "foo")
} }
logger.log(Level.FINE, e.debug()) logger.log(Level.FINE, e.debug())
assertAll("exception validation", assertAll("JokeException validation",
{ assertEquals(106, e.code) { "code should be valid" } }, { assertEquals(106, e.code) { "code should be valid" } },
{ assertTrue(e.error) { "should be an error" } }, { assertTrue(e.error) { "should be an error" } },
{ assertFalse(e.internalError) { "should not be internal error" } }, { assertFalse(e.internalError) { "should not be internal error" } },
@ -184,10 +202,22 @@ internal class JokeApiTest {
{ assertEquals(1, e.causedBy.size) { "causedBy size should be 1" } }, { assertEquals(1, e.causedBy.size) { "causedBy size should be 1" } },
{ assertTrue(e.causedBy[0].startsWith("No jokes")) { "causedBy should start with no jokes" } }, { assertTrue(e.causedBy[0].startsWith("No jokes")) { "causedBy should start with no jokes" } },
{ assertTrue(e.additionalInfo.isNotEmpty()) { "additional info should not be empty" } }, { assertTrue(e.additionalInfo.isNotEmpty()) { "additional info should not be empty" } },
{ assertTrue(e.timestamp > 0) { "timestamp should be > 0" } } { assertTrue(e.timestamp > 0) { "timestamp should be > 0" } })
)
} }
@ParameterizedTest
@ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523])
fun `Validate HTTP Error Exceptions`(input: Int) {
val e = assertThrows<HttpErrorException> {
fetchUrl("https://httpstat.us/$input")
}
assertAll("JokeException validation",
{ assertEquals(input, e.statusCode) { "status code should be $input" } },
{ assertTrue(e.message!!.isNotEmpty()) { "message for $input should not be empty" } },
{ assertTrue(e.cause!!.message!!.isNotEmpty()) { "cause of $input should not be empty" } })
}
companion object { companion object {
@JvmStatic @JvmStatic
@BeforeAll @BeforeAll