diff --git a/solution/01-docker/README.md b/solution/01-docker/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c81e7f09bbff53359ae54d3fb11094289cd1c2f0 --- /dev/null +++ b/solution/01-docker/README.md @@ -0,0 +1,97 @@ +# TP part 01 - Docker + +## Introduction +L'objectif de ce TP est de prendre en main l'environnement Docker et plus si affinitée ! + +Le but de ce TP est de créer trois conteneurs : + +- Un serveur Web : Apache +- Un serveur backend avec une API : Java +- Une base de données : PostgreSQL +- Un client pour la base de données : adminer (Optionnel) + + +Les notions suivantes seront abordées : + +- Création d'un image personalisée avec Dockerfile +- L'exposition des ports d'un container +- Le mappage des ports d'un container +- Le mappage de volume pour bénéficier de la persistance +- L'utilisation d'entrypoint (mis à disposition par l'image) +- L'utilisation de docker-compose +- La configuration très basique d'un reverse proxy + +## Base de donnée + +### Build de l'image + +On build notre image : + +```docker +docker build -t database . +``` + +* ```-t database``` : permet de spécifier un tag à l'image +* ```.``` : spécifie le contexte +* ```-f Database.Dockerfile``` : permet de spécifier le nom du Dockerfile si le nom par défaut n'est pas utilisé + +### Lancement d'un conteneur + +Ensuite on run / exécute notre image : + +```docker +docker run --rm \ + --name database \ + --env-file .env \ + # -p 5432:5432 \ Facultatif -> Utile si on souhaite exposer la BDD, dans notre cas non --> seulement à d'autre conteneur présent sur la même machine --> la solution : utilisé le nom du contenur directement ! + --network=bridge-app-network \ + database +``` +Il faut avoir au préalable crée le network -> ```docker network create app-network``` + +Explication des paramètres : +* ```--rm``` : effectue un clean-up du container quand il est arrêté. + * Càd : Docker supprime automatiquement le container (et tout ce qui est lié à celui-ci) quand il est arrêté et les volumes anonymes associés (équivalent de ```docker rm -v database```). + * A noter : Pas utilisé en prod mais pratique pour tester/accomplir quelque chose dans un temps réduit -> tester, compiler une application au sein d'un conteneur, vérifier un bon fonctionnement et libérer de l'espace une fois fini. + +* ```--name``` : permet de nommer le container pour l'identifier, par ex : utiliser son nom pour le lier à d'autres applications. +* ```-env-file``` : permet de spécifier le fichier qui contient les variables d'environnment. + * A noter : on peut aussi spécifier directement des variables d'environement avec ```-e VAR1=TOTO``` (alias de ```-env```) mais cela devient fastidieux si l'on a beaucoup de variables d'environnement. +* ```-p``` : permet de mapper un ou plusieurs port de la machine hôte avec le conteneur +* ```-v``` : permet de mapper un ou plusieurs volume de la machine hôte avec le conteneur (pour bénéficier de données persistante -> la base de données ne sera pas vide à chaque redémarrage). + + +Ajouter adminer (facultatif) : +``` +docker run --network=app-network -p 8081:8080 adminer +``` +## Application Java + +## Build de l'image + +On build notre image : + +```docker +docker build -t backend . +``` +On lance le container: +``` +docker run --network=app-network -p 8080:8080 --name backend_app backend +``` + +## Reverse-Proxy + +On fait exactement la même chose que pour les étapes d'avant on build et on run l'image d'un proxy httpd +puis récuperer sa conf avec la commande suivante: +```docker exec -it your_running_container cat /usr/local/apache2/conf/httpd.conf > httpd.conf``` + +# Docker-Compose pour les rassembler tous ! + +Docker-Compose est un super outil pour configurer/définir/désigner des applications docker avec plusieurs conteneurs. +Celui-ci est au format YAML. + +* Voici la documentation : https://docs.docker.com/compose/ + +Pour créer et lancer les conteneurs : ```docker-compose up``` et ajouter ```-d``` pour lancer en arrière plan + +Pour arrêter et supprimer l'ensemble des éléments (volumes, netorks, containers, images) : ```docker-compose down``` diff --git a/solution/01-docker/TD/compose/compose.yaml b/solution/01-docker/TD/compose/compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..60692418e1f3e620607633ecf796d738b8ecaa6e --- /dev/null +++ b/solution/01-docker/TD/compose/compose.yaml @@ -0,0 +1,31 @@ +version: '3' +services: + webapp: + image: registry.takima.io/school/proxy/nginx + ports: + - "8082:80" + networks: + - my-network + volumes: + - my-volume:/app/data + environment: + - DATABASE_URL=mysql://dbuser:dbpassword@db/dbname + db: + image: mysql:5.7 + networks: + - my-network + environment: + - MYSQL_ROOT_PASSWORD=rootpassword + - MYSQL_DATABASE=dbname + - MYSQL_USER=dbuser + - MYSQL_PASSWORD=dbpassword + volumes: + - db-data:/var/lib/mysql + +networks: + my-network: + driver: bridge + +volumes: + my-volume: + db-data: \ No newline at end of file diff --git a/solution/01-docker/TD/flask-app/Dockerfile b/solution/01-docker/TD/flask-app/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6c50ce3d549bec00e88f29d2a2a0c09975065449 --- /dev/null +++ b/solution/01-docker/TD/flask-app/Dockerfile @@ -0,0 +1,19 @@ +# notre image de base +FROM alpine:3.6 + +# Installer Python et pip +RUN apk add --update py2-pip + +# Installer les modules Python nécessaires par l'application Python +COPY requirements.txt /usr/src/app/ +RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt + +# Copier les fichiers nécessaires pour l'exécution de l'application +COPY app.py /usr/src/app/ +COPY templates/index.html /usr/src/app/templates/ + +# Indiquer le numéro de port que le conteneur doit exposer +EXPOSE 5000 + +# Exécuter l'application +ENTRYPOINT ["python", "/usr/src/app/app.py"] \ No newline at end of file diff --git a/solution/01-docker/TD/flask-app/app.py b/solution/01-docker/TD/flask-app/app.py new file mode 100644 index 0000000000000000000000000000000000000000..e10974091b17eaf8748bef4f39bd78001906ac8a --- /dev/null +++ b/solution/01-docker/TD/flask-app/app.py @@ -0,0 +1,20 @@ +from flask import Flask, render_template +import random + +app = Flask(__name__) + +# liste des images +images = [ +"https://c.tenor.com/GTcT7HODLRgAAAAM/smiling-cat-creepy-cat.gif", +"https://media0.giphy.com/media/10dU7AN7xsi1I4/giphy.webp?cid=ecf05e47gk63rd81vzlot57qmebr7drtgf6a3khmzvjsdtu7&rid=giphy.webp&ct=g", +"https://media0.giphy.com/media/S6VGjvmFRu5Qk/giphy.webp?cid=ecf05e478yofpawrhffnnvb3sgjkos96vyfo5mtqhds35as6&rid=giphy.webp&ct=g", +"https://media3.giphy.com/media/JIX9t2j0ZTN9S/200w.webp?cid=ecf05e47gk63rd81vzlot57qmebr7drtgf6a3khmzvjsdtu7&rid=200w.webp&ct=g" + ] + +@app.route('/') +def index(): + url = random.choice(images) + return render_template('index.html', url=url) + +if __name__ == "__main__": + app.run(host="0.0.0.0") diff --git a/solution/01-docker/TD/flask-app/requirements.txt b/solution/01-docker/TD/flask-app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..56eeb0c801c00588b696945e343e6086dca10207 --- /dev/null +++ b/solution/01-docker/TD/flask-app/requirements.txt @@ -0,0 +1 @@ +Flask==0.10.1 \ No newline at end of file diff --git a/solution/01-docker/TD/flask-app/templates/index.html b/solution/01-docker/TD/flask-app/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..56b7f6b26dd557d88e747afe6f84de80f7dcf8f2 --- /dev/null +++ b/solution/01-docker/TD/flask-app/templates/index.html @@ -0,0 +1,35 @@ +<html> + <head> + <style type="text/css"> + body { + background: black; + color: white; + } + div.container { + max-width: 500px; + margin: 100px auto; + border: 20px solid white; + padding: 10px; + text-align: center; + } + h4 { + text-transform: uppercase; + } + </style> + </head> + <body> + <div class="container"> + <h4>Cat Gif of the day</h4> + <img src="{{url}}" /> + <p> + <small + >Courtesy: + <a + href="http://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs" + >Buzzfeed</a + ></small + > + </p> + </div> + </body> +</html> \ No newline at end of file diff --git a/solution/01-docker/database/.env b/solution/01-docker/database/.env new file mode 100644 index 0000000000000000000000000000000000000000..87ace36daf378b319ded0233d46e68addda87a59 --- /dev/null +++ b/solution/01-docker/database/.env @@ -0,0 +1,3 @@ +POSTGRES_PASSWORD=pwd +POSTGRES_USER=usr +POSTGRES_DB=db \ No newline at end of file diff --git a/solution/01-docker/database/Dockerfile b/solution/01-docker/database/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..59854c23b1c90acefecd21f60999b98eaa24a391 --- /dev/null +++ b/solution/01-docker/database/Dockerfile @@ -0,0 +1,3 @@ +FROM registry.takima.io/school/proxy/postgres:14.1-alpine + +COPY ./scripts/ /docker-entrypoint-initdb.d diff --git a/solution/01-docker/database/scripts/01-CreateSchema.sql b/solution/01-docker/database/scripts/01-CreateSchema.sql new file mode 100644 index 0000000000000000000000000000000000000000..36e012c87d096b5ebe709423281e2fbf22e959e1 --- /dev/null +++ b/solution/01-docker/database/scripts/01-CreateSchema.sql @@ -0,0 +1,13 @@ +CREATE TABLE public.departments +( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL +); + +CREATE TABLE public.students +( + id SERIAL PRIMARY KEY, + department_id INT NOT NULL REFERENCES departments (id), + first_name VARCHAR(20) NOT NULL, + last_name VARCHAR(20) NOT NULL +); diff --git a/solution/01-docker/database/scripts/02-InsertData.sql b/solution/01-docker/database/scripts/02-InsertData.sql new file mode 100644 index 0000000000000000000000000000000000000000..bfde28418a2bae5219893e64c9682ed4c3548d93 --- /dev/null +++ b/solution/01-docker/database/scripts/02-InsertData.sql @@ -0,0 +1,9 @@ +INSERT INTO departments (name) VALUES ('IRC'); +INSERT INTO departments (name) VALUES ('ETI'); +INSERT INTO departments (name) VALUES ('CGP'); + + +INSERT INTO students (department_id, first_name, last_name) VALUES (1, 'Eli', 'Copter'); +INSERT INTO students (department_id, first_name, last_name) VALUES (2, 'Emma', 'Carena'); +INSERT INTO students (department_id, first_name, last_name) VALUES (2, 'Jack', 'Uzzi'); +INSERT INTO students (department_id, first_name, last_name) VALUES (3, 'Aude', 'Javel'); diff --git a/solution/01-docker/docker-compose-bonus-4.yml b/solution/01-docker/docker-compose-bonus-4.yml new file mode 100644 index 0000000000000000000000000000000000000000..9fc757400aa5403c718bc90f030389423513dead --- /dev/null +++ b/solution/01-docker/docker-compose-bonus-4.yml @@ -0,0 +1,46 @@ +version: '3.3' +services: + backend: + container_name: backend + build: ./simple-api + networks: + - back-network + - front-network + depends_on: + database: + condition: service_healthy + env_file: + - simple-api/.env + + database: + container_name: database + restart: always + build: ./database + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 3 + networks: + - back-network + env_file: + - database/.env + + httpd: + container_name: reverse_proxy + build: ./httpd + ports: + - "80:80" + networks: + - front-network + + +volumes: + my_db_volume: + driver: local + +networks: + back-network: + front-network: + + diff --git a/solution/01-docker/docker-compose-network.yml b/solution/01-docker/docker-compose-network.yml new file mode 100644 index 0000000000000000000000000000000000000000..9d99e0d6911324b36add43bb2479ca017fdc0f04 --- /dev/null +++ b/solution/01-docker/docker-compose-network.yml @@ -0,0 +1,40 @@ +version: '3.3' +services: + backend: + container_name: backend + build: ./simple-api + networks: + - back-network + - front-network + depends_on: + - database + env_file: + - simple-api/.env + + database: + container_name: database + restart: always + build: ./database + networks: + - back-network + env_file: + - database/.env + + httpd: + container_name: reverse_proxy + build: ./httpd + ports: + - "80:80" + networks: + - front-network + + +volumes: + my_db_volume: + driver: local + +networks: + back-network: + front-network: + + diff --git a/solution/01-docker/docker-compose.yml b/solution/01-docker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..0d93f55f34c65fabbb7e5be177b0edbfc4078231 --- /dev/null +++ b/solution/01-docker/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.3' +services: + backend: + container_name: backend + build: ./simple-api + networks: + - app-network + depends_on: + - database + env_file: + - simple-api/.env + + database: + container_name: database + restart: always + build: ./database + networks: + - app-network + env_file: + - database/.env + + httpd: + container_name: reverse_proxy + build: ./httpd + ports: + - "80:80" + networks: + - app-network + + +volumes: + my_db_volume: + driver: local + +networks: + app-network: + + diff --git a/solution/01-docker/httpd/Dockerfile b/solution/01-docker/httpd/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..52d1b7260dfa565e3b307ad02b26d42b4c73e5ea --- /dev/null +++ b/solution/01-docker/httpd/Dockerfile @@ -0,0 +1,7 @@ +FROM registry.takima.io/school/proxy/httpd:2.4 + +COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf + +EXPOSE 80 + +CMD ["httpd", "-D", "FOREGROUND"] diff --git a/solution/01-docker/httpd/httpd.conf b/solution/01-docker/httpd/httpd.conf new file mode 100644 index 0000000000000000000000000000000000000000..282eb898b5cd4ed992a3c7b1dd9429dfae758771 --- /dev/null +++ b/solution/01-docker/httpd/httpd.conf @@ -0,0 +1,550 @@ +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See <URL:http://httpd.apache.org/docs/2.4/> for detailed information. +# In particular, see +# <URL:http://httpd.apache.org/docs/2.4/mod/directives.html> +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so "logs/access_log" +# with ServerRoot set to "/usr/local/apache2" will be interpreted by the +# server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log" +# will be interpreted as '/logs/access_log'. + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to specify a local disk on the +# Mutex directive, if file-based mutexes are used. If you wish to share the +# same ServerRoot for multiple httpd daemons, you will need to change at +# least PidFile. +# +ServerRoot "/usr/local/apache2" + +# +# Mutex: Allows you to set the mutex mechanism and mutex file directory +# for individual mutexes, or change the global defaults +# +# Uncomment and change the directory if mutexes are file-based and the default +# mutex file directory is not on a local disk or is not appropriate for some +# other reason. +# +# Mutex default:logs + +# +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the <VirtualHost> +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +LoadModule mpm_event_module modules/mod_mpm_event.so +#LoadModule mpm_prefork_module modules/mod_mpm_prefork.so +#LoadModule mpm_worker_module modules/mod_mpm_worker.so +LoadModule authn_file_module modules/mod_authn_file.so +#LoadModule authn_dbm_module modules/mod_authn_dbm.so +#LoadModule authn_anon_module modules/mod_authn_anon.so +#LoadModule authn_dbd_module modules/mod_authn_dbd.so +#LoadModule authn_socache_module modules/mod_authn_socache.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_user_module modules/mod_authz_user.so +#LoadModule authz_dbm_module modules/mod_authz_dbm.so +#LoadModule authz_owner_module modules/mod_authz_owner.so +#LoadModule authz_dbd_module modules/mod_authz_dbd.so +LoadModule authz_core_module modules/mod_authz_core.so +#LoadModule authnz_ldap_module modules/mod_authnz_ldap.so +#LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule auth_basic_module modules/mod_auth_basic.so +#LoadModule auth_form_module modules/mod_auth_form.so +#LoadModule auth_digest_module modules/mod_auth_digest.so +#LoadModule allowmethods_module modules/mod_allowmethods.so +#LoadModule isapi_module modules/mod_isapi.so +#LoadModule file_cache_module modules/mod_file_cache.so +#LoadModule cache_module modules/mod_cache.so +#LoadModule cache_disk_module modules/mod_cache_disk.so +#LoadModule cache_socache_module modules/mod_cache_socache.so +#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so +#LoadModule socache_dbm_module modules/mod_socache_dbm.so +#LoadModule socache_memcache_module modules/mod_socache_memcache.so +#LoadModule socache_redis_module modules/mod_socache_redis.so +#LoadModule watchdog_module modules/mod_watchdog.so +#LoadModule macro_module modules/mod_macro.so +#LoadModule dbd_module modules/mod_dbd.so +#LoadModule bucketeer_module modules/mod_bucketeer.so +#LoadModule dumpio_module modules/mod_dumpio.so +#LoadModule echo_module modules/mod_echo.so +#LoadModule example_hooks_module modules/mod_example_hooks.so +#LoadModule case_filter_module modules/mod_case_filter.so +#LoadModule case_filter_in_module modules/mod_case_filter_in.so +#LoadModule example_ipc_module modules/mod_example_ipc.so +#LoadModule buffer_module modules/mod_buffer.so +#LoadModule data_module modules/mod_data.so +#LoadModule ratelimit_module modules/mod_ratelimit.so +LoadModule reqtimeout_module modules/mod_reqtimeout.so +#LoadModule ext_filter_module modules/mod_ext_filter.so +#LoadModule request_module modules/mod_request.so +#LoadModule include_module modules/mod_include.so +LoadModule filter_module modules/mod_filter.so +#LoadModule reflector_module modules/mod_reflector.so +#LoadModule substitute_module modules/mod_substitute.so +#LoadModule sed_module modules/mod_sed.so +#LoadModule charset_lite_module modules/mod_charset_lite.so +#LoadModule deflate_module modules/mod_deflate.so +#LoadModule xml2enc_module modules/mod_xml2enc.so +#LoadModule proxy_html_module modules/mod_proxy_html.so +#LoadModule brotli_module modules/mod_brotli.so +LoadModule mime_module modules/mod_mime.so +#LoadModule ldap_module modules/mod_ldap.so +LoadModule log_config_module modules/mod_log_config.so +#LoadModule log_debug_module modules/mod_log_debug.so +#LoadModule log_forensic_module modules/mod_log_forensic.so +#LoadModule logio_module modules/mod_logio.so +#LoadModule lua_module modules/mod_lua.so +LoadModule env_module modules/mod_env.so +#LoadModule mime_magic_module modules/mod_mime_magic.so +#LoadModule cern_meta_module modules/mod_cern_meta.so +#LoadModule expires_module modules/mod_expires.so +LoadModule headers_module modules/mod_headers.so +#LoadModule ident_module modules/mod_ident.so +#LoadModule usertrack_module modules/mod_usertrack.so +#LoadModule unique_id_module modules/mod_unique_id.so +LoadModule setenvif_module modules/mod_setenvif.so +LoadModule version_module modules/mod_version.so +#LoadModule remoteip_module modules/mod_remoteip.so +LoadModule proxy_module modules/mod_proxy.so +#LoadModule proxy_connect_module modules/mod_proxy_connect.so +#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so +LoadModule proxy_http_module modules/mod_proxy_http.so +#LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so +#LoadModule proxy_scgi_module modules/mod_proxy_scgi.so +#LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so +#LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so +#LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so +#LoadModule proxy_ajp_module modules/mod_proxy_ajp.so +#LoadModule proxy_balancer_module modules/mod_proxy_balancer.so +#LoadModule proxy_express_module modules/mod_proxy_express.so +#LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so +#LoadModule session_module modules/mod_session.so +#LoadModule session_cookie_module modules/mod_session_cookie.so +#LoadModule session_crypto_module modules/mod_session_crypto.so +#LoadModule session_dbd_module modules/mod_session_dbd.so +#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so +#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so +#LoadModule ssl_module modules/mod_ssl.so +#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so +#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so +#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so +#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so +#LoadModule dialup_module modules/mod_dialup.so +#LoadModule http2_module modules/mod_http2.so +#LoadModule proxy_http2_module modules/mod_proxy_http2.so +#LoadModule md_module modules/mod_md.so +#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so +#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so +#LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so +#LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so +LoadModule unixd_module modules/mod_unixd.so +#LoadModule heartbeat_module modules/mod_heartbeat.so +#LoadModule heartmonitor_module modules/mod_heartmonitor.so +#LoadModule dav_module modules/mod_dav.so +LoadModule status_module modules/mod_status.so +LoadModule autoindex_module modules/mod_autoindex.so +#LoadModule asis_module modules/mod_asis.so +#LoadModule info_module modules/mod_info.so +#LoadModule suexec_module modules/mod_suexec.so +<IfModule !mpm_prefork_module> + #LoadModule cgid_module modules/mod_cgid.so +</IfModule> +<IfModule mpm_prefork_module> + #LoadModule cgi_module modules/mod_cgi.so +</IfModule> +#LoadModule dav_fs_module modules/mod_dav_fs.so +#LoadModule dav_lock_module modules/mod_dav_lock.so +#LoadModule vhost_alias_module modules/mod_vhost_alias.so +#LoadModule negotiation_module modules/mod_negotiation.so +LoadModule dir_module modules/mod_dir.so +#LoadModule imagemap_module modules/mod_imagemap.so +#LoadModule actions_module modules/mod_actions.so +#LoadModule speling_module modules/mod_speling.so +#LoadModule userdir_module modules/mod_userdir.so +LoadModule alias_module modules/mod_alias.so +#LoadModule rewrite_module modules/mod_rewrite.so + +<IfModule unixd_module> +# +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +# +User daemon +Group daemon + +</IfModule> + +# 'Main' server configuration +# +# The directives in this section set up the values used by the 'main' +# server, which responds to any requests that aren't handled by a +# <VirtualHost> definition. These values also provide defaults for +# any <VirtualHost> containers you may define later in the file. +# +# All of these directives may appear inside <VirtualHost> containers, +# in which case these default settings will be overridden for the +# virtual host being defined. +# + +# +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +# +ServerAdmin you@example.com +ServerName localhost + +<VirtualHost *:80> + ProxyPreserveHost On + ProxyPass / http://backend:8080/ + ProxyPassReverse / http://backend:8080/ +</VirtualHost> + +# +# ServerName gives the name and port that the server uses to identify itself. +# This can often be determined automatically, but we recommend you specify +# it explicitly to prevent problems during startup. +# +# If your host doesn't have a registered DNS name, enter its IP address here. +# +#ServerName www.example.com:80 + +# +# Deny access to the entirety of your server's filesystem. You must +# explicitly permit access to web content directories in other +# <Directory> blocks below. +# +<Directory /> + AllowOverride none + Require all denied +</Directory> + +# +# Note that from this point forward you must specifically allow +# particular features to be enabled - so if something's not working as +# you might expect, make sure that you have specifically enabled it +# below. +# + +# +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +DocumentRoot "/usr/local/apache2/htdocs" +<Directory "/usr/local/apache2/htdocs"> + # + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + # + Options Indexes FollowSymLinks + + # + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # AllowOverride FileInfo AuthConfig Limit + # + AllowOverride None + + # + # Controls who can get stuff from this server. + # + Require all granted +</Directory> + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed byWeb clients. +# +<Files ".ht*"> + Require all denied +</Files> + +# +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a <VirtualHost> +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a <VirtualHost> +# container, that host's errors will be logged there and not here. +# +ErrorLog /proc/self/fd/2 + +# +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +# +LogLevel warn + +<IfModule log_config_module> + # + # The following directives define some format nicknames for use with + # a CustomLog directive (see below). + # + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + <IfModule logio_module> + # You need to enable mod_logio.c to use %I and %O + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + </IfModule> + + # + # The location and format of the access logfile (Common Logfile Format). + # If you do not define any access logfiles within a <VirtualHost> + # container, they will be logged here. Contrariwise, if you *do* + # define per-<VirtualHost> access logfiles, transactions will be + # logged therein and *not* in this file. + # + CustomLog /proc/self/fd/1 common + + # + # If you prefer a logfile with access, agent, and referer information + # (Combined Logfile Format) you can use the following directive. + # + #CustomLog "logs/access_log" combined +</IfModule> + +<IfModule alias_module> + # + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a <Directory> section to allow access to + # the filesystem path. + + # + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + # + ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/" + +</IfModule> + +<IfModule cgid_module> + # + # ScriptSock: On threaded servers, designate the path to the UNIX + # socket used to communicate with the CGI daemon of mod_cgid. + # + #Scriptsock cgisock +</IfModule> + +# +# "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. +# +<Directory "/usr/local/apache2/cgi-bin"> + AllowOverride None + Options None + Require all granted +</Directory> + +<IfModule headers_module> + # + # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied + # backend servers which have lingering "httpoxy" defects. + # 'Proxy' request header is undefined by the IETF, not listed by IANA + # + RequestHeader unset Proxy early +</IfModule> + +<IfModule mime_module> + # + # TypesConfig points to the file containing the list of mappings from + # filename extension to MIME-type. + # + TypesConfig conf/mime.types + + # + # AddType allows you to add to or override the MIME configuration + # file specified in TypesConfig for specific file types. + # + #AddType application/x-gzip .tgz + # + # AddEncoding allows you to have certain browsers uncompress + # information on the fly. Note: Not all browsers support this. + # + #AddEncoding x-compress .Z + #AddEncoding x-gzip .gz .tgz + # + # If the AddEncoding directives above are commented-out, then you + # probably should define those extensions to indicate media types: + # + AddType application/x-compress .Z + AddType application/x-gzip .gz .tgz + + # + # AddHandler allows you to map certain file extensions to "handlers": + # actions unrelated to filetype. These can be either built into the server + # or added with the Action directive (see below) + # + # To use CGI scripts outside of ScriptAliased directories: + # (You will also need to add "ExecCGI" to the "Options" directive.) + # + #AddHandler cgi-script .cgi + + # For type maps (negotiated resources): + #AddHandler type-map var + + # + # Filters allow you to process content before it is sent to the client. + # + # To parse .shtml files for server-side includes (SSI): + # (You will also need to add "Includes" to the "Options" directive.) + # + #AddType text/html .shtml + #AddOutputFilter INCLUDES .shtml +</IfModule> + +# +# The mod_mime_magic module allows the server to use various hints from the +# contents of the file itself to determine its type. The MIMEMagicFile +# directive tells the module where the hint definitions are located. +# +#MIMEMagicFile conf/magic + +# +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html +# + +# +# MaxRanges: Maximum number of Ranges in a request before +# returning the entire resource, or one of the special +# values 'default', 'none' or 'unlimited'. +# Default setting is to accept 200 Ranges. +#MaxRanges unlimited + +# +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall may be used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +# Defaults: EnableMMAP On, EnableSendfile Off +# +#EnableMMAP off +#EnableSendfile on + +# Supplemental configuration +# +# The configuration files in the conf/extra/ directory can be +# included to add extra features or to modify the default configuration of +# the server, or you may simply copy their contents here and change as +# necessary. + +# Server-pool management (MPM specific) +#Include conf/extra/httpd-mpm.conf + +# Multi-language error messages +#Include conf/extra/httpd-multilang-errordoc.conf + +# Fancy directory listings +#Include conf/extra/httpd-autoindex.conf + +# Language settings +#Include conf/extra/httpd-languages.conf + +# User home directories +#Include conf/extra/httpd-userdir.conf + +# Real-time info on requests and configuration +#Include conf/extra/httpd-info.conf + +# Virtual hosts +#Include conf/extra/httpd-vhosts.conf + +# Local access to the Apache HTTP Server Manual +#Include conf/extra/httpd-manual.conf + +# Distributed authoring and versioning (WebDAV) +#Include conf/extra/httpd-dav.conf + +# Various default settings +#Include conf/extra/httpd-default.conf + +# Configure mod_proxy_html to understand HTML4/XHTML1 +<IfModule proxy_html_module> +Include conf/extra/proxy-html.conf +</IfModule> + +# Secure (SSL/TLS) connections +#Include conf/extra/httpd-ssl.conf +# +# Note: The following must must be present to support +# starting without SSL on platforms with no /dev/random equivalent +# but a statically compiled-in mod_ssl. +# +<IfModule ssl_module> +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin +</IfModule> + diff --git a/solution/01-docker/simple-api/.env b/solution/01-docker/simple-api/.env new file mode 100644 index 0000000000000000000000000000000000000000..0dd6d2dc851ff599fccb9f4462f7a35597685f7f --- /dev/null +++ b/solution/01-docker/simple-api/.env @@ -0,0 +1,5 @@ +POSTGRES_PASSWORD=pwd +POSTGRES_USER=usr +POSTGRES_DB=db +POSTGRES_URL=database +POSTGRES_PORT=5432 \ No newline at end of file diff --git a/solution/01-docker/simple-api/Dockerfile b/solution/01-docker/simple-api/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9ffc7eae2d5a40f4b6bbae95d09c3522e709bbe9 --- /dev/null +++ b/solution/01-docker/simple-api/Dockerfile @@ -0,0 +1,19 @@ +# Build +FROM registry.takima.io/school/proxy/maven:3.8.5-openjdk-17-slim AS myapp-build +ENV MYAPP_HOME /opt/myapp +WORKDIR $MYAPP_HOME +COPY pom.xml . +RUN mvn dependency:go-offline + +COPY src ./src +RUN mvn package -DskipTests + +# Run +FROM registry.takima.io/school/proxy/openjdk:17-slim +ENV MYAPP_HOME /opt/myapp +WORKDIR $MYAPP_HOME +COPY --from=myapp-build $MYAPP_HOME/target/*.jar $MYAPP_HOME/myapp.jar + +EXPOSE 8080 + +ENTRYPOINT java -jar myapp.jar diff --git a/solution/01-docker/simple-api/HELP.md b/solution/01-docker/simple-api/HELP.md new file mode 100644 index 0000000000000000000000000000000000000000..6b6a8917dd465fe34369df43f47facbd5df9fbb2 --- /dev/null +++ b/solution/01-docker/simple-api/HELP.md @@ -0,0 +1,24 @@ +# Read Me First +The following was discovered as part of building this project: + +* The original package name 'fr.takima.training.simple-api' is invalid and this project uses 'fr.takima.training.simpleapi' instead. + +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.2.4.RELEASE/maven-plugin/) +* [Spring Web](https://docs.spring.io/spring-boot/docs/2.2.4.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications) +* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.2.4.RELEASE/reference/htmlsingle/#boot-features-jpa-and-spring-data) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) +* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) +* [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) + diff --git a/solution/01-docker/simple-api/pom.xml b/solution/01-docker/simple-api/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..efda6baa9ab589accd75030d5dd02e653364e497 --- /dev/null +++ b/solution/01-docker/simple-api/pom.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>2.7.5</version> + <relativePath /> <!-- lookup parent from repository --> + </parent> + <groupId>fr.takima.training</groupId> + <artifactId>simple-api</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>simple-api</name> + <description>Simple API demo project for Spring Boot</description> + + <properties> + <java.version>17</java.version> + <jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version> + <testcontainers.version>1.17.4</testcontainers.version> + <validation.version>2.0.1.Final</validation.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-jdbc</artifactId> + </dependency> + + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>jdbc</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>postgresql</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.validation</groupId> + <artifactId>validation-api</artifactId> + <version>${validation.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco-maven-plugin.version}</version> + <configuration> + <append>true</append> + </configuration> + <executions> + <execution> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>post-unit-test</id> + <phase>test</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/SimpleApiApplication.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/SimpleApiApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..6a901c60a133a4a01f31f2cacea5f3b05d8661e8 --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/SimpleApiApplication.java @@ -0,0 +1,13 @@ +package fr.takima.training.simpleapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SimpleApiApplication { + + public static void main(String[] args) { + SpringApplication.run(SimpleApiApplication.class, args); + } + +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/DepartmentController.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/DepartmentController.java new file mode 100644 index 0000000000000000000000000000000000000000..7f9ead0dff6c8702fa7aced918d04403a63984be --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/DepartmentController.java @@ -0,0 +1,63 @@ +package fr.takima.training.simpleapi.controller; + +import fr.takima.training.simpleapi.entity.Department; +import fr.takima.training.simpleapi.service.DepartmentService; +import fr.takima.training.simpleapi.service.StudentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +@RestController +@RequestMapping(value = "/departments") +@CrossOrigin +public class DepartmentController { + private DepartmentService departmentService; + private StudentService studentService; + + @Autowired + public DepartmentController(DepartmentService departmentService, StudentService studentService) { + this.departmentService = departmentService; + this.studentService = studentService; + } + + @GetMapping("/") + public ResponseEntity<Object> getDepartments() { + return ResponseEntity.ok(departmentService.getDepartments()); + } + + @GetMapping("/{departmentName}/students") + public ResponseEntity<Object> getDepartmentList(@PathVariable(name="departmentName") String name) { + Optional<Department> optionalDepartment = Optional.ofNullable(this.departmentService.getDepartmentByName(name)); + if (!optionalDepartment.isPresent()) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(this.studentService.getStudentsByDepartmentName(name)); + } + + @GetMapping("/{departmentName}") + public ResponseEntity<Object> getDepartmentByName(@PathVariable(name="departmentName") String name) { + Optional<Department> optionalDepartment = Optional.ofNullable(this.departmentService.getDepartmentByName(name)); + if (!optionalDepartment.isPresent()) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(this.departmentService.getDepartmentByName(name)); + } + + @GetMapping("/{departmentName}/count") + public ResponseEntity<Object> getDepartmentCountByName(@PathVariable(name="departmentName") String name) { + Optional<Department> optionalDepartment = Optional.ofNullable(this.departmentService.getDepartmentByName(name)); + if (!optionalDepartment.isPresent()) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(this.studentService.getStudentsNumberByDepartmentName(name)); + } +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/GreetingController.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/GreetingController.java new file mode 100644 index 0000000000000000000000000000000000000000..41054223f298dcab9a0004f56c66a880e2653dc6 --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/GreetingController.java @@ -0,0 +1,20 @@ +package fr.takima.training.simpleapi.controller; + +import org.springframework.web.bind.annotation.*; + +import java.util.concurrent.atomic.AtomicLong; + +@RestController +@CrossOrigin +public class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @GetMapping("/") + public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { + return new Greeting(counter.incrementAndGet(), String.format(template, name)); + } + + record Greeting(long id,String content) {} +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/StudentController.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/StudentController.java new file mode 100644 index 0000000000000000000000000000000000000000..f199b0c53edd30856745f1696adf8890e15784d7 --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/controller/StudentController.java @@ -0,0 +1,74 @@ +package fr.takima.training.simpleapi.controller; + +import fr.takima.training.simpleapi.entity.Student; +import fr.takima.training.simpleapi.service.StudentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.Optional; + +@RestController +@RequestMapping(value = "/students") +@CrossOrigin +public class StudentController { + private StudentService studentService; + + @Autowired + public StudentController(StudentService studentService) { + this.studentService = studentService; + } + + @GetMapping(value = "/") + public ResponseEntity<Object> getStudents() { + return ResponseEntity.ok(studentService.getAll()); + } + + @GetMapping(value = "/{id}") + public ResponseEntity<Object> getStudentById(@PathVariable(name="id") long id) { + Optional<Student> studentOptional = Optional.ofNullable(this.studentService.getStudentById(id)); + if (!studentOptional.isPresent()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(studentOptional.get()); + } + + @PostMapping + public ResponseEntity<Object> addStudent(@RequestBody Student student) { + Student savedStudent; + try { + savedStudent = this.studentService.addStudent(student); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + + URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}") + .buildAndExpand(savedStudent.getId()).toUri(); + return ResponseEntity.created(location).build(); + } + + @PutMapping(value = "/{id}") + public ResponseEntity<Object> updateStudent(@RequestBody Student student, @PathVariable(name="id") long id) { + Optional<Student> studentOptional = Optional.ofNullable(studentService.getStudentById(id)); + if (!studentOptional.isPresent()) { + return ResponseEntity.notFound().build(); + } + + student.setId(id); + this.studentService.addStudent(student); + return ResponseEntity.ok(student); + } + + @DeleteMapping(value = "/{id}") + public ResponseEntity<Object> removeStudent(@PathVariable(name="id") long id) { + Optional<Student> studentOptional = Optional.ofNullable(studentService.getStudentById(id)); + if (!studentOptional.isPresent()) { + return ResponseEntity.notFound().build(); + } + this.studentService.removeStudentById(id); + + return ResponseEntity.ok().build(); + } +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/dao/DepartmentDAO.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/dao/DepartmentDAO.java new file mode 100644 index 0000000000000000000000000000000000000000..387d687b75faa01e7c54cb31ceea9adf26552baf --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/dao/DepartmentDAO.java @@ -0,0 +1,10 @@ +package fr.takima.training.simpleapi.dao; + +import fr.takima.training.simpleapi.entity.Department; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DepartmentDAO extends JpaRepository<Department, Long> { + Department findDepartmentByName(String name); +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/dao/StudentDAO.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/dao/StudentDAO.java new file mode 100644 index 0000000000000000000000000000000000000000..9a1c240838eb0a222eedbfa3b39c1d75318eed2c --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/dao/StudentDAO.java @@ -0,0 +1,14 @@ +package fr.takima.training.simpleapi.dao; + +import fr.takima.training.simpleapi.entity.Student; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface StudentDAO extends JpaRepository<Student, Long> { + List<Student> findStudentsByDepartment_Name(String departmentName); + int countAllByDepartment_Name(String departmentName); + Student findById(long id); +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/entity/Department.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/entity/Department.java new file mode 100644 index 0000000000000000000000000000000000000000..412958d55b0b76b6d5e4c24ac0dda941cf88dc5e --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/entity/Department.java @@ -0,0 +1,67 @@ +package fr.takima.training.simpleapi.entity; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +@Entity +@Table(name = "departments") +public class Department { + @Id + @GeneratedValue (strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Size(max = 20) + @Column(name = "name") + private String name; + + @OneToMany(mappedBy = "department") + private List<Student> students; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Long id; + private String name; + + private Builder() { + } + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Department build() { + Department department = new Department(); + department.setId(id); + department.setName(name); + return department; + } + } +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/entity/Student.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/entity/Student.java new file mode 100644 index 0000000000000000000000000000000000000000..79478847865c4af3fb919eeb9806810f9244cd9d --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/entity/Student.java @@ -0,0 +1,101 @@ +package fr.takima.training.simpleapi.entity; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Entity +@Table(name = "students") +public class Student { + @Id + @GeneratedValue (strategy = GenerationType.IDENTITY) + private Long id; + + @Size(max = 20) + @Column(name = "first_name") + private String firstname; + + @NotNull + @Size(max = 30) + @Column(name = "last_name") + private String lastname; + + @ManyToOne + @JoinColumn(name = "department_id", nullable = false) + private Department department; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Long id; + private String firstname; + private String lastname; + private Department department; + + private Builder() { + } + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder firstname(String firstname) { + this.firstname = firstname; + return this; + } + + public Builder lastname(String lastname) { + this.lastname = lastname; + return this; + } + + public Builder department(Department department) { + this.department = department; + return this; + } + + public Student build() { + Student student = new Student(); + student.setId(id); + student.setFirstname(firstname); + student.setLastname(lastname); + student.setDepartment(department); + return student; + } + } +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/service/DepartmentService.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/service/DepartmentService.java new file mode 100644 index 0000000000000000000000000000000000000000..ec00a757f4ca0ace68f5b1a80be9f3f047040f72 --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/service/DepartmentService.java @@ -0,0 +1,30 @@ +package fr.takima.training.simpleapi.service; + +import fr.takima.training.simpleapi.dao.DepartmentDAO; +import fr.takima.training.simpleapi.entity.Department; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DepartmentService { + private DepartmentDAO departmentDAO; + + @Autowired + public DepartmentService(DepartmentDAO departmentDAO) { + this.departmentDAO = departmentDAO; + } + + public Department getDepartmentByName(String departmentName) { + if (departmentName == null || departmentName.length() == 0) { + throw new IllegalArgumentException("The department name must not be null or empty."); + } + + return this.departmentDAO.findDepartmentByName(departmentName); + } + + public List<Department> getDepartments() { + return this.departmentDAO.findAll(); + } +} diff --git a/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/service/StudentService.java b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/service/StudentService.java new file mode 100644 index 0000000000000000000000000000000000000000..31fe5ae7d6e552c475b0d9d9404ced7a439588e7 --- /dev/null +++ b/solution/01-docker/simple-api/src/main/java/fr/takima/training/simpleapi/service/StudentService.java @@ -0,0 +1,63 @@ +package fr.takima.training.simpleapi.service; + +import fr.takima.training.simpleapi.dao.StudentDAO; +import fr.takima.training.simpleapi.entity.Student; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class StudentService { + private StudentDAO studentDAO; + + @Autowired + public StudentService(StudentDAO studentDAO) { + this.studentDAO = studentDAO; + } + + public List<Student> getStudentsByDepartmentName(String departmentName) { + validateDepartmentName(departmentName); + return studentDAO.findStudentsByDepartment_Name(departmentName); + } + + public int getStudentsNumberByDepartmentName(String departmentName) { + validateDepartmentName(departmentName); + return studentDAO.countAllByDepartment_Name(departmentName); + } + + public List<Student> getAll() { + return studentDAO.findAll(); + } + + public Student getStudentById(long id) { + validateStudentId(id); + + return studentDAO.findById(id); + } + + public Student addStudent(Student student) { + if (student.getLastname() == null || student.getLastname().length() == 0 || student.getDepartment() == null) { + throw new IllegalArgumentException("You can't add a student without setting a lastname and a department ID"); + } + + return this.studentDAO.save(student); + } + + public void removeStudentById(long id) { + validateStudentId(id); + this.studentDAO.deleteById(id); + } + + private void validateDepartmentName(String departmentName) { + if (departmentName == null || departmentName.length() == 0) { + throw new IllegalArgumentException("The department name must not be null or empty."); + } + } + + private void validateStudentId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("The student ID must be positive."); + } + } +} diff --git a/solution/01-docker/simple-api/src/main/resources/application.yml b/solution/01-docker/simple-api/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..548999ceef98ea0b8bfc5e38f8f5c950809f4d62 --- /dev/null +++ b/solution/01-docker/simple-api/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + jpa: + properties: + hibernate: + jdbc: + lob: + non_contextual_creation: true + generate-ddl: false + open-in-view: true + datasource: + url: "jdbc:postgresql://${POSTGRES_URL}:${POSTGRES_PORT}/${POSTGRES_DB}" + username: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + driver-class-name: org.postgresql.Driver diff --git a/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/IT/DepartmentControllerTestIT.java b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/IT/DepartmentControllerTestIT.java new file mode 100644 index 0000000000000000000000000000000000000000..a5bf1235c3d252a4185e7922826682e202860568 --- /dev/null +++ b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/IT/DepartmentControllerTestIT.java @@ -0,0 +1,73 @@ +package fr.takima.training.sampleapplication.IT; + +import fr.takima.training.simpleapi.SimpleApiApplication; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest(classes={SimpleApiApplication.class}) +public class DepartmentControllerTestIT { + @Autowired + private MockMvc mockMvc; + + @Test + @Sql({"/InsertData.sql"}) + void testGetDepartmentByName() throws Exception { + mockMvc.perform(get("/departments/ASI/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("id", equalTo(1))) + .andExpect(jsonPath("name", equalTo("ASI"))); + } + + @Test + @Sql({"/InsertData.sql"}) + void testGetNonExistingDepartmentByName() throws Exception { + mockMvc.perform(get("/departments/NIMPORTEQUOI/")) + .andExpect(status().isNotFound()); + } + + @Test + @Sql({"/InsertData.sql"}) + void testGetDepartmentStudentsByName() throws Exception { + mockMvc.perform(get("/departments/ASI/students")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", equalTo(1))) + .andExpect(jsonPath("$[0].firstname", equalTo("Gautier"))) + .andExpect(jsonPath("$[1].lastname", equalTo("Le Bloas"))) + .andExpect(jsonPath("$[1].department.id", equalTo(1))) + .andExpect(jsonPath("$[1].department.name", equalTo("ASI"))); + } + + @Test + @Sql({"/InsertData.sql"}) + void testGetNonExistingDepartmentStudentsByName() throws Exception { + mockMvc.perform(get("/departments/NIMPORTEQUOI/students")) + .andExpect(status().isNotFound()); + } + + @Test + @Sql({"/InsertData.sql"}) + void testGetDepartmentCountByName() throws Exception { + mockMvc.perform(get("/departments/ASI/count")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", equalTo(48))); + } + + @Test + @Sql({"/InsertData.sql"}) + void testGetNonExistingDepartmentCountsByName() throws Exception { + mockMvc.perform(get("/departments/NIMPORTEQUOI/count")) + .andExpect(status().isNotFound()); + } +} diff --git a/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/IT/StudentControllerTestIT.java b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/IT/StudentControllerTestIT.java new file mode 100644 index 0000000000000000000000000000000000000000..7409e02a1556066e3c6e8448296fdf8d1bdd623d --- /dev/null +++ b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/IT/StudentControllerTestIT.java @@ -0,0 +1,130 @@ +package fr.takima.training.sampleapplication.IT; + +import fr.takima.training.simpleapi.SimpleApiApplication; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@AutoConfigureMockMvc +@SpringBootTest(classes={SimpleApiApplication.class}) +public class StudentControllerTestIT { + + @Autowired + private MockMvc mockMvc; + + @Test + @Sql({"/InsertData.sql"}) + void testGetStudentById() throws Exception { + mockMvc.perform(get("/students/6")) + .andExpect(status().isOk()) + .andExpect(jsonPath("id", equalTo(6))) + .andExpect(jsonPath("firstname", equalTo("Jeanne"))) + .andExpect(jsonPath("lastname", equalTo("Ausecours"))) + .andExpect(jsonPath("department.id", equalTo(4))) + .andExpect(jsonPath("department.name", equalTo("GC"))); + } + + @Test + @Sql({"/InsertData.sql"}) + void testGetNonExistingStudentById() throws Exception { + mockMvc.perform(get("/students/666")) + .andExpect(status().isNotFound()); + } + + @Test + @Sql({"/InsertData.sql"}) + void testPostStudent() throws Exception { + String body = "{\n" + + " \"firstname\": \"Didier\",\n" + + " \"lastname\": \"Deschamps\",\n" + + " \"department\": {\n" + + " \"id\": 4,\n" + + " \"name\": \"GC\"\n" + + " }\n" + + "}"; + mockMvc.perform(post("/students/") + .content(body) + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isCreated()) + .andExpect(header().exists("location")); + } + + @Test + @Sql({"/InsertData.sql"}) + void testPostStudentWithoutLastName() throws Exception { + String body = "{\n" + + " \"firstname\": \"Didier\",\n" + + " \"department\": {\n" + + " \"id\": 4,\n" + + " \"name\": \"GC\"\n" + + " }\n" + + "}"; + mockMvc.perform(post("/students/") + .content(body) + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isBadRequest()); + } + + @Test + @Sql({"/InsertData.sql"}) + void testPostStudentWithoutDepartment() throws Exception { + String body = "{\n" + + " \"lastname\": \"Didier\",\n" + + " }\n" + + "}"; + mockMvc.perform(post("/students/") + .content(body) + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isBadRequest()); + } + + @Test + @Sql({"/InsertData.sql"}) + void testUpdateStudent() throws Exception { + mockMvc.perform(get("/students/11")) + .andExpect(jsonPath("id", equalTo(11))) + .andExpect(jsonPath("firstname", equalTo("Sophie"))) + .andExpect(jsonPath("lastname", equalTo("Schutt"))) + .andExpect(jsonPath("department.id", equalTo(9))) + .andExpect(jsonPath("department.name", equalTo("PERF-I"))); + + String body = "{\n" + + " \"firstname\": \"Francis\",\n" + + " \"lastname\": \"Huster\",\n" + + " \"department\": {\n" + + " \"id\": 1,\n" + + " \"name\": \"ASI\"\n" + + " }\n" + + "}"; + mockMvc.perform(put("/students/11") + .content(body) + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()) + .andExpect(jsonPath("id", equalTo(11))) + .andExpect(jsonPath("firstname", equalTo("Francis"))) + .andExpect(jsonPath("lastname", equalTo("Huster"))) + .andExpect(jsonPath("department.id", equalTo(1))) + .andExpect(jsonPath("department.name", equalTo("ASI"))); + } + + @Test + @Sql({"/InsertData.sql"}) + void testDeleteStudent() throws Exception { + mockMvc.perform(get("/students/1")) + .andExpect(jsonPath("id", equalTo(1))); + mockMvc.perform(delete("/students/1")) + .andExpect(status().isOk()); + mockMvc.perform(get("/students/1")) + .andExpect(status().isNotFound()); + } +} diff --git a/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/unit/DepartmentsServiceTest.java b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/unit/DepartmentsServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9e5418516c269a924ecb1f9e55c61dc27ccd1c07 --- /dev/null +++ b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/unit/DepartmentsServiceTest.java @@ -0,0 +1,44 @@ +package fr.takima.training.sampleapplication.unit; + +import fr.takima.training.simpleapi.dao.DepartmentDAO; +import fr.takima.training.simpleapi.entity.Department; +import fr.takima.training.simpleapi.service.DepartmentService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class DepartmentsServiceTest { + + @InjectMocks + private DepartmentService departmentService; + + @Mock + private DepartmentDAO departmentDAO; + + private Department department = Department.builder().id(1L).name("DepartementTest").build(); + + @Test + public void testGetDepartmentByName() { + when(departmentDAO.findDepartmentByName("DepartmentTest")).thenReturn(department); + assertEquals(department, departmentDAO.findDepartmentByName("DepartmentTest")); + } + + @Test + public void testGetDepartmentByNameWithNullValue() { + assertThrows(IllegalArgumentException.class, () -> departmentService.getDepartmentByName(null)); + } + + @Test + public void testGetDepartmentByNameWithEmptyValue() { + assertThrows(IllegalArgumentException.class, () -> departmentService.getDepartmentByName("")); + } +} diff --git a/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/unit/StudentsServiceTest.java b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/unit/StudentsServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..168106c712709b22091efc8dfc78c51f77b52f7a --- /dev/null +++ b/solution/01-docker/simple-api/src/test/java/fr/takima/training/sampleapplication/unit/StudentsServiceTest.java @@ -0,0 +1,117 @@ +package fr.takima.training.sampleapplication.unit; + +import fr.takima.training.simpleapi.dao.StudentDAO; +import fr.takima.training.simpleapi.entity.Student; +import fr.takima.training.simpleapi.entity.Department; +import fr.takima.training.simpleapi.service.StudentService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class StudentsServiceTest { + + @InjectMocks + private StudentService studentService; + + @Mock + private StudentDAO studentDAO; + + private Department department = Department.builder().id(1L).name("DepartementTest").build(); + private Student student = Student + .builder() + .id(1L) + .firstname("Firstname") + .lastname("lastname") + .department(department) + .build(); + + @Test + public void testGetStudentById() { + when(studentDAO.findById(1L)).thenReturn(student); + assertEquals(student, studentService.getStudentById(1L)); + } + + @Test + public void testGetStudentByIdWithNegativeId() { + assertThrows(IllegalArgumentException.class, () -> studentService.getStudentById(-5)); + } + + @Test + public void testGetStudentsByDepartmentName() { + List<Student> students = new ArrayList<>(); + students.add(student); + when(studentDAO.findStudentsByDepartment_Name("DepartmentTest")).thenReturn(students); + + assertEquals(students, studentService.getStudentsByDepartmentName("DepartmentTest")); + } + + @Test + public void testGetStudentsByDepartmentNameWithNullValue() { + assertThrows(IllegalArgumentException.class, () -> studentService.getStudentsByDepartmentName(null)); + } + + @Test + public void testGetStudentsByDepartmentNameWithEmptyValue() { + assertThrows(IllegalArgumentException.class, () -> studentService.getStudentsByDepartmentName("")); + } + + @Test + public void testGetStudentsNumberByDepartmentName() { + when(studentDAO.countAllByDepartment_Name("DepartmentTest")).thenReturn(1); + assertEquals(1, studentService.getStudentsNumberByDepartmentName("DepartmentTest")); + } + + @Test + public void testGetStudentsNumberByDepartmentNameWithNullValue() { + assertThrows(IllegalArgumentException.class, () -> studentService.getStudentsNumberByDepartmentName(null)); + } + + @Test + public void testGetStudentsNumberByDepartmentNameWithEmptyValue() { + assertThrows(IllegalArgumentException.class, () -> studentService.getStudentsNumberByDepartmentName(null)); + } + + @Test + public void testAddStudent() { + when(studentDAO.save(student)).thenReturn(student); + assertEquals(student, studentService.addStudent(student)); + } + + @Test + public void testAddStudentWithBadLastname() { + Student studentWithNullLastname = Student.builder().id(1L).firstname("abc").department(department).build(); + assertThrows(IllegalArgumentException.class, () -> studentService.addStudent(studentWithNullLastname)); + + Student studentWithEmptyLastname = Student.builder().id(1L).firstname("abc").lastname("").department(department).build(); + assertThrows(IllegalArgumentException.class, () -> studentService.addStudent(studentWithEmptyLastname)); + } + + @Test + public void testAddStudentWithoutDepartment() { + Student studentWithoutDepartment = Student.builder().id(1L).lastname("abc").build(); + assertThrows(IllegalArgumentException.class, () -> studentService.addStudent(studentWithoutDepartment)); + } + + @Test + public void testRemoveStudentById() { + assertDoesNotThrow(() -> studentService.removeStudentById(1L)); + } + + @Test + public void testRemoveStudentWithNegativeId() { + assertThrows(IllegalArgumentException.class, () -> studentService.removeStudentById(-5)); + } +} diff --git a/solution/01-docker/simple-api/src/test/resources/InsertData.sql b/solution/01-docker/simple-api/src/test/resources/InsertData.sql new file mode 100644 index 0000000000000000000000000000000000000000..d22392086bcc14c28a39e4efc07a78e2a64257aa --- /dev/null +++ b/solution/01-docker/simple-api/src/test/resources/InsertData.sql @@ -0,0 +1,441 @@ +DROP TABLE IF EXISTS students CASCADE; +DROP TABLE IF EXISTS departments CASCADE ; + + +CREATE TABLE public.departments +( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL +); + +CREATE TABLE public.students +( + id SERIAL PRIMARY KEY, + department_id INT NOT NULL REFERENCES departments (id), + first_name VARCHAR(20) NOT NULL, + last_name VARCHAR(20) NOT NULL +); +INSERT INTO departments (name) +VALUES ('ASI'), + ('CFI'), + ('EP'), + ('GC'), + ('GM'), + ('MECA'), + ('MRIE'), + ('PERF-E'), + ('PERF-I'); + + +INSERT INTO students (first_name, last_name, department_id) +VALUES ('Gautier', 'Darchen', 1), + ('Jonathan', 'Le Bloas', 1), + ('Franck', 'Bolduc', 1), + ('Gaston', 'Lagaffe', 2), + ('Theodor', 'Nomrandom', 3), + ('Jeanne', 'Ausecours', 4), + ('Domitille', 'Monton', 5), + ('Jacky', 'Sardou', 6), + ('Valentin', 'Patrick', 7), + ('Mohammed', 'Benevento', 8), + ('Sophie', 'Schutt', 9), + ('Madonna', 'Soto', 9), + ('Julie', 'Waller', 4), + ('Damon', 'Gregory', 7), + ('Dominic', 'Rowe', 1), + ('Alan', 'Mckay', 6), + ('Quinlan', 'Bartlett', 1), + ('Blake', 'Welch', 6), + ('Jakeem', 'Phelps', 3), + ('Kylee', 'Shepard', 9), + ('Sybil', 'Raymond', 4), + ('Evelyn', 'Holman', 4), + ('Nash', 'Mccray', 7), + ('Ursa', 'Morin', 3), + ('Alan', 'Rasmussen', 6), + ('Iris', 'Bean', 5), + ('Wade', 'Mullen', 9), + ('Addison', 'Boyd', 9), + ('Ebony', 'Whitehead', 6), + ('Isaac', 'Weiss', 4), + ('Keely', 'Massey', 3), + ('Zeus', 'Yates', 9), + ('Summer', 'Bauer', 3), + ('Eagan', 'Beard', 2), + ('Nyssa', 'Andrews', 2), + ('Rhonda', 'Johns', 8), + ('Evan', 'Goodman', 3), + ('Alden', 'Newton', 9), + ('Timon', 'York', 3), + ('Rafael', 'Hudson', 1), + ('MacKensie', 'Meyers', 6), + ('Axel', 'Glenn', 2), + ('Erasmus', 'Schmidt', 7), + ('Mara', 'Hickman', 7), + ('Jescie', 'Moore', 7), + ('Uma', 'Conway', 3), + ('Cherokee', 'Hudson', 6), + ('Martin', 'Peterson', 3), + ('Demetria', 'Santiago', 5), + ('Maya', 'Austin', 7), + ('Charlotte', 'Simmons', 8), + ('Vivian', 'Colon', 4), + ('Patricia', 'Walton', 5), + ('Baker', 'Carlson', 4), + ('Calvin', 'Reyes', 9), + ('Byron', 'Gay', 8), + ('Regan', 'Wiley', 4), + ('Nina', 'Chambers', 6), + ('Octavius', 'Hess', 3), + ('Hedda', 'Mcmillan', 2), + ('Aidan', 'Meadows', 7), + ('Iris', 'Nash', 7), + ('Brenda', 'Maddox', 3), + ('Justin', 'Salinas', 4), + ('Dylan', 'Burris', 4), + ('Olympia', 'Camacho', 6), + ('Armando', 'Kim', 8), + ('Harriet', 'Cooke', 2), + ('Guy', 'Powell', 5), + ('Grant', 'Rocha', 7), + ('Darryl', 'Soto', 6), + ('Martin', 'Nolan', 2), + ('Blair', 'Crosby', 9), + ('Oliver', 'Jennings', 6), + ('Dennis', 'Wells', 4), + ('Lucian', 'Odonnell', 9), + ('Magee', 'Buchanan', 5), + ('Daria', 'Frank', 2), + ('Buffy', 'Stout', 1), + ('Walter', 'Mcgee', 5), + ('Odessa', 'Manning', 7), + ('Keelie', 'Walters', 7), + ('Valentine', 'Shelton', 2), + ('Chloe', 'Dickerson', 1), + ('Lucas', 'Ingram', 2), + ('Plato', 'Davidson', 8), + ('Walter', 'Griffith', 5), + ('Imelda', 'Snider', 4), + ('Chloe', 'Sanford', 5), + ('Colt', 'Olsen', 4), + ('Cherokee', 'Palmer', 7), + ('Colorado', 'Wells', 6), + ('Colorado', 'Williamson', 2), + ('Penelope', 'Murphy', 4), + ('Hamilton', 'Kaufman', 2), + ('Mia', 'Mayo', 8), + ('Kendall', 'Walker', 1), + ('Kathleen', 'Hooper', 5), + ('Gannon', 'Ellis', 8), + ('Audrey', 'Dotson', 3), + ('Dylan', 'Buckley', 2), + ('Hanna', 'Atkins', 2), + ('Miriam', 'Wolf', 2), + ('Maile', 'Henson', 9), + ('Colton', 'Britt', 3), + ('Cleo', 'Whitley', 1), + ('Garrett', 'Valencia', 9), + ('Wyatt', 'Stewart', 1), + ('Pamela', 'Sutton', 3), + ('Ciara', 'Scott', 9), + ('Ivory', 'Mejia', 4), + ('Cameron', 'Frazier', 2), + ('Brody', 'Hood', 3), + ('Christen', 'Quinn', 1), + ('Sawyer', 'Castro', 4), + ('Karly', 'Dixon', 4), + ('Kristen', 'Blackwell', 7), + ('Erin', 'Casey', 1), + ('Curran', 'Shepard', 4), + ('Hannah', 'Gibson', 5), + ('Levi', 'Bennett', 8), + ('Vivien', 'Mcfadden', 4), + ('Alyssa', 'Noble', 2), + ('Lisandra', 'Bender', 6), + ('Valentine', 'Brooks', 3), + ('Tatum', 'Wyatt', 1), + ('Dillon', 'Ratliff', 4), + ('Amela', 'Duran', 9), + ('Casey', 'Whitney', 7), + ('Stone', 'Robertson', 6), + ('Xerxes', 'Collier', 6), + ('Daniel', 'Morrow', 9), + ('Zorita', 'York', 7), + ('Charity', 'Gutierrez', 6), + ('Chadwick', 'Sparks', 7), + ('Alfonso', 'Poole', 8), + ('Graiden', 'Richardson', 8), + ('Deacon', 'Hobbs', 7), + ('Garrett', 'Sharp', 8), + ('Charlotte', 'Campbell', 2), + ('Colin', 'Mills', 5), + ('Xavier', 'Giles', 5), + ('Brennan', 'Buchanan', 8), + ('Xanthus', 'Mann', 6), + ('Coby', 'Watson', 4), + ('Charde', 'Page', 8), + ('Cooper', 'Wilkins', 7), + ('Kane', 'Conrad', 2), + ('Rhiannon', 'Mayo', 6), + ('Charissa', 'Small', 8), + ('Jarrod', 'Rodriquez', 5), + ('Jonah', 'Glenn', 7), + ('Cameron', 'Donovan', 4), + ('Hector', 'Morales', 6), + ('Malcolm', 'Francis', 2), + ('Cade', 'Barry', 8), + ('Buffy', 'Sims', 1), + ('Vaughan', 'Downs', 6), + ('Quinn', 'Eaton', 8), + ('Kaseem', 'Kirby', 7), + ('Zane', 'Wall', 1), + ('Rebekah', 'Richardson', 2), + ('Stephanie', 'Hood', 3), + ('Sandra', 'Hyde', 9), + ('Cassady', 'Bentley', 6), + ('Brock', 'Miles', 6), + ('Kyle', 'Fuentes', 5), + ('Rafael', 'Cunningham', 4), + ('Amela', 'Witt', 4), + ('Gage', 'Poole', 6), + ('Jarrod', 'English', 6), + ('Quail', 'Perry', 9), + ('Preston', 'Randolph', 6), + ('Perry', 'Bond', 3), + ('Mara', 'Nixon', 7), + ('Aspen', 'Carlson', 6), + ('Talon', 'Rosales', 3), + ('Noble', 'Mack', 4), + ('Clayton', 'Carter', 8), + ('Griffin', 'Fisher', 4), + ('Shellie', 'Rosales', 6), + ('Ina', 'Jones', 1), + ('Rudyard', 'Richard', 4), + ('Paki', 'Bell', 3), + ('Igor', 'Short', 8), + ('Blake', 'Mann', 8), + ('Graiden', 'Baird', 6), + ('Valentine', 'Hood', 7), + ('Flavia', 'Wiley', 1), + ('Alfonso', 'Chambers', 8), + ('Wyoming', 'Cote', 9), + ('Len', 'Burton', 9), + ('Fuller', 'Roman', 6), + ('Clarke', 'Vaughn', 3), + ('Raymond', 'Glenn', 3), + ('Athena', 'Ellis', 3), + ('Christian', 'Donaldson', 9), + ('Omar', 'Hyde', 7), + ('Ivor', 'Zamora', 7), + ('Aphrodite', 'Gardner', 6), + ('Mechelle', 'Padilla', 4), + ('Nadine', 'Patrick', 7), + ('Selma', 'Emerson', 1), + ('Lysandra', 'Coleman', 7), + ('Elijah', 'Pitts', 4), + ('Hedley', 'Glenn', 1), + ('Vivien', 'King', 2), + ('Adrienne', 'Powell', 1), + ('Renee', 'George', 6), + ('Sydney', 'Bird', 7), + ('Maile', 'Humphrey', 8), + ('Jason', 'Mckenzie', 4), + ('Tanek', 'Anderson', 8), + ('Amir', 'Alexander', 2), + ('Lillith', 'Logan', 5), + ('Elliott', 'Small', 9), + ('Coby', 'Tillman', 5), + ('Fulton', 'Odom', 3), + ('Hammett', 'Wilkerson', 7), + ('Constance', 'Hawkins', 5), + ('Carson', 'Perkins', 6), + ('Ciara', 'Williamson', 8), + ('Whilemina', 'Randall', 4), + ('Melvin', 'Patel', 6), + ('Odette', 'Rice', 9), + ('Zia', 'Mccormick', 9), + ('Brody', 'Nichols', 5), + ('Colleen', 'Mcgowan', 1), + ('Finn', 'Greer', 7), + ('Kirby', 'Mercer', 4), + ('Emerson', 'Patel', 9), + ('Xantha', 'William', 1), + ('Declan', 'Cabrera', 2), + ('Cynthia', 'Parker', 7), + ('Knox', 'Schroeder', 4), + ('Thane', 'Stone', 5), + ('Daniel', 'Stevenson', 6), + ('Roanna', 'Blair', 4), + ('Nigel', 'Byers', 6), + ('Kristen', 'Black', 1), + ('Rose', 'Oliver', 9), + ('Thor', 'Taylor', 6), + ('Slade', 'Sharpe', 5), + ('Angelica', 'Day', 1), + ('Duncan', 'Knight', 9), + ('Bertha', 'Edwards', 2), + ('Cairo', 'Shaw', 1), + ('Rachel', 'Wagner', 7), + ('Lavinia', 'Goodwin', 3), + ('Hiram', 'Hoffman', 4), + ('Kato', 'Strong', 6), + ('Ainsley', 'Lawson', 6), + ('Mercedes', 'Newman', 4), + ('Ian', 'Graham', 6), + ('Jesse', 'Abbott', 4), + ('Lenore', 'Perez', 2), + ('Cassady', 'Randall', 5), + ('Acton', 'Wyatt', 4), + ('Serena', 'Austin', 4), + ('Byron', 'Chen', 7), + ('Kessie', 'Brewer', 3), + ('Ria', 'Cantu', 4), + ('Dennis', 'Frazier', 1), + ('Rhiannon', 'Cruz', 1), + ('Tyler', 'Lawson', 9), + ('Jerome', 'Cannon', 3), + ('Tamara', 'Becker', 5), + ('Aladdin', 'Brady', 3), + ('Quynn', 'Underwood', 7), + ('Urielle', 'Kane', 9), + ('Dai', 'Wooten', 3), + ('Blythe', 'Patrick', 5), + ('Ivory', 'Hyde', 7), + ('Philip', 'Mcneil', 9), + ('Octavia', 'Bradley', 7), + ('Giacomo', 'Arnold', 4), + ('Allen', 'Koch', 9), + ('Alana', 'Acosta', 8), + ('Tiger', 'Acosta', 3), + ('Jenna', 'Buckley', 3), + ('Curran', 'Sears', 4), + ('Kato', 'Raymond', 1), + ('Brian', 'Lopez', 1), + ('Ezra', 'Roberson', 1), + ('Myles', 'Holland', 4), + ('Brooke', 'Zamora', 6), + ('Giacomo', 'Dorsey', 4), + ('Henry', 'Cole', 7), + ('Brynne', 'Goodman', 9), + ('Jorden', 'Sargent', 3), + ('Charissa', 'Chavez', 2), + ('Graham', 'Nixon', 8), + ('Riley', 'Mclaughlin', 6), + ('Zeph', 'Dyer', 2), + ('Savannah', 'Chaney', 5), + ('Jelani', 'Neal', 5), + ('Lunea', 'Farley', 6), + ('Penelope', 'Wong', 8), + ('Blossom', 'Hyde', 3), + ('Angela', 'Gonzalez', 1), + ('Fay', 'Daniels', 1), + ('Sage', 'Clements', 8), + ('Lucius', 'Rose', 8), + ('Jelani', 'Bryant', 3), + ('Michael', 'Shields', 5), + ('Boris', 'Mayo', 6), + ('Rhoda', 'Holloway', 1), + ('Ivan', 'Hunt', 7), + ('Brandon', 'Moses', 3), + ('Dustin', 'Noel', 5), + ('Tucker', 'Townsend', 4), + ('Kirsten', 'Burke', 5), + ('Amy', 'Graham', 5), + ('Margaret', 'Frazier', 1), + ('Pearl', 'Knight', 8), + ('Penelope', 'Porter', 3), + ('Madeline', 'Michael', 5), + ('Raymond', 'Montgomery', 5), + ('Serena', 'Nichols', 6), + ('Stone', 'Berry', 8), + ('Thomas', 'Henderson', 4), + ('Cora', 'Sanchez', 4), + ('Mohammad', 'Witt', 4), + ('Cathleen', 'Nelson', 3), + ('Keiko', 'Lawrence', 9), + ('Nyssa', 'Frazier', 8), + ('Vaughan', 'Crawford', 2), + ('Blake', 'Mullen', 6), + ('Baxter', 'Coffey', 7), + ('Kristen', 'Vaughn', 3), + ('Minerva', 'Hayden', 6), + ('Logan', 'Hancock', 6), + ('Urielle', 'Glenn', 1), + ('Colt', 'Weiss', 5), + ('Adrienne', 'Cherry', 4), + ('Tanner', 'Thompson', 5), + ('Walter', 'Glenn', 5), + ('Carissa', 'Reynolds', 4), + ('Ima', 'Jacobs', 7), + ('Burke', 'Clark', 5), + ('Acton', 'Gilbert', 7), + ('Solomon', 'Kirby', 6), + ('Herrod', 'Mckay', 1), + ('Jaden', 'Jones', 2), + ('Ina', 'Lowe', 2), + ('Tana', 'Cunningham', 9), + ('Emerson', 'Hurley', 6), + ('Perry', 'Fitzpatrick', 4), + ('Edan', 'Herring', 4), + ('Alfreda', 'Navarro', 2), + ('Imogene', 'Mcgee', 2), + ('Brynn', 'Frost', 7), + ('Hedwig', 'Nichols', 4), + ('Gay', 'Hicks', 7), + ('Emmanuel', 'Irwin', 3), + ('Michael', 'Wood', 2), + ('Cheyenne', 'Hall', 2), + ('Coby', 'Cherry', 2), + ('Todd', 'Lester', 5), + ('Merrill', 'Graham', 2), + ('Dillon', 'Boyd', 4), + ('Adam', 'Houston', 6), + ('Dylan', 'Holmes', 2), + ('Otto', 'Church', 2), + ('Lucius', 'Nixon', 2), + ('Baker', 'Stevenson', 7), + ('Ryder', 'Ferguson', 2), + ('Castor', 'Lindsay', 2), + ('Danielle', 'Kim', 8), + ('Debra', 'Caldwell', 3), + ('Kendall', 'Webster', 4), + ('Len', 'Leblanc', 4), + ('Gwendolyn', 'Cervantes', 1), + ('Jillian', 'Peters', 2), + ('Neve', 'Oneil', 1), + ('Finn', 'Miranda', 1), + ('Jonah', 'Merritt', 3), + ('Leonard', 'Lloyd', 6), + ('Quemby', 'Meyer', 9), + ('Griffin', 'Hatfield', 1), + ('Kibo', 'Daugherty', 8), + ('Nelle', 'French', 8), + ('Kasper', 'Velazquez', 1), + ('Maris', 'Mccoy', 5), + ('Oprah', 'Russo', 5), + ('Emmanuel', 'Padilla', 5), + ('Gretchen', 'Norris', 1), + ('Hamish', 'Terry', 3), + ('Cain', 'Burch', 4), + ('Boris', 'Hewitt', 1), + ('Heidi', 'Holden', 7), + ('Martin', 'Willis', 7), + ('Ryder', 'Bentley', 3), + ('Adele', 'Estes', 6), + ('Paul', 'William', 2), + ('Gemma', 'Wood', 1), + ('Todd', 'Hernandez', 9), + ('John', 'Mcdaniel', 7), + ('Vanna', 'Stafford', 6), + ('Bianca', 'Klein', 4), + ('Ashton', 'Walker', 5), + ('Sigourney', 'Reyes', 9), + ('Amery', 'Short', 6), + ('Marsden', 'Ayala', 1), + ('Tallulah', 'Ayers', 3), + ('Reuben', 'Cherry', 1), + ('Tana', 'Heath', 6), + ('Josephine', 'Travis', 8), + ('Macaulay', 'Mason', 1), + ('Odessa', 'Malone', 7), + ('Ryder', 'Levy', 7); diff --git a/solution/01-docker/simple-api/src/test/resources/application.yml b/solution/01-docker/simple-api/src/test/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..f0690c5f36b701b8c293ce8582cb612ae0502a78 --- /dev/null +++ b/solution/01-docker/simple-api/src/test/resources/application.yml @@ -0,0 +1,13 @@ +spring: + jpa: + properties: + hibernate: + jdbc: + lob: + non_contextual_creation: true + generate-ddl: false + datasource: + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + url: jdbc:tc:postgresql://hostname/simple-api + username: ${embedded.postgresql.user} + password: ${embedded.postgresql.password} \ No newline at end of file