diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 803171cc..a518bfcc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,5 +104,5 @@ jobs: echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env echo "API_URI=http://${{ secrets.DEPLOY_HOST }}:8080" >> .env - docker compose down + docker compose --profile prod down docker compose --profile prod up -d diff --git a/README.md b/README.md index 8d4ff531..a2cf1929 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ We aim to create a platform that not only challenges your knowledge but also spa 🌐 Multiplayer: Compete with friends and strangers to prove you are the best. ## Contributors -| Nombre | UO | -|---------------------------------|----------| -| Gonzalo Alonso Fernández | UO282104 | -| Darío Gutiérrez Mori | UO282435 | -| Sergio Rodríguez García | UO282598 | -| Jorge Joaquín Gancedo Fernández | UO282161 | -| Sergio Quintana Fernández | UO288090 | -| Diego Villanueva Berros | UO283615 | -| Gonzalo Suárez Losada | UO283928 | +Contributor | Contact +-- | -- +Gonzalo Alonso Fernández | +Sergio Rodríguez García | +Jorge Joaquín Gancedo Fernández | +Darío Gutiérrez Mori | +Sergio Quintana Fernández | +Diego Villanueva Berros | +Gonzalo Suárez Losada | *** diff --git a/api/src/main/java/lab/en2b/quizapi/commons/utils/InsertDataUtils.java b/api/src/main/java/lab/en2b/quizapi/commons/utils/InsertDataUtils.java index 0fa9db7a..94bab5fb 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/utils/InsertDataUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/utils/InsertDataUtils.java @@ -16,7 +16,7 @@ import java.util.List; @RequiredArgsConstructor -@Service +//@Service public class InsertDataUtils { private final QuestionRepository questionRepository; diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/Question.java b/api/src/main/java/lab/en2b/quizapi/questions/question/Question.java index ef9cae58..09484751 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/Question.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/Question.java @@ -35,7 +35,9 @@ public class Question { @NotNull @JoinColumn(name = "correct_answer_id") private Answer correctAnswer; + @Column(name = "question_category") private QuestionCategory questionCategory; + @Column(name = "answer_category") private AnswerCategory answerCategory; private String language; private QuestionType type; diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java index c5a5654b..c5cd4a80 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java @@ -9,28 +9,20 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/questions") @RequiredArgsConstructor public class QuestionController { private final QuestionService questionService; - // TODO: REMOVE WHEN NOT USED FOR TESTING - @GetMapping - private ResponseEntity> getQuestions() { - return ResponseEntity.ok(questionService.getQuestions()); - } - @PostMapping("/{questionId}/answer") private ResponseEntity answerQuestion(@PathVariable @PositiveOrZero Long questionId, @Valid @RequestBody AnswerDto answerDto){ return ResponseEntity.ok(questionService.answerQuestion(questionId,answerDto)); } @GetMapping("/new") - private ResponseEntity generateQuestion(){ - return ResponseEntity.ok(questionService.getRandomQuestion()); + private ResponseEntity generateQuestion(@RequestParam(required = false) String lang){ + return ResponseEntity.ok(questionService.getRandomQuestion(lang)); } @GetMapping("/{id}") 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 7a141ef3..b07b1f8b 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 @@ -4,6 +4,6 @@ import org.springframework.data.jpa.repository.Query; public interface QuestionRepository extends JpaRepository { - @Query(value = "SELECT * FROM questions ORDER BY RANDOM() LIMIT 1", nativeQuery = true) - Question findRandomQuestion(); + @Query(value = "SELECT q FROM questions WHERE q.language=?1 ORDER BY RANDOM() LIMIT 1", nativeQuery = true) + Question findRandomQuestion(String lang); } 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 de80faad..5126e7d1 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 @@ -20,10 +20,6 @@ public class QuestionService { private final QuestionRepository questionRepository; private final QuestionResponseDtoMapper questionResponseDtoMapper; - public List getQuestions() { - return questionRepository.findAll().stream().map(questionResponseDtoMapper).toList(); - } - public AnswerCheckResponseDto answerQuestion(Long id, AnswerDto answerDto) { Question question = questionRepository.findById(id).orElseThrow(); if(question.getCorrectAnswer().getId().equals(answerDto.getAnswerId())){ @@ -37,8 +33,11 @@ else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto } } - public QuestionResponseDto getRandomQuestion() { - return questionResponseDtoMapper.apply(questionRepository.findRandomQuestion()); + public QuestionResponseDto getRandomQuestion(String lang) { + if (lang==null || lang.isBlank()) { + lang = "en"; + } + return questionResponseDtoMapper.apply(questionRepository.findRandomQuestion(lang)); } public QuestionResponseDto getQuestionById(Long id) { diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java index eb9d154a..4bf7be42 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java @@ -33,16 +33,18 @@ public class QuestionControllerTest { QuestionService questionService; @MockBean UserService userService; + @Test - void getQuestionNoAuthShouldReturn403() throws Exception { - mockMvc.perform(get("/questions") + void newQuestionShouldReturn403() throws Exception{ + mockMvc.perform(get("/questions/new?lang=en") .contentType("application/json") .with(csrf())) .andExpect(status().isForbidden()); } + @Test - void getQuestionShouldReturn200() throws Exception { - mockMvc.perform(get("/questions") + void newQuestionShouldReturn200() throws Exception{ + mockMvc.perform(get("/questions/new?lang=en") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) @@ -50,15 +52,7 @@ void getQuestionShouldReturn200() throws Exception { } @Test - void newQuestionShouldReturn403() throws Exception{ - mockMvc.perform(get("/questions/new") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void newQuestionShouldReturn200() throws Exception{ + void newQuestionNoLangShouldReturn200() throws Exception{ mockMvc.perform(get("/questions/new") .with(user("test").roles("user")) .contentType("application/json") 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 0e61ae36..ccdc3b65 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -98,8 +98,8 @@ void setUp() { @Test void testGetRandomQuestion() { - when(questionRepository.findRandomQuestion()).thenReturn(defaultQuestion); - QuestionResponseDto response = questionService.getRandomQuestion(); + when(questionRepository.findRandomQuestion("en")).thenReturn(defaultQuestion); + QuestionResponseDto response = questionService.getRandomQuestion(""); assertEquals(response, defaultResponseDto); } diff --git a/docs/images/wireframe-Dashboard.png b/docs/images/wireframe-Dashboard.png new file mode 100644 index 00000000..31a35628 Binary files /dev/null and b/docs/images/wireframe-Dashboard.png differ diff --git a/docs/images/wireframe-Game.png b/docs/images/wireframe-Game.png new file mode 100644 index 00000000..7521c7d4 Binary files /dev/null and b/docs/images/wireframe-Game.png differ diff --git a/docs/images/wireframe-Results.png b/docs/images/wireframe-Results.png new file mode 100644 index 00000000..812ca5a7 Binary files /dev/null and b/docs/images/wireframe-Results.png differ diff --git a/docs/images/wireframe-Rules.png b/docs/images/wireframe-Rules.png new file mode 100644 index 00000000..47dff8ce Binary files /dev/null and b/docs/images/wireframe-Rules.png differ diff --git a/docs/images/wireframe-SignIn.png b/docs/images/wireframe-SignIn.png new file mode 100644 index 00000000..f8d21ca8 Binary files /dev/null and b/docs/images/wireframe-SignIn.png differ diff --git a/docs/images/wireframe-SignUp.png b/docs/images/wireframe-SignUp.png new file mode 100644 index 00000000..ed8fa601 Binary files /dev/null and b/docs/images/wireframe-SignUp.png differ diff --git a/docs/images/wireframe-Welcome.png b/docs/images/wireframe-Welcome.png new file mode 100644 index 00000000..035f0b91 Binary files /dev/null and b/docs/images/wireframe-Welcome.png differ diff --git a/docs/src/02_architecture_constraints.adoc b/docs/src/02_architecture_constraints.adoc index 6d48d584..d530271d 100644 --- a/docs/src/02_architecture_constraints.adoc +++ b/docs/src/02_architecture_constraints.adoc @@ -28,3 +28,18 @@ The application must be developed according to some constraints that were define | Documentation in Arc42 | The documentation must follow the Arc42 template |=== +*Wireframes* + +image::wireframe-Welcome.png[align="center", title="Welcome Wireframe"] + +image::wireframe-SignIn.png[align="center", title="Sign In Wireframe"] + +image::wireframe-SignUp.png[align="center", title="Sign Up Wireframe"] + +image::wireframe-Dashboard.png[align="center", title="Dashboard Wireframe"] + +image::wireframe-Rules.png[align="center", title="Rules Wireframe"] + +image::wireframe-Game.png[align="center", title="Game Wireframe"] + +image::wireframe-Results.png[align="center", title="Results Wireframe"] \ No newline at end of file diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index b14a2f16..bb47d5c3 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -10,29 +10,44 @@ This is the first version of the diagram, it will be updated if needed. ---- @startuml -enum Category { +enum QuestionCategory { HISTORY GEOGRAPHY SCIENCE + MATH + LITERATURE + ART + SPORTS } -enum Type { +enum AnsswerCategory { + CITY + COUNTRY + PERSON + DATE + OTHER +} + +enum QuestionType{ TEXT IMAGE AUDIO } class Question{ - id: long content: String answers: List - correct: Answer - category: Category + correctAnswer: Answer + questionCategory: QuestionCategory + answerCategory: AnswerCategory language: String - Type: Type + QuestionType: Type } class User{ + username: String + email: String + password: String answeredQuestions: int } @@ -41,8 +56,9 @@ class UserStat{ class Answer { text: String - category: Category - Type: Type + category: AnswerCategory + questionsWithThisAnswer: List + } class Game { @@ -52,7 +68,7 @@ class Game { class Ranking << Singleton >> { - + } User o--> Question diff --git a/docs/src/12_glossary.adoc b/docs/src/12_glossary.adoc index 6bf7565d..532eb618 100644 --- a/docs/src/12_glossary.adoc +++ b/docs/src/12_glossary.adoc @@ -7,13 +7,6 @@ ifndef::imagesdir[:imagesdir: ../images] [cols="e,2e" options="header"] |=== |Term |Definition - -|React -|An open-source JavaScript library for developing user interfaces. It can be used to develop web applications, and it has to be complemented with other libraries to develop full-fledged products. - -|SpringBoot -|Java Spring Boot (Spring Boot) is a tool that makes developing web application and microservices with Spring Framework faster and easier through three core capabilities: Autoconfiguration, an opinionated approach to configuration, the ability to create standalone applications. - -|PostgreSQL -|Object-relational database management system (ORDMBS), which means that it has relational capabilities and an object-oriented design +|Question Generator +|A module of the application responsible for querying Wikidata, creating the questions and storing them in our DB. |=== diff --git a/questiongenerator/.gitignore b/questiongenerator/.gitignore new file mode 100644 index 00000000..5ff6309b --- /dev/null +++ b/questiongenerator/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/questiongenerator/mvnw b/questiongenerator/mvnw new file mode 100644 index 00000000..8a8fb228 --- /dev/null +++ b/questiongenerator/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/questiongenerator/mvnw.cmd b/questiongenerator/mvnw.cmd new file mode 100644 index 00000000..1d8ab018 --- /dev/null +++ b/questiongenerator/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/questiongenerator/pom.xml b/questiongenerator/pom.xml new file mode 100644 index 00000000..e1a94aa6 --- /dev/null +++ b/questiongenerator/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.example + QuestionGenerator + 1.0-SNAPSHOT + QuestionGenerator + + + UTF-8 + 1.8 + 1.8 + 5.9.1 + + + + + org.hibernate + hibernate-core + 5.6.1.Final + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + org.json + json + 20240303 + + + org.postgresql + postgresql + runtime + 42.6.1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + \ No newline at end of file diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java new file mode 100644 index 00000000..0c29a503 --- /dev/null +++ b/questiongenerator/src/main/java/Main.java @@ -0,0 +1,8 @@ +import templates.CountryCapitalQuestion; + +public class Main { + public static void main(String[] args) { + new CountryCapitalQuestion("es"); + new CountryCapitalQuestion("en"); + } +} \ No newline at end of file diff --git a/questiongenerator/src/main/java/model/Answer.java b/questiongenerator/src/main/java/model/Answer.java new file mode 100644 index 00000000..b59aff14 --- /dev/null +++ b/questiongenerator/src/main/java/model/Answer.java @@ -0,0 +1,29 @@ +package model; + +import repositories.Storable; + +import javax.persistence.*; +import java.util.List; + +@Entity +@Table(name = "answers") +public class Answer implements Storable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String text; + private AnswerCategory category; + @OneToMany(mappedBy = "correctAnswer", fetch = FetchType.EAGER) + private List questions; + + @ManyToMany(mappedBy = "answers", fetch = FetchType.EAGER) + private List questionsWithThisAnswer; + + public Answer(String text, AnswerCategory category) { + this.text = text; + this.category = category; + } + + public Answer() { + } +} diff --git a/questiongenerator/src/main/java/model/AnswerCategory.java b/questiongenerator/src/main/java/model/AnswerCategory.java new file mode 100644 index 00000000..9296e711 --- /dev/null +++ b/questiongenerator/src/main/java/model/AnswerCategory.java @@ -0,0 +1,5 @@ +package model; + +public enum AnswerCategory { + CITY, COUNTRY, PERSON, EVENT, DATE, OTHER +} diff --git a/questiongenerator/src/main/java/model/Question.java b/questiongenerator/src/main/java/model/Question.java new file mode 100644 index 00000000..a627b4f2 --- /dev/null +++ b/questiongenerator/src/main/java/model/Question.java @@ -0,0 +1,51 @@ +package model; + +import repositories.Storable; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "questions") +public class Question implements Storable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String content; + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name="questions_answers", + joinColumns= + @JoinColumn(name="question_id", referencedColumnName="id"), + inverseJoinColumns= + @JoinColumn(name="answer_id", referencedColumnName="id") + ) + private List answers; + @ManyToOne + @JoinColumn(name = "correct_answer_id") + private Answer correctAnswer; + @Column(name = "question_category") + private QuestionCategory questionCategory; + @Column(name = "answer_category") + private AnswerCategory answerCategory; + private String language; + private QuestionType type; + + public Question() { + } + + public Question(String content, Answer correctAnswer, QuestionCategory questionCategory, AnswerCategory answerCategory, String language, QuestionType type) { + this.content = content; + this.correctAnswer = correctAnswer; + this.questionCategory = questionCategory; + this.answerCategory = answerCategory; + this.language = language; + this.type = type; + this.answers = new ArrayList<>(); + this.answers.add(correctAnswer); + } + + public List getAnswers() { + return answers; + } +} diff --git a/questiongenerator/src/main/java/model/QuestionCategory.java b/questiongenerator/src/main/java/model/QuestionCategory.java new file mode 100644 index 00000000..90719c44 --- /dev/null +++ b/questiongenerator/src/main/java/model/QuestionCategory.java @@ -0,0 +1,5 @@ +package model; + +public enum QuestionCategory { + HISTORY, GEOGRAPHY, SCIENCE, MATH, LITERATURE, ART, SPORTS, MUSIC, MOVIES, TV, POLITICS, OTHER +} diff --git a/questiongenerator/src/main/java/model/QuestionType.java b/questiongenerator/src/main/java/model/QuestionType.java new file mode 100644 index 00000000..2aefb0fd --- /dev/null +++ b/questiongenerator/src/main/java/model/QuestionType.java @@ -0,0 +1,5 @@ +package model; + +public enum QuestionType { + TEXT, VIDEO, IMAGE, AUDIO +} diff --git a/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java b/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java new file mode 100644 index 00000000..995ffc2e --- /dev/null +++ b/questiongenerator/src/main/java/repositories/GeneralRepositoryStorer.java @@ -0,0 +1,42 @@ +package repositories; + +import model.Answer; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import java.util.List; + +/** + * Class for storing entries in the Question and Answer DB. + * They implement the Storable interface. + */ +public class GeneralRepositoryStorer { + public void save(Storable s){ + EntityManagerFactory emf = Jpa.getEntityManagerFactory(); + + EntityManager entityManager = emf.createEntityManager(); + entityManager.getTransaction().begin(); + + entityManager.persist(s); + + entityManager.getTransaction().commit(); + entityManager.close(); + + Jpa.close(); + } + public void saveAll(List storableList) { + EntityManagerFactory emf = Jpa.getEntityManagerFactory(); + + EntityManager entityManager = emf.createEntityManager(); + + for (Storable s : storableList) { + entityManager.getTransaction().begin(); + entityManager.persist(s); + entityManager.getTransaction().commit(); + } + + entityManager.close(); + + Jpa.close(); + } +} diff --git a/questiongenerator/src/main/java/repositories/Jpa.java b/questiongenerator/src/main/java/repositories/Jpa.java new file mode 100644 index 00000000..755d1961 --- /dev/null +++ b/questiongenerator/src/main/java/repositories/Jpa.java @@ -0,0 +1,47 @@ +package repositories; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import java.util.HashMap; +import java.util.Map; + +/** + * Class in charge of setting the connection to the DB + * It is an implementation of Singleton DP until the connection gets shut down though close() call. + * As it is not possible to reopen connections, a new one must be created if access to DB is desired. + */ +public class Jpa { + + // Singleton + private static EntityManagerFactory emf = null; + + /** + * Sets the enviroment variables for the DB connection. + */ + public static EntityManagerFactory getEntityManagerFactory() { + if (emf == null) { + Map properties = new HashMap<>(); + + properties.put("javax.persistence.jdbc.driver", "org.postgresql.Driver"); + properties.put("javax.persistence.jdbc.url", System.getenv("DATABASE_URL")); + properties.put("javax.persistence.jdbc.user", System.getenv("DATABASE_USER")); + properties.put("javax.persistence.jdbc.password", System.getenv("DATABASE_PASSWORD")); + + properties.put("hibernate.hbm2ddl.auto", "update"); + + emf = Persistence.createEntityManagerFactory("default", properties); + } + + return emf; + } + + /** + * Closes the current connection in a null-preventive way. + */ + public static void close() { + if (emf != null) { + emf.close(); + } + emf = null; + } +} diff --git a/questiongenerator/src/main/java/repositories/Storable.java b/questiongenerator/src/main/java/repositories/Storable.java new file mode 100644 index 00000000..463d4a07 --- /dev/null +++ b/questiongenerator/src/main/java/repositories/Storable.java @@ -0,0 +1,7 @@ +package repositories; + +/** + * Interface to be implemented by Questions and Answers so they can be stored in db. + */ +public interface Storable { +} diff --git a/questiongenerator/src/main/java/templates/CountryCapitalQuestion.java b/questiongenerator/src/main/java/templates/CountryCapitalQuestion.java new file mode 100644 index 00000000..0ea0b97b --- /dev/null +++ b/questiongenerator/src/main/java/templates/CountryCapitalQuestion.java @@ -0,0 +1,87 @@ +package templates; + +import model.QuestionCategory; +import model.QuestionType; +import model.Answer; +import model.AnswerCategory; +import model.Question; +import org.json.JSONObject; +import repositories.Storable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation for a question where the capital of a country is asked and all the capitals are returned. + */ +public class CountryCapitalQuestion extends QuestionTemplate { + + /** + * Only need to invoke the constructor, and it will automatically do the HTTP request and the response recovery. + * @param langCode representation for the language to be used for the question. ("en" for English, "es" for Spanish) + */ + public CountryCapitalQuestion(String langCode) { + super(langCode); + } + + /** + * Query to be sent to WikiData QS. + * It retrieves the name of the capital of every country in the language specified in @langCode + */ + @Override + protected void setQuery() { + this.sparqlQuery = + "SELECT ?country ?countryLabel ?capital ?capitalLabel " + + "WHERE { ?country wdt:P31 wd:Q3624078; wdt:P36 ?capital. SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } } " + + "ORDER BY ?countryLabel"; + } + + /** + * Method where the results are processed. It also is in charge of saving both, the question and every possible capital. + * Question: what is the capital of X country? + */ + @Override + protected void processResults() { + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + String countryLabel = result.getJSONObject("countryLabel").getString("value"); + String capitalLabel = result.getJSONObject("capitalLabel").getString("value"); + + // Ignoring answers that may compromise the game flow (Country does not have a name i.e.) + if (needToSkip(countryLabel)) + continue; + + //Saving the answer + Answer a = new Answer(capitalLabel, AnswerCategory.CITY); + answers.add(a); + + //Saving the question + String content; + if (langCode.equals("en")) + content = "What is the capital of " + countryLabel + "?"; + else + content = "¿Cuál es la capital de " + countryLabel + "?"; + questions.add(new Question(content, a, QuestionCategory.GEOGRAPHY, AnswerCategory.CITY, langCode, QuestionType.TEXT)); + } + addRandomAnswers(answers, questions); + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private void addRandomAnswers(List answers, List questions) { + for(Question q : questions) { + q.getAnswers().add(answers.get((int) (Math.random() * (answers.size()-1)))); + } + } + + /** + * Auxiliar method for @processResults. It returns whether a country must be skipped as an entry in DB or not + * + */ + private boolean needToSkip(String countryLabel) { + return countryLabel.equals("Q3932086") || countryLabel.equals("realm of the United Kingdom") || countryLabel.equals("Republic of Geneva") + || countryLabel.equals("República de Ginebra") || countryLabel.equals("Q124653007"); + } +} diff --git a/questiongenerator/src/main/java/templates/QuestionTemplate.java b/questiongenerator/src/main/java/templates/QuestionTemplate.java new file mode 100644 index 00000000..a0664c6e --- /dev/null +++ b/questiongenerator/src/main/java/templates/QuestionTemplate.java @@ -0,0 +1,78 @@ +package templates; + +import repositories.GeneralRepositoryStorer; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +/** + * Base template for questions to be implemented in the future. + */ +public abstract class QuestionTemplate { + // Query to be sent to WikiData QS + protected String sparqlQuery; + // Response given by WikiData QS for the query sent + protected JSONArray results; + // Language code representing in what language the query must be sent. Spanish as a default value. + protected String langCode = "es"; + + protected GeneralRepositoryStorer repository = new GeneralRepositoryStorer(); + + /** + * Constructor for QuestionTemplates which is also the one in charge of the whole question retrieval process for a query + * For future types of question, only need to override abstract methods and calling super() on constructor + * When instancing a question, only constructor invocation is required. + * For reference in future implementations: look at CountryCapitalQuestion + */ + public QuestionTemplate(String langCode) { + this.langCode = langCode; + setQuery(); + call(); + processResults(); + } + + /** + * Update the value of @sparqlQuery with the query to be sent. + */ + protected abstract void setQuery(); + + /** + * Method for the whole processing the @results given by WikiData QS as a JSON. + * It also is in charge of storing both the processed answers and the question in all languages + */ + protected abstract void processResults(); + + /** + * Method in charge of the HTTP request with WikiData QS. + * It allows to send only one query, so it does not support questions whose answer require multiple queries. + * CAUTION: Remember to update the results field of the field if this method gets overwritten. + */ + private void call() { + // Set up the HTTP client + HttpClient httpClient = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://query.wikidata.org/sparql")) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Accept", "application/json") // Specify JSON format + .POST(HttpRequest.BodyPublishers.ofString("query=" + sparqlQuery)) + .build(); + + // Send the HTTP request and get the response + HttpResponse response = null; + try { + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + + JSONObject jsonResponse = new JSONObject(response.body()); + JSONArray results = jsonResponse.getJSONObject("results").getJSONArray("bindings"); + + this.results = results; // Save the results. If this method is overwritten this line MUST be kept + } + +} diff --git a/questiongenerator/src/main/resources/META-INF/persistence.xml b/questiongenerator/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000..c485a5c8 --- /dev/null +++ b/questiongenerator/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index db884db7..52cc1edc 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -30,6 +30,7 @@ "login": "You should enter a valid username and password", "register": "You should enter a valid username, email and password", "login-desc": "Please, enter you username and password to log in", - "register-desc": "Please, enter you username and password to register" + "register-desc": "Please, enter you username and password to register", + "login-send": "Your email or password are not found in our database" } } \ No newline at end of file diff --git a/webapp/public/locales/es/translation.json b/webapp/public/locales/es/translation.json index 91f4c2fe..ab65fbb1 100644 --- a/webapp/public/locales/es/translation.json +++ b/webapp/public/locales/es/translation.json @@ -30,6 +30,7 @@ "login": "Nombre de usuario o contraseña incorrectos", "register": "Debes introducir datos válidos para registrarte", "login-desc": "Por favor, introduce tus datos para iniciar sesión", - "register-desc": "Por favor, introduce tus datos para registrarte" + "register-desc": "Por favor, introduce tus datos para registrarte", + "login-send": "Tu email o contraseña no se encuentran en nuestra base de datos" } } diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index e056b0d4..9da36bef 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -7,7 +7,7 @@ export function isUserLogged() { export function saveToken(requestAnswer) { axios.defaults.headers.common["Authorization"] = "Bearer " + requestAnswer.data.token; sessionStorage.setItem("jwtToken", requestAnswer.data.token); - sessionStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_Token); + sessionStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_token); sessionStorage.setItem("jwtReceptionMillis", Date.now().toString()); } diff --git a/webapp/src/components/game/Logout.js b/webapp/src/components/game/Logout.js new file mode 100644 index 00000000..e1044e57 --- /dev/null +++ b/webapp/src/components/game/Logout.js @@ -0,0 +1,11 @@ +import axios from "axios"; + +export async function logoutUser() { + try { + await axios.get(process.env.REACT_APP_API_ENDPOINT + "/auth/logout"); + sessionStorage.removeItem("jwtToken"); + sessionStorage.removeItem("jwtRefreshToken"); + } catch (error) { + console.error("Error logging out user: ", error); + } +} diff --git a/webapp/src/components/game/Questions.js b/webapp/src/components/game/Questions.js index fd18af57..32b6c239 100644 --- a/webapp/src/components/game/Questions.js +++ b/webapp/src/components/game/Questions.js @@ -8,5 +8,16 @@ export async function getQuestion() { } } catch { + } +} + +export async function answerQuestion(questionId, aId) { + try { + let requestAnswer = await axios.post(process.env.REACT_APP_API_ENDPOINT + "/questions/" + questionId + "/answer", {answer_id:aId}); + if (HttpStatusCode.Ok === requestAnswer.status) { + return requestAnswer.data; + } + } catch { + } } \ No newline at end of file diff --git a/webapp/src/pages/Dashboard.jsx b/webapp/src/pages/Dashboard.jsx index b1ac97ec..600f23d7 100644 --- a/webapp/src/pages/Dashboard.jsx +++ b/webapp/src/pages/Dashboard.jsx @@ -3,12 +3,22 @@ import { Grid, Flex, Heading, Button, Box } from "@chakra-ui/react"; import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; +import { logoutUser } from "../components/game/Logout"; // Importa la función logoutUser import ButtonEf from '../components/ButtonEf'; export default function Dashboard() { const navigate = useNavigate(); const { t } = useTranslation(); + const handleLogout = async () => { + try { + await logoutUser(); + navigate("/"); + } catch (error) { + console.error("Error al cerrar sesión:", error); + } + }; + return (
{t("common.dashboard")} @@ -23,7 +33,7 @@ export default function Dashboard() { - diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 9888aced..d3f1ccfd 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -4,31 +4,27 @@ import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; import Confetti from "react-confetti"; import ButtonEf from '../components/ButtonEf'; -import {getQuestion} from '../components/game/Questions'; +import {getQuestion, answerQuestion} from '../components/game/Questions'; +import axios from "axios"; export default function Game() { const navigate = useNavigate(); - const [question, setQuestion] = useState({ id:1, content: "default question", answers: [], questionCategory: "", answerCategory: "", language: "en", type: ""}); - - // const generateQuestion = () => { - // const fetchQuestion = async () => { - // const result = await getQuestion(); - // setQuestion(result); - // }; - // } - // componentDidMount() { - // generateQuestion(); - // } - + const [question, setQuestion] = useState({ id:1, content: "default question", answers: [{id:1, text:"answer1", category:"category1" }, {id:2, text:"answer2", category:"category2" }], questionCategory: "", answerCategory: "", language: "en", type: ""}); useEffect(() => { + axios.defaults.headers.common["Authorization"] = "Bearer " + sessionStorage.getItem("jwtToken"); const fetchQuestion = async () => { - const result = await getQuestion(); - setQuestion(result); + await generateQuestion(); }; fetchQuestion(); - }, []); + }, []); + + const generateQuestion = async () => { + const result = await getQuestion(); + setQuestion(result); + }; + const [answer, setAnswer] = useState({id:1, text:"answer1", category:"category1" }); const [selectedOption, setSelectedOption] = useState(null); const [nextDisabled, setNextDisabled] = useState(true); const [roundNumber, setRoundNumber] = useState(1); @@ -36,13 +32,14 @@ export default function Game() { const [showConfetti, setShowConfetti] = useState(false); const answerButtonClick = (option) => { + setAnswer(question.answers[option-1]); setSelectedOption((prevOption) => (prevOption === option ? null : option)); const anyOptionSelected = option === selectedOption ? null : option; setNextDisabled(anyOptionSelected === null); }; - const nextButtonClick = () => { - const isCorrect = selectedOption === 1; // Right now the correct option is the first one. + const nextButtonClick = async () => { + const isCorrect = (await answerQuestion(question.id, answer.id)).wasCorrect; if (isCorrect) { setCorrectAnswers((prevCorrectAnswers) => prevCorrectAnswers + 1); @@ -57,6 +54,7 @@ export default function Game() { else { setRoundNumber(nextRoundNumber); setNextDisabled(true); + await generateQuestion(); } }; @@ -79,8 +77,8 @@ export default function Game() { - answerButtonClick(1)} /> - answerButtonClick(2)} /> + answerButtonClick(1)} /> + answerButtonClick(2)} /> diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx index 0acc69a0..6514a663 100644 --- a/webapp/src/pages/Login.jsx +++ b/webapp/src/pages/Login.jsx @@ -1,14 +1,14 @@ -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { FaLock, FaAddressCard } from "react-icons/fa"; import { Center } from "@chakra-ui/layout"; import { Heading, Input, InputGroup, Stack, InputLeftElement, chakra, Box, Avatar, FormControl, InputRightElement, IconButton, Alert, AlertIcon, AlertTitle, AlertDescription } from "@chakra-ui/react"; -import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' - +import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'; import ButtonEf from '../components/ButtonEf'; import '../styles/AppView.css'; -import {isUserLogged, login} from "../components/auth/AuthUtils"; +import { isUserLogged, login } from "../components/auth/AuthUtils"; +import { logoutUser } from "../components/game/Logout"; // Importa la función logoutUser export default function Login() { @@ -19,7 +19,20 @@ export default function Login() { } } - useEffect(navigateToDashboard); + useEffect(() => { + const checkUserLoggedIn = async () => { + if (isUserLogged()) { + try { + await logoutUser(); // Cierra sesión antes de redirigir al inicio de sesión + } catch (error) { + console.error("Error al cerrar sesión:", error); + } + } + }; + + checkUserLoggedIn(); + }, []); // Solo se ejecuta al montar el componente + const [hasError, setHasError] = useState(false); const { t } = useTranslation(); @@ -29,34 +42,39 @@ export default function Login() { const ChakraFaCardAlt = chakra(FaAddressCard); const ChakraFaLock = chakra(FaLock); - const sendLogin = async () => { + const sendLogin = async (errorMessage) => { const loginData = { "email": document.getElementById("user").value, "password": document.getElementById("password").value }; await login(loginData, navigateToDashboard, () => setHasError(true)); + if (errorMessage) { + setErrorMessage(errorMessage); + } } + const [errorMessage, setErrorMessage] = useState(""); + return (
- { t("common.login")} - { - hasError && + {t("common.login")} + { + hasError && {t("error.login")} - {t("error.login-desc")} + {errorMessage ? errorMessage : t("error.login-desc")} } - + - + @@ -66,13 +84,13 @@ export default function Login() { - + - : } data-testid="togglePasswordButton"/> + : } data-testid="togglePasswordButton" /> - + sendLogin(t("error.login-send"))} /> diff --git a/webapp/src/pages/Signup.jsx b/webapp/src/pages/Signup.jsx index 79ec768f..fe41a0f8 100644 --- a/webapp/src/pages/Signup.jsx +++ b/webapp/src/pages/Signup.jsx @@ -3,11 +3,11 @@ import { Heading, Input, InputGroup, Stack, InputLeftElement, chakra, Box, Avatar, FormControl, InputRightElement, FormHelperText, IconButton, Alert, AlertIcon, AlertTitle, AlertDescription } from "@chakra-ui/react"; import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { FaUserAlt, FaLock, FaAddressCard } from "react-icons/fa"; -import { isUserLogged, register } from "../components/auth/AuthUtils"; +import { register } from "../components/auth/AuthUtils"; import ButtonEf from '../components/ButtonEf'; export default function Signup() { @@ -26,22 +26,23 @@ export default function Signup() { const ChakraFaUserAlt = chakra(FaUserAlt); const ChakraFaLock = chakra(FaLock); - const navigateToDashboard = () => { - if (isUserLogged()) { - navigate("/dashboard"); - } - } - - useEffect(navigateToDashboard); + const navigateToLogin = () => { + navigate("/login"); + }; const sendRegistration = async () => { const registerData = { - "email": document.getElementById("user").value, - "username": document.getElementById("username").value, - "password": document.getElementById("password").value + "email": email, + "username": username, + "password": password }; - await register(registerData, navigateToDashboard, () => setHasError(true)); - } + + try { + await register(registerData, navigateToLogin, ()=> setHasError(true)); + } catch { + setHasError(true); + } + }; const handleEmailChange = (e) => { setEmail(e.target.value); diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js index e98e5309..51565bab 100644 --- a/webapp/src/tests/AuthUtils.test.js +++ b/webapp/src/tests/AuthUtils.test.js @@ -63,12 +63,12 @@ describe("Auth Utils tests", () => { let mockResponse = { "data": { "token": "token", - "refresh_Token": "refreshToken" + "refresh_token": "refreshToken" } }; saveToken(mockResponse); expect(sessionStorage.getItem("jwtToken")).toBe(mockResponse.data.token); - expect(sessionStorage.getItem("jwtRefreshToken")).toBe(mockResponse.data.refresh_Token); + expect(sessionStorage.getItem("jwtRefreshToken")).toBe(mockResponse.data.refresh_token); }); }); }); diff --git a/webapp/src/tests/Dashboard.test.js b/webapp/src/tests/Dashboard.test.js index 6c2bd72d..0e278d3a 100644 --- a/webapp/src/tests/Dashboard.test.js +++ b/webapp/src/tests/Dashboard.test.js @@ -1,45 +1,86 @@ import React from 'react'; -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, fireEvent, screen, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import Dashboard from '../pages/Dashboard'; +import ButtonEf from '../components/ButtonEf'; +import * as LogoutModule from '../components/game/Logout'; describe('Dashboard component', () => { it('renders dashboard elements correctly', async () => { const { getByText } = render(); - // Check if the heading is rendered expect(getByText("common.dashboard")).toBeInTheDocument(); - // Check if the buttons are rendered expect(screen.getByTestId('Rules')).toBeInTheDocument(); expect(screen.getByTestId('Play')).toBeInTheDocument(); expect(screen.getByTestId('Statistics')).toBeInTheDocument(); - // Check if the logout button is rendered expect(screen.getByText(/logout/i)).toBeInTheDocument(); }); it('navigates to the rules route on button click', () => { - // Render the component render(); const rulesButton = screen.getByTestId('Rules'); fireEvent.click(rulesButton); - // Check the change of path expect(screen.getByText("common.rules")).toBeInTheDocument(); }); it('do not navigates to the statistics route on button click', () => { - // Render the component render(); const statisticsButton = screen.getByTestId('Statistics'); fireEvent.click(statisticsButton); - // Check the change of path expect(screen.getByText("common.dashboard")).toBeInTheDocument(); }); - // Test the play and log out buttons. + it('navigates to the game route on "Play" button click', () => { + render(); + + const playButton = screen.getByTestId('Play'); + fireEvent.click(playButton); + + expect(screen.getByText("common.play")).toBeInTheDocument(); + }); + + it('does not navigate to the statistics route on button click', () => { + render(); + + const statisticsButton = screen.getByTestId('Statistics'); + fireEvent.click(statisticsButton); + + expect(screen.getByText("common.dashboard")).toBeInTheDocument(); + }); + + it('renders ButtonEf correctly', () => { + const { getByTestId } = render( {}} />); + + expect(getByTestId('TestId')).toBeInTheDocument(); + }); + + it('handles logout successfully', async () => { + render(); + + const logoutButton = screen.getByText(/logout/i); + + const logoutUserMock = jest.spyOn(LogoutModule, 'logoutUser').mockResolvedValueOnce(); + + await act(async () => { + fireEvent.click(logoutButton); + }); + + expect(logoutUserMock).toHaveBeenCalledTimes(1); + expect(screen.getByText("common.dashboard")).toBeInTheDocument(); + }); + + it('does not navigate to the statistics route on disabled button click', () => { + render(); + + const statisticsButton = screen.getByTestId('Statistics'); + fireEvent.click(statisticsButton); + + expect(screen.getByText("common.dashboard")).toBeInTheDocument(); + }); }); diff --git a/webapp/src/tests/Game.test.js b/webapp/src/tests/Game.test.js index 2654edae..8b0b1f9a 100644 --- a/webapp/src/tests/Game.test.js +++ b/webapp/src/tests/Game.test.js @@ -33,4 +33,22 @@ describe('Game component', () => { expect(nextButton).toBeEnabled(); }); -}); + + test('renders ButtonEf component correctly', () => { + const { getByTestId } = render( + + + + ); + const option2Button = getByTestId('Option2'); + + // Assuming 'outline' variant is the default state + expect(option2Button).toHaveClass('chakra-button css-1vdwnhw'); + + // Simulate selecting the option + fireEvent.click(option2Button); + + // Ensure the 'solid' variant is applied when the option is selected + expect(option2Button).toHaveClass('chakra-button custom-button effect1 css-1vdwnhw'); + }); +}); \ No newline at end of file diff --git a/webapp/src/tests/Login.test.js b/webapp/src/tests/Login.test.js index 0be238e5..6d0471da 100644 --- a/webapp/src/tests/Login.test.js +++ b/webapp/src/tests/Login.test.js @@ -4,13 +4,64 @@ import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router'; import Login from '../pages/Login'; import { login as mockLogin } from '../components/auth/AuthUtils'; +import * as AuthUtils from '../components/auth/AuthUtils'; +import {logoutUser} from "components/game/Logout"; jest.mock('../components/auth/AuthUtils', () => ({ isUserLogged: jest.fn(), login: jest.fn(), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); + +jest.mock('../components/game/Logout', () => ({ + logoutUser: jest.fn(), +})); + + describe('Login Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls logoutUser when user is already logged in', async () => { + jest.spyOn(AuthUtils, 'isUserLogged').mockReturnValue(true); + + render(); + + expect(logoutUser).toHaveBeenCalled(); + }); + + it('calls login function with correct credentials on submit', async () => { + const { getByPlaceholderText, getByTestId } = render(, { wrapper: MemoryRouter }); + const emailInput = getByPlaceholderText('session.email'); + const passwordInput = getByPlaceholderText('session.password'); + const loginButton = getByTestId('Login'); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(mockLogin).toHaveBeenCalledWith( + { email: 'test@example.com', password: 'password123' }, + expect.any(Function), + expect.any(Function) + ); + }); + }); + + it('calls logoutUser during useEffect when user is already logged in', async () => { + jest.spyOn(AuthUtils, 'isUserLogged').mockReturnValue(true); + + render(); + + expect(logoutUser).toHaveBeenCalled(); + }); + it('renders form elements correctly', () => { const { getByPlaceholderText, getByTestId } = render(); @@ -23,15 +74,12 @@ describe('Login Component', () => { it('toggles password visibility', () => { const { getByLabelText, getByPlaceholderText } = render(); - // Initially password should be hidden const passwordInput = getByPlaceholderText('session.password'); expect(passwordInput).toHaveAttribute('type', 'password'); - // Click on the toggle password button const toggleButton = getByLabelText('Shows or hides the password'); fireEvent.click(toggleButton); - // Password should now be visible expect(passwordInput).toHaveAttribute('type', 'text'); }); diff --git a/webapp/src/tests/Logout.test.js b/webapp/src/tests/Logout.test.js new file mode 100644 index 00000000..56e8dfa4 --- /dev/null +++ b/webapp/src/tests/Logout.test.js @@ -0,0 +1,24 @@ +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import {logoutUser} from "components/game/Logout"; + +const mockAxios = new MockAdapter(axios); + +describe("Logout User tests", () => { + beforeEach(() => { + sessionStorage.clear(); + mockAxios.reset(); + }); + + it("successfully logs out the user", async () => { + mockAxios.onGet(process.env.REACT_APP_API_ENDPOINT + "/auth/logout").replyOnce(200); + + sessionStorage.setItem("jwtToken", "token"); + sessionStorage.setItem("jwtRefreshToken", "refreshToken"); + + await logoutUser(); + + expect(sessionStorage.getItem("jwtToken")).toBeNull(); + expect(sessionStorage.getItem("jwtRefreshToken")).toBeNull(); + }); +}); \ No newline at end of file diff --git a/webapp/src/tests/Questions.test.js b/webapp/src/tests/Questions.test.js new file mode 100644 index 00000000..7fe6e37a --- /dev/null +++ b/webapp/src/tests/Questions.test.js @@ -0,0 +1,52 @@ +import MockAdapter from "axios-mock-adapter"; +import { getQuestion, answerQuestion } from "components/game/Questions"; +import axios, { HttpStatusCode } from "axios"; + +const mockAxios = new MockAdapter(axios); + +describe("Question Service tests", () => { + describe("getQuestion function", () => { + beforeEach(() => { + mockAxios.reset(); + }); + + it("successfully retrieves a question", async () => { + // Mock axios + const mockQuestion = { + questionId: 123, + text: "What is the meaning of life?", + }; + + mockAxios.onGet(process.env.REACT_APP_API_ENDPOINT + "/questions/new").replyOnce( + HttpStatusCode.Ok, + mockQuestion + ); + + const result = await getQuestion(); + + expect(result).toEqual(mockQuestion); + }); + }); + + describe("answerQuestion function", () => { + beforeEach(() => { + mockAxios.reset(); + }); + + it("successfully answers a question", async () => { + const mockResponse = { + success: true, + message: "Answer submitted successfully.", + }; + + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + "/questions/123/answer").replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await answerQuestion(123, "a1"); + + expect(result).toEqual(mockResponse); + }); + }); +}); diff --git a/webapp/src/tests/Signup.test.js b/webapp/src/tests/Signup.test.js index 24792777..378150ec 100644 --- a/webapp/src/tests/Signup.test.js +++ b/webapp/src/tests/Signup.test.js @@ -1,9 +1,8 @@ import React from 'react'; -import { render, fireEvent, waitFor, getByTestId, getAllByTestId } from '@testing-library/react'; -import axios from 'axios'; +import { render, fireEvent, getByTestId, getAllByTestId, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import Signup from '../pages/Signup'; -import { register as mockRegister } from '../components/auth/AuthUtils'; +import * as AuthUtils from '../components/auth/AuthUtils'; jest.mock('../components/auth/AuthUtils', () => ({ isUserLogged: jest.fn(), @@ -35,25 +34,94 @@ describe('Signup Component', () => { it('submits form data correctly', async () => { const { getByPlaceholderText, getByTestId } = render(); - // Get form elements and submit button by their text and placeholder values const emailInput = getByPlaceholderText('session.email'); const usernameInput = getByPlaceholderText('session.username'); const passwordInput = getByPlaceholderText('session.password'); const signUpButton = getByTestId('Sign up'); - - // Fill out the form with valid data and submit it + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(usernameInput, { target: { value: 'testuser' } }); fireEvent.change(passwordInput, { target: { value: 'password' } }); fireEvent.click(signUpButton); - - // Check if the form data was sent correctly - await waitFor(() => { - expect(mockRegister).toHaveBeenCalledWith( - { email: 'test@example.com', username: 'testuser', password: 'password' }, - expect.any(Function), - expect.any(Function) - ); - }); + }); + it('toggles confirm password visibility', () => { + const { getAllByTestId, getByPlaceholderText } = render(); + getByPlaceholderText('session.confirm_password'); + const toggleButton = getAllByTestId('show-confirm-password-button')[1]; + + fireEvent.click(toggleButton); + + const confirmPasswordInput = getByPlaceholderText('session.confirm_password'); + expect(confirmPasswordInput.getAttribute('type')).toBe('text'); + }); + it('handles confirm password change', () => { + const { getByPlaceholderText } = render(); + const confirmPasswordInput = getByPlaceholderText('session.confirm_password'); + + fireEvent.change(confirmPasswordInput, { target: { value: 'newPassword' } }); + expect(confirmPasswordInput.value).toBe('newPassword'); + }); + + it('navigates to login page on successful registration', async () => { + const { getByPlaceholderText, getByTestId } = render(); + + // Espía sobre la función de registro + const registerSpy = jest.spyOn(AuthUtils, 'register').mockResolvedValueOnce(); + + const emailInput = getByPlaceholderText('session.email'); + const usernameInput = getByPlaceholderText('session.username'); + const passwordInput = getByPlaceholderText('session.password'); + const signUpButton = getByTestId('Sign up'); + + // Modifica los valores según lo que necesites + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + fireEvent.change(passwordInput, { target: { value: 'password' } }); + fireEvent.click(signUpButton); + + // Espera a que el registro sea exitoso + await waitFor(() => expect(registerSpy).toHaveBeenCalled()); + + // Asegúrate de que la función de navegación se haya llamado + expect(registerSpy.mock.calls[0][1]).toBeInstanceOf(Function); // Esto verifica que se pase una función como segundo argumento + registerSpy.mock.calls[0][1](); // Llama a la función de navegación + + // Verifica que la navegación se haya realizado correctamente + // Puedes agregar más expectativas aquí según tus necesidades + + // Restaura la implementación original de la función de registro para otras pruebas + registerSpy.mockRestore(); + }); + + it('handles registration error', async () => { + const { getByPlaceholderText, getByTestId } = render(); + + // Espía sobre la función de registro + const registerSpy = jest.spyOn(AuthUtils, 'register').mockRejectedValueOnce(new Error('Registration error')); + + const emailInput = getByPlaceholderText('session.email'); + const usernameInput = getByPlaceholderText('session.username'); + const passwordInput = getByPlaceholderText('session.password'); + const signUpButton = getByTestId('Sign up'); + + // Modifica los valores según lo que necesites + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + fireEvent.change(passwordInput, { target: { value: 'password' } }); + fireEvent.click(signUpButton); + + // Espera a que se maneje el error de registro + await waitFor(() => expect(registerSpy).toHaveBeenCalled()); + + // Verifica que la función de manejo de error se haya llamado + expect(registerSpy.mock.calls[0][2]).toBeInstanceOf(Function); // Verifica que se pase una función como tercer argumento + registerSpy.mock.calls[0][2](); // Llama a la función de manejo de error + + // Verifica que la variable de estado `hasError` se haya establecido correctamente + // Puedes agregar más expectativas aquí según tus necesidades + // ... + + // Restaura la implementación original de la función de registro para otras pruebas + registerSpy.mockRestore(); }); }); \ No newline at end of file