From c27321d31a0bdca3612156cd21f64e5a192f7cff Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Fri, 23 Sep 2022 00:44:58 -0700 Subject: [PATCH] Added httpError function Renamed apiCall to getRawJoke Added HTTP Error Exception class Added models and exceptions --- README.md | 68 +++++++++ detekt-baseline.xml | 11 +- .../net/thauvin/erik/jokeapi/JokeApi.kt | 135 +++++++++++------- .../jokeapi/exceptions/HttpErrorException.kt | 45 ++++++ .../jokeapi/{ => exceptions}/JokeException.kt | 7 +- .../erik/jokeapi/{ => models}/Category.kt | 2 +- .../thauvin/erik/jokeapi/{ => models}/Flag.kt | 2 +- .../erik/jokeapi/{ => models}/Format.kt | 2 +- .../erik/jokeapi/{ => models}/IdRange.kt | 2 +- .../thauvin/erik/jokeapi/{ => models}/Joke.kt | 2 +- .../erik/jokeapi/{ => models}/Language.kt | 2 +- .../thauvin/erik/jokeapi/{ => models}/Type.kt | 2 +- .../net/thauvin/erik/jokeapi/JokeApiTest.kt | 116 +++++++++------ 13 files changed, 288 insertions(+), 108 deletions(-) create mode 100644 src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => exceptions}/JokeException.kt (91%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/Category.kt (98%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/Flag.kt (97%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/Format.kt (97%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/IdRange.kt (97%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/Joke.kt (97%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/Language.kt (97%) rename src/main/kotlin/net/thauvin/erik/jokeapi/{ => models}/Type.kt (97%) diff --git a/README.md b/README.md index 9098a4d..c049d8c 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,71 @@ data class Joke( 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, + 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" + +``` + + + + + diff --git a/detekt-baseline.xml b/detekt-baseline.xml index b268e77..997bf2d 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -2,10 +2,9 @@ - ComplexMethod:JokeApi.kt$JokeApi.Companion$@JvmStatic @JvmOverloads @Throws(IOException::class) fun apiCall( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Set<Type> = emptySet(), format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ): String - LongMethod:JokeApi.kt$JokeApi.Companion$@Throws(JokeException::class, IOException::class) private fun fetchUrl(url: String) : String - LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Set<Type> = emptySet(), format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ) - LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Set<Type> = emptySet(), search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false ) + ComplexMethod:JokeApi.kt$JokeApi.Companion$@JvmStatic @JvmOverloads @Throws(HttpErrorException::class, IOException::class) fun getRawJoke( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ): String + LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ) + LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = true ) LongParameterList:JokeException.kt$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 ) MagicNumber:JokeApi.kt$JokeApi.Companion$10 MagicNumber:JokeApi.kt$JokeApi.Companion$200 @@ -14,10 +13,10 @@ MagicNumber:JokeApi.kt$JokeApi.Companion$403 MagicNumber:JokeApi.kt$JokeApi.Companion$404 MagicNumber:JokeApi.kt$JokeApi.Companion$413 - MagicNumber:JokeApi.kt$JokeApi.Companion$428 + MagicNumber:JokeApi.kt$JokeApi.Companion$414 + MagicNumber:JokeApi.kt$JokeApi.Companion$429 MagicNumber:JokeApi.kt$JokeApi.Companion$500 MagicNumber:JokeApi.kt$JokeApi.Companion$523 - ThrowsCount:JokeApi.kt$JokeApi.Companion$@Throws(JokeException::class, IOException::class) private fun fetchUrl(url: String) : String UtilityClassWithPublicConstructor:JokeApi.kt$JokeApi diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt index 3dd7305..4f377f7 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt @@ -32,6 +32,15 @@ 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 java.io.IOException import java.net.HttpURLConnection @@ -45,12 +54,14 @@ import java.util.stream.Collectors class JokeApi { companion object { private const val API_URL = "https://v2.jokeapi.dev/joke/" + + @JvmStatic val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) } @JvmStatic @JvmOverloads - @Throws(IOException::class) - fun apiCall( + @Throws(HttpErrorException::class, IOException::class) + fun getRawJoke( categories: Set = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set = emptySet(), @@ -135,8 +146,8 @@ class JokeApi { return fetchUrl(urlBuilder.toString()) } - @Throws(JokeException::class, IOException::class) - private fun fetchUrl(url: String): String { + @Throws(HttpErrorException::class, IOException::class) + internal fun fetchUrl(url: String): String { logger.log(Level.FINE, url) val connection = URL(url).openConnection() as HttpURLConnection @@ -151,54 +162,72 @@ class JokeApi { } return body } else { - when (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") - } + throw httpError(connection.responseCode) } } + 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 @JvmOverloads - @Throws(JokeException::class, IOException::class) + @Throws(JokeException::class, HttpErrorException::class, IOException::class) fun getJoke( categories: Set = setOf(Category.ANY), language: Language = Language.ENGLISH, @@ -210,7 +239,17 @@ class JokeApi { splitNewLine: Boolean = true ): Joke { 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")) { val causedBy = json.getJSONArray("causedBy") val causes = MutableList(causedBy.length()) { i -> causedBy.getString(i) } diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt new file mode 100644 index 0000000..61107d5 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt @@ -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 + } +} diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt similarity index 91% rename from src/main/kotlin/net/thauvin/erik/jokeapi/JokeException.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt index b12fb1d..5d13923 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeException.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt @@ -30,7 +30,7 @@ * 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( val error: Boolean, @@ -47,8 +47,7 @@ class JokeException @JvmOverloads constructor( } 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)" } - - } diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/Category.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt similarity index 98% rename from src/main/kotlin/net/thauvin/erik/jokeapi/Category.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt index 1cb7b4e..7262868 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/Category.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt @@ -30,7 +30,7 @@ * 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. diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/Flag.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt similarity index 97% rename from src/main/kotlin/net/thauvin/erik/jokeapi/Flag.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt index f43bed7..00715ac 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/Flag.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt @@ -30,7 +30,7 @@ * 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. diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/Format.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt similarity index 97% rename from src/main/kotlin/net/thauvin/erik/jokeapi/Format.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt index 9784907..f0ac032 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/Format.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt @@ -30,7 +30,7 @@ * 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. diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/IdRange.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt similarity index 97% rename from src/main/kotlin/net/thauvin/erik/jokeapi/IdRange.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt index 84b4d66..07dcaa4 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/IdRange.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt @@ -30,6 +30,6 @@ * 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) diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/Joke.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt similarity index 97% rename from src/main/kotlin/net/thauvin/erik/jokeapi/Joke.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt index 4d4c709..07ec05d 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/Joke.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt @@ -30,7 +30,7 @@ * 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( val error: Boolean, diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/Language.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt similarity index 97% rename from src/main/kotlin/net/thauvin/erik/jokeapi/Language.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt index dd17c5c..ec99251 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/Language.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt @@ -30,7 +30,7 @@ * 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. diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/Type.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt similarity index 97% rename from src/main/kotlin/net/thauvin/erik/jokeapi/Type.kt rename to src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt index f74a33e..c21ce51 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/Type.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt @@ -30,7 +30,7 @@ * 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. diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeApiTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeApiTest.kt index 71dcb64..a75224c 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeApiTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeApiTest.kt @@ -32,9 +32,18 @@ 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.getRawJoke 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.assertEquals 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.Test 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.Level 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("\n\n false")) { "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 fun `Get Joke`() { val joke = getJoke() @@ -86,8 +66,7 @@ internal class JokeApiTest { { assertTrue(joke.joke.isNotEmpty()) { "joke should not be empty" } }, { assertTrue(joke.type == Type.TWOPART || joke.type == Type.SINGLE) { "type should validate" } }, { 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 @@ -98,8 +77,7 @@ internal class JokeApiTest { assertAll("joke by id", { assertTrue(joke.flags.contains(Flag.NSFW) && joke.flags.contains(Flag.EXPLICIT)) { "nsfw & explicit" } }, { 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 @@ -141,8 +119,7 @@ internal class JokeApiTest { logger.log(Level.FINE, joke.toString()) assertAll("safe joke", { assertTrue(joke.safe) { "should be safe" } }, - { assertTrue(joke.flags.isEmpty()) { "flags should be empty" } } - ) + { assertTrue(joke.flags.isEmpty()) { "flags should be empty" } }) } @Test @@ -158,8 +135,7 @@ internal class JokeApiTest { logger.log(Level.FINE, joke.toString()) assertAll("two-part joke", { 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 @@ -170,13 +146,55 @@ internal class JokeApiTest { 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("\n\n false")) { "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 { + 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 fun `Validate Joke Exception`() { val e = assertThrows { getJoke(categories = setOf(Category.CHRISTMAS), search = "foo") } logger.log(Level.FINE, e.debug()) - assertAll("exception validation", + assertAll("JokeException validation", { assertEquals(106, e.code) { "code should be valid" } }, { assertTrue(e.error) { "should be an 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" } }, { 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.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 { + 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 { @JvmStatic @BeforeAll