Initial import

This commit is contained in:
David Renault 2024-10-31 11:47:53 +01:00
commit dc5ca236d0
11 changed files with 740 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Ignore local gradle install files
.gradle
# Ignore local build dir
build

30
DESIGN.md Normal file
View File

@ -0,0 +1,30 @@
# Document de design
Ceci est le document de template pour décrire l'architecture de votre programme. Vous pouvez le modifier à votre guise, mais assurez-vous de répondre à toutes les questions posées.
***Suivant certaines architectures, certaines des questions peuvent ne pas être pertinentes. Dans ce cas, vous pouvez les ignorer.***
Vous pouvez utiliser autant de diagrammes que vous le souhaitez pour expliquer votre architecture.
Nous vous conseillons d'utiliser le logiciel PlantUML pour générer vos diagrammes.
## Schéma général
Décrivez ici le schéma général de votre programme. Quels sont les composants principaux et comment interagissent-ils?
## Utilisation du polymorphisme
Comment utilisez-vous le polymorphisme dans votre programme?
## Utilisation de la déléguation
Comment utilisez-vous la délégation dans votre programme?
## Utilisation de l'héritage
Comment utilisez-vous l'héritage dans votre programme?
## Utilisation de la généricité
Comment utilisez-vous la généricité dans votre programme?
## Utilisation des exceptions
Comment utilisez-vous les exceptions dans votre programme?

75
README.md Normal file
View File

@ -0,0 +1,75 @@
---
documentclass: book
papersize: a4
fontsize: 10pt
header-includes: |
\hypersetup{
colorlinks = true,
linkbordercolor = {pink},
}
---
# Projet de PG203
Ce starter kit vous permet de démarrer un projet d'application en
ligne de commande Java. La gestion du build est effectuée par l'outil
Gradle. Deux exécutables sont fournis: `gradlew` pour Unix ou MacOS et
`gradlew.bat` pour Windows.
Le starter kit vient avec:
- le framework [`JUnit 5`](https://junit.org/junit5/docs/current/user-guide/) pour gérer les tests;
- la bibliothèque [`JSON-Java`](https://github.com/stleary/JSON-java)
pour la manipulation de fichiers JSON;
- l'outil [`Jacoco`](https://www.jacoco.org/) pour la couverture du
code par les tests.
Le starter-kit contient un fichier
`src/main/java/eirb/pg203/Main.java` qui contient un programme de
démonstration. Ce programme récupère via l'API [Chuck Norris
Joke](https://api.chucknorris.io/) une blague sur Chuck Norris au
format JSON. Cette blague est ensuite parsée par la librarie
`org-json` et affichée sur la console.
Le fichier `src/main/java/eirb/pg203/SampleTest.java` contient un
petit exemple de test unitaire de la fonction qui télécharge et parse
la blague en question.
Voici comment effectuer les différentes commandes importantes.
## Compilation
```bash
./gradlew build
```
## Lancement des tests
```bash
./gradlew test
```
## Génération du rapport de couverture
```bash
./gradlew jacocoTestReport
```
Le rapport se trouve dans `build/reports/jacoco/test/html/index.html`.
## Lancement du programme
```
./gradlew run --args="arg1 arg2"
```
## Documentation
- WeatherAPI : la [documentation](https://www.weatherapi.com/docs)
ainsi qu'une [page de test](https://www.weatherapi.com/api-explorer.aspx)
- OpenMeteo : la [documentation](https://open-meteo.com/en/docs)
- OpenWeatherMap : la [documentation](https://openweathermap.org/api)

202
SUBJECT.md Normal file
View File

@ -0,0 +1,202 @@
---
documentclass: book
papersize: a4
fontsize: 10pt
header-includes: |
\hypersetup{
colorlinks = true,
linkbordercolor = {pink},
}
\usepackage{newunicodechar}
\usepackage{./twemojis/twemojis}
\newunicodechar{🌤}{\twemoji{sun behind cloud}}
\newunicodechar{🌧}{\twemoji{cloud with rain}}
---
# Projet Programmation Orientée Objet (PG203)
Lobjectif de ce projet est de réaliser un agrégateur de flux météo en
ligne de commande. Il permet de récupérer automatiquement depuis le
web les prévisions météo dune ville spécifiée et de les afficher dans
la console. Bien entendu, l'objectif principal de ce projet est de
mettre en oeuvre les principes fondamentaux de la programmation
orientée objet: encapsulation, délégation, héritage, polymorphisme. Le
développement d'un programme qui implémente les fonctionnalités
demandées est secondaire.
## Organisation
Les groupes de projet seront découpés en binômes. Pour faciliter le
travail, le projet est découpé en trois itérations successives :
- la première et la seconde itération feront lobjet dune démo et
dune rapide revue de code durant le début de chaque séance de suivi
de projet;
- la troisième itération sera complétée et rendue avec un ensemble de
livrables à la fin du projet.
### Starter-kit
Pour faciliter le développement, un starter-kit qui contient un
squelette de projet est fourni.
Il utilise l'outil de build `Gradle` pour gérér les dépendances aux
bibliothèques externes, pour compiler le projet, pour lancer les tests
et pour lancer le client.
### Première itération
Lobjectif de cette première livraison est de développer un premier
client qui récupère des informations sur la météo provenant de lAPI
[WeatherAPI](https://www.weatherapi.com/). Une API (*Application
Programming Interface*) permet davoir accès aux fonctionnalités
proposées par une application. Une API REST nest rien dautre quune
API accessible depuis le web via les [commandes
standard](https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview)
du protocole HTTP.
Dans le cadre de notre projet, utiliser une API revient donc à
simplement faire une requête HTTP et analyser sa réponse JSON :
- La classe
[`java.net.HttpUrlConnection`](https://docs.oracle.com/javase/8/docs/api/java/net/HttpURLConnection.html)
de la bibliothèque standard de Java permet de réaliser une requête
HTTP. Cette requête renvoie un
[code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) ainsi
qu'une réponse de type `String`.
- La plupart des APIs retournent leurs réponses par défaut au format
JSON (*JavaScript Object Notation*). Pour extraire les données de la
réponse obtenue, il est donc nécessaire de la parser. Pour ce faire,
il suffit d'utiliser
[`JSON-java`](https://stleary.github.io/JSON-java), disponible dans
le starter-kit, afin de produire un
[`JSONObject`](https://stleary.github.io/JSON-java/org/json/JSONObject.html).
Le client devra être capable de produire les températures d'une ville
donnée. Il fonctionnera de la manière suivante (le style de
l'affichage est laissé libre) :
```bash
shell$ java -jar weather.jar -l Bordeaux
              +-----+-----+-----+-----+
              | J+0 | J+1 | J+2 | J+3 |
+-------------+-----+-----+-----+-----+
|             | 10° | 12° | 8°  | 15° |
+-------------+-----+-----+-----+-----+
```
### Seconde itération
Pour la réalisation de notre aggrégateur, nous avons sélectionné une
liste dAPIs gratuites.
La liste est la suivante :
- [WeatherAPI](https://www.weatherapi.com/) (implémentée dans l'itération 1)
- [Open-Meteo](https://open-meteo.com/)
- [OpenWeatherMap](https://openweathermap.org/price)
Chaque API possède sa propre documentation à laquelle vous pouvez
avoir accès sur le site correspondant. Lobjectif de la deuxième
livraison est de rajouter ces API dans l'agrégateur.
En plus d'afficher la température, vous devrez afficher le temps qu'il
fait (ensoleillé, nuageux, pluvieux, etc.) et le vent (vitesse et
direction). Vous devez aussi gérer la possibilité d'avoir des données
manquantes (jours ou types d'information) dans les résultats d'une API
donnée.
Le client devra fonctionner de cette manière (pour simplifier nous
n'affichons qu'un jour mais un tableau de trois jours devra être
affiché):
```bash
shell$ java -jar weather.jar -l Bordeaux
+---------------+
| J+0 |
+-------------+---------------+
| WeatherAPI | 10° 🌤 03km/h |
+-------------+---------------+
| OpenMeteo | 11° 🌤 24km/h |
+-------------+---------------+
| Open WM | 10° 🌧 10km/h |
+-------------+---------------+
```
### Troisième itération
Pour la troisième itération nous allons améliorer la gestion des erreurs et introduire un cache pour éviter de faire des requêtes inutiles.
Le cache devra être implémenté dans un fichier JSON. Les fichiers devront être stockés dans un dossier cache à la racine du projets.
Le format de fichier du cache est le suivant:
```json
[
{
"city": "Bordeaux",
"api": "WeatherAPI",
"timestamp": 1616425200,
"value": {
...
}
},
{
"city": "Bordeaux",
"api": "OpenMeteo",
"timestamp": 1616425860,
"value": {
...
}
},
]
```
Le cache devra être mis à jour toutes les 24 heures.
Pour la gestion des erreurs, l'idée est de disposer d'un client
robuste qui ne plante pas à la première erreur d'une API donnée. Pour
ce faire, vous devrez gérer les erreurs réseaux et les erreurs de
parsing. En cas d'erreur, le client devra afficher un message d'erreur
explicite et continuer à fonctionner sur les APIs restantes. Il est
explicitement demandé d'introduire une exception contrôlée de type
`WeatherFetchingException` qui sera utilisée pour gérer les erreurs
sur les API sous-jacentes.
Exemple :
```bash
shell$ java -jar weather.jar -l Bordeaux -j 0
Erreur: Impossible de récupérer les données de OpenMeteo
+-------------------+
| J+0 |
+-------------+-------------------+
| WeatherAPI | 10° 🌤 69% 3km/h |
+-------------+-------------------+
| Open WM | 10° 🌤 71% 10km/h |
+-------------+-------------------+
```
## Livrables
Pour la dernière itération, les livrables sont les suivants :
- Le code source intégral du projet, utilisant une indentation claire,
des noms de variables explicites et des commentaires pertinents. La
lisibilité du code sera un des critères de notation.
- Outre la lisibilité, le code fourni devra pouvoir être compilé et
exécuté sans erreurs en utilisant les commandes de bases décrites
dans le starter-kit :
```
gradlew build
gradlew run
gradlew test
```
- Un document de conception qui explique les choix de conception utilisé dans votre programme. Vous trouverez un template de ce document dans le fichier `DESIGN.md`. Ce document rentrera aussi en compte dans la notation.
- Un jeu de tests unitaires qui couvre toutes les parties importantes
du code. L'outil `Jacoco` installé dans le starter-kit permet de
calculer et visualiser la couverture de code des tests.

26
build.gradle Normal file
View File

@ -0,0 +1,26 @@
plugins {
id 'application'
id 'jacoco'
}
application {
mainClass = 'eirb.pg203.Main'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.json:json:20240303'
testImplementation(platform('org.junit:junit-bom:5.11.2'))
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
gradlew vendored Executable file
View File

@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,32 @@
package eirb.pg203;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
System.out.println("Args: " + Arrays.toString(args));
System.out.println(fetchChuckNorrisJoke());
}
public static JSONObject fetchChuckNorrisJoke() throws IOException {
StringBuilder result = new StringBuilder();
URL url = URI.create("https://api.chucknorris.io/jokes/random").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
for (String line; (line = reader.readLine()) != null; ) {
result.append(line);
}
}
return new JSONObject(result.toString());
}
}

View File

@ -0,0 +1,17 @@
package eirb.pg203;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class SampleTest {
@Test
public void testFetchChuckNorrisJoke() throws IOException {
JSONObject res = Main.fetchChuckNorrisJoke();
Assertions.assertTrue(res.has("value"));
}
}