From 095d1c4d79f6b214de57628b1cd7f5d5fdf459b5 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 6 Apr 2024 23:54:19 +0200 Subject: [PATCH 01/86] feat: add nginx reverse proxy for grafana, prometheus and the webapp --- docker-compose.yml | 238 ++++++++++++++++++++++++--------------------- nginx.conf | 20 ++++ 2 files changed, 146 insertions(+), 112 deletions(-) create mode 100644 nginx.conf diff --git a/docker-compose.yml b/docker-compose.yml index 3e8e23bf..08057dab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,112 +1,126 @@ -version: '3' -services: - WIQ_DB: - container_name: postgresql-${teamname:-defaultASW} - environment: - POSTGRES_USER: ${DATABASE_USER} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - image: postgres:latest - profiles: ["dev", "prod"] - networks: - - mynetwork - ports: - - "5432:5432" - - api: - container_name: api-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/api:latest - profiles: [ "dev", "prod" ] - build: - context: ./api - args: - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - JWT_SECRET: ${JWT_SECRET} - environment: - - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - - JWT_SECRET=${JWT_SECRET} - networks: - - mynetwork - depends_on: - - WIQ_DB - ports: - - "8080:8080" - - question-generator: - container_name: question-generator-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/question-generator:latest - profiles: [ "dev", "prod" ] - build: - context: ./questiongenerator - args: - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - environment: - - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - networks: - - mynetwork - depends_on: - - WIQ_DB - - webapp: - container_name: webapp-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/webapp:latest - profiles: [ "dev", "prod" ] - build: - args: - REACT_APP_API_ENDPOINT: ${API_URI} - context: ./webapp - environment: - - REACT_APP_API_ENDPOINT=${API_URI} - ports: - - "80:3000" - - prometheus: - image: prom/prometheus - container_name: prometheus-${teamname:-defaultASW} - profiles: ["dev"] - networks: - - mynetwork - volumes: - - ./quiz-api/monitoring/prometheus:/etc/prometheus - - prometheus_data:/prometheus - ports: - - "9090:9090" - depends_on: - - api - - grafana: - image: grafana/grafana - container_name: grafana-${teamname:-defaultASW} - profiles: [ "dev" ] - networks: - - mynetwork - volumes: - - grafana_data:/var/lib/grafana - - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning - environment: - - GF_SERVER_HTTP_PORT=9091 - - GF_AUTH_DISABLE_LOGIN_FORM=true - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - ports: - - "9091:9091" - depends_on: - - prometheus - - -volumes: - postgres_data: - prometheus_data: - grafana_data: - - -networks: - mynetwork: - driver: bridge +version: '3' +services: + WIQ_DB: + container_name: postgresql-${teamname:-defaultASW} + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + image: postgres:latest + profiles: ["dev", "prod"] + networks: + mynetwork: + ipv4_address: 10.5.0.10 + + api: + container_name: api-${teamname:-defaultASW} + image: api:latest + profiles: [ "dev", "prod" ] + build: + context: ./api + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + environment: + - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + networks: + mynetwork: + ipv4_address: 10.5.0.6 + depends_on: + - WIQ_DB + + question-generator: + container_name: question-generator-${teamname:-defaultASW} + image: question-generator:latest + profiles: [ "dev", "prod" ] + build: + context: ./questiongenerator + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + environment: + - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + networks: + mynetwork: + ipv4_address: 10.5.0.11 + depends_on: + - WIQ_DB + + grafana: + image: grafana/grafana + container_name: grafana-${teamname:-defaultASW} + profiles: [ "dev" ] + networks: + mynetwork: + ipv4_address: 10.5.0.7 + volumes: + - grafana_data:/var/lib/grafana + - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SERVER_HTTP_PORT=9091 + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + depends_on: + - prometheus + + prometheus: + image: prom/prometheus + container_name: prometheus-${teamname:-defaultASW} + profiles: ["dev"] + networks: + mynetwork: + ipv4_address: 10.5.0.8 + volumes: + - ./quiz-api/monitoring/prometheus:/etc/prometheus + - prometheus_data:/prometheus + depends_on: + - api + + webapp: + container_name: webapp-${teamname:-defaultASW} + image: webapp:latest + profiles: [ "dev", "prod" ] + build: + args: + REACT_APP_API_ENDPOINT: ${API_URI} + context: ./webapp + environment: + - REACT_APP_API_ENDPOINT=${API_URI} + networks: + mynetwork: + ipv4_address: 10.5.0.9 + + nginx: + image: nginx:latest + container_name: nginx + networks: + mynetwork: + ipv4_address: 10.5.0.12 + ports: + - "80:80" + depends_on: + - webapp + - api + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + +volumes: + postgres_data: + prometheus_data: + grafana_data: + +networks: + mynetwork: + driver: bridge + ipam: + config: + - subnet: 10.5.0.0/16 + gateway: 10.5.0.1 \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..ad69ed0e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,20 @@ +events {} + +http { + server { + listen 80; + + location / { + proxy_pass http://10.5.0.9:3000; + } + + location /grafana { + proxy_pass http://10.5.0.7:9091; + } + + location /prometheus { + proxy_pass http://10.5.0.8:9090; + } + + } +} \ No newline at end of file From 86a103deaf4fe8875a127e16210701f7703a5a42 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 14:31:52 +0200 Subject: [PATCH 02/86] feat: add SSL configuration --- docker-compose.yml | 12 ++++++++---- nginx.conf | 12 +++--------- nginx_conf/nginx_conf.sh | 13 +++++++++++++ nginx_conf/self-signed.conf | 2 ++ nginx_conf/ssl-params.conf | 18 ++++++++++++++++++ 5 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 nginx_conf/nginx_conf.sh create mode 100644 nginx_conf/self-signed.conf create mode 100644 nginx_conf/ssl-params.conf diff --git a/docker-compose.yml b/docker-compose.yml index 08057dab..1cf40951 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: - DATABASE_USER=${DATABASE_USER} - DATABASE_PASSWORD=${DATABASE_PASSWORD} - JWT_SECRET=${JWT_SECRET} + ports: + - 8080:8080 networks: mynetwork: ipv4_address: 10.5.0.6 @@ -99,18 +101,20 @@ services: ipv4_address: 10.5.0.9 nginx: - image: nginx:latest + image: ubuntu:latest container_name: nginx networks: mynetwork: ipv4_address: 10.5.0.12 ports: - "80:80" + - "443:443" depends_on: - webapp - api - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro + - grafana + - prometheus + command: curl -s https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf | bash volumes: postgres_data: @@ -121,6 +125,6 @@ networks: mynetwork: driver: bridge ipam: - config: + config: - subnet: 10.5.0.0/16 gateway: 10.5.0.1 \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index ad69ed0e..29e2d1e0 100644 --- a/nginx.conf +++ b/nginx.conf @@ -2,19 +2,13 @@ events {} http { server { - listen 80; + listen 80, 443 ssl; + include snippets/self-signed.conf; + include snippets/ssl-params.conf; location / { proxy_pass http://10.5.0.9:3000; } - - location /grafana { - proxy_pass http://10.5.0.7:9091; - } - - location /prometheus { - proxy_pass http://10.5.0.8:9090; - } } } \ No newline at end of file diff --git a/nginx_conf/nginx_conf.sh b/nginx_conf/nginx_conf.sh new file mode 100644 index 00000000..ce69edab --- /dev/null +++ b/nginx_conf/nginx_conf.sh @@ -0,0 +1,13 @@ +apt-get update -y +apt upgrade -y +apt install -y curl +apt-get install -y nginx openssl +curl -o /etc/nginx/nginx.conf https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf +mkdir /etc/nginx/certs +openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/certs/nginx-selfsigned.key -out /etc/nginx/certs/nginx-selfsigned.crt -subj '/CN=localhost' +mkdir /etc/nginx/snippets +curl -o /etc/nginx/snippets/self-signed.conf https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/self_signed.conf +curl -o /etc/nginx/snippets/ssl-params.conf +service nginx start +tail -f > /dev/null \ No newline at end of file diff --git a/nginx_conf/self-signed.conf b/nginx_conf/self-signed.conf new file mode 100644 index 00000000..a8952e7e --- /dev/null +++ b/nginx_conf/self-signed.conf @@ -0,0 +1,2 @@ +ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; +ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; \ No newline at end of file diff --git a/nginx_conf/ssl-params.conf b/nginx_conf/ssl-params.conf new file mode 100644 index 00000000..05ed0f08 --- /dev/null +++ b/nginx_conf/ssl-params.conf @@ -0,0 +1,18 @@ +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_prefer_server_ciphers on; +ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; +ssl_ecdh_curve secp384r1; +ssl_session_cache shared:SSL:10m; +ssl_session_tickets off; +#ssl_stapling on; +#ssl_stapling_verify on; +resolver 8.8.8.8 8.8.4.4 valid=300s; +resolver_timeout 5s; + +#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; +#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; + +add_header X-Frame-Options DENY; +add_header X-Content-Type-Options nosniff; + +ssl_dhparam /etc/ssl/certs/dhparam.pem; \ No newline at end of file From 31fdf58a41cec3a7c5a6818e0e45251b98a04687 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 14:35:38 +0200 Subject: [PATCH 03/86] fix: fix curl error --- docker-compose.yml | 2 +- nginx_conf/nginx_conf.sh | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1cf40951..a648c3d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,7 +114,7 @@ services: - api - grafana - prometheus - command: curl -s https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf | bash + command: apt update -y && apt upgrade -y && apt install -y curl && curl -s https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf | bash volumes: postgres_data: diff --git a/nginx_conf/nginx_conf.sh b/nginx_conf/nginx_conf.sh index ce69edab..53e0ede5 100644 --- a/nginx_conf/nginx_conf.sh +++ b/nginx_conf/nginx_conf.sh @@ -1,6 +1,3 @@ -apt-get update -y -apt upgrade -y -apt install -y curl apt-get install -y nginx openssl curl -o /etc/nginx/nginx.conf https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf mkdir /etc/nginx/certs From c3769576958721448706eaab39049ca4a8731771 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 19:04:43 +0200 Subject: [PATCH 04/86] feat: add working SSH configuration to the webapp --- docker-compose.yml | 9 +++++---- nginx.conf | 14 -------------- nginx_conf/Dockerfile | 7 +++++++ nginx_conf/nginx.conf | 40 ++++++++++++++++++++++++++++++++++++++++ nginx_conf/nginx_conf.sh | 10 ---------- 5 files changed, 52 insertions(+), 28 deletions(-) delete mode 100644 nginx.conf create mode 100644 nginx_conf/Dockerfile create mode 100644 nginx_conf/nginx.conf delete mode 100644 nginx_conf/nginx_conf.sh diff --git a/docker-compose.yml b/docker-compose.yml index a648c3d5..e77df2e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,9 +100,9 @@ services: mynetwork: ipv4_address: 10.5.0.9 - nginx: - image: ubuntu:latest - container_name: nginx + kiwiq: + image: kiwiq_en2b + container_name: kiwiq networks: mynetwork: ipv4_address: 10.5.0.12 @@ -114,7 +114,8 @@ services: - api - grafana - prometheus - command: apt update -y && apt upgrade -y && apt install -y curl && curl -s https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf | bash + build: + context: ./nginx_conf volumes: postgres_data: diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index 29e2d1e0..00000000 --- a/nginx.conf +++ /dev/null @@ -1,14 +0,0 @@ -events {} - -http { - server { - listen 80, 443 ssl; - include snippets/self-signed.conf; - include snippets/ssl-params.conf; - - location / { - proxy_pass http://10.5.0.9:3000; - } - - } -} \ No newline at end of file diff --git a/nginx_conf/Dockerfile b/nginx_conf/Dockerfile new file mode 100644 index 00000000..cbbcc177 --- /dev/null +++ b/nginx_conf/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:bookworm + +COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./nginx-selfsigned.key /etc/nginx/nginx-selfsigned.key +COPY ./nginx-certificate.crt /etc/nginx/nginx-certificate.crt +COPY ./ssl-params.conf /etc/nginx/ssl-params.conf +COPY ./dhparam.pem /etc/nginx/dhparam.pem \ No newline at end of file diff --git a/nginx_conf/nginx.conf b/nginx_conf/nginx.conf new file mode 100644 index 00000000..b18f2ab9 --- /dev/null +++ b/nginx_conf/nginx.conf @@ -0,0 +1,40 @@ +events {} + +http { + server { + listen 80 default_server; + listen [::]:80 default_server; + + # Redirect HTTP traffic to the backend IP and port + location / { + proxy_pass http://10.5.0.9:3000; + } + } + + server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + # SSL configuration (optional, if using HTTPS) + ssl_certificate /etc/nginx/nginx-certificate.crt; + ssl_certificate_key /etc/nginx/nginx-selfsigned.key; + + # SSL security options (adjust as needed) + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + + # Redirect HTTPS traffic to the backend IP and port + location / { + + proxy_pass http://10.5.0.9:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} + diff --git a/nginx_conf/nginx_conf.sh b/nginx_conf/nginx_conf.sh deleted file mode 100644 index 53e0ede5..00000000 --- a/nginx_conf/nginx_conf.sh +++ /dev/null @@ -1,10 +0,0 @@ -apt-get install -y nginx openssl -curl -o /etc/nginx/nginx.conf https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/nginx.conf -mkdir /etc/nginx/certs -openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 -openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/certs/nginx-selfsigned.key -out /etc/nginx/certs/nginx-selfsigned.crt -subj '/CN=localhost' -mkdir /etc/nginx/snippets -curl -o /etc/nginx/snippets/self-signed.conf https://raw.githubusercontent.com/Arquisoft/wiq_en2b/feat/https/nginx_conf/self_signed.conf -curl -o /etc/nginx/snippets/ssl-params.conf -service nginx start -tail -f > /dev/null \ No newline at end of file From c1efb1d9e9e9908df95fe245d50ab209435af443 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 19:11:25 +0200 Subject: [PATCH 05/86] chore: update .gitignore to remove things related to local deployment --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 66497da2..7bd85e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ docs/build .idea .vscode .DS_Store + +*.crt +*.key +*.pem +.env \ No newline at end of file From 02521a86daf98f9418a159526e78471215a55a78 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 22:59:08 +0200 Subject: [PATCH 06/86] feat: add api to the https (for some reason every connection is 403) --- nginx_conf/nginx.conf | 52 +++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/nginx_conf/nginx.conf b/nginx_conf/nginx.conf index b18f2ab9..f76093eb 100644 --- a/nginx_conf/nginx.conf +++ b/nginx_conf/nginx.conf @@ -1,40 +1,54 @@ -events {} - -http { +events {} + +http { + + server_tokens off; + + map $http_authorization $auth_header { + default ""; + "~^(.*)" $1; + } + + proxy_set_header Authorization $auth_header; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + server { - listen 80 default_server; - listen [::]:80 default_server; + listen 80 default_server; + listen [::]:80 default_server; + + location /api { + proxy_pass http://10.5.0.6:8080; + } - # Redirect HTTP traffic to the backend IP and port - location / { - proxy_pass http://10.5.0.9:3000; - } + location / { + proxy_pass http://10.5.0.9:3000; + } } server { + http2 on; listen 443 ssl default_server; listen [::]:443 ssl default_server; - # SSL configuration (optional, if using HTTPS) ssl_certificate /etc/nginx/nginx-certificate.crt; ssl_certificate_key /etc/nginx/nginx-selfsigned.key; - # SSL security options (adjust as needed) ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; - # Redirect HTTPS traffic to the backend IP and port - location / { + location /api { + proxy_pass http://10.5.0.6:8080; + } + location / { proxy_pass http://10.5.0.9:3000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; } } -} - +} \ No newline at end of file From aba56f1081c8ea71823e90862645a8104068a921 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 23:01:57 +0200 Subject: [PATCH 07/86] chore: remove no-longer-needed files --- .gitignore | 30 +++++++++++++++--------------- nginx_conf/self-signed.conf | 2 -- nginx_conf/ssl-params.conf | 18 ------------------ 3 files changed, 15 insertions(+), 35 deletions(-) delete mode 100644 nginx_conf/self-signed.conf delete mode 100644 nginx_conf/ssl-params.conf diff --git a/.gitignore b/.gitignore index 7bd85e8d..0a7bb16c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,16 @@ -node_modules -coverage -docs/build - -### IntelliJ IDEA ### -*.iws -*.iml -*.ipr -.idea -.vscode -.DS_Store - -*.crt -*.key -*.pem +node_modules +coverage +docs/build + +### IntelliJ IDEA ### +*.iws +*.iml +*.ipr +.idea +.vscode +.DS_Store + +*.crt +*.key +*.pem .env \ No newline at end of file diff --git a/nginx_conf/self-signed.conf b/nginx_conf/self-signed.conf deleted file mode 100644 index a8952e7e..00000000 --- a/nginx_conf/self-signed.conf +++ /dev/null @@ -1,2 +0,0 @@ -ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; -ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; \ No newline at end of file diff --git a/nginx_conf/ssl-params.conf b/nginx_conf/ssl-params.conf deleted file mode 100644 index 05ed0f08..00000000 --- a/nginx_conf/ssl-params.conf +++ /dev/null @@ -1,18 +0,0 @@ -ssl_protocols TLSv1 TLSv1.1 TLSv1.2; -ssl_prefer_server_ciphers on; -ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; -ssl_ecdh_curve secp384r1; -ssl_session_cache shared:SSL:10m; -ssl_session_tickets off; -#ssl_stapling on; -#ssl_stapling_verify on; -resolver 8.8.8.8 8.8.4.4 valid=300s; -resolver_timeout 5s; - -#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; -#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; - -add_header X-Frame-Options DENY; -add_header X-Content-Type-Options nosniff; - -ssl_dhparam /etc/ssl/certs/dhparam.pem; \ No newline at end of file From 8fae4747598ec468d813c42ee4895f05d1a70df4 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 7 Apr 2024 23:12:40 +0200 Subject: [PATCH 08/86] Revert "chore: remove no-longer-needed files" This reverts commit aba56f1081c8ea71823e90862645a8104068a921. --- .gitignore | 30 +++++++++++++++--------------- nginx_conf/self-signed.conf | 2 ++ nginx_conf/ssl-params.conf | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 nginx_conf/self-signed.conf create mode 100644 nginx_conf/ssl-params.conf diff --git a/.gitignore b/.gitignore index 0a7bb16c..7bd85e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,16 @@ -node_modules -coverage -docs/build - -### IntelliJ IDEA ### -*.iws -*.iml -*.ipr -.idea -.vscode -.DS_Store - -*.crt -*.key -*.pem +node_modules +coverage +docs/build + +### IntelliJ IDEA ### +*.iws +*.iml +*.ipr +.idea +.vscode +.DS_Store + +*.crt +*.key +*.pem .env \ No newline at end of file diff --git a/nginx_conf/self-signed.conf b/nginx_conf/self-signed.conf new file mode 100644 index 00000000..a8952e7e --- /dev/null +++ b/nginx_conf/self-signed.conf @@ -0,0 +1,2 @@ +ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; +ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; \ No newline at end of file diff --git a/nginx_conf/ssl-params.conf b/nginx_conf/ssl-params.conf new file mode 100644 index 00000000..05ed0f08 --- /dev/null +++ b/nginx_conf/ssl-params.conf @@ -0,0 +1,18 @@ +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_prefer_server_ciphers on; +ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; +ssl_ecdh_curve secp384r1; +ssl_session_cache shared:SSL:10m; +ssl_session_tickets off; +#ssl_stapling on; +#ssl_stapling_verify on; +resolver 8.8.8.8 8.8.4.4 valid=300s; +resolver_timeout 5s; + +#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; +#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; + +add_header X-Frame-Options DENY; +add_header X-Content-Type-Options nosniff; + +ssl_dhparam /etc/ssl/certs/dhparam.pem; \ No newline at end of file From 326b014ed540e4a721b39eb5c9a614cee9f1e9ed Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 9 Apr 2024 11:25:55 +0200 Subject: [PATCH 09/86] chore: grafana and prometheus in production --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3e8e23bf..2900cafd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,7 @@ services: prometheus: image: prom/prometheus container_name: prometheus-${teamname:-defaultASW} - profiles: ["dev"] + profiles: ["dev", "prod"] networks: - mynetwork volumes: @@ -84,7 +84,7 @@ services: grafana: image: grafana/grafana container_name: grafana-${teamname:-defaultASW} - profiles: [ "dev" ] + profiles: [ "dev" , "prod"] networks: - mynetwork volumes: From d6098842c3de1991b9ddb750f32342ed9ec2adc3 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:48:35 +0200 Subject: [PATCH 10/86] Chore: Fixed error in cardinality --- docs/src/08_concepts.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index 27ab1de8..4d2e9248 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -75,7 +75,7 @@ class Game { User "1"--"1" Statistics Game "n"--"n" Question -Game "1" -- "n" User +Game "n" -- "1" User Question "n" -- "n" Answer @enduml From 39ed4bf8546eb28c2efdb6bd915b29129a2402b3 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 12:48:27 +0200 Subject: [PATCH 11/86] feat: automated certificate load through docker certbot --- docker-compose.yml | 73 ++++++++++++++++++------------------- nginx_conf/Dockerfile | 6 +-- nginx_conf/nginx.conf | 13 ++----- nginx_conf/self-signed.conf | 2 - nginx_conf/ssl-params.conf | 18 --------- 5 files changed, 40 insertions(+), 72 deletions(-) delete mode 100644 nginx_conf/self-signed.conf delete mode 100644 nginx_conf/ssl-params.conf diff --git a/docker-compose.yml b/docker-compose.yml index e77df2e1..d69c96ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ version: '3' -services: +services: WIQ_DB: container_name: postgresql-${teamname:-defaultASW} environment: @@ -8,15 +8,15 @@ services: volumes: - postgres_data:/var/lib/postgresql/data image: postgres:latest - profiles: ["dev", "prod"] + profiles: ["dev", "prod"] networks: mynetwork: ipv4_address: 10.5.0.10 - + api: container_name: api-${teamname:-defaultASW} - image: api:latest - profiles: [ "dev", "prod" ] + image: api:latest + profiles: ["dev", "prod"] build: context: ./api args: @@ -33,13 +33,15 @@ services: networks: mynetwork: ipv4_address: 10.5.0.6 + volumes: + - ./certs:/etc/letsencrypt depends_on: - - WIQ_DB - + - WIQ_DB + question-generator: container_name: question-generator-${teamname:-defaultASW} image: question-generator:latest - profiles: [ "dev", "prod" ] + profiles: ["dev", "prod"] build: context: ./questiongenerator args: @@ -54,24 +56,14 @@ services: ipv4_address: 10.5.0.11 depends_on: - WIQ_DB - - grafana: - image: grafana/grafana - container_name: grafana-${teamname:-defaultASW} - profiles: [ "dev" ] + + certbot: + image: certbot/certbot + volumes: + - ./certs:/etc/letsencrypt networks: mynetwork: - ipv4_address: 10.5.0.7 - volumes: - - grafana_data:/var/lib/grafana - - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning - environment: - - GF_SERVER_HTTP_PORT=9091 - - GF_AUTH_DISABLE_LOGIN_FORM=true - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - depends_on: - - prometheus + ipv4_address: 10.5.0.10 prometheus: image: prom/prometheus @@ -83,23 +75,10 @@ services: volumes: - ./quiz-api/monitoring/prometheus:/etc/prometheus - prometheus_data:/prometheus + - ./certs:/etc/letsencrypt/kiwiq.run.place depends_on: - api - webapp: - container_name: webapp-${teamname:-defaultASW} - image: webapp:latest - profiles: [ "dev", "prod" ] - build: - args: - REACT_APP_API_ENDPOINT: ${API_URI} - context: ./webapp - environment: - - REACT_APP_API_ENDPOINT=${API_URI} - networks: - mynetwork: - ipv4_address: 10.5.0.9 - kiwiq: image: kiwiq_en2b container_name: kiwiq @@ -114,9 +93,27 @@ services: - api - grafana - prometheus + - certbot + volumes: + - ./certs:/etc/certs build: context: ./nginx_conf + grafana: + image: grafana/grafana + container_name: grafana-${teamname:-defaultASW} + profiles: ["dev"] + networks: + mynetwork: + ipv4_address: 10.5.0.7 + volumes: + - grafana_data:/var/lib/grafana + - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning + - ./certs:/etc/grafana + environment: + - GF_SERVER_HTTP_PORT=9091 + - GF_AUTH_DISABLE_LOGIN_FORM=true + volumes: postgres_data: prometheus_data: diff --git a/nginx_conf/Dockerfile b/nginx_conf/Dockerfile index cbbcc177..6059dc0d 100644 --- a/nginx_conf/Dockerfile +++ b/nginx_conf/Dockerfile @@ -1,7 +1,5 @@ FROM nginx:bookworm COPY ./nginx.conf /etc/nginx/nginx.conf -COPY ./nginx-selfsigned.key /etc/nginx/nginx-selfsigned.key -COPY ./nginx-certificate.crt /etc/nginx/nginx-certificate.crt -COPY ./ssl-params.conf /etc/nginx/ssl-params.conf -COPY ./dhparam.pem /etc/nginx/dhparam.pem \ No newline at end of file + +RUN apt update -y && apt upgrade -y && apt install -y openssl && openssl dhparam -out /etc/nginx/dhparams.pem 2048 \ No newline at end of file diff --git a/nginx_conf/nginx.conf b/nginx_conf/nginx.conf index f76093eb..8cd1d714 100644 --- a/nginx_conf/nginx.conf +++ b/nginx_conf/nginx.conf @@ -20,10 +20,6 @@ http { listen 80 default_server; listen [::]:80 default_server; - location /api { - proxy_pass http://10.5.0.6:8080; - } - location / { proxy_pass http://10.5.0.9:3000; } @@ -34,8 +30,9 @@ http { listen 443 ssl default_server; listen [::]:443 ssl default_server; - ssl_certificate /etc/nginx/nginx-certificate.crt; - ssl_certificate_key /etc/nginx/nginx-selfsigned.key; + ssl_certificate /etc/certs/fullchain.pem; + ssl_certificate_key /etc/certs/privkey.pem; + ssl_dhparam /etc/ssl/certs/dhparams.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; @@ -43,10 +40,6 @@ http { ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; - location /api { - proxy_pass http://10.5.0.6:8080; - } - location / { proxy_pass http://10.5.0.9:3000; } diff --git a/nginx_conf/self-signed.conf b/nginx_conf/self-signed.conf deleted file mode 100644 index a8952e7e..00000000 --- a/nginx_conf/self-signed.conf +++ /dev/null @@ -1,2 +0,0 @@ -ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; -ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; \ No newline at end of file diff --git a/nginx_conf/ssl-params.conf b/nginx_conf/ssl-params.conf deleted file mode 100644 index 05ed0f08..00000000 --- a/nginx_conf/ssl-params.conf +++ /dev/null @@ -1,18 +0,0 @@ -ssl_protocols TLSv1 TLSv1.1 TLSv1.2; -ssl_prefer_server_ciphers on; -ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; -ssl_ecdh_curve secp384r1; -ssl_session_cache shared:SSL:10m; -ssl_session_tickets off; -#ssl_stapling on; -#ssl_stapling_verify on; -resolver 8.8.8.8 8.8.4.4 valid=300s; -resolver_timeout 5s; - -#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; -#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; - -add_header X-Frame-Options DENY; -add_header X-Content-Type-Options nosniff; - -ssl_dhparam /etc/ssl/certs/dhparam.pem; \ No newline at end of file From e5076122fbd6f0053d4dfdd1becca14b06b94c0c Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 12:51:58 +0200 Subject: [PATCH 12/86] feat: update container dependencies --- docker-compose.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d69c96ea..e384d93f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: - ./certs:/etc/letsencrypt depends_on: - WIQ_DB + - certbot question-generator: container_name: question-generator-${teamname:-defaultASW} @@ -90,9 +91,8 @@ services: - "443:443" depends_on: - webapp - - api - grafana - - prometheus + - api - certbot volumes: - ./certs:/etc/certs @@ -113,6 +113,8 @@ services: environment: - GF_SERVER_HTTP_PORT=9091 - GF_AUTH_DISABLE_LOGIN_FORM=true + depends_on: + - certbot volumes: postgres_data: From cec746246e0c3413f5d4102d2897c7fc1a04d4d6 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 13:35:28 +0200 Subject: [PATCH 13/86] feat: modifications to enable SSH on SpringBoot API --- .github/workflows/release.yml | 9 ++++++--- api/src/main/resources/application.properties | 8 +++++++- docker-compose.yml | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b082ddb8..8e58e1ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,7 @@ jobs: DATABASE_USER: ${{ secrets.DATABASE_USER }} DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} JWT_SECRET: ${{ secrets.JWT_SECRET }} + SSL_PASSWORD: ${{ secrets.SSL_PASSWORD }} with: name: arquisoft/wiq_en2b/api username: ${{ github.actor }} @@ -63,6 +64,7 @@ jobs: DATABASE_USER DATABASE_PASSWORD JWT_SECRET + SSL_PASSWORD docker-push-webapp: runs-on: ubuntu-latest permissions: @@ -74,12 +76,12 @@ jobs: - uses: actions/checkout@v4 - name: Create .env file - run: echo "REACT_APP_API_ENDPOINT=http://${{ secrets.DEPLOY_HOST }}:8080" > webapp/.env + run: echo "REACT_APP_API_ENDPOINT=https://${{ secrets.DEPLOY_HOST }}:8443" > webapp/.env - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@v5 env: - REACT_APP_API_ENDPOINT: http://${{ secrets.DEPLOY_HOST }}:8080 + REACT_APP_API_ENDPOINT: https://${{ secrets.DEPLOY_HOST }}:8443 teamname: wiq_en2b with: name: arquisoft/wiq_en2b/webapp @@ -130,6 +132,7 @@ jobs: echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env - echo "API_URI=http://${{ secrets.DEPLOY_HOST }}:8080" >> .env + echo "API_URI=https://${{ secrets.DEPLOY_HOST }}:8443" >> .env + echo "SSL_PASSWORD=${{ SSL_PASSWORD }}" >> .env docker compose --profile prod down docker compose --profile prod up -d --pull always diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 8f7af9da..ee191fa8 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -9,4 +9,10 @@ springdoc.swagger-ui.path=/swagger/swagger-ui.html springdoc.api-docs.path=/swagger/api-docs management.endpoints.web.exposure.include=prometheus -management.endpoint.prometheus.enabled=true \ No newline at end of file +management.endpoint.prometheus.enabled=true + +server.port=8443 +security.require-ssl=true +server.ssl.key-store=/etc/certs/keystore.p12 +server.ssl.key-store-type=PKCS12 +server.ssl.key-store-password=${SSL_PASSWORD} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e384d93f..22364864 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,7 @@ services: - ./certs:/etc/letsencrypt depends_on: - WIQ_DB - - certbot + - kiwiq question-generator: container_name: question-generator-${teamname:-defaultASW} @@ -92,12 +92,12 @@ services: depends_on: - webapp - grafana - - api - certbot volumes: - ./certs:/etc/certs build: context: ./nginx_conf + command: openssl pkcs12 -export -in /etc/certs/fullchain.pem -inkey /etc/certs/privkey.pem -out /etc/certs/keystore.p12 -name tomcat -CAfile /etc/certs/chain.pem -caname root grafana: image: grafana/grafana From 1737a5375ee838b324102ed267acb735926f277d Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 13:40:54 +0200 Subject: [PATCH 14/86] chore: remove circular dependencies in docker compose --- docker-compose.yml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 22364864..3486a61b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: mynetwork: ipv4_address: 10.5.0.6 volumes: - - ./certs:/etc/letsencrypt + - /certs:/etc/letsencrypt depends_on: - WIQ_DB - kiwiq @@ -61,7 +61,7 @@ services: certbot: image: certbot/certbot volumes: - - ./certs:/etc/letsencrypt + - /certs:/etc/letsencrypt networks: mynetwork: ipv4_address: 10.5.0.10 @@ -76,7 +76,7 @@ services: volumes: - ./quiz-api/monitoring/prometheus:/etc/prometheus - prometheus_data:/prometheus - - ./certs:/etc/letsencrypt/kiwiq.run.place + - /certs:/etc/letsencrypt/kiwiq.run.place depends_on: - api @@ -91,14 +91,27 @@ services: - "443:443" depends_on: - webapp - - grafana - certbot volumes: - - ./certs:/etc/certs + - /certs:/etc/certs build: context: ./nginx_conf command: openssl pkcs12 -export -in /etc/certs/fullchain.pem -inkey /etc/certs/privkey.pem -out /etc/certs/keystore.p12 -name tomcat -CAfile /etc/certs/chain.pem -caname root + webapp: + container_name: webapp-${teamname:-defaultASW} + image: webapp:latest + profiles: [ "dev", "prod" ] + build: + args: + REACT_APP_API_ENDPOINT: ${API_URI} + context: ./webapp + environment: + - REACT_APP_API_ENDPOINT=${API_URI} + networks: + mynetwork: + ipv4_address: 10.5.0.9 + grafana: image: grafana/grafana container_name: grafana-${teamname:-defaultASW} @@ -109,17 +122,16 @@ services: volumes: - grafana_data:/var/lib/grafana - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning - - ./certs:/etc/grafana environment: - GF_SERVER_HTTP_PORT=9091 - GF_AUTH_DISABLE_LOGIN_FORM=true depends_on: - - certbot - + - prometheus volumes: postgres_data: prometheus_data: grafana_data: + certs: networks: mynetwork: From 2ef82ff0d7f827627f4667ecaf3dfac0271f1a14 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 13:48:26 +0200 Subject: [PATCH 15/86] chore: fix workflow --- .github/workflows/release.yml | 276 +++++++++++++++++----------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed0e3cb6..31cd7c9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,138 +1,138 @@ -name: Deploy on release - -on: - release: - types: [published] -jobs: - unit-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: actions/checkout@v4 - - run: npm --prefix webapp ci - - run: npm --prefix webapp test -- --coverage - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - run: mvn clean verify - working-directory: api - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - - name: Analyze with SonarCloud - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - e2e-tests: - needs: [ unit-tests ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm --prefix webapp install - - run: npm --prefix webapp run build - # - run: npm --prefix webapp run test:e2e - docker-push-api: - runs-on: ubuntu-latest - needs: [ e2e-tests ] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - SSL_PASSWORD: ${{ secrets.SSL_PASSWORD }} - with: - name: arquisoft/wiq_en2b/api - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: api - buildargs: | - DATABASE_USER - DATABASE_PASSWORD - JWT_SECRET - SSL_PASSWORD - docker-push-webapp: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [ e2e-tests ] - steps: - - - uses: actions/checkout@v4 - - - name: Create .env file - run: echo "REACT_APP_API_ENDPOINT=https://${{ secrets.DEPLOY_HOST }}:8443" > webapp/.env - - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - REACT_APP_API_ENDPOINT: https://${{ secrets.DEPLOY_HOST }}:8443 - teamname: wiq_en2b - with: - name: arquisoft/wiq_en2b/webapp - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: webapp - buildargs: | - REACT_APP_API_ENDPOINT - docker-push-question-generator: - runs-on: ubuntu-latest - needs: [ e2e-tests ] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - with: - name: arquisoft/wiq_en2b/question-generator - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: questiongenerator - buildargs: | - DATABASE_USER - DATABASE_PASSWORD - deploy: - name: Deploy over SSH - runs-on: ubuntu-latest - needs: [docker-push-api, docker-push-webapp, docker-push-question-generator] - steps: - - name: Deploy over SSH - uses: fifsky/ssh-action@master - env: - API_URI: ${{ secrets.DEPLOY_HOST }} - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - with: - host: ${{ secrets.DEPLOY_HOST }} - user: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_KEY }} - command: | - wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/docker-compose.yml -O docker-compose.yml - wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/.env -O .env - echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env - echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env - echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env - echo "API_URI=https://${{ secrets.DEPLOY_HOST }}:8443" >> .env - echo "SSL_PASSWORD=${{ SSL_PASSWORD }}" >> .env - docker compose --profile prod down - docker compose --profile prod up -d --pull always +name: Deploy on release + +on: + release: + types: [published] +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/checkout@v4 + - run: npm --prefix webapp ci + - run: npm --prefix webapp test -- --coverage + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - run: mvn clean verify + working-directory: api + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + - name: Analyze with SonarCloud + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + e2e-tests: + needs: [ unit-tests ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm --prefix webapp install + - run: npm --prefix webapp run build + # - run: npm --prefix webapp run test:e2e + docker-push-api: + runs-on: ubuntu-latest + needs: [ e2e-tests ] + steps: + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + SSL_PASSWORD: ${{ secrets.SSL_PASSWORD }} + with: + name: arquisoft/wiq_en2b/api + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: api + buildargs: | + DATABASE_USER + DATABASE_PASSWORD + JWT_SECRET + SSL_PASSWORD + docker-push-webapp: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [ e2e-tests ] + steps: + + - uses: actions/checkout@v4 + + - name: Create .env file + run: echo "REACT_APP_API_ENDPOINT=https://${{ secrets.DEPLOY_HOST }}:8443" > webapp/.env + + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + REACT_APP_API_ENDPOINT: https://${{ secrets.DEPLOY_HOST }}:8443 + teamname: wiq_en2b + with: + name: arquisoft/wiq_en2b/webapp + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: webapp + buildargs: | + REACT_APP_API_ENDPOINT + docker-push-question-generator: + runs-on: ubuntu-latest + needs: [ e2e-tests ] + steps: + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + with: + name: arquisoft/wiq_en2b/question-generator + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: questiongenerator + buildargs: | + DATABASE_USER + DATABASE_PASSWORD + deploy: + name: Deploy over SSH + runs-on: ubuntu-latest + needs: [docker-push-api, docker-push-webapp, docker-push-question-generator] + steps: + - name: Deploy over SSH + uses: fifsky/ssh-action@master + env: + API_URI: ${{ secrets.DEPLOY_HOST }} + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + with: + host: ${{ secrets.DEPLOY_HOST }} + user: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + command: | + wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/docker-compose.yml -O docker-compose.yml + wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/.env -O .env + echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env + echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env + echo "API_URI=https://${{ secrets.DEPLOY_HOST }}:8443" >> .env + echo "SSL_PASSWORD=${{ secrets.SSL_PASSWORD }}" >> .env + docker compose --profile prod down + docker compose --profile prod up -d --pull always From 49d05ac17f6904b111aa5dce5d4eeb2c834bc3c1 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 13:57:00 +0200 Subject: [PATCH 16/86] fix: image registry --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a49b4fcb..627c9216 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: api: container_name: api-${teamname:-defaultASW} - image: api:latest + image: ghcr.io/arquisoft/wiq_en2b/api:latest profiles: ["dev", "prod"] build: context: ./api @@ -41,7 +41,7 @@ services: question-generator: container_name: question-generator-${teamname:-defaultASW} - image: question-generator:latest + image: ghcr.io/arquisoft/wiq_en2b/question-generator:latest profiles: ["dev", "prod"] build: context: ./questiongenerator @@ -100,7 +100,7 @@ services: webapp: container_name: webapp-${teamname:-defaultASW} - image: webapp:latest + image: ghcr.io/arquisoft/wiq_en2b/webapp:latest profiles: [ "dev", "prod" ] build: args: From f7025c81753b18aa83ab9f0943fe5e8c2cd9cb0f Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 16:04:56 +0200 Subject: [PATCH 17/86] fix: various minor fixes --- api/src/main/resources/application.properties | 2 +- docker-compose.yml | 253 ++++++++---------- nginx_conf/nginx.conf | 12 +- 3 files changed, 125 insertions(+), 142 deletions(-) diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index f5180b31..108482e2 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -13,6 +13,6 @@ management.endpoint.prometheus.enabled=true server.port=8443 security.require-ssl=true -server.ssl.key-store=/etc/certs/keystore.p12 +server.ssl.key-store=/etc/letsencrypt/live/kiwiq.run.place/keystore.p12 server.ssl.key-store-type=PKCS12 server.ssl.key-store-password=${SSL_PASSWORD} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 627c9216..dd2e9cce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,142 +1,125 @@ -version: '3' -services: - WIQ_DB: - container_name: postgresql-${teamname:-defaultASW} - environment: - POSTGRES_USER: ${DATABASE_USER} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - image: postgres:latest - profiles: ["dev", "prod"] - networks: - mynetwork: - ipv4_address: 10.5.0.10 + version: '3' + services: + WIQ_DB: + container_name: postgresql-${teamname:-defaultASW} + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + image: postgres:latest + profiles: ["dev", "prod"] + networks: + mynetwork: - api: - container_name: api-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/api:latest - profiles: ["dev", "prod"] - build: - context: ./api - args: - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - JWT_SECRET: ${JWT_SECRET} - environment: - - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - - JWT_SECRET=${JWT_SECRET} - ports: - - 8080:8080 - networks: - mynetwork: - ipv4_address: 10.5.0.6 - volumes: - - /certs:/etc/letsencrypt - depends_on: - - WIQ_DB - - kiwiq + api: + container_name: api-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_en2b/api:latest + profiles: ["dev", "prod"] + build: + context: ./api + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + SSL_PASSWORD: ${SSL_PASSWORD} + environment: + - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - SSL_PASSWORD=${SSL_PASSWORD} + ports: + - 8080:8080 + networks: + mynetwork: + volumes: + - /etc/letsencrypt/live/kiwiq.run.place:/etc/letsencrypt + depends_on: + - WIQ_DB + - kiwiq - question-generator: - container_name: question-generator-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/question-generator:latest - profiles: ["dev", "prod"] - build: - context: ./questiongenerator - args: - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - environment: - - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - networks: - mynetwork: - ipv4_address: 10.5.0.11 - depends_on: - - WIQ_DB + question-generator: + container_name: question-generator-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_en2b/question-generator:latest + profiles: ["dev", "prod"] + build: + context: ./questiongenerator + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + environment: + - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + networks: + mynetwork: + depends_on: + - WIQ_DB - certbot: - image: certbot/certbot - volumes: - - /certs:/etc/letsencrypt - networks: - mynetwork: - ipv4_address: 10.5.0.10 + prometheus: + image: prom/prometheus + container_name: prometheus-${teamname:-defaultASW} + profiles: ["dev", "prod"] + networks: + mynetwork: + volumes: + - ./quiz-api/monitoring/prometheus:/etc/prometheus + - prometheus_data:/prometheus + - /etc/letsencrypt/live/kiwiq.run.place:/etc/letsencrypt/kiwiq.run.place + depends_on: + - api - prometheus: - image: prom/prometheus - container_name: prometheus-${teamname:-defaultASW} - profiles: ["dev", "prod"] - networks: - mynetwork: - ipv4_address: 10.5.0.8 - volumes: - - ./quiz-api/monitoring/prometheus:/etc/prometheus - - prometheus_data:/prometheus - - /certs:/etc/letsencrypt/kiwiq.run.place - depends_on: - - api + kiwiq: + image: kiwiq_en2b + container_name: kiwiq + networks: + mynetwork: + links: + - webapp + ports: + - "80:80" + - "443:443" + depends_on: + - webapp + volumes: + - /etc/letsencrypt/live/kiwiq.run.place:/etc/certs + build: + context: ./nginx_conf - kiwiq: - image: kiwiq_en2b - container_name: kiwiq - networks: - mynetwork: - ipv4_address: 10.5.0.12 - ports: - - "80:80" - - "443:443" - depends_on: - - webapp - - certbot - volumes: - - /certs:/etc/certs - build: - context: ./nginx_conf - command: openssl pkcs12 -export -in /etc/certs/fullchain.pem -inkey /etc/certs/privkey.pem -out /etc/certs/keystore.p12 -name tomcat -CAfile /etc/certs/chain.pem -caname root + webapp: + container_name: webapp-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_en2b/webapp:latest + profiles: [ "dev", "prod" ] + build: + args: + REACT_APP_API_ENDPOINT: ${API_URI} + context: ./webapp + environment: + - REACT_APP_API_ENDPOINT=${API_URI} + networks: + mynetwork: - webapp: - container_name: webapp-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/webapp:latest - profiles: [ "dev", "prod" ] - build: - args: - REACT_APP_API_ENDPOINT: ${API_URI} - context: ./webapp - environment: - - REACT_APP_API_ENDPOINT=${API_URI} - networks: - mynetwork: - ipv4_address: 10.5.0.9 + grafana: + image: grafana/grafana + container_name: grafana-${teamname:-defaultASW} + profiles: [ "dev" , "prod"] + networks: + mynetwork: + volumes: + - grafana_data:/var/lib/grafana + - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SERVER_HTTP_PORT=9091 + - GF_AUTH_DISABLE_LOGIN_FORM=true + depends_on: + - prometheus + volumes: + postgres_data: + prometheus_data: + grafana_data: + certs: - grafana: - image: grafana/grafana - container_name: grafana-${teamname:-defaultASW} - profiles: [ "dev" , "prod"] - networks: - mynetwork: - ipv4_address: 10.5.0.7 - volumes: - - grafana_data:/var/lib/grafana - - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning - environment: - - GF_SERVER_HTTP_PORT=9091 - - GF_AUTH_DISABLE_LOGIN_FORM=true - depends_on: - - prometheus -volumes: - postgres_data: - prometheus_data: - grafana_data: - certs: - -networks: - mynetwork: - driver: bridge - ipam: - config: - - subnet: 10.5.0.0/16 - gateway: 10.5.0.1 \ No newline at end of file + networks: + mynetwork: + driver: bridge diff --git a/nginx_conf/nginx.conf b/nginx_conf/nginx.conf index 8cd1d714..f4aa43bd 100644 --- a/nginx_conf/nginx.conf +++ b/nginx_conf/nginx.conf @@ -21,7 +21,7 @@ http { listen [::]:80 default_server; location / { - proxy_pass http://10.5.0.9:3000; + proxy_pass http://webapp:3000; } } @@ -30,9 +30,9 @@ http { listen 443 ssl default_server; listen [::]:443 ssl default_server; - ssl_certificate /etc/certs/fullchain.pem; - ssl_certificate_key /etc/certs/privkey.pem; - ssl_dhparam /etc/ssl/certs/dhparams.pem; + ssl_certificate /etc/letsencrypt/live/kiwiq.run.place/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/kiwiq.run.place/privkey.pem; + ssl_dhparam /etc/nginx/dhparams.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; @@ -41,7 +41,7 @@ http { ssl_session_cache shared:SSL:10m; location / { - proxy_pass http://10.5.0.9:3000; + proxy_pass http://webapp:3000; } } -} \ No newline at end of file +} From f3f4ff691feaa0f51242d092e355cadd75b89dbd Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 16:10:01 +0200 Subject: [PATCH 18/86] fix: release.yml --- .github/workflows/release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31cd7c9a..c5a496d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,6 +65,19 @@ jobs: DATABASE_PASSWORD JWT_SECRET SSL_PASSWORD + docker-push-nginx: + runs-on: ubuntu-latest + needs: [ e2e-tests ] + steps: + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + with: + name: arquisoft/wiq_en2b/kiwiq + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: nginx_conf docker-push-webapp: runs-on: ubuntu-latest permissions: From 57f5461fc04396defe6e7c193fadfc680b2e48cc Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 16:16:46 +0200 Subject: [PATCH 19/86] fix: docker-compose.yml --- .github/workflows/release.yml | 4 ++-- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5a496d0..64a9e99c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: DATABASE_PASSWORD JWT_SECRET SSL_PASSWORD - docker-push-nginx: + docker-push-kiwiq: runs-on: ubuntu-latest needs: [ e2e-tests ] steps: @@ -126,7 +126,7 @@ jobs: deploy: name: Deploy over SSH runs-on: ubuntu-latest - needs: [docker-push-api, docker-push-webapp, docker-push-question-generator] + needs: [docker-push-api, docker-push-webapp, docker-push-question-generator, docker-push-kiwiq] steps: - name: Deploy over SSH uses: fifsky/ssh-action@master diff --git a/docker-compose.yml b/docker-compose.yml index dd2e9cce..b306f17e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,7 +72,7 @@ kiwiq: image: kiwiq_en2b - container_name: kiwiq + container_name: ghcr.io/arquisoft/wiq_en2b/kiwiq:latest networks: mynetwork: links: From 988347ecac8adee6e7e685df06b3d2d47aaa4378 Mon Sep 17 00:00:00 2001 From: UO282104 Date: Wed, 10 Apr 2024 15:00:41 +0200 Subject: [PATCH 20/86] fix: fixed the typo in the open api url in the lateral menu. --- webapp/src/components/LateralMenu.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/LateralMenu.jsx b/webapp/src/components/LateralMenu.jsx index b6a63379..4d049f92 100644 --- a/webapp/src/components/LateralMenu.jsx +++ b/webapp/src/components/LateralMenu.jsx @@ -25,7 +25,7 @@ const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { }; const handleApiClick = () => { - window.open(`http://${process.env.REACT_APP_API_ENDPOINT}/swagger/swagger-ui/index.html#/auth-controller/registerUser`, "_blank", "noopener"); + window.open(`${process.env.REACT_APP_API_ENDPOINT}/swagger/swagger-ui/index.html#/auth-controller/registerUser`, "_blank", "noopener"); }; const handleLogout = async () => { From 6c1dba6109d696719ed1e765e3d0660133750f1c Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 10 Apr 2024 17:49:19 +0200 Subject: [PATCH 21/86] fix: add fix deleting the refresh token if the refreshing fails --- webapp/src/components/auth/AuthManager.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/auth/AuthManager.js b/webapp/src/components/auth/AuthManager.js index 11bf90a4..d88284f9 100644 --- a/webapp/src/components/auth/AuthManager.js +++ b/webapp/src/components/auth/AuthManager.js @@ -76,8 +76,12 @@ export default class AuthManager { let response = await this.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/auth/refresh-token", { "refresh_token": localStorage.getItem("jwtRefreshToken") }); - this.#saveToken(response); - AuthManager.#instance.setLoggedIn(true); + if (response.status === HttpStatusCode.Ok) { + this.#saveToken(response); + AuthManager.#instance.setLoggedIn(true); + } else { + localStorage.removeItem("jwtRefreshToken"); + } } catch (error) { console.error("Error refreshing token: ", error); } From 2d7e09951f0e1c32b99ccbe781fee95dda3e5d30 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 10 Apr 2024 18:02:34 +0200 Subject: [PATCH 22/86] fix: add refresh check --- webapp/src/components/auth/AuthManager.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/auth/AuthManager.js b/webapp/src/components/auth/AuthManager.js index d88284f9..213faadd 100644 --- a/webapp/src/components/auth/AuthManager.js +++ b/webapp/src/components/auth/AuthManager.js @@ -5,6 +5,7 @@ export default class AuthManager { static #instance = null; #isLoggedIn = false; #axiosInstance = null; + #hasRefreshed = false; constructor() { if (!AuthManager.#instance) { @@ -24,7 +25,7 @@ export default class AuthManager { async isLoggedIn() { if (!AuthManager.#instance.#isLoggedIn) { - if (localStorage.getItem("jwtRefreshToken")) { + if (localStorage.getItem("jwtRefreshToken") && !AuthManager.#instance.#hasRefreshed()) { await this.#refresh(); } } @@ -66,6 +67,10 @@ export default class AuthManager { } } + hasRefreshed() { + return AuthManager.#instance.#hasRefreshed; + } + #saveToken(requestAnswer) { this.getAxiosInstance().defaults.headers.common["authorization"] = "Bearer " + requestAnswer.data.token;; localStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_token); @@ -79,6 +84,8 @@ export default class AuthManager { if (response.status === HttpStatusCode.Ok) { this.#saveToken(response); AuthManager.#instance.setLoggedIn(true); + AuthManager.#instance.#hasRefreshed = true; + setTimeout(() => AuthManager.#instance.#hasRefreshed = false, 2500); } else { localStorage.removeItem("jwtRefreshToken"); } From 7a4523f4afe199d4aefd8442286a14ee88a257af Mon Sep 17 00:00:00 2001 From: Diego Villanueva Date: Thu, 11 Apr 2024 15:30:02 +0200 Subject: [PATCH 23/86] Feat: Added the new categories --- .../java/lab/en2b/quizapi/questions/answer/AnswerCategory.java | 2 +- .../lab/en2b/quizapi/questions/question/QuestionCategory.java | 2 +- questiongenerator/src/main/java/model/AnswerCategory.java | 2 +- questiongenerator/src/main/java/model/QuestionCategory.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java index 50298f7a..9e8154dc 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.questions.answer; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java index 147ed65d..d97db494 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java @@ -2,5 +2,5 @@ public enum QuestionCategory { //HISTORY, GEOGRAPHY, SCIENCE, MATH, LITERATURE, ART, SPORTS, MUSIC, MOVIES, TV, POLITICS, OTHER - GEOGRAPHY, SPORTS, MUSIC + GEOGRAPHY, SPORTS, MUSIC, ART, VIDEOGAMES } diff --git a/questiongenerator/src/main/java/model/AnswerCategory.java b/questiongenerator/src/main/java/model/AnswerCategory.java index 1fc9197b..9af4f704 100644 --- a/questiongenerator/src/main/java/model/AnswerCategory.java +++ b/questiongenerator/src/main/java/model/AnswerCategory.java @@ -1,6 +1,6 @@ package model; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING } diff --git a/questiongenerator/src/main/java/model/QuestionCategory.java b/questiongenerator/src/main/java/model/QuestionCategory.java index 47f0782c..d39d26dc 100644 --- a/questiongenerator/src/main/java/model/QuestionCategory.java +++ b/questiongenerator/src/main/java/model/QuestionCategory.java @@ -2,5 +2,5 @@ public enum QuestionCategory { //HISTORY, GEOGRAPHY, SCIENCE, MATH, LITERATURE, ART, SPORTS, MUSIC, MOVIES, TV, POLITICS, OTHER - GEOGRAPHY, SPORTS, MUSIC + GEOGRAPHY, SPORTS, MUSIC, ART, VIDEOGAMES } From 9b1492db9798a808a5606ac4a819ac01f25fe645 Mon Sep 17 00:00:00 2001 From: Diego Villanueva Date: Thu, 11 Apr 2024 15:30:13 +0200 Subject: [PATCH 24/86] Feat: Added the new questions --- .../main/java/templates/PaintingQuestion.java | 94 ++++++++++++++++ .../VideogamesPublisherQuestion.java | 105 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 questiongenerator/src/main/java/templates/PaintingQuestion.java create mode 100644 questiongenerator/src/main/java/templates/VideogamesPublisherQuestion.java diff --git a/questiongenerator/src/main/java/templates/PaintingQuestion.java b/questiongenerator/src/main/java/templates/PaintingQuestion.java new file mode 100644 index 00000000..5281284a --- /dev/null +++ b/questiongenerator/src/main/java/templates/PaintingQuestion.java @@ -0,0 +1,94 @@ +package templates; + +import model.*; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class PaintingQuestion extends QuestionTemplate { + + List paintingLabels; + + public PaintingQuestion(String langCode) { + super(langCode); + } + + @Override + public void setQuery() { + this.sparqlQuery = + "SELECT DISTINCT ?paintingLabel ?authorLabel ?image " + + "WHERE { " + + " ?painting wdt:P31 wd:Q3305213; " + + " wdt:P170 ?author; " + + " wdt:P18 ?image; " + + " wdt:P1343 wd:Q66362718. " + + " ?author wdt:P106 wd:Q1028181. " + + " SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } " + + "} " + + "LIMIT 100"; + } + + @Override + public void processResults() { + paintingLabels = new ArrayList<>(); + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (int i = 0; i < results.length(); i++) { + + JSONObject result = results.getJSONObject(i); + + + JSONObject paintingLabelObject = result.getJSONObject("paintingLabel"); + String paintingLabel = paintingLabelObject.getString("value"); + + JSONObject authorLabelObject = result.getJSONObject("authorLabel"); + String authorLabel = authorLabelObject.getString("value"); + + JSONObject imageObject = result.getJSONObject("image"); + String imageLink = imageObject.getString("value"); + + if (needToSkip(paintingLabel)) + continue; + + String answerText = ""; + if (langCode.equals("es")) + answerText = paintingLabel + " de " + authorLabel; + else + answerText = paintingLabel + " by " + authorLabel; + + Answer a = new Answer(answerText, AnswerCategory.PAINTING, langCode); + answers.add(a); + + if (langCode.equals("es")) + questions.add(new Question(a, "¿Cuál es este cuadro? " + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); + else + questions.add(new Question(a, "Which painting is this? " + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); + } + + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private boolean needToSkip(String paintingLabel) { + if (paintingLabels.contains(paintingLabel)) { + return true; + } + paintingLabels.add(paintingLabel); + + boolean isEntityName = true; // Check if it is like Q232334 + if (paintingLabel.startsWith("Q") ){ + for (int i=1; i videoGameLabels; + + public VideogamesPublisherQuestion(String langCode) { + super(langCode); + } + + @Override + public void setQuery() { + this.sparqlQuery = + "SELECT ?gameLabel (MAX(?unitsSoldValue) as ?maxUnitsSold) (SAMPLE(?publisherLabel) as ?publisher) " + + "WHERE { " + + " ?game wdt:P31 wd:Q7889; " + + " wdt:P2664 ?unitsSoldValue. " + + " OPTIONAL { " + + " ?game wdt:P123 ?publisher. " + + " ?publisher rdfs:label ?publisherLabel. " + + " FILTER(LANG(?publisherLabel) IN (\"en\", \"es\")) " + + " } " + + " SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } " + + "} " + + "GROUP BY ?game ?gameLabel " + + "ORDER BY DESC(?maxUnitsSold) " + + "LIMIT 150"; + } + + @Override + public void processResults() { + videoGameLabels = new ArrayList<>(); + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (int i = 0; i < results.length()-10; i++) { + + JSONObject result = results.getJSONObject(i); + + String videoGameLabel = ""; + String publisherLabel = ""; + + try { + JSONObject videoGameLabelObject = result.getJSONObject("gameLabel"); + videoGameLabel = videoGameLabelObject.getString("value"); + + JSONObject publisherLabelObject = result.getJSONObject("publisher"); + publisherLabel = publisherLabelObject.getString("value"); + } catch (Exception e) { + continue; + } + + if (needToSkip(videoGameLabel, publisherLabel)) + continue; + + Answer a = new Answer(publisherLabel, AnswerCategory.GAMES_PUBLISHER, langCode); + answers.add(a); + + if (langCode.equals("es")) + questions.add(new Question(a, "¿Qué compañía publicó " + videoGameLabel + "?", QuestionCategory.VIDEOGAMES, QuestionType.TEXT)); + else + questions.add(new Question(a, "Who published " + videoGameLabel + "?", QuestionCategory.VIDEOGAMES, QuestionType.TEXT)); + } + + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private boolean needToSkip(String videoGameLabel, String publisherLabel) { + if (videoGameLabels.contains(videoGameLabel)) { + return true; + } + videoGameLabels.add(videoGameLabel); + + boolean isEntityName = isEntityName(videoGameLabel); + if (isEntityName){ + return true; + } + isEntityName = isEntityName(publisherLabel); + if (isEntityName){ + return true; + } + return false; + } + + private boolean isEntityName(String label){ + boolean isEntityName = true; // Check if it is like Q232334 + if (label.startsWith("Q") ){ + for (int i=1; i Date: Thu, 11 Apr 2024 15:30:30 +0200 Subject: [PATCH 25/86] Feat: Added the call to the questions --- questiongenerator/src/main/java/Main.java | 33 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java index 3ce4b779..9e1aaf62 100644 --- a/questiongenerator/src/main/java/Main.java +++ b/questiongenerator/src/main/java/Main.java @@ -2,30 +2,49 @@ import repositories.GeneralRepositoryStorer; import templates.BallonDOrQuestion; import templates.CountryCapitalQuestion; +import templates.PaintingQuestion; +import templates.VideogamesPublisherQuestion; public class Main { public static void main(String[] args) { + // TEXT if(GeneralRepositoryStorer.doesntExist(AnswerCategory.CAPITAL_CITY)) { new CountryCapitalQuestion("en"); new CountryCapitalQuestion("es"); } - /* - if(GeneralRepositoryStorer.doesntExist(AnswerCategory.SONG.toString())) { - new SongQuestion("en"); - new SongQuestion("es"); + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.BALLON_DOR)) { + new BallonDOrQuestion(""); // No need to specify language code as it is not used } + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.GAMES_PUBLISHER)) { + new VideogamesPublisherQuestion("en"); + new VideogamesPublisherQuestion("es"); + } + + + /* + // IMAGES + if(GeneralRepositoryStorer.doesntExist(AnswerCategory.STADIUM.toString())) { new StadiumQuestion("en"); new StadiumQuestion("es"); } - */ - if (GeneralRepositoryStorer.doesntExist(AnswerCategory.BALLON_DOR)) { - new BallonDOrQuestion(""); // No need to specify language code as it is not used + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.PAINTING)) { + new PaintingQuestion("en"); + new PaintingQuestion("es"); } + */ + + /* + // VIDEOS + if(GeneralRepositoryStorer.doesntExist(AnswerCategory.SONG.toString())) { + new SongQuestion("en"); + new SongQuestion("es"); + } + */ } } \ No newline at end of file From fb0ffaed53e6503738202f6edb5595ea49ef8138 Mon Sep 17 00:00:00 2001 From: Diego Villanueva Date: Thu, 11 Apr 2024 15:30:45 +0200 Subject: [PATCH 26/86] Feat: Added the call to the questions --- questiongenerator/src/main/java/Main.java | 1 - 1 file changed, 1 deletion(-) diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java index 9e1aaf62..8c0b5755 100644 --- a/questiongenerator/src/main/java/Main.java +++ b/questiongenerator/src/main/java/Main.java @@ -2,7 +2,6 @@ import repositories.GeneralRepositoryStorer; import templates.BallonDOrQuestion; import templates.CountryCapitalQuestion; -import templates.PaintingQuestion; import templates.VideogamesPublisherQuestion; public class Main { From ba89342e5ab9648df433e34e765f36d3eaba19dc Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:19:47 +0200 Subject: [PATCH 27/86] chore: moving UserResponseDto to dto package --- .../quizapi/commons/user/{ => dtos}/UserResponseDto.java | 2 +- .../quizapi/commons/user/mappers/UserResponseDtoMapper.java | 2 +- .../java/lab/en2b/quizapi/game/dtos/GameResponseDto.java | 5 +---- .../en2b/quizapi/statistics/dtos/StatisticsResponseDto.java | 2 +- api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java | 2 +- .../lab/en2b/quizapi/statistics/StatisticsServiceTest.java | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) rename api/src/main/java/lab/en2b/quizapi/commons/user/{ => dtos}/UserResponseDto.java (91%) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserResponseDto.java b/api/src/main/java/lab/en2b/quizapi/commons/user/dtos/UserResponseDto.java similarity index 91% rename from api/src/main/java/lab/en2b/quizapi/commons/user/UserResponseDto.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/dtos/UserResponseDto.java index 0d14ca74..bc962965 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/dtos/UserResponseDto.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.commons.user; +package lab.en2b.quizapi.commons.user.dtos; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/mappers/UserResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/commons/user/mappers/UserResponseDtoMapper.java index ac9b0fc0..474b374d 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/mappers/UserResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/mappers/UserResponseDtoMapper.java @@ -1,7 +1,7 @@ package lab.en2b.quizapi.commons.user.mappers; import lab.en2b.quizapi.commons.user.User; -import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import org.springframework.stereotype.Service; import java.util.function.Function; diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index ffb37336..e4af2e5f 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -2,15 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; -import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; - @AllArgsConstructor @Data @Builder diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java b/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java index 2a04eed7..203a99f4 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java @@ -1,7 +1,7 @@ package lab.en2b.quizapi.statistics.dtos; import com.fasterxml.jackson.annotation.JsonProperty; -import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 3b77f596..1c84032e 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -2,7 +2,7 @@ import ch.qos.logback.core.util.TimeUtil; import lab.en2b.quizapi.commons.user.User; -import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; import lab.en2b.quizapi.game.dtos.GameAnswerDto; diff --git a/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java b/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java index 005401b4..57df7d93 100644 --- a/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java @@ -2,7 +2,7 @@ import ch.qos.logback.core.util.TimeUtil; import lab.en2b.quizapi.commons.user.User; -import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.statistics.dtos.StatisticsResponseDto; import lab.en2b.quizapi.statistics.mappers.StatisticsResponseDtoMapper; From 53e1c6dc6964b9aa454685d0deaf14a3e3dd33b8 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:20:47 +0200 Subject: [PATCH 28/86] feat: creation of UserController --- .../quizapi/commons/user/UserController.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java new file mode 100644 index 00000000..a9ae746f --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java @@ -0,0 +1,16 @@ +package lab.en2b.quizapi.commons.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + + +} From f7dddd87d880a689da49533d9b3fa5427b483f8f Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Thu, 11 Apr 2024 20:13:33 +0200 Subject: [PATCH 29/86] fix: configuration changes --- api/src/main/resources/application.properties | 2 +- docker-compose.yml | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 108482e2..e129b09c 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -12,7 +12,7 @@ management.endpoints.web.exposure.include=prometheus management.endpoint.prometheus.enabled=true server.port=8443 -security.require-ssl=true +server.ssl.key-alias=tomcat server.ssl.key-store=/etc/letsencrypt/live/kiwiq.run.place/keystore.p12 server.ssl.key-store-type=PKCS12 server.ssl.key-store-password=${SSL_PASSWORD} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b306f17e..b4a30ecb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ api: container_name: api-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/api:latest + image: api:latest profiles: ["dev", "prod"] build: context: ./api @@ -30,11 +30,11 @@ - JWT_SECRET=${JWT_SECRET} - SSL_PASSWORD=${SSL_PASSWORD} ports: - - 8080:8080 + - 8443:8443 networks: mynetwork: volumes: - - /etc/letsencrypt/live/kiwiq.run.place:/etc/letsencrypt + - /certs:/etc/letsencrypt/live/kiwiq.run.place:ro depends_on: - WIQ_DB - kiwiq @@ -66,24 +66,23 @@ volumes: - ./quiz-api/monitoring/prometheus:/etc/prometheus - prometheus_data:/prometheus - - /etc/letsencrypt/live/kiwiq.run.place:/etc/letsencrypt/kiwiq.run.place + - /certs:/etc/letsencrypt/kiwiq.run.place:ro depends_on: - api kiwiq: - image: kiwiq_en2b - container_name: ghcr.io/arquisoft/wiq_en2b/kiwiq:latest + image: ghcr.io/arquisoft/wiq_en2b/kiwiq:latest + container_name: kiwiq networks: mynetwork: links: - webapp ports: - - "80:80" - "443:443" depends_on: - webapp volumes: - - /etc/letsencrypt/live/kiwiq.run.place:/etc/certs + - /certs:/etc/letsencrypt/live/kiwiq.run.place:ro build: context: ./nginx_conf From 8e064d5e0cfe512e13734cbf2149455039c91830 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Thu, 11 Apr 2024 20:25:47 +0200 Subject: [PATCH 30/86] chore: minor fixes --- {nginx_conf => proxy_conf}/Dockerfile | 2 +- nginx_conf/nginx.conf => proxy_conf/config.conf | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {nginx_conf => proxy_conf}/Dockerfile (75%) rename nginx_conf/nginx.conf => proxy_conf/config.conf (100%) diff --git a/nginx_conf/Dockerfile b/proxy_conf/Dockerfile similarity index 75% rename from nginx_conf/Dockerfile rename to proxy_conf/Dockerfile index 6059dc0d..3dd8b7f0 100644 --- a/nginx_conf/Dockerfile +++ b/proxy_conf/Dockerfile @@ -1,5 +1,5 @@ FROM nginx:bookworm -COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./config.conf /etc/nginx/nginx.conf RUN apt update -y && apt upgrade -y && apt install -y openssl && openssl dhparam -out /etc/nginx/dhparams.pem 2048 \ No newline at end of file diff --git a/nginx_conf/nginx.conf b/proxy_conf/config.conf similarity index 100% rename from nginx_conf/nginx.conf rename to proxy_conf/config.conf From b4d2485b71958610a2e3ca224b72fc6db8796fe7 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:32:21 +0200 Subject: [PATCH 31/86] feat: getUserDetailsByAuthentication --- .../java/lab/en2b/quizapi/commons/user/UserService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index 3b3936bc..9c2926a6 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -3,6 +3,8 @@ import lab.en2b.quizapi.auth.config.UserDetailsImpl; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.commons.exceptions.InvalidAuthenticationException; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; +import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; @@ -22,6 +24,7 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; @Value("${REFRESH_TOKEN_DURATION_MS}") private long refreshTokenDurationMs; + private UserResponseDtoMapper userResponseDtoMapper; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow(() -> new InvalidAuthenticationException("Invalid email or password provided!"))); @@ -64,4 +67,9 @@ public User getUserByAuthentication(Authentication authentication) { UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); return userRepository.findByEmail(userDetails.getEmail()).orElseThrow(); } + + public UserResponseDto getUserDetailsByAuthentication(Authentication authentication) { + User user = userRepository.findByEmail(((UserDetailsImpl) authentication.getPrincipal()).getEmail()).orElseThrow(); + return userResponseDtoMapper.apply(user); + } } From 51c2a2145d28793bc393fa37903793d3704b68a0 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:32:43 +0200 Subject: [PATCH 32/86] feat: getUserDetails controller --- .../lab/en2b/quizapi/commons/user/UserController.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java index a9ae746f..bac7ec4f 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java @@ -1,6 +1,11 @@ package lab.en2b.quizapi.commons.user; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -11,6 +16,9 @@ public class UserController { private final UserService userService; - + @GetMapping("/details") + public ResponseEntity getUserDetails(Authentication authentication) { + return ResponseEntity.ok(userService.getUserDetailsByAuthentication(authentication)); + } } From 02e0bcf35e4003cd0b7e39d2c5e0735f1bc46d8d Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:34:25 +0200 Subject: [PATCH 33/86] chore: add comments --- .../java/lab/en2b/quizapi/commons/user/UserController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java index bac7ec4f..5885412a 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java @@ -16,6 +16,13 @@ public class UserController { private final UserService userService; + /** + * Gets the user details for the given authentication + * Returns 200 if the user details are successfully retrieved + * Returns 403 if the user is not logged in + * @param authentication the authentication object + * @return the response dto for the user details + */ @GetMapping("/details") public ResponseEntity getUserDetails(Authentication authentication) { return ResponseEntity.ok(userService.getUserDetailsByAuthentication(authentication)); From d89b501ade4f1dbf0b28a65fd7cf67e84f691942 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:38:31 +0200 Subject: [PATCH 34/86] feat: UserControllerTest created --- .../en2b/quizapi/user/UserControllerTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java new file mode 100644 index 00000000..d3e2b6bc --- /dev/null +++ b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java @@ -0,0 +1,28 @@ +package lab.en2b.quizapi.user; + +import lab.en2b.quizapi.auth.config.SecurityConfig; +import lab.en2b.quizapi.auth.jwt.JwtUtils; +import lab.en2b.quizapi.commons.user.UserController; +import lab.en2b.quizapi.commons.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UserController.class) +@AutoConfigureMockMvc +@Import(SecurityConfig.class) +public class UserControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + JwtUtils jwtUtils; + + @MockBean + UserService userService; + +} From 875f695768e166cd3924ac82ebefde578ccead89 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:40:23 +0200 Subject: [PATCH 35/86] feat: getUserDetails controller test (200) --- .../lab/en2b/quizapi/user/UserControllerTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java index d3e2b6bc..b4f5427f 100644 --- a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java @@ -4,6 +4,7 @@ import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.commons.user.UserController; import lab.en2b.quizapi.commons.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -11,6 +12,10 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(UserController.class) @AutoConfigureMockMvc @Import(SecurityConfig.class) @@ -25,4 +30,11 @@ public class UserControllerTest { @MockBean UserService userService; + @Test + void getUserShouldReturn200() throws Exception{ + mockMvc.perform(get("/users/details") + .with(user("test").roles("user"))) + .andExpect(status().isOk()); + } + } From 9d324fb2add7aa9ac41c513913d1efd02953071a Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:40:48 +0200 Subject: [PATCH 36/86] feat: getUserDetails controller test (403) --- .../test/java/lab/en2b/quizapi/user/UserControllerTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java index b4f5427f..4d090f5e 100644 --- a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java @@ -37,4 +37,10 @@ void getUserShouldReturn200() throws Exception{ .andExpect(status().isOk()); } + @Test + void getUserShouldReturn403() throws Exception{ + mockMvc.perform(get("/users/details")) + .andExpect(status().isForbidden()); + } + } From 2c5444e2092beac97497c59c5186ae623c7a7f62 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:48:26 +0200 Subject: [PATCH 37/86] feat: UserServiceTest created --- .../en2b/quizapi/user/UserServiceTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java new file mode 100644 index 00000000..48b31ae6 --- /dev/null +++ b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java @@ -0,0 +1,46 @@ +package lab.en2b.quizapi.user; + +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.UserRepository; +import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; +import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith({MockitoExtension.class, SpringExtension.class}) +public class UserServiceTest { + + @InjectMocks + private UserService userService; + + @Mock + private UserRepository userRepository; + + @Mock + private UserResponseDtoMapper userResponseDtoMapper; + + private User defaultUser; + + private UserResponseDto defaultUserResponseDto; + + @BeforeEach + public void setUp() { + userService = new UserService(userRepository); + defaultUser = User.builder() + .id(1L) + .username("HordyJurtado") + .email("test@test.com") + .build(); + defaultUserResponseDto = UserResponseDto.builder() + .id(1L) + .username("HordyJurtado") + .email("test@test.com") + .build(); + } + +} From 89741c736eea4c8a41c987c2d68b3ad694be857b Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 20:55:10 +0200 Subject: [PATCH 38/86] fix: getUserDetailsByAuthenitcation improved --- .../main/java/lab/en2b/quizapi/commons/user/UserService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index 9c2926a6..a63afa94 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -69,7 +69,6 @@ public User getUserByAuthentication(Authentication authentication) { } public UserResponseDto getUserDetailsByAuthentication(Authentication authentication) { - User user = userRepository.findByEmail(((UserDetailsImpl) authentication.getPrincipal()).getEmail()).orElseThrow(); - return userResponseDtoMapper.apply(user); + return userResponseDtoMapper.apply(getUserByAuthentication(authentication)); } } From d274ef18daf5a5b83e3b8591990a78610e1f77ce Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Thu, 11 Apr 2024 21:00:12 +0200 Subject: [PATCH 39/86] fix: test getUserDetails test (tests fail, need to check) --- .../lab/en2b/quizapi/user/UserServiceTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java index 48b31ae6..3ee29111 100644 --- a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java @@ -5,13 +5,22 @@ import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; +import org.junit.jupiter.api.Assertions; 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.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + @ExtendWith({MockitoExtension.class, SpringExtension.class}) public class UserServiceTest { @@ -35,6 +44,8 @@ public void setUp() { .id(1L) .username("HordyJurtado") .email("test@test.com") + .password("password") + .role("ROLE_USER") .build(); defaultUserResponseDto = UserResponseDto.builder() .id(1L) @@ -43,4 +54,11 @@ public void setUp() { .build(); } + @Test + public void getUserDetailsTest(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(any())).thenReturn(defaultUser); + Assertions.assertEquals(defaultUserResponseDto, userService.getUserDetailsByAuthentication(authentication)); + } + } From e061db860adbf268969054dd83d4f78134d474ed Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Fri, 12 Apr 2024 11:58:11 +0200 Subject: [PATCH 40/86] feat: gamemode added --- .../main/java/lab/en2b/quizapi/game/Game.java | 59 +++++++++++++++++-- .../lab/en2b/quizapi/game/GameController.java | 4 +- .../java/lab/en2b/quizapi/game/GameMode.java | 11 ++++ .../lab/en2b/quizapi/game/GameService.java | 13 +--- 4 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/game/GameMode.java diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index eec37592..f311865e 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -8,10 +8,11 @@ import lombok.*; import java.time.Instant; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.List; +import static lab.en2b.quizapi.game.GameMode.*; + @Entity @Table(name = "games") @NoArgsConstructor @@ -20,7 +21,6 @@ @Setter @Builder public class Game { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Setter(AccessLevel.NONE) @@ -35,7 +35,8 @@ public class Game { @NonNull private Integer roundDuration; private boolean currentQuestionAnswered; - + @Enumerated(EnumType.STRING) + private GameMode gamemode; @ManyToOne @NotNull @JoinColumn(name = "user_id") @@ -53,6 +54,14 @@ public class Game { private List questions; private boolean isGameOver; + public Game(User user, GameMode gamemode,String lang) { + this.user = user; + setGamemode(gamemode); + this.questions = new ArrayList<>(); + this.actualRound = 0L; + this.language = lang; + } + public void newRound(Question question){ if(getActualRound() != 0){ if (isGameOver()) @@ -110,11 +119,53 @@ public boolean answerQuestion(Long answerId){ return q.isCorrectAnswer(answerId); } public void setLanguage(String language){ + if(language == null){ + language = "en"; + } if(!isLanguageSupported(language)) throw new IllegalArgumentException("The language you provided is not supported"); this.language = language; } + public void setGamemode(GameMode gamemode){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } + setGamemodeParams(gamemode); + } + private void setGamemodeParams(GameMode gamemode){ //This could be moved to a GameMode entity if we have time + switch(gamemode){ + case KIWI_QUEST: + setRounds(9L); + setRoundDuration(30); + break; + case FOOTBALL_SHOWDOWN: + setRounds(9L); + setRoundDuration(30); + break; + case GEO_GENIUS: + setRounds(9L); + setRoundDuration(30); + break; + case VIDEOGAME_ADVENTURE: + setRounds(9L); + setRoundDuration(30); + break; + case ANCIENT_ODYSSEY: + setRounds(9L); + setRoundDuration(30); + break; + case RANDOM: + setRounds(9L); + setRoundDuration(30); + break; + case CUSTOM: + setRounds(9L); + setRoundDuration(30); + break; + } + + } private boolean isLanguageSupported(String language) { return language.equals("en") || language.equals("es"); } diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index c39409ae..4bfa3757 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -27,8 +27,8 @@ public class GameController { @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) @PostMapping("/new") - public ResponseEntity newGame(Authentication authentication){ - return ResponseEntity.ok(gameService.newGame(authentication)); + public ResponseEntity newGame(@RequestParam(required = false) String lang,@RequestParam(required=false) GameMode gamemode, Authentication authentication){ + return ResponseEntity.ok(gameService.newGame(lang,gamemode,authentication)); } @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameMode.java b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java new file mode 100644 index 00000000..0c5d1e8b --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java @@ -0,0 +1,11 @@ +package lab.en2b.quizapi.game; + +public enum GameMode { + KIWI_QUEST, + FOOTBALL_SHOWDOWN, + GEO_GENIUS, + VIDEOGAME_ADVENTURE, + ANCIENT_ODYSSEY, + RANDOM, + CUSTOM +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index ab75b519..fb081b32 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -34,9 +34,8 @@ public class GameService { private final StatisticsRepository statisticsRepository; @Transactional - public GameResponseDto newGame(Authentication authentication) { + public GameResponseDto newGame(String lang, GameMode gamemode, Authentication authentication) { Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); - if (game.isPresent()){ if (game.get().shouldBeGameOver()){ game.get().setGameOver(true); @@ -46,15 +45,7 @@ public GameResponseDto newGame(Authentication authentication) { return gameResponseDtoMapper.apply(game.get()); } } - return gameResponseDtoMapper.apply(gameRepository.save(Game.builder() - .user(userService.getUserByAuthentication(authentication)) - .questions(new ArrayList<>()) - .rounds(9L) - .actualRound(0L) - .correctlyAnsweredQuestions(0L) - .roundDuration(30) - .language("en") - .build())); + return gameResponseDtoMapper.apply(gameRepository.save(new Game(userService.getUserByAuthentication(authentication),gamemode,lang))); } public GameResponseDto startRound(Long id, Authentication authentication) { From 70c9e520c8082ba4bfa3a57b1f53026e143f79bb Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 17:36:20 +0200 Subject: [PATCH 41/86] feat:custom game --- .../main/java/lab/en2b/quizapi/game/Game.java | 43 ++++++++++++++++--- .../lab/en2b/quizapi/game/GameController.java | 7 +-- .../lab/en2b/quizapi/game/GameService.java | 6 +-- .../en2b/quizapi/game/dtos/CustomGameDto.java | 33 ++++++++++++++ .../en2b/quizapi/game/GameServiceTest.java | 30 ++++++------- 5 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index f311865e..46401956 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -3,8 +3,10 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.question.Question; +import lab.en2b.quizapi.questions.question.QuestionCategory; import lombok.*; import java.time.Instant; @@ -53,13 +55,17 @@ public class Game { @OrderColumn private List questions; private boolean isGameOver; + private List questionCategoriesForCustom; - public Game(User user, GameMode gamemode,String lang) { + public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ this.user = user; - setGamemode(gamemode); this.questions = new ArrayList<>(); this.actualRound = 0L; this.language = lang; + if(gamemode == CUSTOM) + setCustomGameMode(gameDto); + else + setGamemode(gamemode); } public void newRound(Question question){ @@ -126,6 +132,12 @@ public void setLanguage(String language){ throw new IllegalArgumentException("The language you provided is not supported"); this.language = language; } + public void setCustomGameMode(CustomGameDto gameDto){ + setRounds(gameDto.getRounds()); + setRoundDuration(gameDto.getRoundDuration()); + setQuestionCategoriesForCustom(gameDto.getCategories()); + this.gamemode = CUSTOM; + } public void setGamemode(GameMode gamemode){ if(gamemode == null){ gamemode = KIWI_QUEST; @@ -159,12 +171,31 @@ private void setGamemodeParams(GameMode gamemode){ //This could be moved to a Ga setRounds(9L); setRoundDuration(30); break; - case CUSTOM: - setRounds(9L); - setRoundDuration(30); - break; + default: + throw new IllegalStateException("Invalid gamemode!"); } + this.gamemode = gamemode; + + } + + public void setQuestionCategoriesForCustom(List questionCategoriesForCustom) { + if(gamemode != CUSTOM) + throw new IllegalStateException("You can't set custom categories for a non-custom gamemode!"); + if(questionCategoriesForCustom == null || questionCategoriesForCustom.isEmpty()) + throw new IllegalArgumentException("You can't set an empty list of categories for a custom gamemode!"); + this.questionCategoriesForCustom = questionCategoriesForCustom; + } + public List getQuestionCategoriesForGamemode(){ + return switch (gamemode) { + case KIWI_QUEST -> List.of(QuestionCategory.GEOGRAPHY, QuestionCategory.MUSIC); + case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); + case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); + case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); + case ANCIENT_ODYSSEY -> List.of(QuestionCategory.MUSIC,QuestionCategory.ART); + case RANDOM -> List.of(QuestionCategory.values()); + case CUSTOM -> questionCategoriesForCustom; + }; } private boolean isLanguageSupported(String language) { return language.equals("en") || language.equals("es"); diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 4bfa3757..a0bbb197 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.questions.question.QuestionCategory; @@ -26,9 +27,9 @@ public class GameController { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) - @PostMapping("/new") - public ResponseEntity newGame(@RequestParam(required = false) String lang,@RequestParam(required=false) GameMode gamemode, Authentication authentication){ - return ResponseEntity.ok(gameService.newGame(lang,gamemode,authentication)); + @PostMapping("/start") + public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody CustomGameDto customGameDto, Authentication authentication){ + return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); } @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index fb081b32..68043d71 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -2,6 +2,7 @@ import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; @@ -29,12 +30,11 @@ public class GameService { private final GameResponseDtoMapper gameResponseDtoMapper; private final UserService userService; private final QuestionService questionService; - private final QuestionRepository questionRepository; private final QuestionResponseDtoMapper questionResponseDtoMapper; private final StatisticsRepository statisticsRepository; @Transactional - public GameResponseDto newGame(String lang, GameMode gamemode, Authentication authentication) { + public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto newGameDto, Authentication authentication) { Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); if (game.isPresent()){ if (game.get().shouldBeGameOver()){ @@ -45,7 +45,7 @@ public GameResponseDto newGame(String lang, GameMode gamemode, Authentication au return gameResponseDtoMapper.apply(game.get()); } } - return gameResponseDtoMapper.apply(gameRepository.save(new Game(userService.getUserByAuthentication(authentication),gamemode,lang))); + return gameResponseDtoMapper.apply(gameRepository.save(new Game(userService.getUserByAuthentication(authentication),gamemode,lang,newGameDto))); } public GameResponseDto startRound(Long id, Authentication authentication) { diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java new file mode 100644 index 00000000..4dd9fa22 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java @@ -0,0 +1,33 @@ +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lombok.*; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class CustomGameDto { + @Positive + @NotNull + @NonNull + @Schema(description = "Number of rounds for the custom game",example = "9") + private Long rounds; + @Positive + @NotNull + @NonNull + @JsonProperty("round_duration") + @Schema(description = "Duration of the round in seconds",example = "30") + private Integer roundDuration; + @NotNull + @NonNull + @Schema(description = "Categories selected for questions",example = "[\"HISTORY\",\"SCIENCE\"]") + private List categories; +} diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 3b77f596..7b75e883 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -45,9 +45,6 @@ public class GameServiceTest { @Mock private GameRepository gameRepository; - @Mock - private QuestionRepository questionRepository; - @Mock private StatisticsRepository statisticsRepository; @@ -73,7 +70,7 @@ public class GameServiceTest { @BeforeEach void setUp() { this.questionResponseDtoMapper = new QuestionResponseDtoMapper(); - this.gameService = new GameService(gameRepository,new GameResponseDtoMapper(new UserResponseDtoMapper()), userService, questionService, questionRepository, questionResponseDtoMapper, statisticsRepository); + this.gameService = new GameService(gameRepository,new GameResponseDtoMapper(new UserResponseDtoMapper()), userService, questionService, questionResponseDtoMapper, statisticsRepository); this.defaultUser = User.builder() .id(1L) .email("test@email.com") @@ -135,6 +132,7 @@ void setUp() { .user(defaultUserResponseDto) .rounds(9L) .correctlyAnsweredQuestions(0L) + .roundStartTime(Instant.ofEpochSecond(0L).toString()) .actualRound(0L) .roundDuration(30) .build(); @@ -156,7 +154,7 @@ public void newGame(){ Authentication authentication = mock(Authentication.class); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - GameResponseDto gameDto = gameService.newGame(authentication); + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); assertEquals(defaultGameResponseDto, gameDto); } @@ -243,7 +241,7 @@ public void answerQuestionCorrectly(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); gameService.getGameDetails(1L, authentication); @@ -257,7 +255,7 @@ public void answerQuestionIncorrectly(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(2L), authentication); gameService.getGameDetails(1L, authentication); @@ -271,7 +269,7 @@ public void answerQuestionWhenGameHasFinished(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(30L); @@ -284,7 +282,7 @@ public void answerQuestionWhenRoundHasFinished(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); @@ -296,7 +294,7 @@ public void answerQuestionInvalidId(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); assertThrows(IllegalArgumentException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(3L), authentication)); } @@ -306,7 +304,7 @@ public void changeLanguage(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.changeLanguage(1L, "es", authentication); gameService.getGameDetails(1L, authentication); @@ -319,7 +317,7 @@ public void changeLanguageGameOver(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(10L); @@ -332,7 +330,7 @@ public void changeLanguageInvalidLanguage(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); assertThrows(IllegalArgumentException.class, () -> gameService.changeLanguage(1L, "patata", authentication)); } @@ -341,9 +339,11 @@ public void getGameDetails(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - GameResponseDto gameDto = gameService.newGame(authentication); + + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.getGameDetails(1L, authentication); + assertEquals(defaultGameResponseDto, gameDto); } @@ -352,7 +352,7 @@ public void getGameDetailsInvalidId(){ when(gameRepository.findByIdForUser(1L, 1L)).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); assertThrows(NoSuchElementException.class, () -> gameService.getGameDetails(2L, authentication)); } From 1abc7fd66618d4083ce1d95fae2b2495e4df93f7 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:42:32 +0200 Subject: [PATCH 42/86] feat: Centralizing the texrt --- webapp/src/components/statistics/UserStatistics.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index f1b36b86..b707534e 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -15,7 +15,7 @@ export default function UserStatistics() { const getData = async () => { try { const request = await new AuthManager().getAxiosInstance() - .get(process.env.REACT_APP_API_ENDPOINT + "/statistics/personal"); + .get("http://localhost:8080" + "/statistics/personal"); if (request.status === HttpStatusCode.Ok) { setUserData({ "raw": [ @@ -67,7 +67,7 @@ export default function UserStatistics() { {t("statistics.rightAnswers")} - + {t("statistics.texts.personalRight", {right: userData.raw[0].value})} @@ -75,7 +75,7 @@ export default function UserStatistics() { {t("statistics.wrongAnswers")} - + {t("statistics.texts.personalWrong", {wrong: userData.raw[1].value}) } @@ -83,7 +83,7 @@ export default function UserStatistics() { {t("statistics.percentage")} - + {t("statistics.texts.personalRate", {rate: userData.rate})} From a5b1992c282efc89a7a85ba58792d133bba54c83 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:43:21 +0200 Subject: [PATCH 43/86] feat: Adding the correct api endpoint --- webapp/src/components/statistics/UserStatistics.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index b707534e..b8e76328 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -15,7 +15,7 @@ export default function UserStatistics() { const getData = async () => { try { const request = await new AuthManager().getAxiosInstance() - .get("http://localhost:8080" + "/statistics/personal"); + .get(process.env.REACT_APP_API_ENDPOINT + "/statistics/personal"); if (request.status === HttpStatusCode.Ok) { setUserData({ "raw": [ From 20436ed675b2ea52c5b57643075db3a1fedcb7bd Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 17:59:05 +0200 Subject: [PATCH 44/86] refactor: /new is now /play --- .../lab/en2b/quizapi/game/GameController.java | 6 +++-- .../en2b/quizapi/game/GameControllerTest.java | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index a0bbb197..e0981306 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -27,8 +27,10 @@ public class GameController { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) - @PostMapping("/start") - public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody CustomGameDto customGameDto, Authentication authentication){ + @PostMapping("/play") + public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) CustomGameDto customGameDto, Authentication authentication){ + if(gamemode == GameMode.CUSTOM && customGameDto == null) + throw new IllegalArgumentException("Custom game mode requires a body"); return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index f9865ad8..d20d9924 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -41,20 +41,36 @@ public class GameControllerTest { @Test void newQuestionShouldReturn403() throws Exception{ - mockMvc.perform(post("/games/new") + mockMvc.perform(post("/games/play") .contentType("application/json") .with(csrf())) .andExpect(status().isForbidden()); } @Test - void newQuestionShouldReturn200() throws Exception{ - mockMvc.perform(post("/games/new") + void newGameShouldReturn200() throws Exception{ + mockMvc.perform(post("/games/play") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) .andExpect(status().isOk()); } + @Test + void newGameCustomNoBodyShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=CUSTOM") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + @Test + void newGameInvalidGameModeShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=patata") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } @Test void startRoundShouldReturn403() throws Exception{ From f86ad7b8d165ea5fcce67a2ce932f8b1662e8f42 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 17:59:39 +0200 Subject: [PATCH 45/86] feat: question generation takes gamemode into account --- .../lab/en2b/quizapi/game/GameService.java | 2 +- .../question/QuestionRepository.java | 8 +++-- .../questions/question/QuestionService.java | 32 +++++++++++++++---- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 68043d71..09192db0 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -55,7 +55,7 @@ public GameResponseDto startRound(Long id, Authentication authentication) { gameRepository.save(game); saveStatistics(game); } - game.newRound(questionService.findRandomQuestion(game.getLanguage())); + game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getGamemode(),game.getQuestionCategoriesForCustom())); return gameResponseDtoMapper.apply(gameRepository.save(game)); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java index 35c61d92..3e163b20 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java @@ -3,7 +3,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; + public interface QuestionRepository extends JpaRepository { - @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 ORDER BY RANDOM() LIMIT 1", nativeQuery = true) - Question findRandomQuestion(String lang); + @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 " + + "AND a.category IN ?2 " + + " ORDER BY RANDOM() LIMIT 1 ", nativeQuery = true) + Question findRandomQuestion(String lang, List questionCategories); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 264e3605..cf20cc07 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.questions.question; import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; +import lab.en2b.quizapi.game.GameMode; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.answer.AnswerRepository; import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; @@ -42,19 +43,21 @@ else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto } public QuestionResponseDto getRandomQuestion(String lang) { - return questionResponseDtoMapper.apply(findRandomQuestion(lang)); + return questionResponseDtoMapper.apply(findRandomQuestion(lang, GameMode.KIWI_QUEST, null)); } /** * Find a random question for the specified language - * @param lang The language to find the question for + * @param language The language to find the question for * @return The random question */ - public Question findRandomQuestion(String lang){ - if (lang==null || lang.isBlank()) { - lang = "en"; + + public Question findRandomQuestion(String language, GameMode gamemode, List questionCategoriesForCustom) { + if (language==null || language.isBlank()) { + language = "en"; } - Question q = questionRepository.findRandomQuestion(lang); + List questionCategories = getQuestionCategoriesForGamemode(gamemode, questionCategoriesForCustom); + Question q = questionRepository.findRandomQuestion(language,questionCategories); if(q==null) { throw new InternalApiErrorException("No questions found for the specified language!"); } @@ -62,6 +65,19 @@ public Question findRandomQuestion(String lang){ return q; } + private List getQuestionCategoriesForGamemode(GameMode gamemode, List questionCategoriesForCustom) { + return switch (gamemode) { + case KIWI_QUEST -> + new ArrayList<>(List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY)); + case FOOTBALL_SHOWDOWN -> new ArrayList<>(List.of(QuestionCategory.SPORTS)); + case GEO_GENIUS -> new ArrayList<>(List.of(QuestionCategory.GEOGRAPHY)); + case VIDEOGAME_ADVENTURE -> new ArrayList<>(List.of(QuestionCategory.VIDEOGAMES)); + case ANCIENT_ODYSSEY -> new ArrayList<>(List.of(QuestionCategory.ART)); + case CUSTOM -> questionCategoriesForCustom; + default -> new ArrayList<>(List.of(QuestionCategory.values())); + }; + } + public QuestionResponseDto getQuestionById(Long id) { return questionResponseDtoMapper.apply(questionRepository.findById(id).orElseThrow()); } @@ -74,6 +90,9 @@ public QuestionResponseDto getQuestionById(Long id) { //TODO: CHAPUZAS, FIXEAR ESTO private void loadAnswers(Question question) { // Create the new answers list with the distractors + if(question.getAnswers().size() > 1) { + return; + } List answers = new ArrayList<>(QuestionHelper.getDistractors(answerRepository, question)); // Add the correct answers.add(question.getCorrectAnswer()); @@ -84,4 +103,5 @@ private void loadAnswers(Question question) { question.setAnswers(answers); questionRepository.save(question); } + } From 8fde4d91222c0fb66300805eaefed3af3f452677 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 17:59:49 +0200 Subject: [PATCH 46/86] test: fixed tests --- .../en2b/quizapi/game/GameServiceTest.java | 20 +++++++++---------- .../questions/QuestionServiceTest.java | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 7b75e883..fdb59c2c 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -163,7 +163,7 @@ public void newGame(){ public void startRound(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); GameResponseDto gameDto = gameService.startRound(1L, authentication); GameResponseDto result = defaultGameResponseDto; @@ -176,7 +176,7 @@ public void startRound(){ @Test public void startRoundGameOver(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); @@ -198,7 +198,7 @@ public void startRoundWhenRoundNotFinished(){ public void getCurrentQuestion() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); QuestionResponseDto questionDto = gameService.getCurrentQuestion(1L,authentication); @@ -216,7 +216,7 @@ public void getCurrentQuestionRoundNotStarted() { public void getCurrentQuestionRoundFinished() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); @@ -228,7 +228,7 @@ public void getCurrentQuestionGameFinished() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); gameService.startRound(1L,authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(10L); @@ -240,7 +240,7 @@ public void answerQuestionCorrectly(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); @@ -254,7 +254,7 @@ public void answerQuestionIncorrectly(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(2L), authentication); @@ -268,7 +268,7 @@ public void answerQuestionWhenGameHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); @@ -281,7 +281,7 @@ public void answerQuestionWhenRoundHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); @@ -293,7 +293,7 @@ public void answerQuestionInvalidId(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); assertThrows(IllegalArgumentException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(3L), authentication)); diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java index 347790a4..cac7c77d 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -98,7 +98,7 @@ void setUp() { @Test void testGetRandomQuestion() { - when(questionRepository.findRandomQuestion("en")).thenReturn(defaultQuestion); + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); QuestionResponseDto response = questionService.getRandomQuestion(""); assertEquals(response.getId(), defaultResponseDto.getId()); From 6d19d3c4f16543e91e070b370142cd0ab92255ac Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 18:09:45 +0200 Subject: [PATCH 47/86] refactor: moved question types for gamemode to game --- .../main/java/lab/en2b/quizapi/game/Game.java | 3 +-- .../lab/en2b/quizapi/game/GameService.java | 4 +--- .../questions/question/QuestionService.java | 20 +++--------------- .../en2b/quizapi/game/GameControllerTest.java | 2 -- .../en2b/quizapi/game/GameServiceTest.java | 21 ++++++++++--------- 5 files changed, 16 insertions(+), 34 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index 46401956..12ac79cd 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -175,7 +175,6 @@ private void setGamemodeParams(GameMode gamemode){ //This could be moved to a Ga throw new IllegalStateException("Invalid gamemode!"); } this.gamemode = gamemode; - } public void setQuestionCategoriesForCustom(List questionCategoriesForCustom) { @@ -188,7 +187,7 @@ public void setQuestionCategoriesForCustom(List questionCatego public List getQuestionCategoriesForGamemode(){ return switch (gamemode) { - case KIWI_QUEST -> List.of(QuestionCategory.GEOGRAPHY, QuestionCategory.MUSIC); + case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 09192db0..4d2ec474 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -7,7 +7,6 @@ import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; import lab.en2b.quizapi.questions.question.QuestionCategory; -import lab.en2b.quizapi.questions.question.QuestionRepository; import lab.en2b.quizapi.questions.question.QuestionService; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; @@ -18,7 +17,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -55,7 +53,7 @@ public GameResponseDto startRound(Long id, Authentication authentication) { gameRepository.save(game); saveStatistics(game); } - game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getGamemode(),game.getQuestionCategoriesForCustom())); + game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getQuestionCategoriesForGamemode())); return gameResponseDtoMapper.apply(gameRepository.save(game)); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index cf20cc07..77a5241e 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -43,7 +43,7 @@ else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto } public QuestionResponseDto getRandomQuestion(String lang) { - return questionResponseDtoMapper.apply(findRandomQuestion(lang, GameMode.KIWI_QUEST, null)); + return questionResponseDtoMapper.apply(findRandomQuestion(lang, List.of(QuestionCategory.values()))); } /** @@ -52,12 +52,11 @@ public QuestionResponseDto getRandomQuestion(String lang) { * @return The random question */ - public Question findRandomQuestion(String language, GameMode gamemode, List questionCategoriesForCustom) { + public Question findRandomQuestion(String language, List questionCategoriesForCustom) { if (language==null || language.isBlank()) { language = "en"; } - List questionCategories = getQuestionCategoriesForGamemode(gamemode, questionCategoriesForCustom); - Question q = questionRepository.findRandomQuestion(language,questionCategories); + Question q = questionRepository.findRandomQuestion(language,questionCategoriesForCustom); if(q==null) { throw new InternalApiErrorException("No questions found for the specified language!"); } @@ -65,19 +64,6 @@ public Question findRandomQuestion(String language, GameMode gamemode, List getQuestionCategoriesForGamemode(GameMode gamemode, List questionCategoriesForCustom) { - return switch (gamemode) { - case KIWI_QUEST -> - new ArrayList<>(List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY)); - case FOOTBALL_SHOWDOWN -> new ArrayList<>(List.of(QuestionCategory.SPORTS)); - case GEO_GENIUS -> new ArrayList<>(List.of(QuestionCategory.GEOGRAPHY)); - case VIDEOGAME_ADVENTURE -> new ArrayList<>(List.of(QuestionCategory.VIDEOGAMES)); - case ANCIENT_ODYSSEY -> new ArrayList<>(List.of(QuestionCategory.ART)); - case CUSTOM -> questionCategoriesForCustom; - default -> new ArrayList<>(List.of(QuestionCategory.values())); - }; - } - public QuestionResponseDto getQuestionById(Long id) { return questionResponseDtoMapper.apply(questionRepository.findById(id).orElseThrow()); } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index d20d9924..5aed8c5f 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -4,8 +4,6 @@ import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; -import lab.en2b.quizapi.questions.question.QuestionController; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index fdb59c2c..1cee4ad8 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -143,6 +143,7 @@ void setUp() { .rounds(9L) .actualRound(0L) .roundStartTime(0L) + .gamemode(GameMode.KIWI_QUEST) .correctlyAnsweredQuestions(0L) .language("en") .roundDuration(30) @@ -163,7 +164,7 @@ public void newGame(){ public void startRound(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); GameResponseDto gameDto = gameService.startRound(1L, authentication); GameResponseDto result = defaultGameResponseDto; @@ -176,7 +177,7 @@ public void startRound(){ @Test public void startRoundGameOver(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); @@ -198,7 +199,7 @@ public void startRoundWhenRoundNotFinished(){ public void getCurrentQuestion() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); QuestionResponseDto questionDto = gameService.getCurrentQuestion(1L,authentication); @@ -216,7 +217,7 @@ public void getCurrentQuestionRoundNotStarted() { public void getCurrentQuestionRoundFinished() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); @@ -228,7 +229,7 @@ public void getCurrentQuestionGameFinished() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.startRound(1L,authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(10L); @@ -240,7 +241,7 @@ public void answerQuestionCorrectly(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); @@ -254,7 +255,7 @@ public void answerQuestionIncorrectly(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(2L), authentication); @@ -268,7 +269,7 @@ public void answerQuestionWhenGameHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); @@ -281,7 +282,7 @@ public void answerQuestionWhenRoundHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); @@ -293,7 +294,7 @@ public void answerQuestionInvalidId(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any(),any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); assertThrows(IllegalArgumentException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(3L), authentication)); From 648fbb0a67aea515206a65ea639b8dc10a57280a Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Fri, 12 Apr 2024 18:25:09 +0200 Subject: [PATCH 48/86] feat: Changing appearence of the statistics --- .../components/statistics/UserStatistics.jsx | 6 ++--- webapp/src/pages/Statistics.jsx | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index b8e76328..e2534144 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -64,7 +64,7 @@ export default function UserStatistics() { {t("common.statistics.personal")} - + {t("statistics.rightAnswers")} @@ -72,7 +72,7 @@ export default function UserStatistics() { - + {t("statistics.wrongAnswers")} @@ -80,7 +80,7 @@ export default function UserStatistics() { - + {t("statistics.percentage")} diff --git a/webapp/src/pages/Statistics.jsx b/webapp/src/pages/Statistics.jsx index c59f32bc..523167b2 100644 --- a/webapp/src/pages/Statistics.jsx +++ b/webapp/src/pages/Statistics.jsx @@ -47,8 +47,8 @@ export default function Statistics() { const formatTopTen = () => { return topTen.map((element, counter) => { - return - {counter + 1} + return + {counter + 1} {element.user.username} {element.right} {element.wrong} @@ -82,7 +82,7 @@ export default function Statistics() { {t("common.statistics.title")} } minH="50vh" - p="1rem" backgroundColor="whiteAlpha.900" shadow="2xl" + p="1rem" backgroundColor="whiteAlpha.900" shadow="1.25em" boxShadow="md" rounded="1rem" alignItems={"center"} data-testid={"leaderboard-component"}> {retrievedData ? @@ -92,15 +92,15 @@ export default function Statistics() { { topTen.length === 0 ? {t("statistics.empty")} : - +
- - - - - - - + + + + + + + From 4949fcea1eae678ac736d80038b6cdda3b7ae34f Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 18:39:33 +0200 Subject: [PATCH 49/86] feat; list of gamemodes --- .../lab/en2b/quizapi/game/GameController.java | 15 ++- .../java/lab/en2b/quizapi/game/GameMode.java | 1 + .../lab/en2b/quizapi/game/GameService.java | 125 +++++++++++++----- .../en2b/quizapi/game/dtos/GameModeDto.java | 19 +++ .../questions/question/QuestionService.java | 1 - 5 files changed, 125 insertions(+), 36 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index e0981306..123b7f96 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -3,10 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; -import lab.en2b.quizapi.game.dtos.CustomGameDto; -import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.game.dtos.GameResponseDto; +import lab.en2b.quizapi.game.dtos.*; import lab.en2b.quizapi.questions.question.QuestionCategory; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lombok.RequiredArgsConstructor; @@ -86,6 +83,16 @@ public ResponseEntity getGameDetails(@PathVariable Long id, Aut return ResponseEntity.ok(gameService.getGameDetails(id, authentication)); } + @Operation(summary = "Get the list of gamemodes a game can have") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) + }) + @GetMapping("/gamemodes") + public ResponseEntity> getQuestionGameModes(){ + return ResponseEntity.ok(gameService.getQuestionGameModes()); + } + @GetMapping("/questionCategories") public ResponseEntity> getQuestionCategories(){ return ResponseEntity.ok(gameService.getQuestionCategories()); diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameMode.java b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java index 0c5d1e8b..60e6bf7f 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameMode.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java @@ -9,3 +9,4 @@ public enum GameMode { RANDOM, CUSTOM } + diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 4d2ec474..5adaef68 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -1,10 +1,7 @@ package lab.en2b.quizapi.game; import lab.en2b.quizapi.commons.user.UserService; -import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; -import lab.en2b.quizapi.game.dtos.CustomGameDto; -import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.game.dtos.GameResponseDto; +import lab.en2b.quizapi.game.dtos.*; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; import lab.en2b.quizapi.questions.question.QuestionCategory; import lab.en2b.quizapi.questions.question.QuestionService; @@ -31,68 +28,106 @@ public class GameService { private final QuestionResponseDtoMapper questionResponseDtoMapper; private final StatisticsRepository statisticsRepository; + /** + * Creates a new game for the user + * @param lang the language of the game, default is ENGLISH + * @param gamemode the gamemode of the game, default is KIWI_QUEST + * @param newGameDto the custom game dto, only required if the gamemode is CUSTOM + * @param authentication the authentication of the user + * @return the newly created game + */ @Transactional public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto newGameDto, Authentication authentication) { + // Check if there is an active game for the user Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); - if (game.isPresent()){ - if (game.get().shouldBeGameOver()){ - game.get().setGameOver(true); - gameRepository.save(game.get()); - saveStatistics(game.get()); - }else{ - return gameResponseDtoMapper.apply(game.get()); - } + if (game.isPresent() && !wasGameMeantToBeOver(game.get())){ + // If there is an active game and it should not be over, return it + return gameResponseDtoMapper.apply(game.get()); } - return gameResponseDtoMapper.apply(gameRepository.save(new Game(userService.getUserByAuthentication(authentication),gamemode,lang,newGameDto))); + return gameResponseDtoMapper.apply(gameRepository.save( + new Game(userService.getUserByAuthentication(authentication),gamemode,lang,newGameDto) + )); } + /** + * Starts a new round for the game + * @param id the id of the game to start the round for + * @param authentication the authentication of the user + * @return the game with the new round started + */ public GameResponseDto startRound(Long id, Authentication authentication) { + // Get the game by id and user Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - if (game.shouldBeGameOver()){ - game.setGameOver(true); - gameRepository.save(game); - saveStatistics(game); - } + // Check if the game should be over + wasGameMeantToBeOver(game); + // Start a new round game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getQuestionCategoriesForGamemode())); return gameResponseDtoMapper.apply(gameRepository.save(game)); } + /** + * Gets the current question for the game + * @param id the id of the game to get the question for + * @param authentication the authentication of the user + * @return the current question + */ public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentication){ Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); return questionResponseDtoMapper.apply(game.getCurrentQuestion()); } + /** + * Answers the current question for the game + * @param id the id of the game to answer the question for + * @param dto the answer dto + * @param authentication the authentication of the user + * @return the response of the answer + */ @Transactional public AnswerGameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + // Check if the game should be over + wasGameMeantToBeOver(game); + // Answer the question boolean wasCorrect = game.answerQuestion(dto.getAnswerId()); - - if (game.shouldBeGameOver()){ - game.setGameOver(true); - gameRepository.save(game); - saveStatistics(game); - } + // Check if the game is over after the answer + wasGameMeantToBeOver(game); return new AnswerGameResponseDto(wasCorrect); } + + /** + * Saves the statistics of the game + * @param game the game to save the statistics for + */ private void saveStatistics(Game game){ + Statistics statistics; if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ - Statistics statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); + // If there are statistics for the user, update them + statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); statistics.updateStatistics(game.getCorrectlyAnsweredQuestions(), game.getQuestions().size()-game.getCorrectlyAnsweredQuestions(), game.getRounds()); - statisticsRepository.save(statistics); } else { - Statistics statistics = Statistics.builder() + // If there are no statistics for the user, create new ones + statistics = Statistics.builder() .user(game.getUser()) .correct(game.getCorrectlyAnsweredQuestions()) .wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()) .total(game.getRounds()) .build(); - statisticsRepository.save(statistics); } + statisticsRepository.save(statistics); } + + /** + * Changes the language of the game. The game language will only change after the next round. + * @param id the id of the game to change the language for + * @param language the language to change to + * @param authentication the authentication of the user + * @return the game with the new language + */ public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); if(game.isGameOver()){ @@ -102,17 +137,45 @@ public GameResponseDto changeLanguage(Long id, String language, Authentication a return gameResponseDtoMapper.apply(gameRepository.save(game)); } + /** + * Gets the game details + * @param id the id of the game to get the details for + * @param authentication the authentication of the user + * @return the game details + */ public GameResponseDto getGameDetails(Long id, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + wasGameMeantToBeOver(game); + return gameResponseDtoMapper.apply(game); + } + + public List getQuestionCategories() { + return Arrays.asList(QuestionCategory.values()); + } + + private boolean wasGameMeantToBeOver(Game game) { if (game.shouldBeGameOver()){ game.setGameOver(true); gameRepository.save(game); saveStatistics(game); + return true; } - return gameResponseDtoMapper.apply(game); + return false; } - public List getQuestionCategories() { - return Arrays.asList(QuestionCategory.values()); + /** + * Gets the list of gamemodes a game can have + * @return the list of gamemodes + */ + public List getQuestionGameModes() { + return List.of( + new GameModeDto("Kiwi Quest","Our curated selection of the most exquisite questions. Enjoy with a glass of wine",GameMode.KIWI_QUEST,"FaKiwiBird"), + new GameModeDto("Football Showdown","Like sports? Like balls? This gamemode is for you!",GameMode.FOOTBALL_SHOWDOWN,"IoIosFootball"), + new GameModeDto("Geo Genius","Do you know the capital of Mongolia? I don't, so if you do this game is for you!",GameMode.GEO_GENIUS,"FaGlobeAmericas"), + new GameModeDto("Videogame Adventure","It's dangerous to go alone, guess this!",GameMode.VIDEOGAME_ADVENTURE,"IoLogoGameControllerB"), + new GameModeDto("Ancient Odyssey","Antiques are pricey for a reason!",GameMode.ANCIENT_ODYSSEY,"FaPalette"), + new GameModeDto("Random","Try a bit of everything!",GameMode.RANDOM,"FaRandom"), + new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") + ); } } diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java new file mode 100644 index 00000000..01127ccf --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java @@ -0,0 +1,19 @@ +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lab.en2b.quizapi.game.GameMode; +import lombok.*; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class GameModeDto { + private String name; + private String description; + @JsonProperty("internal_representation") + private GameMode internalRepresentation; + @JsonProperty("icon_name") + private String iconName; +} diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 77a5241e..7c37356c 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -1,7 +1,6 @@ package lab.en2b.quizapi.questions.question; import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; -import lab.en2b.quizapi.game.GameMode; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.answer.AnswerRepository; import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; From af375f9ce3783058f32cdc9ca879f23e63939967 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 19:06:43 +0200 Subject: [PATCH 50/86] fix: language bugs, question selection and others --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 3 +++ api/src/main/java/lab/en2b/quizapi/game/GameService.java | 1 + .../main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java | 4 ++++ .../lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java | 1 + .../en2b/quizapi/questions/question/QuestionRepository.java | 4 ++-- .../lab/en2b/quizapi/questions/question/QuestionService.java | 2 +- api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java | 1 + 7 files changed, 13 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index 12ac79cd..371d2904 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -186,6 +186,9 @@ public void setQuestionCategoriesForCustom(List questionCatego } public List getQuestionCategoriesForGamemode(){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } return switch (gamemode) { case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 5adaef68..9b084ef0 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -55,6 +55,7 @@ public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto new * @param authentication the authentication of the user * @return the game with the new round started */ + @Transactional public GameResponseDto startRound(Long id, Authentication authentication) { // Get the game by id and user Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index ffb37336..57a63abc 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.game.GameMode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -43,4 +44,7 @@ public class GameResponseDto { @Schema(description = "Whether the game has finished or not", example = "true") private boolean isGameOver; + + @Schema(description = "Game mode for the game", example = "KIWI_QUEST") + private GameMode gamemode; } diff --git a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java index 4f061c70..3fbc0b0f 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java @@ -23,6 +23,7 @@ public GameResponseDto apply(Game game) { .actualRound(game.getActualRound()) .roundDuration(game.getRoundDuration()) .roundStartTime(game.getRoundStartTime() != null? Instant.ofEpochMilli(game.getRoundStartTime()).toString(): null) + .gamemode(game.getGamemode()) .isGameOver(game.isGameOver()) .build(); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java index 3e163b20..fd8e2eca 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java @@ -7,7 +7,7 @@ public interface QuestionRepository extends JpaRepository { @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 " + - "AND a.category IN ?2 " + + "AND q.question_category IN ?2 " + " ORDER BY RANDOM() LIMIT 1 ", nativeQuery = true) - Question findRandomQuestion(String lang, List questionCategories); + Question findRandomQuestion(String lang, List questionCategories); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 7c37356c..47a8e549 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -55,7 +55,7 @@ public Question findRandomQuestion(String language, List quest if (language==null || language.isBlank()) { language = "en"; } - Question q = questionRepository.findRandomQuestion(language,questionCategoriesForCustom); + Question q = questionRepository.findRandomQuestion(language,questionCategoriesForCustom.stream().map(Enum::toString).toList()); if(q==null) { throw new InternalApiErrorException("No questions found for the specified language!"); } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 1cee4ad8..44b6a06d 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -135,6 +135,7 @@ void setUp() { .roundStartTime(Instant.ofEpochSecond(0L).toString()) .actualRound(0L) .roundDuration(30) + .gamemode(GameMode.KIWI_QUEST) .build(); this.defaultGame = Game.builder() .id(1L) From 282d2346781484fab259db81320c91b3a9124521 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 19:12:58 +0200 Subject: [PATCH 51/86] docs: documented play endpoint --- .../main/java/lab/en2b/quizapi/game/GameController.java | 8 +++++++- .../main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 123b7f96..774de14a 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -22,6 +22,7 @@ public class GameController { @Operation(summary = "Starts new game", description = "Requests the API to create a new game for a given authentication (a player)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Given when: \n * language provided is not valid \n * gamemode provided is not valid \n * body is not provided with custom game", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) @PostMapping("/play") @@ -65,7 +66,7 @@ public ResponseEntity answerQuestion(@PathVariable Long i @Operation(summary = "Changing languages", description = "Changes the language of the game for a given authentication (a player) and a language supported. Changes may are applied on the next round.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "400", description = "Not a valid answer", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "400", description = "Not a valid language to change to", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) @PutMapping("/{id}/language") @@ -93,6 +94,11 @@ public ResponseEntity> getQuestionGameModes(){ return ResponseEntity.ok(gameService.getQuestionGameModes()); } + @Operation(summary = "Get the list of categories a game can have") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) + }) @GetMapping("/questionCategories") public ResponseEntity> getQuestionCategories(){ return ResponseEntity.ok(gameService.getQuestionCategories()); diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java index 01127ccf..82550eba 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.game.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lab.en2b.quizapi.game.GameMode; import lombok.*; @@ -10,10 +11,14 @@ @Builder @Setter public class GameModeDto { + @Schema(description = "Beautified name of the game mode",example = "Quiwi Quest") private String name; + @Schema(description = "Description of the game mode",example = "Test description of the game mode") private String description; @JsonProperty("internal_representation") + @Schema(description = "Internal code used for describing the game mode",example = "KIWI_QUEST") private GameMode internalRepresentation; @JsonProperty("icon_name") + @Schema(description = "Code for the icon used in the frontend of the application",example = "FaKiwiBird") private String iconName; } From 203b12c8f07847f78f1ffc422d91f4f7b98403c0 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 19:23:34 +0200 Subject: [PATCH 52/86] docs: params and body --- .../lab/en2b/quizapi/game/GameController.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 774de14a..3855863d 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -1,6 +1,8 @@ package lab.en2b.quizapi.game; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lab.en2b.quizapi.game.dtos.*; @@ -25,6 +27,11 @@ public class GameController { @ApiResponse(responseCode = "400", description = "Given when: \n * language provided is not valid \n * gamemode provided is not valid \n * body is not provided with custom game", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameters({ + @Parameter(name = "lang", description = "The language of the game", example = "en"), + @Parameter(name = "gamemode", description = "The gamemode of the game", example = "KIWI_QUEST") + }) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") @PostMapping("/play") public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) CustomGameDto customGameDto, Authentication authentication){ if(gamemode == GameMode.CUSTOM && customGameDto == null) @@ -37,27 +44,30 @@ public ResponseEntity newGame(@RequestParam(required = false) S @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to start the round for", example = "1") @PostMapping("/{id}/startRound") public ResponseEntity startRound(@PathVariable Long id, Authentication authentication){ return ResponseEntity.ok(gameService.startRound(id, authentication)); } - @Operation(summary = "Starts a new round", description = "Gets the question and its possible answers from the API for a given authentication (a player)") + @Operation(summary = "Gets the current question", description = "Gets the question and its possible answers from the API for a given authentication (a player)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to get the current question for", example = "1") @GetMapping("/{id}/question") public ResponseEntity getCurrentQuestion(@PathVariable Long id, Authentication authentication){ return ResponseEntity.ok(gameService.getCurrentQuestion(id, authentication)); } - @Operation(summary = "Starts a new round", description = "Starts the round (getting a question and its possible answers and start the timer) for a given authentication (a player)") + @Operation(summary = "Answers the question", description = "Answers the question for the current game") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "400", description = "Not a valid answer", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to answer the question for", example = "1") @PostMapping("/{id}/answer") public ResponseEntity answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){ return ResponseEntity.ok(gameService.answerQuestion(id, dto, authentication)); @@ -69,6 +79,7 @@ public ResponseEntity answerQuestion(@PathVariable Long i @ApiResponse(responseCode = "400", description = "Not a valid language to change to", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to change the language for", example = "1") @PutMapping("/{id}/language") public ResponseEntity changeLanguage(@PathVariable Long id, @RequestParam String language, Authentication authentication){ return ResponseEntity.ok(gameService.changeLanguage(id, language, authentication)); @@ -79,6 +90,7 @@ public ResponseEntity changeLanguage(@PathVariable Long id, @Re @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to get the summary for", example = "1") @GetMapping("/{id}/details") public ResponseEntity getGameDetails(@PathVariable Long id, Authentication authentication){ return ResponseEntity.ok(gameService.getGameDetails(id, authentication)); From 310f558c01f8d4059da4c6d6e35ebf777b6c9ba5 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:24:48 +0200 Subject: [PATCH 53/86] test: game tests --- .../main/java/lab/en2b/quizapi/game/Game.java | 7 +- .../en2b/quizapi/game/GameControllerTest.java | 17 ++++ .../en2b/quizapi/game/GameServiceTest.java | 83 +++++++++++++++++-- 3 files changed, 97 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index 371d2904..ad83db8d 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -135,8 +135,8 @@ public void setLanguage(String language){ public void setCustomGameMode(CustomGameDto gameDto){ setRounds(gameDto.getRounds()); setRoundDuration(gameDto.getRoundDuration()); - setQuestionCategoriesForCustom(gameDto.getCategories()); this.gamemode = CUSTOM; + setQuestionCategoriesForCustom(gameDto.getCategories()); } public void setGamemode(GameMode gamemode){ if(gamemode == null){ @@ -172,7 +172,8 @@ private void setGamemodeParams(GameMode gamemode){ //This could be moved to a Ga setRoundDuration(30); break; default: - throw new IllegalStateException("Invalid gamemode!"); + setRounds(9L); + setRoundDuration(30); } this.gamemode = gamemode; } @@ -204,6 +205,6 @@ private boolean isLanguageSupported(String language) { } public boolean shouldBeGameOver() { - return getActualRound() >= getRounds() && !isGameOver; + return getActualRound() >= getRounds() && !isGameOver && currentRoundIsOver(); } } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index 5aed8c5f..b0dc2982 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -192,4 +192,21 @@ void getQuestionCategoriesShouldReturn403() throws Exception{ .andExpect(status().isForbidden()); } + @Test + void getGameModeshouldReturn200() throws Exception{ + mockMvc.perform(get("/games/gamemodes") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getGameModesShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/gamemodes") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 44b6a06d..4c8f0cb3 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -5,6 +5,7 @@ import lab.en2b.quizapi.commons.user.UserResponseDto; import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; @@ -14,6 +15,7 @@ import lab.en2b.quizapi.questions.question.*; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import lab.en2b.quizapi.statistics.Statistics; import lab.en2b.quizapi.statistics.StatisticsRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,8 +32,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith({MockitoExtension.class, SpringExtension.class}) public class GameServiceTest { @@ -151,16 +152,73 @@ void setUp() { .build(); } + // NEW GAME TESTS @Test public void newGame(){ Authentication authentication = mock(Authentication.class); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + assertEquals(defaultGameResponseDto, gameDto); + } + @Test + public void newGameActiveGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + defaultGameResponseDto.setId(1L); assertEquals(defaultGameResponseDto, gameDto); } + @Test + public void newGameWasMeantToBeOver(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + defaultGame.setActualRound(10L); + gameService.newGame(null,null,null,authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void newGameWasMeantToBeOverExistingLeaderboard(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(Statistics.builder().user(new User()) + .correct(0L) + .wrong(0L) + .total(0L) + .build())); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + defaultGame.setActualRound(10L); + gameService.newGame(null,null,null,authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void newGameCustomGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + GameResponseDto gameDto = gameService.newGame("es",GameMode.CUSTOM, + CustomGameDto.builder() + .roundDuration(30) + .categories(List.of(QuestionCategory.GEOGRAPHY)) + .rounds(10L) + .build() + ,authentication); + defaultGameResponseDto.setGamemode(GameMode.CUSTOM); + defaultGameResponseDto.setRounds(10L); + defaultGameResponseDto.setRoundDuration(30); + + assertEquals(defaultGameResponseDto, gameDto); + } + + // START ROUND TESTS @Test public void startRound(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); @@ -184,17 +242,15 @@ public void startRoundGameOver(){ assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); } - /** @Test public void startRoundWhenRoundNotFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); } - **/ @Test public void getCurrentQuestion() { @@ -207,12 +263,12 @@ public void getCurrentQuestion() { assertEquals(defaultQuestionResponseDto, questionDto); } - /*@Test + @Test public void getCurrentQuestionRoundNotStarted() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); - }*/ + } @Test public void getCurrentQuestionRoundFinished() { @@ -278,6 +334,19 @@ public void answerQuestionWhenGameHasFinished(){ assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); } + @Test + public void answerQuestionLastRound(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + defaultGame.setActualRound(8L); + gameService.startRound(1L, authentication); + gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); + verify(statisticsRepository, times(1)).save(any()); + } + @Test public void answerQuestionWhenRoundHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); From a8861b9021026e7f089e7f771e52197d8a005d4f Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:38:47 +0200 Subject: [PATCH 54/86] fix: invalid custom game body --- .../java/lab/en2b/quizapi/game/GameController.java | 3 ++- .../lab/en2b/quizapi/game/GameControllerTest.java | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 3855863d..10234d5e 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lab.en2b.quizapi.game.dtos.*; import lab.en2b.quizapi.questions.question.QuestionCategory; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; @@ -33,7 +34,7 @@ public class GameController { }) @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") @PostMapping("/play") - public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) CustomGameDto customGameDto, Authentication authentication){ + public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) @Valid CustomGameDto customGameDto, Authentication authentication){ if(gamemode == GameMode.CUSTOM && customGameDto == null) throw new IllegalArgumentException("Custom game mode requires a body"); return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index b0dc2982..2dcac8a1 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -3,6 +3,7 @@ import lab.en2b.quizapi.auth.config.SecurityConfig; import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -62,6 +63,15 @@ void newGameCustomNoBodyShouldReturn400() throws Exception{ .andExpect(status().isBadRequest()); } @Test + void newGameInvalidBodyForCustomShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=CUSTOM") + .with(user("test").roles("user")) + .content(asJsonString(new CustomGameDto())) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + @Test void newGameInvalidGameModeShouldReturn400() throws Exception{ mockMvc.perform(post("/games/play?gamemode=patata") .with(user("test").roles("user")) @@ -209,4 +219,5 @@ void getGameModesShouldReturn403() throws Exception{ .andExpect(status().isForbidden()); } + } From 291af7f2a8f8338b65e9ac6fa50ec9106460322b Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:44:15 +0200 Subject: [PATCH 55/86] refactor: renamed get question categories endpoint --- api/src/main/java/lab/en2b/quizapi/game/GameController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 10234d5e..08af1201 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -112,7 +112,7 @@ public ResponseEntity> getQuestionGameModes(){ @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) }) - @GetMapping("/questionCategories") + @GetMapping("/question-categories") public ResponseEntity> getQuestionCategories(){ return ResponseEntity.ok(gameService.getQuestionCategories()); } From 1dee3b6108ab578d8d32d7ce570e5faec3099c3b Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:44:31 +0200 Subject: [PATCH 56/86] fix: language not setting correctly --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index ad83db8d..0fcb18da 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -61,7 +61,7 @@ public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ this.user = user; this.questions = new ArrayList<>(); this.actualRound = 0L; - this.language = lang; + setLanguage(lang); if(gamemode == CUSTOM) setCustomGameMode(gameDto); else From f73f6a229c9dcb52fa785aee5cd73336740c31d7 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:48:29 +0200 Subject: [PATCH 57/86] fix:saving categories now works with string --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index 0fcb18da..a75c9ca5 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -55,6 +55,7 @@ public class Game { @OrderColumn private List questions; private boolean isGameOver; + @Enumerated(EnumType.STRING) private List questionCategoriesForCustom; public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ From 6c05fd55ea1c9f10649a9c7d8444a2357da342f4 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:53:59 +0200 Subject: [PATCH 58/86] test: no questions found --- .../en2b/quizapi/questions/QuestionServiceTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java index cac7c77d..a46ed90d 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.questions; +import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.answer.AnswerCategory; import lab.en2b.quizapi.questions.answer.AnswerRepository; @@ -103,6 +104,18 @@ void testGetRandomQuestion() { assertEquals(response.getId(), defaultResponseDto.getId()); } + @Test + void testGetRandomQuestionAnswersNotYetLoaded() { + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + defaultQuestion.setAnswers(List.of()); + QuestionResponseDto response = questionService.getRandomQuestion(""); + + assertEquals(response.getId(), defaultResponseDto.getId()); + } + @Test + void testGetRandomQuestionNoQuestionsFound() { + assertThrows(InternalApiErrorException.class,() -> questionService.getRandomQuestion("")); + } @Test void testGetQuestionById(){ From 55235405631be478d1bcac69a612de5c7f5994cf Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 20:57:26 +0200 Subject: [PATCH 59/86] test: fixed broken test --- .../test/java/lab/en2b/quizapi/game/GameControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index 2dcac8a1..d570e709 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -187,7 +187,7 @@ void getGameDetailsShouldReturn200() throws Exception{ @Test void getQuestionCategoriesShouldReturn200() throws Exception{ - mockMvc.perform(get("/games/questionCategories") + mockMvc.perform(get("/games/question-categories") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) @@ -196,7 +196,7 @@ void getQuestionCategoriesShouldReturn200() throws Exception{ @Test void getQuestionCategoriesShouldReturn403() throws Exception{ - mockMvc.perform(get("/games/questionCategories") + mockMvc.perform(get("/games/question-categories") .contentType("application/json") .with(csrf())) .andExpect(status().isForbidden()); From 871ddddbac2e9151c9002ebd166288b70208ae72 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 12 Apr 2024 21:06:13 +0200 Subject: [PATCH 60/86] refactor: moved game mode logic to utils --- .../quizapi/commons/utils/GameModeUtils.java | 57 +++++++++++++++++++ .../main/java/lab/en2b/quizapi/game/Game.java | 52 ++--------------- 2 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java diff --git a/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java new file mode 100644 index 00000000..4e6c2886 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java @@ -0,0 +1,57 @@ +package lab.en2b.quizapi.commons.utils; + +import lab.en2b.quizapi.game.Game; +import lab.en2b.quizapi.game.GameMode; +import lab.en2b.quizapi.questions.question.QuestionCategory; + +import java.util.List; + +import static lab.en2b.quizapi.game.GameMode.KIWI_QUEST; + +public class GameModeUtils { + public static List getQuestionCategoriesForGamemode(GameMode gamemode, List questionCategoriesForCustom){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } + return switch (gamemode) { + case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); + case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); + case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); + case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); + case ANCIENT_ODYSSEY -> List.of(QuestionCategory.MUSIC,QuestionCategory.ART); + case RANDOM -> List.of(QuestionCategory.values()); + case CUSTOM -> questionCategoriesForCustom; + }; + } + public static void setGamemodeParams(Game game){ + switch(game.getGamemode()){ + case KIWI_QUEST: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case FOOTBALL_SHOWDOWN: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case GEO_GENIUS: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case VIDEOGAME_ADVENTURE: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case ANCIENT_ODYSSEY: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case RANDOM: + game.setRounds(9L); + game.setRoundDuration(30); + break; + default: + game.setRounds(9L); + game.setRoundDuration(30); + } + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index a75c9ca5..19097219 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.utils.GameModeUtils; import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.question.Question; @@ -66,7 +67,7 @@ public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ if(gamemode == CUSTOM) setCustomGameMode(gameDto); else - setGamemode(gamemode); + setGameMode(gamemode); } public void newRound(Question question){ @@ -139,44 +140,12 @@ public void setCustomGameMode(CustomGameDto gameDto){ this.gamemode = CUSTOM; setQuestionCategoriesForCustom(gameDto.getCategories()); } - public void setGamemode(GameMode gamemode){ + public void setGameMode(GameMode gamemode){ if(gamemode == null){ gamemode = KIWI_QUEST; } - setGamemodeParams(gamemode); - } - - private void setGamemodeParams(GameMode gamemode){ //This could be moved to a GameMode entity if we have time - switch(gamemode){ - case KIWI_QUEST: - setRounds(9L); - setRoundDuration(30); - break; - case FOOTBALL_SHOWDOWN: - setRounds(9L); - setRoundDuration(30); - break; - case GEO_GENIUS: - setRounds(9L); - setRoundDuration(30); - break; - case VIDEOGAME_ADVENTURE: - setRounds(9L); - setRoundDuration(30); - break; - case ANCIENT_ODYSSEY: - setRounds(9L); - setRoundDuration(30); - break; - case RANDOM: - setRounds(9L); - setRoundDuration(30); - break; - default: - setRounds(9L); - setRoundDuration(30); - } this.gamemode = gamemode; + GameModeUtils.setGamemodeParams(this); } public void setQuestionCategoriesForCustom(List questionCategoriesForCustom) { @@ -188,18 +157,7 @@ public void setQuestionCategoriesForCustom(List questionCatego } public List getQuestionCategoriesForGamemode(){ - if(gamemode == null){ - gamemode = KIWI_QUEST; - } - return switch (gamemode) { - case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); - case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); - case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); - case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); - case ANCIENT_ODYSSEY -> List.of(QuestionCategory.MUSIC,QuestionCategory.ART); - case RANDOM -> List.of(QuestionCategory.values()); - case CUSTOM -> questionCategoriesForCustom; - }; + return GameModeUtils.getQuestionCategoriesForGamemode(gamemode,questionCategoriesForCustom); } private boolean isLanguageSupported(String language) { return language.equals("en") || language.equals("es"); From 5a9ba95aace8366e563f2c9b6b5f30404280d63a Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Sun, 14 Apr 2024 11:36:12 +0200 Subject: [PATCH 61/86] test: tests finished --- .../quizapi/commons/user/UserService.java | 2 +- .../en2b/quizapi/auth/AuthServiceTest.java | 2 +- .../en2b/quizapi/user/UserServiceTest.java | 22 ++++++++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index a63afa94..2c4f44cc 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -24,7 +24,7 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; @Value("${REFRESH_TOKEN_DURATION_MS}") private long refreshTokenDurationMs; - private UserResponseDtoMapper userResponseDtoMapper; + private final UserResponseDtoMapper userResponseDtoMapper; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow(() -> new InvalidAuthenticationException("Invalid email or password provided!"))); diff --git a/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java b/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java index 3f83e084..29273089 100644 --- a/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java @@ -41,7 +41,7 @@ public class AuthServiceTest { User defaultUser; @BeforeEach void setUp() { - this.userService = new UserService(userRepository); + this.userService = new UserService(userRepository,null); this.authService = new AuthService(authenticationManager,userService,jwtUtils); this.defaultUser = User.builder() .id(1L) diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java index 3ee29111..b99981af 100644 --- a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.user; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; import lab.en2b.quizapi.commons.user.User; import lab.en2b.quizapi.commons.user.UserRepository; import lab.en2b.quizapi.commons.user.UserService; @@ -12,9 +13,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.NoSuchElementException; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; @@ -30,16 +33,13 @@ public class UserServiceTest { @Mock private UserRepository userRepository; - @Mock - private UserResponseDtoMapper userResponseDtoMapper; - private User defaultUser; private UserResponseDto defaultUserResponseDto; @BeforeEach public void setUp() { - userService = new UserService(userRepository); + userService = new UserService(userRepository, new UserResponseDtoMapper()); defaultUser = User.builder() .id(1L) .username("HordyJurtado") @@ -57,8 +57,18 @@ public void setUp() { @Test public void getUserDetailsTest(){ Authentication authentication = mock(Authentication.class); - when(userService.getUserByAuthentication(any())).thenReturn(defaultUser); - Assertions.assertEquals(defaultUserResponseDto, userService.getUserDetailsByAuthentication(authentication)); + when(authentication.getPrincipal()).thenReturn(UserDetailsImpl.build(defaultUser)); + when(userRepository.findByEmail(any())).thenReturn(Optional.of(defaultUser)); + UserResponseDto result = userService.getUserDetailsByAuthentication(authentication); + Assertions.assertEquals(defaultUserResponseDto, result); + } + + @Test + public void getUserDetailsWhenNotFound() { + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(UserDetailsImpl.build(defaultUser)); + when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); + Assertions.assertThrows(NoSuchElementException.class, () -> userService.getUserDetailsByAuthentication(authentication)); } } From 94797e9eb970b9819bb8ede05cba956c531a3028 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Sun, 14 Apr 2024 12:06:25 +0200 Subject: [PATCH 62/86] fix: imports --- .../main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index c3b70dad..8abb6cc4 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; -import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lab.en2b.quizapi.game.GameMode; import lombok.AllArgsConstructor; import lombok.Builder; From 2587850d48881e5033d487e39d8b0de9504d97e4 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:04:36 +0200 Subject: [PATCH 63/86] feat: Fixing the chart and aligning the items with it --- .../components/statistics/UserStatistics.jsx | 120 +++++++++--------- webapp/src/pages/Statistics.jsx | 1 - 2 files changed, 62 insertions(+), 59 deletions(-) diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index e2534144..1a80463a 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -2,33 +2,32 @@ import { Box, Flex, Heading, Stack, Text, CircularProgress } from "@chakra-ui/re import { HttpStatusCode } from "axios"; import ErrorMessageAlert from "components/ErrorMessageAlert"; import AuthManager from "components/auth/AuthManager"; -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Cell, Pie, PieChart } from "recharts"; export default function UserStatistics() { - const {t} = useTranslation(); + const { t } = useTranslation(); const [userData, setUserData] = useState(null); const [retrievedData, setRetrievedData] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const getData = async () => { try { - const request = await new AuthManager().getAxiosInstance() - .get(process.env.REACT_APP_API_ENDPOINT + "/statistics/personal"); + const request = await new AuthManager().getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/statistics/personal"); if (request.status === HttpStatusCode.Ok) { setUserData({ - "raw": [ + raw: [ { - "name": t("statistics.texts.personalRight"), - "value": request.data.right + name: t("statistics.texts.personalRight"), + value: 60, }, { - "name": t("statistics.texts.personalWrong"), - "value": request.data.wrong - } + name: t("statistics.texts.personalWrong"), + value: 40, + }, ], - "rate": request.data.correct_rate + rate: 60 }); setRetrievedData(true); } else { @@ -38,67 +37,72 @@ export default function UserStatistics() { let errorType; switch (error.response ? error.response.status : null) { case 400: - errorType = { type: t("error.validation.type"), message: t("error.validation.message")}; + errorType = { type: t("error.validation.type"), message: t("error.validation.message") }; break; case 404: - errorType = { type: t("error.notFound.type"), message: t("error.notFound.message")}; + errorType = { type: t("error.notFound.type"), message: t("error.notFound.message") }; break; default: - errorType = { type: t("error.unknown.type"), message: t("error.unknown.message")}; + errorType = { type: t("error.unknown.type"), message: t("error.unknown.message") }; break; } setErrorMessage(errorType); } - } + }; useEffect(() => { - if(!retrievedData){ + if (!retrievedData) { getData(); } }); - return - { - retrievedData ? - <> - - - {t("common.statistics.personal")} - - - {t("statistics.rightAnswers")} - - - {t("statistics.texts.personalRight", {right: userData.raw[0].value})} - - - - + + return ( + + {retrievedData ? ( + + + + {t("common.statistics.personal")} + + + + + + {t("statistics.rightAnswers")} + + + {t("statistics.texts.personalRight", { right: userData.raw[0].value })} + + + + {t("statistics.wrongAnswers")} - - - {t("statistics.texts.personalWrong", {wrong: userData.raw[1].value}) } - - - - - {t("statistics.percentage")} - - - {t("statistics.texts.personalRate", {rate: userData.rate})} - - - - - - + + + {t("statistics.texts.personalWrong", { wrong: userData.raw[1].value })} + + + + + {t("statistics.percentage")} + + + {t("statistics.texts.personalRate", { rate: userData.rate })} + + + + + + - - - - : - } + + + + + ) : ( + + )} -} \ No newline at end of file + ); +} diff --git a/webapp/src/pages/Statistics.jsx b/webapp/src/pages/Statistics.jsx index 523167b2..0fcb86cd 100644 --- a/webapp/src/pages/Statistics.jsx +++ b/webapp/src/pages/Statistics.jsx @@ -18,7 +18,6 @@ export default function Statistics() { const [errorMessage, setErrorMessage] = useState(null); const getData = async () => { - console.log('lmao') try { const request = await new AuthManager().getAxiosInstance() .get(process.env.REACT_APP_API_ENDPOINT + "/statistics/top"); From 4d411c658b520e7ebb33f4fa2256bd75d581e72a Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:08:38 +0200 Subject: [PATCH 64/86] feat: Changing the harcoded rates --- webapp/src/components/statistics/UserStatistics.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index 1a80463a..9f90eac7 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -20,14 +20,14 @@ export default function UserStatistics() { raw: [ { name: t("statistics.texts.personalRight"), - value: 60, + value: request.data.right, }, { name: t("statistics.texts.personalWrong"), - value: 40, + value: request.data.wrong, }, ], - rate: 60 + rate: request.data.correct_rate }); setRetrievedData(true); } else { From 683cd1e744a5352ac8321110656f06a12a9b9fd1 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:48:10 +0200 Subject: [PATCH 65/86] feat: Adding new version for the rules page --- webapp/public/locales/en/translation.json | 9 +++++---- webapp/public/locales/es/translation.json | 9 +++++---- webapp/src/pages/Rules.jsx | 6 ++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index b61195dc..d6cdce40 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -53,10 +53,11 @@ } }, "rules": { - "description1": "The WIQ game consists of quick games of 9 rounds. In each round there is one question and two possible answers. The key to earning points lies in choosing the correct answer.", - "description2": "There is only one correct answer.", - "description3": "You have to select a question before time runs out.", - "description4": "To start playing you have to click on the Play button." + "description1": "Welcome to the exciting world of KiWiQ! In this challenging game, your goal is to embark on a journey full of knowledge and fun.", + "description2": "Each question is a door to a universe of possibilities, with four options before you. But be careful, only one of those options is the golden key that will unlock the treasure of the correct answer.", + "description3": "With just 30 seconds for each question, you'll feel the adrenaline rushing through your veins as you fight against the clock to find the perfect answer. Take on the standard challenge with 9 questions waiting for you in each match, or venture into the custom terrain, where you can choose your own route. Do you feel brave? Or do you prefer strategy?", + "description4": "At the end of the road, your score will be your badge of honor. Can you achieve maximum glory and become the KiWiQ champion? Only time will tell!", + "description5": "Let the game begin!" }, "statistics": { diff --git a/webapp/public/locales/es/translation.json b/webapp/public/locales/es/translation.json index a67bc786..3b5b5b90 100644 --- a/webapp/public/locales/es/translation.json +++ b/webapp/public/locales/es/translation.json @@ -53,10 +53,11 @@ } }, "rules": { - "description1": "El juego de WIQ consiste en juegos rápidos de 9 rondas. En cada ronda hay una pregunta y dos posibles respuestas. La clave para ganar puntos está en elegir la respuesta correcta.", - "description2": "Solo hay una respuesta correcta.", - "description3": "Debes seleccionar una pregunta antes de que se acabe el tiempo.", - "description4": "Para comenzar a jugar, debes hacer clic en el botón Jugar." + "description1": "¡Bienvenidos al emocionante mundo de KiWiQ! En este desafiante juego, tu objetivo es embarcarte en un viaje lleno de conocimiento y diversión.", + "description2": "Cada pregunta es una puerta hacia un universo de posibilidades, con cuatro opciones ante ti. Pero cuidado, solo una de esas opciones es la clave dorada que desbloqueará el tesoro de la respuesta correcta.", + "description3": "Con solo 30 segundos para cada pregunta, sentirás la adrenalina corriendo por tus venas mientras luchas contra el reloj para encontrar la respuesta perfecta. Enfrenta el desafío estándar con 9 preguntas esperándote en cada partida, o aventúrate en el terreno personalizado, donde puedes elegir tu propia ruta. ¿Te sientes valiente? ¿O prefieres la estrategia?", + "description4": "Al final del camino, tu puntuación será tu insignia de honor. ¿Podrás alcanzar la gloria máxima y convertirte en el campeón de KiWiQ? ¡Solo el tiempo lo dirá!", + "description5": "¡Que comience el juego!" }, "statistics": { "position": "Posición", diff --git a/webapp/src/pages/Rules.jsx b/webapp/src/pages/Rules.jsx index 7b528024..c8c33b60 100644 --- a/webapp/src/pages/Rules.jsx +++ b/webapp/src/pages/Rules.jsx @@ -23,8 +23,8 @@ export default function Rules() { setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> {t("common.rules")} - - + + {t("rules.description1")}

{t("rules.description2")} @@ -32,6 +32,8 @@ export default function Rules() { {t("rules.description3")}

{t("rules.description4")} +

+ {t("rules.description5")}
From cdfa9c9c92dafd674a91ad2e660d143d8ed6a582 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:12:52 +0200 Subject: [PATCH 66/86] fix: Adding tests to the Statistics page --- webapp/src/tests/Statistics.test.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/webapp/src/tests/Statistics.test.js b/webapp/src/tests/Statistics.test.js index a0402cdd..5f0e5134 100644 --- a/webapp/src/tests/Statistics.test.js +++ b/webapp/src/tests/Statistics.test.js @@ -105,6 +105,30 @@ describe("Statistics", () => { }); }); + test("renders initial loading state", () => { + render(); + expect(screen.getByTestId("leaderboard-spinner")).toBeVisible(); + }); + + test("displays error message when data retrieval fails", async () => { + const errorMessage = "error.unknown.typeerror.unknown.message"; + const mockAxios = new MockAdapter(authManager.getAxiosInstance()); + mockAxios.onGet().reply(HttpStatusCode.InternalServerError); + render(); + await waitFor(() => { + expect(screen.getByTestId("error-message")).toHaveTextContent(errorMessage); + }); + }); + + test("displays empty state when no data is returned", async () => { + const mockAxios = new MockAdapter(authManager.getAxiosInstance()); + mockAxios.onGet().reply(HttpStatusCode.Ok, []); + render(); + await waitFor(() => { + expect(screen.getByText("statistics.empty")).toBeVisible(); + }); + }); + describe("the petition fails", () => { each([HttpStatusCode.BadRequest, HttpStatusCode.NotFound, From e064e003dc02e5d9201cc7341676285d1b5f9f78 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:35:15 +0200 Subject: [PATCH 67/86] feat: Adding more tests to the LateralMenu --- webapp/src/tests/LateralMenu.test.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/webapp/src/tests/LateralMenu.test.js b/webapp/src/tests/LateralMenu.test.js index b5524ac1..27a9876a 100644 --- a/webapp/src/tests/LateralMenu.test.js +++ b/webapp/src/tests/LateralMenu.test.js @@ -1,10 +1,11 @@ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import { ChakraProvider } from '@chakra-ui/react'; import theme from '../styles/theme'; import AuthManager from '../components/auth/AuthManager'; import LateralMenu from '../components/LateralMenu'; +import userEvent from '@testing-library/user-event'; jest.mock('react-i18next', () => ({ useTranslation: () => { @@ -121,4 +122,13 @@ describe('LateralMenu component', () => { const aboutButton = screen.getByLabelText('About'); expect(aboutButton).toBeInTheDocument(); }); + it('changes language on select change', async () => { + const changeLanguageMock = jest.fn(); + render( {}} changeLanguage={changeLanguageMock} isDashboard={false} />); + + userEvent.selectOptions(screen.getByTestId('language-select'), 'en'); + await waitFor(() => { + expect(changeLanguageMock).toHaveBeenCalledWith('en'); + }); + }); }); From 36156bd662001da517a0dd3fa75ef0b887e4210f Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:46:29 +0200 Subject: [PATCH 68/86] feat: Adding more tests to the LateralMenu page --- webapp/src/tests/LateralMenu.test.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/webapp/src/tests/LateralMenu.test.js b/webapp/src/tests/LateralMenu.test.js index 27a9876a..519ccdaa 100644 --- a/webapp/src/tests/LateralMenu.test.js +++ b/webapp/src/tests/LateralMenu.test.js @@ -117,6 +117,14 @@ describe('LateralMenu component', () => { expect(logoutButton).toBeNull(); }); + it('renders logout button when isLoggedIn is true', async () => { + authManager.setLoggedIn(true); + const { getByText } = render(); + await waitFor(() => { + expect(getByText('common.logout')).toBeInTheDocument(); + }); + }); + it('renders about button', () => { render(); const aboutButton = screen.getByLabelText('About'); @@ -130,5 +138,5 @@ describe('LateralMenu component', () => { await waitFor(() => { expect(changeLanguageMock).toHaveBeenCalledWith('en'); }); - }); + }); }); From 9325a326090ba75e91fa6ca82df317eb84ff8f1e Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:59:12 +0200 Subject: [PATCH 69/86] feat: Adding more tests to the LateralMenu --- webapp/src/tests/LateralMenu.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webapp/src/tests/LateralMenu.test.js b/webapp/src/tests/LateralMenu.test.js index 519ccdaa..fd5781e0 100644 --- a/webapp/src/tests/LateralMenu.test.js +++ b/webapp/src/tests/LateralMenu.test.js @@ -139,4 +139,15 @@ describe('LateralMenu component', () => { expect(changeLanguageMock).toHaveBeenCalledWith('en'); }); }); + it('renders API button when isLoggedIn is true', async () => { + authManager.setLoggedIn(true); + const { getByText } = render(); + await waitFor(() => { + expect(getByText('API')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('API')); + await waitFor(() => { + expect(screen.getByText('KIWIQ')).toBeInTheDocument(); + }); + }); }); From af12e9d9b11d9760062d5e4fa1ac27035f51b2bb Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 15 Apr 2024 22:38:41 +0200 Subject: [PATCH 70/86] Revert "fix: add refresh check" This reverts commit 2d7e09951f0e1c32b99ccbe781fee95dda3e5d30. --- webapp/src/components/auth/AuthManager.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/webapp/src/components/auth/AuthManager.js b/webapp/src/components/auth/AuthManager.js index 213faadd..d88284f9 100644 --- a/webapp/src/components/auth/AuthManager.js +++ b/webapp/src/components/auth/AuthManager.js @@ -5,7 +5,6 @@ export default class AuthManager { static #instance = null; #isLoggedIn = false; #axiosInstance = null; - #hasRefreshed = false; constructor() { if (!AuthManager.#instance) { @@ -25,7 +24,7 @@ export default class AuthManager { async isLoggedIn() { if (!AuthManager.#instance.#isLoggedIn) { - if (localStorage.getItem("jwtRefreshToken") && !AuthManager.#instance.#hasRefreshed()) { + if (localStorage.getItem("jwtRefreshToken")) { await this.#refresh(); } } @@ -67,10 +66,6 @@ export default class AuthManager { } } - hasRefreshed() { - return AuthManager.#instance.#hasRefreshed; - } - #saveToken(requestAnswer) { this.getAxiosInstance().defaults.headers.common["authorization"] = "Bearer " + requestAnswer.data.token;; localStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_token); @@ -84,8 +79,6 @@ export default class AuthManager { if (response.status === HttpStatusCode.Ok) { this.#saveToken(response); AuthManager.#instance.setLoggedIn(true); - AuthManager.#instance.#hasRefreshed = true; - setTimeout(() => AuthManager.#instance.#hasRefreshed = false, 2500); } else { localStorage.removeItem("jwtRefreshToken"); } From 0596ef884d319be0f946e1c82f2ee97cdb400390 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 15 Apr 2024 23:00:36 +0200 Subject: [PATCH 71/86] fix: set the authManager instance as a ref --- webapp/src/components/utils/ProtectedRoute.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/utils/ProtectedRoute.jsx b/webapp/src/components/utils/ProtectedRoute.jsx index a7c49e06..8c2fe7c9 100644 --- a/webapp/src/components/utils/ProtectedRoute.jsx +++ b/webapp/src/components/utils/ProtectedRoute.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Outlet, useNavigate } from "react-router-dom"; import AuthManager from "../auth/AuthManager"; import { CircularProgress } from "@chakra-ui/react"; @@ -7,10 +7,11 @@ const ProtectedRoutes = () => { const navigate = useNavigate(); const [hasLoaded, setHasLoaded] = useState(false); + const authManager = useRef(new AuthManager()); useEffect(() => { async function protectRoute() { - let isLoggedIn = await new AuthManager().isLoggedIn(); + let isLoggedIn = await authManager.current.isLoggedIn(); setHasLoaded(true); if (!(isLoggedIn)) { navigate("/login"); From e91f00e859c79824195cab1ee8c5804251f213dd Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 15 Apr 2024 23:06:03 +0200 Subject: [PATCH 72/86] fix: axios await throws exception if error is detected --- webapp/src/components/auth/AuthManager.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/auth/AuthManager.js b/webapp/src/components/auth/AuthManager.js index d88284f9..b94db36e 100644 --- a/webapp/src/components/auth/AuthManager.js +++ b/webapp/src/components/auth/AuthManager.js @@ -79,11 +79,13 @@ export default class AuthManager { if (response.status === HttpStatusCode.Ok) { this.#saveToken(response); AuthManager.#instance.setLoggedIn(true); - } else { + } + } catch (error) { + if (error.response.status === HttpStatusCode.Forbidden) { localStorage.removeItem("jwtRefreshToken"); + } else { + console.error("Error refreshing token: ", error); } - } catch (error) { - console.error("Error refreshing token: ", error); } } From b1c4c13ce0d35bb6202b3edc349ff54e8d6924ca Mon Sep 17 00:00:00 2001 From: Diego Villanueva Date: Mon, 15 Apr 2024 23:58:14 +0200 Subject: [PATCH 73/86] Feat: Added image questions --- questiongenerator/src/main/java/Main.java | 14 ++++++------- .../src/main/java/model/Question.java | 1 + .../repositories/GeneralRepositoryStorer.java | 21 +++++++++++++++++++ .../main/java/templates/PaintingQuestion.java | 5 +++-- .../main/java/templates/StadiumQuestion.java | 5 +++-- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java index 8c0b5755..6d0a7b54 100644 --- a/questiongenerator/src/main/java/Main.java +++ b/questiongenerator/src/main/java/Main.java @@ -1,12 +1,15 @@ import model.AnswerCategory; import repositories.GeneralRepositoryStorer; -import templates.BallonDOrQuestion; -import templates.CountryCapitalQuestion; -import templates.VideogamesPublisherQuestion; +import templates.*; public class Main { + + + public static void main(String[] args) { + GeneralRepositoryStorer.editConstraints(); + // TEXT if(GeneralRepositoryStorer.doesntExist(AnswerCategory.CAPITAL_CITY)) { new CountryCapitalQuestion("en"); @@ -23,10 +26,8 @@ public static void main(String[] args) { } - /* // IMAGES - - if(GeneralRepositoryStorer.doesntExist(AnswerCategory.STADIUM.toString())) { + if(GeneralRepositoryStorer.doesntExist(AnswerCategory.STADIUM)) { new StadiumQuestion("en"); new StadiumQuestion("es"); } @@ -35,7 +36,6 @@ public static void main(String[] args) { new PaintingQuestion("en"); new PaintingQuestion("es"); } - */ /* diff --git a/questiongenerator/src/main/java/model/Question.java b/questiongenerator/src/main/java/model/Question.java index dc9c3ae8..eef4ed7c 100644 --- a/questiongenerator/src/main/java/model/Question.java +++ b/questiongenerator/src/main/java/model/Question.java @@ -9,6 +9,7 @@ @Entity @Table(name = "questions") public class Question implements Storable { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java b/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java index 2d130468..1ef56146 100644 --- a/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java +++ b/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java @@ -14,6 +14,8 @@ */ public class GeneralRepositoryStorer { + public static final String LINKCONCAT = "#* &%"; + public void saveAll(List storableList) { EntityManagerFactory emf = Jpa.getEntityManagerFactory(); EntityManager entityManager = emf.createEntityManager(); @@ -40,7 +42,26 @@ public static boolean doesntExist(AnswerCategory category) { Jpa.close(); return count == 0; + } + + public static void editConstraints() { + EntityManagerFactory emf = Jpa.getEntityManagerFactory(); + EntityManager entityManager = emf.createEntityManager(); + + entityManager.getTransaction().begin(); + + // Drop constraint "answers_category_check" from table "answers" if exists + entityManager.createNativeQuery("ALTER TABLE answers DROP CONSTRAINT IF EXISTS answers_category_check").executeUpdate(); + // Drop constraint "questions_question_category_check" from table "questions" if exists + entityManager.createNativeQuery("ALTER TABLE questions DROP CONSTRAINT IF EXISTS questions_question_category_check").executeUpdate(); + + entityManager.getTransaction().commit(); + + entityManager.close(); + Jpa.close(); } + + } diff --git a/questiongenerator/src/main/java/templates/PaintingQuestion.java b/questiongenerator/src/main/java/templates/PaintingQuestion.java index 5281284a..eafac578 100644 --- a/questiongenerator/src/main/java/templates/PaintingQuestion.java +++ b/questiongenerator/src/main/java/templates/PaintingQuestion.java @@ -2,6 +2,7 @@ import model.*; import org.json.JSONObject; +import repositories.GeneralRepositoryStorer; import java.util.ArrayList; import java.util.List; @@ -62,9 +63,9 @@ public void processResults() { answers.add(a); if (langCode.equals("es")) - questions.add(new Question(a, "¿Cuál es este cuadro? " + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); + questions.add(new Question(a, "¿Cuál es este cuadro?" + GeneralRepositoryStorer.LINKCONCAT + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); else - questions.add(new Question(a, "Which painting is this? " + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); + questions.add(new Question(a, "Which painting is this?" + GeneralRepositoryStorer.LINKCONCAT + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); } repository.saveAll(new ArrayList<>(answers)); diff --git a/questiongenerator/src/main/java/templates/StadiumQuestion.java b/questiongenerator/src/main/java/templates/StadiumQuestion.java index 9b7b83dd..2d473e4c 100644 --- a/questiongenerator/src/main/java/templates/StadiumQuestion.java +++ b/questiongenerator/src/main/java/templates/StadiumQuestion.java @@ -2,6 +2,7 @@ import model.*; import org.json.JSONObject; +import repositories.GeneralRepositoryStorer; import java.util.ArrayList; import java.util.List; @@ -55,9 +56,9 @@ public void processResults() { answers.add(a); if (langCode.equals("es")) - questions.add(new Question(a, "¿Cuál es este estadio? " + imageLink, QuestionCategory.SPORTS, QuestionType.IMAGE)); + questions.add(new Question(a, "¿Cuál es este estadio?" + GeneralRepositoryStorer.LINKCONCAT + imageLink, QuestionCategory.SPORTS, QuestionType.IMAGE)); else - questions.add(new Question(a, "Which stadium is this? " + imageLink, QuestionCategory.SPORTS, QuestionType.IMAGE)); + questions.add(new Question(a, "Which stadium is this?" + GeneralRepositoryStorer.LINKCONCAT + imageLink, QuestionCategory.SPORTS, QuestionType.IMAGE)); } repository.saveAll(new ArrayList<>(answers)); From cf6151e07e3f860bcdbe000f044ce922ad2ca92d Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 09:53:15 +0200 Subject: [PATCH 74/86] feat: commented custom gamemode --- api/src/main/java/lab/en2b/quizapi/game/GameService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 9b084ef0..5d33dbfa 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -175,8 +175,8 @@ public List getQuestionGameModes() { new GameModeDto("Geo Genius","Do you know the capital of Mongolia? I don't, so if you do this game is for you!",GameMode.GEO_GENIUS,"FaGlobeAmericas"), new GameModeDto("Videogame Adventure","It's dangerous to go alone, guess this!",GameMode.VIDEOGAME_ADVENTURE,"IoLogoGameControllerB"), new GameModeDto("Ancient Odyssey","Antiques are pricey for a reason!",GameMode.ANCIENT_ODYSSEY,"FaPalette"), - new GameModeDto("Random","Try a bit of everything!",GameMode.RANDOM,"FaRandom"), - new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") + new GameModeDto("Random","Try a bit of everything!",GameMode.RANDOM,"FaRandom") + //new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") ); } } From a3b59f07fe8cd032628a56c7066a010a11665ccf Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 11:32:17 +0200 Subject: [PATCH 75/86] feat: image questions are parsed --- .../question/dtos/QuestionResponseDto.java | 3 +++ .../question/mappers/QuestionResponseDtoMapper.java | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionResponseDto.java b/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionResponseDto.java index 263b36fd..2c8c00c9 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionResponseDto.java @@ -39,4 +39,7 @@ public class QuestionResponseDto { @Schema(description = "Type of the question",example = "MULTIPLE_CHOICE") private QuestionType type; + + @Schema(description = "Image for the question",example = "https://www.example.com/image.jpg") + private String image; } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/mappers/QuestionResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/questions/question/mappers/QuestionResponseDtoMapper.java index 2752f4e3..39be8d87 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/mappers/QuestionResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/mappers/QuestionResponseDtoMapper.java @@ -2,6 +2,7 @@ import lab.en2b.quizapi.questions.answer.mappers.AnswerResponseDtoMapper; import lab.en2b.quizapi.questions.question.Question; +import lab.en2b.quizapi.questions.question.QuestionType; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import org.springframework.stereotype.Service; @@ -12,6 +13,18 @@ public class QuestionResponseDtoMapper implements Function Date: Tue, 16 Apr 2024 11:32:24 +0200 Subject: [PATCH 76/86] test: image questions are parsed --- .../questions/QuestionServiceTest.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java index a46ed90d..3f30fc88 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -52,6 +52,7 @@ void setUp() { defaultQuestion = Question.builder() .id(1L) .answers(new ArrayList<>()) + .content("What is the capital of France?") .questionCategory(QuestionCategory.GEOGRAPHY) .type(QuestionType.TEXT) .build(); @@ -60,6 +61,7 @@ void setUp() { .text("Paris") .category(AnswerCategory.CAPITAL_CITY) .questions(List.of(defaultQuestion)) + .language("en") .questionsWithThisAnswer(List.of(defaultQuestion)) .build(); @@ -104,13 +106,29 @@ void testGetRandomQuestion() { assertEquals(response.getId(), defaultResponseDto.getId()); } + + @Test + void testGetRandomQuestionImageType() { + defaultQuestion.setType(QuestionType.IMAGE); + defaultQuestion.setContent("What is the capital of France?#* &%https://www.example.com/image.jpg"); + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + QuestionResponseDto response = questionService.getRandomQuestion("en"); + defaultResponseDto.setType(QuestionType.IMAGE); + defaultResponseDto.setImage("https://www.example.com/image.jpg"); + assertEquals(response, defaultResponseDto); + } + @Test void testGetRandomQuestionAnswersNotYetLoaded() { when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); defaultQuestion.setAnswers(List.of()); - QuestionResponseDto response = questionService.getRandomQuestion(""); - - assertEquals(response.getId(), defaultResponseDto.getId()); + QuestionResponseDto response = questionService.getRandomQuestion("en"); + defaultResponseDto.setAnswers(List.of(AnswerResponseDto.builder() + .id(1L) + .category(AnswerCategory.CAPITAL_CITY) + .text("Paris") + .build())); + assertEquals(response, defaultResponseDto); } @Test void testGetRandomQuestionNoQuestionsFound() { @@ -122,7 +140,7 @@ void testGetQuestionById(){ when(questionRepository.findById(any())).thenReturn(Optional.of(defaultQuestion)); QuestionResponseDto response = questionService.getQuestionById(1L); - assertEquals(response.getId(), defaultResponseDto.getId()); + assertEquals(response, defaultResponseDto); } @Test From b7afbe85137e7bce211efba95b282098d53a4733 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 11:45:50 +0200 Subject: [PATCH 77/86] fix: get by id loads questions --- .../lab/en2b/quizapi/questions/question/QuestionService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 47a8e549..e0c420e4 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -64,7 +64,9 @@ public Question findRandomQuestion(String language, List quest } public QuestionResponseDto getQuestionById(Long id) { - return questionResponseDtoMapper.apply(questionRepository.findById(id).orElseThrow()); + Question q = questionRepository.findById(id).orElseThrow(); + loadAnswers(q); + return questionResponseDtoMapper.apply(q); } From 5ccef4f88a1fcfb7e5c115b1761fd5d752680856 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 16 Apr 2024 11:55:34 +0200 Subject: [PATCH 78/86] fix: add proxy deployment on release --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index b4a30ecb..e53e48a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,6 +73,7 @@ kiwiq: image: ghcr.io/arquisoft/wiq_en2b/kiwiq:latest container_name: kiwiq + profiles: ["dev", "prod"] networks: mynetwork: links: From 0c60bb3351f326656944943bc433a86b94544df9 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 13:17:50 +0200 Subject: [PATCH 79/86] feat: get current game when /play is called --- .../lab/en2b/quizapi/game/GameController.java | 15 ++++++++++++++ .../lab/en2b/quizapi/game/GameService.java | 20 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 08af1201..b4b2f89e 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -39,6 +39,21 @@ public ResponseEntity newGame(@RequestParam(required = false) S throw new IllegalArgumentException("Custom game mode requires a body"); return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); } + @Operation(summary = "Gets the current game", description = "Requests the API to get the current game") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameters({ + @Parameter(name = "lang", description = "The language of the game", example = "en"), + @Parameter(name = "gamemode", description = "The gamemode of the game", example = "KIWI_QUEST") + }) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") + @GetMapping("/play") + public ResponseEntity getGame(Authentication authentication){ + return ResponseEntity.ok(gameService.getGame(authentication)); + } @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") @ApiResponses(value = { diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 5d33dbfa..d1c61101 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -39,7 +39,7 @@ public class GameService { @Transactional public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto newGameDto, Authentication authentication) { // Check if there is an active game for the user - Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); + Optional game = getCurrentGameForAuth(authentication); if (game.isPresent() && !wasGameMeantToBeOver(game.get())){ // If there is an active game and it should not be over, return it return gameResponseDtoMapper.apply(game.get()); @@ -179,4 +179,22 @@ public List getQuestionGameModes() { //new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") ); } + + /** + * Gets the game + * @param authentication the authentication of the user + * @return the game that is currently active + */ + public GameResponseDto getGame(Authentication authentication) { + return gameResponseDtoMapper.apply(getCurrentGameForAuth(authentication).orElseThrow()); + } + + /** + * Gets the current game for the user + * @param authentication the authentication of the user + * @return the current game + */ + private Optional getCurrentGameForAuth(Authentication authentication){ + return gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); + } } From db6dad9bf5e647033d624547fc63e855aa11e3c6 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 13:37:24 +0200 Subject: [PATCH 80/86] test: get current game when /play is called --- .../lab/en2b/quizapi/game/GameServiceTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index b9c4b9d4..ff9a948c 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -217,6 +217,22 @@ public void newGameCustomGame(){ assertEquals(defaultGameResponseDto, gameDto); } + // GET GAME + @Test + public void getGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(any())).thenReturn(Optional.of(defaultGame)); + GameResponseDto gameDto = gameService.getGame(authentication); + gameDto.setId(null); + assertEquals(defaultGameResponseDto, gameDto); + } + @Test + public void getGameNotActive(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + assertThrows(NoSuchElementException.class, () -> gameService.getGame(authentication)); + } // START ROUND TESTS @Test From 6f8cbc9bff42459b6980ccd6a5ed5b67e3920cf5 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 13:40:04 +0200 Subject: [PATCH 81/86] feat: is game active endpoint --- .../lab/en2b/quizapi/game/GameController.java | 17 +++++++++++------ .../java/lab/en2b/quizapi/game/GameService.java | 9 +++++++++ .../game/dtos/GameActiveResponseDto.java | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/game/dtos/GameActiveResponseDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index b4b2f89e..3192b857 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -43,18 +43,23 @@ public ResponseEntity newGame(@RequestParam(required = false) S @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameters({ - @Parameter(name = "lang", description = "The language of the game", example = "en"), - @Parameter(name = "gamemode", description = "The gamemode of the game", example = "KIWI_QUEST") + @ApiResponse(responseCode = "404", description = "No active game", content = @io.swagger.v3.oas.annotations.media.Content), }) - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") @GetMapping("/play") public ResponseEntity getGame(Authentication authentication){ return ResponseEntity.ok(gameService.getGame(authentication)); } + @Operation(summary = "Checks if there is an active game", description = "Requests the API to check if there exists an active game for a given authentication (a player)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @GetMapping("/is-active") + public ResponseEntity isActive(Authentication authentication){ + return ResponseEntity.ok(gameService.isActive(authentication)); + } + @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index d1c61101..d75940ab 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -197,4 +197,13 @@ public GameResponseDto getGame(Authentication authentication) { private Optional getCurrentGameForAuth(Authentication authentication){ return gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); } + + /** + * Checks if the game is active + * @param authentication the authentication of the user + * @return the response of the check + */ + public GameActiveResponseDto isActive(Authentication authentication) { + return new GameActiveResponseDto(getCurrentGameForAuth(authentication).isPresent()); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameActiveResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameActiveResponseDto.java new file mode 100644 index 00000000..e68219e8 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameActiveResponseDto.java @@ -0,0 +1,16 @@ +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Data +@NoArgsConstructor +public class GameActiveResponseDto { + @JsonProperty("is_active") + @Schema(description = "Whether the game is active or not",example = "true") + private boolean isActive; +} From 8f1434483ff8a7122d880001e07dcb584d0079c5 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 13:57:01 +0200 Subject: [PATCH 82/86] feat: get active game and is active --- .../en2b/quizapi/game/GameControllerTest.java | 36 ++++++++++++++++++- .../en2b/quizapi/game/GameServiceTest.java | 18 ++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index d570e709..92a970c9 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -203,7 +203,7 @@ void getQuestionCategoriesShouldReturn403() throws Exception{ } @Test - void getGameModeshouldReturn200() throws Exception{ + void getGameModeShouldReturn200() throws Exception{ mockMvc.perform(get("/games/gamemodes") .with(user("test").roles("user")) .contentType("application/json") @@ -219,5 +219,39 @@ void getGameModesShouldReturn403() throws Exception{ .andExpect(status().isForbidden()); } + @Test + void getGameShouldReturn200() throws Exception{ + mockMvc.perform(get("/games/play") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getGameShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/play") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void getGameIsActiveShouldReturn200() throws Exception{ + mockMvc.perform(get("/games/is-active") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getGameIsActiveShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/is-active") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index ff9a948c..39f39365 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -234,6 +234,19 @@ public void getGameNotActive(){ assertThrows(NoSuchElementException.class, () -> gameService.getGame(authentication)); } + // IS GAME ACTIVE TESTS + @Test + public void isGameActive(){ + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + assertTrue(gameService.isActive(authentication).isActive()); + } + @Test + public void isGameActiveNoActiveGame(){ + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + assertFalse(gameService.isActive(authentication).isActive()); + } + // START ROUND TESTS @Test public void startRound(){ @@ -449,4 +462,9 @@ public void testGetQuestionCategories(){ assertEquals(Arrays.asList(QuestionCategory.values()), gameService.getQuestionCategories()); } + @Test + public void testGetGameModes(){ + assertFalse(gameService.getQuestionGameModes().isEmpty()); + } + } From 99690e33f8bfad8aed9f6575b0960c7980cd6d8c Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Tue, 16 Apr 2024 16:44:42 +0200 Subject: [PATCH 83/86] chore: add count finished games to repository --- api/src/main/java/lab/en2b/quizapi/game/GameRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java b/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java index fd3094bb..6f747994 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java @@ -13,4 +13,7 @@ public interface GameRepository extends JpaRepository { @Query(value = "SELECT * FROM Games g WHERE user_id = ?1 AND g.is_game_over = false LIMIT 1", nativeQuery = true) Optional findActiveGameForUser(Long userId); + + @Query(value = "COUNT(*) FROM Games g WHERE user_id = ?1 AND g.is_game_over = true", nativeQuery = true) + Long countFinishedGamesForUser(Long userId); } From 8c4bab3d1763612da3363999f851bc15bdb4b3fc Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Tue, 16 Apr 2024 16:47:25 +0200 Subject: [PATCH 84/86] chore: added finished games --- api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java | 3 +++ .../java/lab/en2b/quizapi/statistics/StatisticsService.java | 3 +++ .../en2b/quizapi/statistics/dtos/StatisticsResponseDto.java | 2 ++ .../statistics/mappers/StatisticsResponseDtoMapper.java | 1 + 4 files changed, 9 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java b/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java index 5edba8f7..b054ac7c 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java @@ -31,6 +31,9 @@ public class Statistics { @JoinColumn(name = "user_id") private User user; + @NonNull + private Long finishedGames; + public Long getCorrectRate() { if(total == 0){ return 0L; diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java index e067afba..a4839924 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java @@ -2,6 +2,7 @@ import lab.en2b.quizapi.commons.user.User; import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.GameRepository; import lab.en2b.quizapi.statistics.dtos.StatisticsResponseDto; import lab.en2b.quizapi.statistics.mappers.StatisticsResponseDtoMapper; import lombok.RequiredArgsConstructor; @@ -21,6 +22,7 @@ public class StatisticsService { private final StatisticsRepository statisticsRepository; private final UserService userService; private final StatisticsResponseDtoMapper statisticsResponseDtoMapper; + private final GameRepository gameRepository; /** * Updates the statistics for a user. If no statistics are found for the user, they are created. @@ -37,6 +39,7 @@ public StatisticsResponseDto getStatisticsForUser(Authentication authentication) .correct(0L) .wrong(0L) .total(0L) + .finishedGames(gameRepository.countFinishedGamesForUser(user.getId())) .build())); } diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java b/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java index 203a99f4..f0503def 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java @@ -20,5 +20,7 @@ public class StatisticsResponseDto { private UserResponseDto user; @JsonProperty("correct_rate") private Long correctRate; + @JsonProperty("finished_games") + private Long finishedGames; } diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java index c4b301f0..0d6652eb 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java @@ -23,6 +23,7 @@ public StatisticsResponseDto apply(Statistics statistics) { .total(statistics.getTotal()) .user(userResponseDtoMapper.apply(statistics.getUser())) .correctRate(statistics.getCorrectRate()) + .finishedGames(statistics.getFinishedGames()) .build(); } } From c60fc3f44fb5ddaad8d72d2b2564b929c1abf3b6 Mon Sep 17 00:00:00 2001 From: sergioqfeg1 Date: Tue, 16 Apr 2024 16:52:16 +0200 Subject: [PATCH 85/86] fix: previous tests fixed --- .../lab/en2b/quizapi/game/GameService.java | 1 + .../en2b/quizapi/game/GameServiceTest.java | 1 + .../statistics/StatisticsServiceTest.java | 38 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 5d33dbfa..8534c68c 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -117,6 +117,7 @@ private void saveStatistics(Game game){ .correct(game.getCorrectlyAnsweredQuestions()) .wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()) .total(game.getRounds()) + .finishedGames(gameRepository.countFinishedGamesForUser(game.getUser().getId())) .build(); } statisticsRepository.save(statistics); diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index b9c4b9d4..6938c914 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -192,6 +192,7 @@ public void newGameWasMeantToBeOverExistingLeaderboard(){ .correct(0L) .wrong(0L) .total(0L) + .finishedGames(1L) .build())); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); defaultGame.setActualRound(10L); diff --git a/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java b/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java index 57df7d93..60349876 100644 --- a/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java @@ -4,6 +4,7 @@ import lab.en2b.quizapi.commons.user.User; import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.GameRepository; import lab.en2b.quizapi.statistics.dtos.StatisticsResponseDto; import lab.en2b.quizapi.statistics.mappers.StatisticsResponseDtoMapper; import org.junit.jupiter.api.Assertions; @@ -42,6 +43,9 @@ public class StatisticsServiceTest { @Mock private StatisticsResponseDtoMapper statisticsResponseDtoMapper; + @Mock + private GameRepository gameRepository; + private User defaultUser; private Statistics defaultStatistics1; @@ -56,7 +60,7 @@ public class StatisticsServiceTest { @BeforeEach public void setUp(){ - this.statisticsService = new StatisticsService(statisticsRepository, userService, statisticsResponseDtoMapper); + this.statisticsService = new StatisticsService(statisticsRepository, userService, statisticsResponseDtoMapper, gameRepository); this.defaultUser = User.builder() .id(1L) .email("test@email.com") @@ -79,6 +83,7 @@ public void setUp(){ .correct(5L) .wrong(5L) .total(10L) + .finishedGames(1L) .build(); this.defaultStatisticsResponseDto1 = StatisticsResponseDto.builder() @@ -88,6 +93,7 @@ public void setUp(){ .total(10L) .correctRate(50L) .user(defaultUserResponseDto) + .finishedGames(1L) .build(); this.defaultStatistics2 = Statistics.builder() @@ -96,6 +102,7 @@ public void setUp(){ .correct(7L) .wrong(3L) .total(10L) + .finishedGames(1L) .build(); this.defaultStatisticsResponseDto2 = StatisticsResponseDto.builder() @@ -105,6 +112,7 @@ public void setUp(){ .total(10L) .correctRate(70L) .user(defaultUserResponseDto) + .finishedGames(1L) .build(); } @@ -135,6 +143,7 @@ public void getTopTenStatisticsTestWhenThereAreNotTenAndAreEqual(){ .correct(5L) .wrong(5L) .total(10L) + .finishedGames(1L) .build(); StatisticsResponseDto defaultStatisticsResponseDto3 = StatisticsResponseDto.builder() .id(2L) @@ -143,6 +152,7 @@ public void getTopTenStatisticsTestWhenThereAreNotTenAndAreEqual(){ .total(10L) .correctRate(50L) .user(defaultUserResponseDto) + .finishedGames(1L) .build(); when(statisticsRepository.findAll()).thenReturn(List.of(defaultStatistics1, defaultStatistics3)); when(statisticsResponseDtoMapper.apply(defaultStatistics1)).thenReturn(defaultStatisticsResponseDto1); @@ -159,6 +169,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .correct(1L) .wrong(9L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics4 = Statistics.builder() .id(4L) @@ -166,6 +177,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .correct(2L) .wrong(8L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics5 = Statistics.builder() .id(5L) @@ -173,6 +185,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .correct(3L) .wrong(7L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics6 = Statistics.builder() .id(6L) @@ -180,6 +193,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .correct(4L) .wrong(6L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics7 = Statistics.builder() .id(7L) @@ -187,6 +201,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .correct(6L) .wrong(4L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics8 = Statistics.builder() .id(8L) @@ -194,6 +209,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .correct(8L) .wrong(2L) .total(10L) + .finishedGames(1L) .build(); List statistics = List.of(defaultStatistics8, defaultStatistics2, defaultStatistics7, defaultStatistics1, defaultStatistics6, defaultStatistics5, defaultStatistics4, defaultStatistics3); @@ -206,6 +222,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .wrong(9L) .total(10L) .correctRate(10L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics4)).thenReturn(StatisticsResponseDto.builder() @@ -214,6 +231,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .wrong(8L) .total(10L) .correctRate(20L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics5)).thenReturn(StatisticsResponseDto.builder() @@ -222,6 +240,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .wrong(7L) .total(10L) .correctRate(30L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics6)).thenReturn(StatisticsResponseDto.builder() @@ -230,6 +249,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .wrong(6L) .total(10L) .correctRate(40L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics7)).thenReturn(StatisticsResponseDto.builder() @@ -238,6 +258,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .wrong(4L) .total(10L) .correctRate(60L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics8)).thenReturn(StatisticsResponseDto.builder() @@ -246,6 +267,7 @@ public void getTopTenStatisticsWhenThereAreTen(){ .wrong(2L) .total(10L) .correctRate(80L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); List result = statistics.stream().map(statisticsResponseDtoMapper::apply).toList(); @@ -260,6 +282,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(1L) .wrong(9L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics4 = Statistics.builder() .id(4L) @@ -267,6 +290,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(2L) .wrong(8L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics5 = Statistics.builder() .id(5L) @@ -274,6 +298,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(3L) .wrong(7L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics6 = Statistics.builder() .id(6L) @@ -281,6 +306,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(4L) .wrong(6L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics7 = Statistics.builder() .id(7L) @@ -288,6 +314,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(6L) .wrong(4L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics8 = Statistics.builder() .id(8L) @@ -295,6 +322,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(8L) .wrong(2L) .total(10L) + .finishedGames(1L) .build(); Statistics defaultStatistics9 = Statistics.builder() .id(9L) @@ -302,6 +330,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .correct(9L) .wrong(1L) .total(10L) + .finishedGames(1L) .build(); List statistics = List.of(defaultStatistics9, defaultStatistics8, defaultStatistics2, defaultStatistics7, defaultStatistics1, defaultStatistics6, defaultStatistics5, defaultStatistics4, @@ -315,6 +344,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(9L) .total(10L) .correctRate(10L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics4)).thenReturn(StatisticsResponseDto.builder() @@ -323,6 +353,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(8L) .total(10L) .correctRate(20L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics5)).thenReturn(StatisticsResponseDto.builder() @@ -331,6 +362,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(7L) .total(10L) .correctRate(30L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics6)).thenReturn(StatisticsResponseDto.builder() @@ -339,6 +371,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(6L) .total(10L) .correctRate(40L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics7)).thenReturn(StatisticsResponseDto.builder() @@ -347,6 +380,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(4L) .total(10L) .correctRate(60L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics8)).thenReturn(StatisticsResponseDto.builder() @@ -355,6 +389,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(2L) .total(10L) .correctRate(80L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); when(statisticsResponseDtoMapper.apply(defaultStatistics9)).thenReturn(StatisticsResponseDto.builder() @@ -363,6 +398,7 @@ public void getTopTenWhenThereAreMoreThanTen(){ .wrong(1L) .total(10L) .correctRate(90L) + .finishedGames(1L) .user(defaultUserResponseDto) .build()); List result = statistics.stream().limit(10). From 745c50a78903efa01f3169fd4c72280fafbd7551 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 16 Apr 2024 19:00:13 +0200 Subject: [PATCH 86/86] test: getCurrentQuestion no roundstarttime --- .../test/java/lab/en2b/quizapi/game/GameServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 39f39365..e5d2d045 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -292,6 +292,14 @@ public void getCurrentQuestion() { assertEquals(defaultQuestionResponseDto, questionDto); } + @Test + public void getCurrentQuestionRoundTimeNull() { + defaultGame.setRoundStartTime(null); + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); + } + @Test public void getCurrentQuestionRoundNotStarted() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame));
{t("statistics.position")}{t("statistics.username")}{t("statistics.rightAnswers")}{t("statistics.wrongAnswers")}{t("statistics.totalAnswers")}{t("statistics.percentage")}
{t("statistics.position")}{t("statistics.username")}{t("statistics.rightAnswers")}{t("statistics.wrongAnswers")}{t("statistics.totalAnswers")}{t("statistics.percentage")}