diff --git a/.circleci/config.yml b/.circleci/config.yml index 77889be..8dabc3f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,8 @@ -version: 2 +version: 2.1 + +orbs: + sdkman: joshdholtz/sdkman@0.2.0 + defaults: &defaults working_directory: ~/repo environment: @@ -6,18 +10,31 @@ defaults: &defaults TERM: dumb CI_NAME: "CircleCI" -defaults_gradle: &defaults_bld - steps: - - checkout - - run: - name: Download the bld dependencies - command: ./bld download - - run: - name: Compile source with bld - command: ./bld compile - - run: - name: Run tests with bld - command: ./bld test +commands: + build_and_test: + parameters: + reports-dir: + type: string + default: "build/reports/test_results" + steps: + - checkout + - sdkman/setup-sdkman + - sdkman/sdkman-install: + candidate: kotlin + version: 2.0.20 + - run: + name: Download dependencies + command: ./bld download + - run: + name: Compile source + command: ./bld compile + - run: + name: Run tests + command: ./bld jacoco -reports-dir=<< parameters.reports-dir >> + - store_test_results: + path: << parameters.reports-dir >> + - store_artifacts: + path: build/reports/jacoco/test/html jobs: bld_jdk17: @@ -26,7 +43,8 @@ jobs: docker: - image: cimg/openjdk:17.0 - <<: *defaults_bld + steps: + - build_and_test bld_jdk20: <<: *defaults @@ -34,10 +52,10 @@ jobs: docker: - image: cimg/openjdk:20.0 - <<: *defaults_bld + steps: + - build_and_test workflows: - version: 2 bld: jobs: - bld_jdk17 diff --git a/.github/workflows/bld.yml b/.github/workflows/bld.yml index ba51eb1..007e63a 100644 --- a/.github/workflows/bld.yml +++ b/.github/workflows/bld.yml @@ -2,48 +2,50 @@ name: bld-ci on: [ push, pull_request, workflow_dispatch ] +env: + COVERAGE_JDK: "21" + COVERAGE_KOTLIN: "2.1.20" + jobs: build-bld-project: - runs-on: ubuntu-latest - - env: - COVERAGE_SDK: "17" - strategy: matrix: - java-version: [ 17, 20 ] + java-version: [ 17, 21, 24 ] + kotlin-version: [ 1.9.25, 2.0.21, 2.1.20 ] + os: [ ubuntu-latest, windows-latest, macos-latest ] + + runs-on: ${{ matrix.os }} steps: - name: Checkout source repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up JDK ${{ matrix.java-version }} - uses: actions/setup-java@v3 + - name: Set up JDK ${{ matrix.java-version }} with Kotlin ${{ matrix.kotlin-version }} + uses: actions/setup-java@v4 with: - distribution: 'zulu' + distribution: "zulu" java-version: ${{ matrix.java-version }} - - name: Grant bld execute permission - run: chmod +x bld - - - name: Download the bld dependencies + - name: Download dependencies run: ./bld download - - name: Compile source with bld + - name: Compile source run: ./bld compile - - name: Run tests with bld + - name: Run tests run: ./bld jacoco - name: Remove pom.xml - if: success() && matrix.java-version == env.COVERAGE_SDK + if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN + && matrix.os == 'ubuntu-latest' run: rm -rf pom.xml - name: SonarCloud Scan uses: sonarsource/sonarcloud-github-action@master - if: success() && matrix.java-version == env.COVERAGE_SDK + if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN + && matrix.os == 'ubuntu-latest' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.idea/bld.xml b/.idea/bld.xml new file mode 100644 index 0000000..6600cee --- /dev/null +++ b/.idea/bld.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/intellij-javadocs-4.0.1.xml b/.idea/intellij-javadocs-4.0.1.xml new file mode 100644 index 0000000..4b17413 --- /dev/null +++ b/.idea/intellij-javadocs-4.0.1.xml @@ -0,0 +1,204 @@ + + + + + UPDATE + false + true + + TYPE + METHOD + FIELD + + + DEFAULT + PROTECTED + PUBLIC + + + + + + ^.*(public|protected|private)*.+interface\s+\w+.* + /**\n + * The interface ${name}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> + */ + + + ^.*(public|protected|private)*.+enum\s+\w+.* + /**\n + * The enum ${name}.\n + */ + + + ^.*(public|protected|private)*.+class\s+\w+.* + /**\n + * The type ${name}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> + */ + + + .+ + /**\n + * The type ${name}.\n + */ + + + + + .+ + /**\n + * Instantiates a new ${name}.\n +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + + + ^.*(public|protected|private)*\s*.*(\w(\s*<.+>)*)+\s+get\w+\s*\(.*\).+ + /**\n + * Gets ${partName}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if isNotVoid> + *\n + * @return the ${partName}\n +</#if> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + ^.*(public|protected|private)*\s*.*(void|\w(\s*<.+>)*)+\s+set\w+\s*\(.*\).+ + /**\n + * Sets ${partName}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if isNotVoid> + *\n + * @return the ${partName}\n +</#if> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + ^.*((public\s+static)|(static\s+public))\s+void\s+main\s*\(\s*String\s*(\[\s*\]|\.\.\.)\s+\w+\s*\).+ + /**\n + * The entry point of application.\n + + <#if element.parameterList.parameters?has_content> + *\n +</#if> + * @param ${element.parameterList.parameters[0].name} the input arguments\n +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + .+ + /**\n + * ${name}<#if isNotVoid> ${return}</#if>.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if isNotVoid> + *\n + * @return the ${return}\n +</#if> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + + + ^.*(public|protected|private)*.+static.*(\w\s\w)+.+ + /**\n + * The constant ${element.getName()}.\n + */ + + + ^.*(public|protected|private)*.*(\w\s\w)+.+ + /**\n + <#if element.parent.isInterface()> + * The constant ${element.getName()}.\n +<#else> + * The ${name}.\n +</#if> */ + + + .+ + /**\n + <#if element.parent.isEnum()> + *${name} ${typeName}.\n +<#else> + * The ${name}.\n +</#if>*/ + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..a8d9757 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - - \ No newline at end of file + diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml index ca84ff0..153a060 100644 --- a/.idea/libraries/bld.xml +++ b/.idea/libraries/bld.xml @@ -2,12 +2,12 @@ - + - + diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml index 9bd86aa..99cc0c0 100644 --- a/.idea/libraries/compile.xml +++ b/.idea/libraries/compile.xml @@ -7,7 +7,7 @@ - - + + \ No newline at end of file diff --git a/.idea/libraries/runtime.xml b/.idea/libraries/runtime.xml index 2ae5c4b..d4069f2 100644 --- a/.idea/libraries/runtime.xml +++ b/.idea/libraries/runtime.xml @@ -8,7 +8,7 @@ - - + + \ No newline at end of file diff --git a/.idea/libraries/test.xml b/.idea/libraries/test.xml index b80486a..57ed5ef 100644 --- a/.idea/libraries/test.xml +++ b/.idea/libraries/test.xml @@ -8,7 +8,7 @@ - - + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7408350..f2b4c1e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,21 +1,19 @@ + + + + - - - - diff --git a/.idea/runConfigurations/Run Tests.xml b/.idea/runConfigurations/Run Tests.xml deleted file mode 100644 index df4d7d6..0000000 --- a/.idea/runConfigurations/Run Tests.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 9f84d53..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "java", - "name": "Run Tests", - "request": "launch", - "mainClass": "net.thauvin.erik.JokeapiTest" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 133aa45..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "java.project.sourcePaths": [ - "src/main/java", - "src/main/resources", - "src/test/java", - "src/bld/java" - ], - "java.configuration.updateBuildConfiguration": "automatic", - "java.project.referencedLibraries": [ - "${HOME}/.bld/dist/bld-1.7.5.jar", - "lib/compile/*.jar", - "lib/runtime/*.jar", - "lib/test/*.jar" - ] -} diff --git a/LICENSE.txt b/LICENSE.txt index 4331a4d..82ecd17 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) +Copyright 2022-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: diff --git a/README.md b/README.md index 5308277..bb86972 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause) -[![Kotlin](https://img.shields.io/badge/kotlin-1.9.21-7f52ff)](https://kotlinlang.org/) +[![Kotlin](https://img.shields.io/badge/kotlin-2.1.20-7f52ff)](https://kotlinlang.org/) +[![bld](https://img.shields.io/badge/2.2.1-FA9052?label=bld&labelColor=2392FF)](https://rife2.com/bld) [![Release](https://img.shields.io/github/release/ethauvin/jokeapi.svg)](https://github.com/ethauvin/jokeapi/releases/latest) [![Maven Central](https://img.shields.io/maven-central/v/net.thauvin.erik/jokeapi?color=blue)](https://central.sonatype.com/artifact/net.thauvin.erik/jokeapi) [![Nexus Snapshot](https://img.shields.io/nexus/s/net.thauvin.erik/jokeapi?label=snapshot&server=https%3A%2F%2Foss.sonatype.org%2F)](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/) @@ -15,7 +16,7 @@ A simple library to retrieve jokes from [Sv443's JokeAPI](https://v2.jokeapi.dev ## Examples (TL;DR) ```kotlin -import net.thauvin.erik.jokeapi.getJoke +import net.thauvin.erik.jokeapi.joke val joke = joke() val safe = joke(safe = true) @@ -94,10 +95,10 @@ joke.getJoke().forEach(System.out::println); To use with [bld](https://rife2.com/bld), include the following dependency in your build file: ```java -repositories = List.of(MAVEN_CENTRAL); +repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY); scope(compile) - .include(dependency("net.thauvin.erik:cryptoprice:1.0.1")); + .include(dependency("net.thauvin.erik", "jokeapi", "1.0.0")); ``` Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project. @@ -111,7 +112,7 @@ repositories { } dependencies { - implementation("net.thauvin.erik:jokeapi:0.9.0") + implementation("net.thauvin.erik:jokeapi:1.0.0") } ``` @@ -123,9 +124,10 @@ You can also retrieve one or more raw (unprocessed) jokes in all [supported form For example for YAML: ```kotlin -var joke = getRawJokes(format = Format.YAML, idRange = IdRange(22)) -println(joke) +var jokes = getRawJokes(format = Format.YAML, idRange = IdRange(22)) +println(jokes.data) ``` + ```yaml error: false category: "Programming" @@ -141,8 +143,8 @@ flags: id: 22 safe: true lang: "en" - ``` + - View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt#L46)... ## Extending @@ -152,15 +154,37 @@ A generic `apiCall()` function is available to access other [JokeAPI endpoints]( For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint): ```kotlin -val lang = JokeApi.apiCall( +val response = JokeApi.apiCall( endPoint = "langcode", path = "french", params = mapOf(Parameter.FORMAT to Format.YAML.value) ) -println(lang) +if (response.statusCode == 200) { + println(response.data) +} ``` + ```yaml error: false code: "fr" ``` - View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt#L48)... + +## Contributing + +If you want to contribute to this project, all you have to do is clone the GitHub +repository: + +```console +git clone git@github.com:ethauvin/jokeapi.git +``` + +Then use [bld](https://rife2.com/bld) to build: + +```console +cd jokeapi +./bld compile +``` + +The project has an [IntelliJ IDEA](https://www.jetbrains.com/idea/) project structure. You can just open it after all +the dependencies were downloaded and peruse the code. diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 50acbf1..1a99819 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -1,12 +1,12 @@ - + - + - LongParameterList:JokeApi.kt$( amount: Int, categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false ) - LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false ) - LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" ) - LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set<Category>, val language: Language, val flags: Set<Flag>, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String ) - LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List<String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null ) + LongParameterList:JokeApi.kt$( amount: Int, categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false ) + LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false ) + LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" ) + LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set<Category>, val language: Language, val flags: Set<Flag>, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String ) + LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List<String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null ) MagicNumber:JokeUtil.kt$200 MagicNumber:JokeUtil.kt$399 MagicNumber:JokeUtil.kt$400 @@ -22,6 +22,7 @@ WildcardImport:GetJokeTest.kt$import assertk.assertions.* WildcardImport:GetJokeTest.kt$import net.thauvin.erik.jokeapi.models.* WildcardImport:GetJokesTest.kt$import assertk.assertions.* + WildcardImport:GetRawJokesTest.kt$import assertk.assertions.* WildcardImport:JokeApi.kt$import net.thauvin.erik.jokeapi.models.* WildcardImport:JokeConfig.kt$import net.thauvin.erik.jokeapi.models.* WildcardImport:JokeConfigTest.kt$import assertk.assertions.* diff --git a/lib/bld/bld-wrapper.jar b/lib/bld/bld-wrapper.jar index 431fce9..1eb86cf 100644 Binary files a/lib/bld/bld-wrapper.jar and b/lib/bld/bld-wrapper.jar differ diff --git a/lib/bld/bld-wrapper.properties b/lib/bld/bld-wrapper.properties index d2d7adf..fc9463a 100644 --- a/lib/bld/bld-wrapper.properties +++ b/lib/bld/bld-wrapper.properties @@ -1,9 +1,10 @@ bld.downloadExtensionJavadoc=false bld.downloadExtensionSources=true -bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.1 -bld.extensions=com.uwyn.rife2:bld-kotlin:0.9.0-SNAPSHOT -bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.0-SNAPSHOT -bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES bld.downloadLocation= +bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.10-SNAPSHOT +bld.extension-dokka=com.uwyn.rife2:bld-dokka:1.0.4-SNAPSHOT +bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.10-SNAPSHOT +bld.extension-kotlin=com.uwyn.rife2:bld-kotlin:1.1.0-SNAPSHOT +bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES bld.sourceDirectories= -bld.version=1.7.5 +bld.version=2.2.1 diff --git a/pom.xml b/pom.xml index f2be8ba..e480d48 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 net.thauvin.erik jokeapi - 0.9.1 + 1.0.1-SNAPSHOT jokeapi Retrieve jokes from Sv443's JokeAPI https://github.com/ethauvin/jokeapi @@ -18,37 +18,19 @@ org.jetbrains.kotlin kotlin-stdlib - 1.9.21 - compile - - - org.jetbrains.kotlin - kotlin-stdlib-common - 1.9.21 - compile - - - org.jetbrains.kotlin - kotlin-stdlib-jdk7 - 1.9.21 - compile - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - 1.9.21 + 2.1.20 compile org.json json - 20231013 + 20250107 compile net.thauvin.erik.urlencoder urlencoder-lib-jvm - 1.4.0 + 1.6.0 compile diff --git a/src/bld/java/net/thauvin/erik/JokeApiBuild.java b/src/bld/java/net/thauvin/erik/JokeApiBuild.java index 2d216a8..62b9d9a 100644 --- a/src/bld/java/net/thauvin/erik/JokeApiBuild.java +++ b/src/bld/java/net/thauvin/erik/JokeApiBuild.java @@ -1,7 +1,7 @@ /* * JokeApiBuild.java * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -35,10 +35,11 @@ import rife.bld.BuildCommand; import rife.bld.Project; import rife.bld.extension.CompileKotlinOperation; import rife.bld.extension.DetektOperation; +import rife.bld.extension.DokkaOperation; import rife.bld.extension.JacocoReportOperation; -import rife.bld.extension.dokka.DokkaOperation; import rife.bld.extension.dokka.LoggingLevel; import rife.bld.extension.dokka.OutputFormat; +import rife.bld.extension.kotlin.CompileOptions; import rife.bld.operations.exceptions.ExitStatusException; import rife.bld.publish.PomBuilder; import rife.bld.publish.PublishDeveloper; @@ -49,41 +50,45 @@ import rife.tools.exceptions.FileUtilsErrorException; import java.io.File; import java.io.IOException; import java.util.List; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; import static rife.bld.dependencies.Repository.*; import static rife.bld.dependencies.Scope.compile; import static rife.bld.dependencies.Scope.test; public class JokeApiBuild extends Project { + final File srcMainKotlin = new File(srcMainDirectory(), "kotlin"); + public JokeApiBuild() { pkg = "net.thauvin.erik"; name = "jokeapi"; - version = version(0, 9, 1); + version = version(1, 0, 1, "SNAPSHOT"); javaRelease = 11; downloadSources = true; autoDownloadPurge = true; repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL); - final var kotlin = version(1, 9, 21); + final var kotlin = version(2, 1, 20); scope(compile) .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin)) - .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-common", kotlin)) - .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk7", kotlin)) - .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", kotlin)) - .include(dependency("org.json", "json", "20231013")) - .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 4, 0))); + .include(dependency("org.json", "json", "20250107")) + .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 6, 0))); scope(test) - .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", version(1, 9, 21))) - .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 10, 1))) - .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 10, 1))) - .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 27, 0))); + .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin)) + .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 1))) + .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 1))) + .include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 1))) + .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1))); publishOperation() .repository(version.isSnapshot() ? repository(SONATYPE_SNAPSHOTS_LEGACY.location()) .withCredentials(property("sonatype.user"), property("sonatype.password")) : repository(SONATYPE_RELEASES_LEGACY.location()) .withCredentials(property("sonatype.user"), property("sonatype.password"))) + .repository(repository("github")) .info() .groupId(pkg) .artifactId(name) @@ -110,18 +115,29 @@ public class JokeApiBuild extends Project { .signKey(property("sign.key")) .signPassphrase(property("sign.passphrase")); - jarSourcesOperation().sourceDirectories(new File(srcMainDirectory(), "kotlin")); + jarSourcesOperation().sourceDirectories(srcMainKotlin); } public static void main(String[] args) { + // Enable detailed logging for the extensions + var level = Level.ALL; + var logger = Logger.getLogger("rife.bld.extension"); + var consoleHandler = new ConsoleHandler(); + + consoleHandler.setLevel(level); + logger.addHandler(consoleHandler); + logger.setLevel(level); + logger.setUseParentHandlers(false); + new JokeApiBuild().start(args); } @BuildCommand(summary = "Compiles the Kotlin project") @Override - public void compile() throws IOException { + public void compile() throws Exception { new CompileKotlinOperation() .fromProject(this) + .compileOptions(new CompileOptions().verbose(true)) .execute(); } @@ -142,9 +158,10 @@ public class JokeApiBuild extends Project { } @BuildCommand(summary = "Generates JaCoCo Reports") - public void jacoco() throws IOException { + public void jacoco() throws Exception { new JacocoReportOperation() .fromProject(this) + .sourceFiles(srcMainKotlin) .execute(); } @@ -166,6 +183,12 @@ public class JokeApiBuild extends Project { pomRoot(); } + @Override + public void publishLocal() throws Exception { + super.publishLocal(); + pomRoot(); + } + @BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory") public void pomRoot() throws FileUtilsErrorException { PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(), diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt index b4df9aa..474aa27 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt @@ -1,7 +1,7 @@ /* * JokeApi.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -45,13 +45,16 @@ import java.util.stream.Collectors object JokeApi { private const val API_URL = "https://v2.jokeapi.dev/" + /** + * The logger instance. + */ @JvmStatic val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) } /** * Makes a direct API call. * - * Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details. + * See the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details. */ @JvmStatic @JvmOverloads @@ -61,7 +64,7 @@ object JokeApi { path: String = "", params: Map = emptyMap(), auth: String = "" - ): String { + ): JokeResponse { val urlBuilder = StringBuilder("$API_URL$endPoint") if (path.isNotEmpty()) { @@ -95,11 +98,11 @@ object JokeApi { */ @JvmStatic @Throws(HttpErrorException::class) - fun getRawJokes(config: JokeConfig): String { + fun getRawJokes(config: JokeConfig): JokeResponse { return rawJokes( categories = config.categories, - lang = config.language, - blacklistFlags = config.flags, + lang = config.lang, + blacklistFlags = config.blacklistFlags, type = config.type, format = config.format, contains = config.contains, @@ -121,8 +124,8 @@ object JokeApi { fun joke(config: JokeConfig = JokeConfig.Builder().build()): Joke { return joke( categories = config.categories, - lang = config.language, - blacklistFlags = config.flags, + lang = config.lang, + blacklistFlags = config.blacklistFlags, type = config.type, contains = config.contains, idRange = config.idRange, @@ -142,8 +145,8 @@ object JokeApi { fun jokes(config: JokeConfig): Array { return jokes( categories = config.categories, - lang = config.language, - blacklistFlags = config.flags, + lang = config.lang, + blacklistFlags = config.blacklistFlags, type = config.type, contains = config.contains, idRange = config.idRange, @@ -161,6 +164,32 @@ object JokeApi { * * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. * + * @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is + * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark + * humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use + * [Category.ANY], which will make JokeAPI randomly choose a category. + * @param lang There are two types of languages; system languages and joke languages. Both are separate from each other. + * All system messages like errors can have a certain system language, while jokes can only have a joke language. + * It is possible, that system languages don't yet exist for your language while jokes already do. + * If no suitable system language is found, JokeAPI will default to English. + * @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be + * set on each joke, and they tell you something about the offensiveness of each joke. + * @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type + * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated + * because you might want to present the users the delivery after a timeout or in a different section of the UI. + * A joke of type [Type.SINGLE] only has a single string, which is the entire joke. + * @param contains If the search string filter is used, only jokes that contain the specified string will be returned. + * @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs. + * You don't necessarily need to provide an ID range though, a single ID will work just fine as well. + * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will + * mean you will only get the 6th joke. + * @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for + * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its + * references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as + * unsafe. + * @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token. + * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business + * and need more than 120 requests per minute. * @param splitNewLine Split newline within [Type.SINGLE] joke. */ fun joke( @@ -184,7 +213,7 @@ fun joke( idRange = idRange, safe = safe, auth = auth - ) + ).data ) if (json.getBoolean("error")) { throw parseError(json) @@ -198,7 +227,35 @@ fun joke( * * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. * - * @param amount The required amount of jokes to return. + * @param amount This filter allows you to set a certain amount of jokes to receive in a single call. Setting the + * filter to an invalid number will result in the API defaulting to serving a single joke. Setting it to a number + * larger than 10 will make JokeAPI default to the maximum (10). + * @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is + * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark + * humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use + * [Category.ANY], which will make JokeAPI randomly choose a category. + * @param lang There are two types of languages; system languages and joke languages. Both are separate from each other. + * All system messages like errors can have a certain system language, while jokes can only have a joke language. + * It is possible, that system languages don't yet exist for your language while jokes already do. + * If no suitable system language is found, JokeAPI will default to English. + * @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be + * set on each joke, and they tell you something about the offensiveness of each joke. + * @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type + * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated + * because you might want to present the users the delivery after a timeout or in a different section of the UI. + * A joke of type [Type.SINGLE] only has a single string, which is the entire joke. + * @param contains If the search string filter is used, only jokes that contain the specified string will be returned. + * @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs. + * You don't necessarily need to provide an ID range though, a single ID will work just fine as well. + * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will + * mean you will only get the 6th joke. + * @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for + * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its + * references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as + * unsafe. + * @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token. + * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business + * and need more than 120 requests per minute. * @param splitNewLine Split newline within [Type.SINGLE] joke. */ fun jokes( @@ -224,7 +281,7 @@ fun jokes( amount = amount, safe = safe, auth = auth - ) + ).data ) if (json.getBoolean("error")) { throw parseError(json) @@ -241,8 +298,42 @@ fun jokes( /** * Returns one or more jokes. * - * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. + * See the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. + * + * @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is + * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark + * humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use + * [Category.ANY], which will make JokeAPI randomly choose a category. + * @param lang There are two types of languages; system languages and joke languages. Both are separate from each other. + * All system messages like errors can have a certain system language, while jokes can only have a joke language. + * It is possible, that system languages don't yet exist for your language while jokes already do. + * If no suitable system language is found, JokeAPI will default to English. + * @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be + * set on each joke, and they tell you something about the offensiveness of each joke. + * @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type + * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated + * because you might want to present the users the delivery after a timeout or in a different section of the UI. + * A joke of type [Type.SINGLE] only has a single string, which is the entire joke. + * @param contains If the search string filter is used, only jokes that contain the specified string will be returned. + * @param format Response Formats (or just "Formats") are a way to get your data in a different file format. + * Maybe your environment or language doesn't support JSON natively. In that case, JokeAPI is able to convert the + * JSON-formatted joke to a different format for you. + * @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs. + * You don't necessarily need to provide an ID range though, a single ID will work just fine as well. + * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will + * mean you will only get the 6th joke. + * @param amount This filter allows you to set a certain amount of jokes to receive in a single call. Setting the + * filter to an invalid number will result in the API defaulting to serving a single joke. Setting it to a number + * larger than 10 will make JokeAPI default to the maximum (10). + * @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for + * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its + * references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as + * unsafe. + * @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token. + * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business + * and need more than 120 requests per minute. */ +@Throws(HttpErrorException::class) fun rawJokes( categories: Set = setOf(Category.ANY), lang: Language = Language.EN, @@ -254,7 +345,7 @@ fun rawJokes( amount: Int = 1, safe: Boolean = false, auth: String = "" -): String { +): JokeResponse { val params = mutableMapOf() // Categories diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt index 4537d13..a4d4901 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt @@ -1,7 +1,7 @@ /* * JokeConfig.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -39,19 +39,19 @@ import net.thauvin.erik.jokeapi.models.* * * Use the [Builder] to create a new configuration. */ -class JokeConfig private constructor( - val categories: Set, - val language: Language, - val flags: Set, - val type: Type, - val format: Format, - val contains: String, - val idRange: IdRange, - val amount: Int, - val safe: Boolean, - val splitNewLine: Boolean, - val auth: String -) { +class JokeConfig private constructor(builder: Builder) { + val categories = builder.categories + val lang = builder.lang + val blacklistFlags = builder.blacklistFlags + val type = builder.type + val format = builder.format + val contains = builder.contains + val idRange = builder.idRange + val amount = builder.amount + val safe = builder.safe + val splitNewLine = builder.splitNewLine + val auth = builder.auth + /** * [Builds][build] a new configuration. * @@ -72,20 +72,86 @@ class JokeConfig private constructor( var splitNewLine: Boolean = false, var auth: String = "" ) { - fun categories(categories: Set) = apply { this.categories = categories } - fun lang(language: Language) = apply { lang = language } - fun blacklistFlags(flags: Set) = apply { blacklistFlags = flags } - fun type(type: Type) = apply { this.type = type } - fun format(format: Format) = apply { this.format = format } - fun contains(search: String) = apply { contains = search } - fun idRange(idRange: IdRange) = apply { this.idRange = idRange } - fun amount(amount: Int) = apply { this.amount = amount } - fun safe(safe: Boolean) = apply { this.safe = safe } - fun splitNewLine(splitNewLine: Boolean) = apply { this.splitNewLine = splitNewLine } - fun auth(auth: String) = apply { this.auth = auth } + /** + * JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is + * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] + * category, dark humor will be in the [Category.DARK] category and so on. If you want jokes from all + * categories, you can instead use [Category.ANY], which will make JokeAPI randomly choose a category. + */ + fun categories(categories: Set): Builder = apply { this.categories = categories } - fun build() = JokeConfig( - categories, lang, blacklistFlags, type, format, contains, idRange, amount, safe, splitNewLine, auth - ) + /** + * There are two types of languages; system languages and joke languages. Both are separate from each other. + * All system messages like errors can have a certain system language, while jokes can only have a joke + * language. It is possible, that system languages don't yet exist for your language while jokes already do. + * If no suitable system language is found, JokeAPI will default to English. + */ + fun lang(language: Language): Builder = apply { lang = language } + + /** + * Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be + * set on each joke, and they tell you something about the offensiveness of each joke. + */ + fun blacklistFlags(flags: Set): Builder = apply { blacklistFlags = flags } + + /** + * Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type + * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are + * separated because you might want to present the users the delivery after a timeout or in a different section + * of the UI. A joke of type [Type.SINGLE] only has a single string, which is the entire joke. + */ + fun type(type: Type): Builder = apply { this.type = type } + + /** + * Response Formats (or just "Formats") are a way to get your data in a different file format. + * Maybe your environment or language doesn't support JSON natively. In that case, JokeAPI is able to convert + * the JSON-formatted joke to a different format for you. + */ + fun format(format: Format): Builder = apply { this.format = format } + + /** + * If the search string filter is used, only jokes that contain the specified string will be returned. + */ + fun contains(search: String): Builder = apply { contains = search } + + /** + * If this filter is used, you will only get jokes that are within the provided range of IDs. + * You don't necessarily need to provide an ID range though, a single ID will work just fine as well. + * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range + * of 5 will mean you will only get the 6th joke. + */ + fun idRange(idRange: IdRange): Builder = apply { this.idRange = idRange } + + /** + * This filter allows you to set a certain amount of jokes to receive in a single call. Setting the + * filter to an invalid number will result in the API defaulting to serving a single joke. Setting it to a + * number larger than 10 will make JokeAPI default to the maximum (10). + */ + fun amount(amount: Int): Builder = apply { this.amount = amount } + + /** + * Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for + * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, + * its references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally + * marked as unsafe. + */ + fun safe(safe: Boolean): Builder = apply { this.safe = safe } + + /** + * Split newline within [Type.SINGLE] joke. + */ + fun splitNewLine(splitNewLine: Boolean): Builder = apply { this.splitNewLine = splitNewLine } + + /** + * JokeAPI has a way of whitelisting certain clients. This is achieved through an API token. + * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a + * business and need more than 120 requests per minute. + */ + fun auth(auth: String): Builder = apply { this.auth = auth } + + /** + * Builds a new configuration. + */ + fun build() = JokeConfig(this) } } diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt index ff9f3e6..651844c 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt @@ -1,7 +1,7 @@ /* * JokeUtil.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -39,30 +39,37 @@ import net.thauvin.erik.jokeapi.models.* import org.json.JSONObject import java.io.IOException import java.net.HttpURLConnection -import java.net.URL +import java.net.URI import java.util.logging.Level -internal fun fetchUrl(url: String, auth: String = ""): String { +/** + * Fetch a URL. + */ +internal fun fetchUrl(url: String, auth: String = ""): JokeResponse { if (JokeApi.logger.isLoggable(Level.FINE)) { JokeApi.logger.fine(url) } - val connection = URL(url).openConnection() as HttpURLConnection - connection.setRequestProperty( - "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0" - ) - if (auth.isNotEmpty()) { - connection.setRequestProperty("Authentication", auth) - } + val connection = URI(url).toURL().openConnection() as HttpURLConnection + try { + connection.setRequestProperty( + "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0" + ) + if (auth.isNotEmpty()) { + connection.setRequestProperty("Authentication", auth) + } - if (connection.responseCode in 200..399) { - val body = connection.inputStream.bufferedReader().use { it.readText() } - if (JokeApi.logger.isLoggable(Level.FINE)) { + val isSuccess = connection.responseCode in 200..399 + val stream = if (isSuccess) connection.inputStream else connection.errorStream + val body = stream.bufferedReader().use { it.readText() } + if (!isSuccess && (body.isBlank() || connection.contentType.contains("text/html"))) { + throw httpError(connection.responseCode) + } else if (JokeApi.logger.isLoggable(Level.FINE)) { JokeApi.logger.fine(body) } - return body - } else { - throw httpError(connection.responseCode) + return JokeResponse(connection.responseCode, body) + } finally { + connection.disconnect() } } @@ -123,6 +130,9 @@ private fun httpError(responseCode: Int): HttpErrorException { return httpException } +/** + * Parse Error. + */ internal fun parseError(json: JSONObject): JokeException { val causedBy = json.getJSONArray("causedBy") val causes = List(causedBy.length()) { i -> causedBy.getString(i) } @@ -136,6 +146,9 @@ internal fun parseError(json: JSONObject): JokeException { ) } +/** + * Parse Joke. + */ internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke { val jokes = mutableListOf() if (json.has("setup")) { diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt index 815afcc..f2e8529 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt @@ -1,7 +1,7 @@ /* * HttpErrorException.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -44,7 +44,6 @@ class HttpErrorException @JvmOverloads constructor( cause: Throwable? = null ) : IOException(message, cause) { companion object { - @Suppress("ConstPropertyName") private const val serialVersionUID = 1L } } diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt index 16d4ec8..ac77344 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt @@ -1,7 +1,7 @@ /* * JokeException.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt index 4951d4a..cfb008e 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt @@ -1,7 +1,7 @@ /* * Category.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt index af92e90..be2e21f 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt @@ -1,7 +1,7 @@ /* * Flag.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt index 2678a21..1beb9d3 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt @@ -1,7 +1,7 @@ /* * Format.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt index 62a6eb6..73d45ec 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt @@ -1,7 +1,7 @@ /* * IdRange.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt index 0309977..c2124ae 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt @@ -1,7 +1,7 @@ /* * Joke.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/JokeResponse.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/JokeResponse.kt new file mode 100644 index 0000000..d34f2c3 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/JokeResponse.kt @@ -0,0 +1,39 @@ +/* + * JokeResponse.kt + * + * Copyright 2022-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.jokeapi.models + +/** + * The Joke API response. + * + * @property statusCode The HTTP status code. + * @property data The response body text. + */ +data class JokeResponse(val statusCode: Int, val data: String) diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt index 10c00fb..3ee166e 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt @@ -1,7 +1,7 @@ /* * Language.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt index b9e1106..8962b2a 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt @@ -1,7 +1,7 @@ /* * Parameter.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -34,6 +34,7 @@ package net.thauvin.erik.jokeapi.models /** * The available [URL Parameters](https://jokeapi.dev/#url-parameters). */ +@Suppress("unused") object Parameter { const val AMOUNT = "amount" const val CONTAINS = "contains" diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt index 59126b4..4fd80fe 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt @@ -1,7 +1,7 @@ /* * Type.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt index d9f9b30..6153825 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt @@ -1,7 +1,7 @@ /* * ApiCallTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -32,6 +32,7 @@ package net.thauvin.erik.jokeapi import assertk.assertThat +import assertk.assertions.isEqualTo import assertk.assertions.isGreaterThan import assertk.assertions.startsWith import net.thauvin.erik.jokeapi.JokeApi.apiCall @@ -51,8 +52,9 @@ internal class ApiCallTest { fun `Get Flags`() { // See https://v2.jokeapi.dev/#flags-endpoint val response = apiCall(endPoint = "flags") - val json = JSONObject(response) - assertAll("Validate JSON", + val json = JSONObject(response.data) + assertAll( + "Validate JSON", { assertFalse(json.getBoolean("error"), "apiCall(flags).error") }, { assertThat(json.getJSONArray("flags").length(), "apiCall(flags).flags").isGreaterThan(0) }, { assertThat(json.getLong("timestamp"), "apiCall(flags).timestamp").isGreaterThan(0) }) @@ -65,14 +67,16 @@ internal class ApiCallTest { endPoint = "langcode", path = "french", params = mapOf(Parameter.FORMAT to Format.YAML.value) ) - assertContains(lang, "code: \"fr\"", false, "apiCall(langcode, french, yaml)") + assertThat(lang.statusCode).isEqualTo(200) + assertContains(lang.data, "code: \"fr\"", false, "apiCall(langcode, french, yaml)") } @Test fun `Get Ping Response`() { // See https://v2.jokeapi.dev/#ping-endpoint val ping = apiCall(endPoint = "ping", params = mapOf(Parameter.FORMAT to Format.TXT.value)) - assertThat(ping, "apiCall(ping, txt)").startsWith("Pong!") + assertThat(ping.statusCode).isEqualTo(200) + assertThat(ping.data).startsWith("Pong!") } @Test @@ -82,6 +86,7 @@ internal class ApiCallTest { endPoint = "languages", params = mapOf(Parameter.FORMAT to Format.XML.value, Parameter.LANG to Language.FR.value) ) - assertThat(lang).startsWith("") + assertThat(lang.statusCode).isEqualTo(200) + assertThat(lang.data).startsWith("") } } diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt index de9d48a..50ce4b2 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt @@ -1,7 +1,7 @@ /* * BeforeAllTests.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt index 3932afd..eb6837a 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt @@ -1,7 +1,7 @@ /* * ExceptionsTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -41,8 +41,6 @@ import net.thauvin.erik.jokeapi.models.Category import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource @ExtendWith(BeforeAllTests::class) internal class ExceptionsTest { @@ -63,19 +61,20 @@ internal class ExceptionsTest { } } - @ParameterizedTest - @ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666]) - fun `Validate HTTP Exceptions`(code: Int) { - val e = assertThrows { - fetchUrl("https://httpstat.us/$code") - } - assertThat(e, "fetchUrl($code)").all { - prop(HttpErrorException::statusCode).isEqualTo(code) - prop(HttpErrorException::message).isNotNull().isNotEmpty() - if (code < 600) - prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull() - else - prop(HttpErrorException::cause).isNull() + @Test + fun `Validate HTTP Exceptions`() { + val locs = ArrayList>() + locs.add(Pair("https://apichallenges.herokuapp.com/secret/note", 401)) + locs.add(Pair("https://apichallenges.herokuapp.com/todo", 404)) + + for ((url, code) in locs) { + val e = assertThrows { + fetchUrl(url) + } + assertThat(e, "fetchUrl($code)").all { + prop(HttpErrorException::statusCode).isEqualTo(code) + prop(HttpErrorException::message).isNotNull().isNotEmpty() + } } } } diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt index c08ce39..e5a7d39 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt @@ -1,7 +1,7 @@ /* * GetJokeTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -70,15 +70,12 @@ internal class GetJokeTest { @Test fun `Get Joke with ID`() { - val id = 172 + val id = 201 val joke = joke(idRange = IdRange(id)) logger.fine(joke.toString()) assertThat(joke, "joke($id)").all { - prop(Joke::flags).all { - contains(Flag.EXPLICIT) - contains(Flag.NSFW) - } - prop(Joke::id).isEqualTo(172) + prop(Joke::flags).contains(Flag.RELIGIOUS); + prop(Joke::id).isEqualTo(id) prop(Joke::category).isEqualTo(Category.PUN) } } @@ -137,12 +134,10 @@ internal class GetJokeTest { @Test fun `Get Joke with Split Newline`() { - val joke = joke( - categories = setOf(Category.DARK), type = Type.SINGLE, idRange = IdRange(178), splitNewLine = true - ) + val joke = joke(type = Type.SINGLE, idRange = IdRange(18), splitNewLine = true) logger.fine(joke.toString()) assertThat(joke::joke, "joke(splitNewLine=true)").all { - size().isEqualTo(2) + size().isGreaterThanOrEqualTo(2) each { containsNone("\n") } @@ -177,13 +172,12 @@ internal class GetJokeTest { @Test fun `Get Joke using Search`() { - val id = 265 - val search = "his wife" + val search = "UDP joke" val joke = - joke(contains = search, categories = setOf(Category.PROGRAMMING), idRange = IdRange(id), safe = true) + joke(contains = search, categories = setOf(Category.PROGRAMMING), safe = true) logger.fine(joke.toString()) assertThat(joke, "joke($search)").all { - prop(Joke::id).isEqualTo(id) + prop(Joke::id).isEqualTo(0) prop(Joke::joke).any { it.contains(search) } diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt index 2e07a2d..ea49211 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt @@ -1,7 +1,7 @@ /* * GetJokesTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt index 7bcf1c6..aa85337 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt @@ -1,7 +1,7 @@ /* * GetRawJokesTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -33,47 +33,60 @@ package net.thauvin.erik.jokeapi import assertk.all import assertk.assertThat -import assertk.assertions.doesNotContain -import assertk.assertions.isNotEmpty -import assertk.assertions.startsWith +import assertk.assertions.* import net.thauvin.erik.jokeapi.models.Format import net.thauvin.erik.jokeapi.models.IdRange +import net.thauvin.erik.jokeapi.models.JokeResponse import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import kotlin.test.assertContains @ExtendWith(BeforeAllTests::class) internal class GetRawJokesTest { @Test fun `Get Raw Joke with TXT`() { val response = rawJokes(format = Format.TXT) - assertThat(response, "rawJoke(txt)").all { - isNotEmpty() - doesNotContain("Error") + assertThat(response).all { + prop("statusCode", JokeResponse::statusCode).isEqualTo(200) + prop("data", JokeResponse::data).all { + isNotEmpty() + doesNotContain("Error") + } } } @Test fun `Get Raw Joke with XML`() { val response = rawJokes(format = Format.XML) - assertThat(response, "rawJoke(xml)").startsWith("\n\n false") + assertThat(response).all { + prop("statusCode", JokeResponse::statusCode).isEqualTo(200) + prop("data", JokeResponse::data).startsWith("\n\n false") + } } @Test fun `Get Raw Joke with YAML`() { val response = rawJokes(format = Format.YAML) - assertThat(response, "rawJoke(yaml)").startsWith("error: false") + assertThat(response).all { + prop("statusCode", JokeResponse::statusCode).isEqualTo(200) + prop("data", JokeResponse::data).startsWith("error: false") + } } @Test fun `Get Raw Jokes`() { val response = rawJokes(amount = 2) - assertContains(response, "\"amount\": 2", false, "rawJoke(2)") + assertThat(response).all { + prop("statusCode", JokeResponse::statusCode).isEqualTo(200) + prop("data", JokeResponse::data).isNotEmpty() + } } @Test fun `Get Raw Invalid Jokes`() { val response = rawJokes(contains = "foo", safe = true, amount = 2, idRange = IdRange(160, 161)) - assertContains(response, "\"error\": true", false, "getRawJokes(foo)") + assertThat(response).all { + prop("statusCode", JokeResponse::statusCode).isEqualTo(400) + prop("data", JokeResponse::data).contains("\"error\": true") + } } } diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt index e617dfc..a4d4e0c 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt @@ -1,7 +1,7 @@ /* * JokeConfigTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -102,8 +102,9 @@ internal class JokeConfigTest { amount(2) safe(true) }.build() - val joke = getRawJokes(config) - assertContains(joke, "----------------------------------------------", false, "config.amount(2)") + val jokes = getRawJokes(config) + assertThat(jokes.statusCode).isEqualTo(200) + assertContains(jokes.data, "----------------------------------------------", false, "config.amount(2)") } @Test @@ -154,8 +155,8 @@ internal class JokeConfigTest { }.build() assertThat(config, "config").all { prop(JokeConfig::categories).isEqualTo(categories) - prop(JokeConfig::language).isEqualTo(language) - prop(JokeConfig::flags).isEqualTo(flags) + prop(JokeConfig::lang).isEqualTo(language) + prop(JokeConfig::blacklistFlags).isEqualTo(flags) prop(JokeConfig::type).isEqualTo(type) prop(JokeConfig::format).isEqualTo(format) prop(JokeConfig::contains).isEqualTo(search) diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt index 4b390c8..d50b97a 100644 --- a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt +++ b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt @@ -1,7 +1,7 @@ /* * JokeUtilTest.kt * - * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2022-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: @@ -33,6 +33,7 @@ package net.thauvin.erik.jokeapi import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.isEqualTo import org.json.JSONException import org.json.JSONObject import org.junit.jupiter.api.Test @@ -54,7 +55,8 @@ internal class JokeUtilTest { @Test fun `Validate Authentication Header`() { val token = "AUTH-TOKEN" - val body = fetchUrl("https://postman-echo.com/get", token) - assertThat(body, "body").contains("\"authentication\": \"$token\"") + val response = fetchUrl("https://postman-echo.com/get", token) + assertThat(response.statusCode).isEqualTo(200) + assertThat(response.data, "body").contains("\"authentication\": \"$token\"") } }