diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 410e842..29e5ad2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,8 @@ jobs: - run: npm --prefix userservice/authservice test -- --coverage - run: npm --prefix userservice/userservice test -- --coverage - run: npm --prefix gatewayservice test -- --coverage - #- run: npm --prefix questionservice test -- --coverage - #- run: npm --prefix webapp test -- --coverage + - run: npm --prefix questionservice test -- --coverage + - run: npm --prefix webapp test -- --coverage - name: Analyze with SonarCloud uses: sonarsource/sonarcloud-github-action@master env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c474c2..48d5656 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,6 +105,10 @@ jobs: needs: [e2e-tests] steps: - uses: actions/checkout@v4 + - name: Update OpenAPI configuration + run: | + DEPLOY_HOST=${{ secrets.DEPLOY_HOST }} + sed -i "s/SOMEIP/${DEPLOY_HOST}/g" gatewayservice/openapi.yaml - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@v5 with: diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 4cbbf76..9e240ac 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -3,13 +3,17 @@ const axios = require('axios'); const cors = require('cors'); const promBundle = require('express-prom-bundle'); +//librerias para OpenAPI-Swagger +const swaggerUi = require('swagger-ui-express'); +const fs = require("fs"); +const YAML = require('yaml'); + const app = express(); const port = 8000; const userServiceUrl = process.env.USER_SERVICE_URL || 'http://localhost:8001'; const authServiceUrl = process.env.AUTH_SERVICE_URL || 'http://localhost:8002'; const questionServiceUrl = process.env.QUESTION_SERVICE_URL || 'http://localhost:8003'; -const statServiceUrl = process.env.STAT_SERVICE_URL || 'http://localhost:8004'; app.use(cors()); app.use(express.json()); @@ -54,9 +58,9 @@ app.get('/pregunta', async (req, res) => { app.get('/updateCorrectAnswers', async (req, res) => { console.log(req.query) - const { username } = req.query; + const params = {username: req.query.username, numAnswers: req.query.numAnswers}; try{ - const updateStatsResponse = await axios.get(userServiceUrl+ `/updateCorrectAnswers?username=${username}`) + const updateStatsResponse = await axios.get(userServiceUrl+ `/updateCorrectAnswers?params=${params}`) res.json(updateStatsResponse.data); }catch(error){ res.status(error.response.status).json({error: error.response.data.error}); @@ -64,10 +68,9 @@ app.get('/updateCorrectAnswers', async (req, res) => { }); app.get('/updateIncorrectAnswers', async (req, res) => { - console.log(req.query) - const { username } = req.query; + const params = {username: req.query.username, numAnswers: req.query.numAnswers}; try{ - const updateStatsResponse = await axios.get(userServiceUrl+ `/updateIncorrectAnswers?username=${username}`) + const updateStatsResponse = await axios.get(userServiceUrl+ `/updateIncorrectAnswers?params=${params}`) res.json(updateStatsResponse.data); }catch(error){ res.status(error.response.status).json({error: error.response.data.error}); @@ -75,7 +78,6 @@ app.get('/updateIncorrectAnswers', async (req, res) => { }); app.get('/updateCompletedGames', async (req, res) => { - console.log(req.query) const { username } = req.query; try{ const updateStatsResponse = await axios.get(userServiceUrl+ `/updateCompletedGames?username=${username}`) @@ -98,6 +100,22 @@ app.get('/getUserData', async (req, res) => { } }); +// Read the OpenAPI YAML file synchronously +let openapiPath='./openapi.yaml'; +if (fs.existsSync(openapiPath)) { + const file = fs.readFileSync(openapiPath, 'utf8'); + + // Parse the YAML content into a JavaScript object representing the Swagger document + const swaggerDocument = YAML.parse(file); + + // Serve the Swagger UI documentation at the '/api-doc' endpoint + // This middleware serves the Swagger UI files and sets up the Swagger UI page + // It takes the parsed Swagger document as input + app.use('/api-doc', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +} else { + console.log("Not configuring OpenAPI. Configuration file not present.") +} + // Start the gateway service const server = app.listen(port, () => { console.log(`Gateway Service listening at http://localhost:${port}`); diff --git a/gatewayservice/openapi.yaml b/gatewayservice/openapi.yaml new file mode 100644 index 0000000..31a02ff --- /dev/null +++ b/gatewayservice/openapi.yaml @@ -0,0 +1,414 @@ +openapi: 3.0.0 +info: + title: Gatewayservice API + description: Gateway OpenAPI specification. + version: 0.1.0 +servers: + - url: http://localhost:8000 + description: Development server + - url: http://SOMEIP:8000 + description: Production server +paths: + /adduser: + post: + summary: Add a new user to the database. + operationId: addUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID. + example: student + password: + type: string + description: User password. + example: pass + responses: + '200': + description: User added successfully. + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID + password: + type: string + description: Hashed password + example: $2b$10$ZKdNYLWFQxzt5Rei/YTc/OsZNi12YiWz30JeUFHNdAt7MyfmkTuvC + _id: + type: string + description: Identification + example: 65f756db3fa22d227a4b7c7d + createdAt: + type: string + description: Creation date. + example: '2024-03-17T20:47:23.935Z' + ___v: + type: integer + example: '0' + '400': + description: Failed to add user. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: getaddrinfo EAI_AGAIN mongodb + /health: + get: + summary: Check the health status of the service. + operationId: checkHealth + responses: + '200': + description: Service is healthy. + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: Health status. + example: OK + /login: + post: + summary: Log in to the system. + operationId: loginUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID. + example: student + password: + type: string + description: User password. + example: pass + responses: + '200': + description: Login successful. Returns user token, username, and creation date. + content: + application/json: + schema: + type: object + properties: + token: + type: string + description: User token. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NWY3NTZkYjNmYTIyZDIyN2E0YjdjN2QiLCJpYXQiOjE3MTA3MDg3NDUsImV4cCI6MTcxMDcxMjM0NX0.VMG_5DOyQ4GYlJQRcu1I6ICG1IGzuo2Xuei093ONHxw + username: + type: string + description: Username. + example: student + createdAt: + type: string + description: Creation date. + example: '2024-03-17T20:47:23.935Z' + '401': + description: Invalid credentials. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Shows the error info.. + example: Invalid credentials + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error + /pregunta: + get: + summary: Generate a question. + operationId: pregunta + responses: + '200': + description: Returns the generated question with a right and three incorrect answers. + content: + application/json: + schema: + type: object + properties: + question: + type: string + description: Generated question. + example: ¿Cuál es la capital de España? + answerGood: + type: string + description: Respuesta correcta a la pregunta generada. + example: Madrid + answers: + type: array + description: Respuestas correcta e incorrectas. + items: + type: string + example: + - Madrid + - Abu Dabi + - Washinton D. C. + - Tokyo + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error + /updateCorrectAnswers: + get: + summary: Updates the data of the user, increasing his number of correct answers. + operationId: updateCorrectAnswers + parameters: + in: path + name: params + required: true + description: Parameters for the update (username and numAnswers) + schema: + type: object + properties: + username: + type: string + numAnswers: + type: integer + responses: + '200': + description: Updates the data correctly. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: Respuestas correctas actualizada con éxito + '404': + description: The user is not found. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Usuario no encontrado + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + description: Error information. + example: Error al actualizar las respuestas correctas + /updateIncorrectAnswers: + get: + summary: Updates the data of the user, increasing his number of incorrect answers. + operationId: updateIncorrectAnswers + parameters: + in: path + name: params + required: true + description: Parameters for the update (username and numAnswers) + schema: + type: object + properties: + username: + type: string + numAnswers: + type: integer + responses: + '200': + description: Updates the data correctly. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: Respuestas incorrectas actualizada con éxito + '404': + description: The user is not found. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Usuario no encontrado + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + description: Error information. + example: Error al actualizar las respuestas incorrectas + /updateCompletedGames: + get: + summary: Update the number of completed games by the user. + operationId: updateCompletedGames + parameters: + in: path + name: username + required: true + description: Username for the update of data + schema: + type: string + responses: + '200': + description: Finds the data of the user correctly. + content: + application/json: + schema: + type: user + properties: + success: + type: boolean + example: true + message: + type: string + example: Juegos completados actualizado con éxito + '404': + description: The user is not found. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Usuario no encontrado + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + description: Error information. + example: Error al actualizar Juegos completados + /getUserData: + get: + summary: Gets the data of an user. + operationId: getUserData + parameters: + in: path + name: username + required: true + description: Username for the search of data + schema: + type: string + responses: + '200': + description: Finds the data of the user correctly. + content: + application/json: + schema: + type: user + properties: + username: + type: string + example: Pablo + password: + type: integer + example: pass + createdAt: + type: date + correctAnswers: + type: integer + incorrectAnswers: + type: integer + completedGames: + type: integer + averageTime: + type: integer + '404': + description: The user is not found. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Usuario no encontrado + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + description: Error information. + example: Error al obtener los datos de usuario + diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json index fc5f2d6..f89984c 100644 --- a/gatewayservice/package-lock.json +++ b/gatewayservice/package-lock.json @@ -12,7 +12,9 @@ "axios": "^1.6.5", "cors": "^2.8.5", "express": "^4.18.2", - "express-prom-bundle": "^7.0.0" + "express-prom-bundle": "^7.0.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.1" }, "devDependencies": { "jest": "^29.7.0", @@ -4389,6 +4391,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.14.0.tgz", + "integrity": "sha512-7qsKvc3gs5dnEIOclY4xkzacY85Pu9a/Gzkf+eezKLQ4BpErlI8BxYWADhnCx6PmFyU4fxH4AMKH+/d3Kml0Gg==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -4636,6 +4657,17 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/gatewayservice/package.json b/gatewayservice/package.json index bf85178..cf14d52 100644 --- a/gatewayservice/package.json +++ b/gatewayservice/package.json @@ -21,7 +21,9 @@ "axios": "^1.6.5", "cors": "^2.8.5", "express": "^4.18.2", - "express-prom-bundle": "^7.0.0" + "express-prom-bundle": "^7.0.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.1" }, "devDependencies": { "jest": "^29.7.0", diff --git a/questionservice/server.js b/questionservice/server.js index 0b38cac..16884dc 100644 --- a/questionservice/server.js +++ b/questionservice/server.js @@ -14,55 +14,59 @@ const questions = JSON.parse(fs.readFileSync('questions.json', 'utf8')); // Definimos una ruta GET en '/pregunta' app.get('/pregunta', async (req, res) => { - // Seleccionamos una consulta SPARQL de forma aleatoria del fichero de configuración - const questionItem = questions[Math.floor(Math.random() * questions.length)]; + try{ + // Seleccionamos una consulta SPARQL de forma aleatoria del fichero de configuración + const questionItem = questions[Math.floor(Math.random() * questions.length)]; - // URL del endpoint SPARQL de Wikidata - const url = "https://query.wikidata.org/sparql"; - // Consulta SPARQL seleccionada - const query = questionItem.query; + // URL del endpoint SPARQL de Wikidata + const url = "https://query.wikidata.org/sparql"; + // Consulta SPARQL seleccionada + const query = questionItem.query; - // Realizamos la solicitud HTTP GET al endpoint SPARQL con la consulta - const response = await axios.get(url, { params: { format: 'json', query } }); - // Extraemos los resultados de la consulta - const bindings = response.data.results.bindings; + // Realizamos la solicitud HTTP GET al endpoint SPARQL con la consulta + const response = await axios.get(url, { params: { format: 'json', query } }); + // Extraemos los resultados de la consulta + const bindings = response.data.results.bindings; - let wikidataCodePattern = /^Q\d+$/; - let correctAnswer = null; - let correctAnswerIndex = 0; + let wikidataCodePattern = /^Q\d+$/; + let correctAnswer = null; + let correctAnswerIndex = 0; - do { - // Seleccionamos un índice aleatorio para la respuesta correcta - correctAnswerIndex = Math.floor(Math.random() * bindings.length); - // Obtenemos la respuesta correcta - correctAnswer = bindings[correctAnswerIndex]; - } while (wikidataCodePattern.test(correctAnswer.questionSubjectLabel.value)); - - // Creamos la pregunta - console.log(questionItem.question); - const question = questionItem.question.replace('{sujetoPregunta}', correctAnswer.questionSubjectLabel.value); - - // Inicializamos las respuestas con la respuesta correcta - const answerGood = correctAnswer.answerSubjectLabel.value; - const answers = [answerGood]; - // Añadimos tres respuestas incorrectas - for (let i = 0; i < 3; i++) { - let randomIndex; do { - // Seleccionamos un índice aleatorio distinto al de la respuesta correcta - randomIndex = Math.floor(Math.random() * bindings.length); - } while (randomIndex === correctAnswerIndex); - // Añadimos la capital del país seleccionado aleatoriamente a las respuestas - answers.push(bindings[randomIndex].answerSubjectLabel.value); - } - // Mezclamos las respuestas - for (let i = answers.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - // Intercambiamos las respuestas en los índices i y j - [answers[i], answers[j]] = [answers[j], answers[i]]; + // Seleccionamos un índice aleatorio para la respuesta correcta + correctAnswerIndex = Math.floor(Math.random() * bindings.length); + // Obtenemos la respuesta correcta + correctAnswer = bindings[correctAnswerIndex]; + } while (wikidataCodePattern.test(correctAnswer.questionSubjectLabel.value)); + + // Creamos la pregunta + console.log(questionItem.question); + const question = questionItem.question.replace('{sujetoPregunta}', correctAnswer.questionSubjectLabel.value); + + // Inicializamos las respuestas con la respuesta correcta + const answerGood = correctAnswer.answerSubjectLabel.value; + const answers = [answerGood]; + // Añadimos tres respuestas incorrectas + for (let i = 0; i < 3; i++) { + let randomIndex; + do { + // Seleccionamos un índice aleatorio distinto al de la respuesta correcta + randomIndex = Math.floor(Math.random() * bindings.length); + } while (randomIndex === correctAnswerIndex); + // Añadimos la capital del país seleccionado aleatoriamente a las respuestas + answers.push(bindings[randomIndex].answerSubjectLabel.value); + } + // Mezclamos las respuestas + for (let i = answers.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + // Intercambiamos las respuestas en los índices i y j + [answers[i], answers[j]] = [answers[j], answers[i]]; + } + // Enviamos la pregunta y las respuestas como respuesta a la solicitud HTTP + res.status(200).json({ question: question, answerGood: answerGood, answers: answers }); + }catch(error){ + res.status(500).json({ error: 'Internal Server Error' }); } - // Enviamos la pregunta y las respuestas como respuesta a la solicitud HTTP - res.json({ question, answerGood, answers }); }); // Iniciamos el servidor en el puerto 8003 diff --git a/sonar-project.properties b/sonar-project.properties index 90d5c49..acbc46c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,7 +12,7 @@ sonar.projectName=wiq_es05a sonar.coverage.exclusions=**/*.test.js sonar.coverage.exclusions=* -sonar.sources=webapp/src/components,userservice/authservice,userservice/userservice,gatewayservice, questionservice +sonar.sources=webapp/src/components,userservice/authservice,userservice/userservice,gatewayservice,questionservice sonar.sourceEncoding=UTF-8 sonar.exclusions=node_modules/** sonar.javascript.lcov.reportPaths=**/coverage/lcov.info diff --git a/userservice/authservice/auth-service.test.js b/userservice/authservice/auth-service.test.js index afbedfa..7b55ee7 100644 --- a/userservice/authservice/auth-service.test.js +++ b/userservice/authservice/auth-service.test.js @@ -1,7 +1,7 @@ const request = require('supertest'); const { MongoMemoryServer } = require('mongodb-memory-server'); const bcrypt = require('bcrypt'); -const User = require('../../webapp/src/model/auth-model'); +const User = require('./auth-model'); let mongoServer; let app; diff --git a/userservice/userservice/user-service.js b/userservice/userservice/user-service.js index 3172ff1..99fb9a0 100644 --- a/userservice/userservice/user-service.js +++ b/userservice/userservice/user-service.js @@ -47,40 +47,58 @@ app.post('/adduser', async (req, res) => { app.get('/updateCorrectAnswers', async (req,res) => { console.log(req.query) - const { username } = req.query; + //const { username } = req.query; + const { username } = req.query.username; + const { numAnswers } = req.query.numAnswers; try { const user = await User.findOne({ username }); if (!user) { return res.status(404).json({ success: false, message: 'Usuario no encontrado' }); } // Incrementa las respuestas correctas del usuario - user.correctAnswers += 1; + user.correctAnswers = numAnswers; await user.save(); - return res.status(200).json({ success: true, message: 'Respuesta correcta actualizada con éxito' }); + return res.status(200).json({ success: true, message: 'Respuestas correctas actualizada con éxito' }); } catch (error) { - console.error('Error al actualizar la respuesta correcta:', error); - return res.status(500).json({ success: false, message: 'Error al actualizar la respuesta correcta' }); + console.error('Error al actualizar las respuestas correctas:', error); + return res.status(500).json({ success: false, message: 'Error al actualizar las respuestas correctas' }); } }) app.get('/updateIncorrectAnswers', async (req,res) => { console.log(req.query) - const { username } = req.query; + //const { username } = req.query; + const { username } = req.query.username; + const { numAnswers } = req.query.numAnswers; try { const user = await User.findOne({ username }); if (!user) { return res.status(404).json({ success: false, message: 'Usuario no encontrado' }); } // Incrementa las respuestas incorrectas del usuario - user.incorrectAnswers += 1; + user.incorrectAnswers = numAnswers; await user.save(); - return res.status(200).json({ success: true, message: 'Respuesta incorrecta actualizada con éxito' }); + return res.status(200).json({ success: true, message: 'Respuestas incorrectas actualizada con éxito' }); } catch (error) { console.error('Error al actualizar la respuesta correcta:', error); - return res.status(500).json({ success: false, message: 'Error al actualizar la respuesta incorrecta' }); + return res.status(500).json({ success: false, message: 'Error al actualizar las respuestas incorrectas' }); } }) +app.get('/getUserData', async (req, res) => { + const { username } = req.query; + try { + const user = await User.findOne({ username }); + if (!user) { + return res.status(404).json({ success: false, message: 'Usuario no encontrado' }); + } + return res.status(200).json({ user }); + } catch (error) { + console.error('Error al obtener los datos de usuario:', error); + return res.status(500).json({ success: false, message: 'Error al obtener los datos de usuario' }); + } +}); + app.get('/updateCompletedGames', async (req,res) => { console.log(req.query) @@ -100,25 +118,10 @@ app.get('/updateCompletedGames', async (req,res) => { }) - const server = app.listen(port, () => { console.log(`User Service listening at http://localhost:${port}`); }); -app.get('/getUserData', async (req, res) => { - const { username } = req.query; - try { - const user = await User.findOne({ username }); - if (!user) { - return res.status(404).json({ success: false, message: 'Usuario no encontrado' }); - } - return res.status(200).json({ user }); - } catch (error) { - console.error('Error al obtener los datos de usuario:', error); - return res.status(500).json({ success: false, message: 'Error al obtener los datos de usuario' }); - } -}); - // Listen for the 'close' event on the Express.js server server.on('close', () => { // Close the Mongoose connection diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 4410b88..8cb5c32 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -13,7 +13,6 @@ "@mui/material": "^5.15.3", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", - "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", "bootstrap": "^5.3.3", "browserify-zlib": "^0.2.0", @@ -33,6 +32,7 @@ "web-vitals": "^3.5.1" }, "devDependencies": { + "@testing-library/user-event": "^14.5.2", "axios-mock-adapter": "^1.22.0", "expect-puppeteer": "^9.0.2", "jest": "^29.3.1", @@ -6204,6 +6204,7 @@ "version": "14.5.2", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, "engines": { "node": ">=12", "npm": ">=6" @@ -33230,6 +33231,7 @@ "version": "14.5.2", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, "requires": {} }, "@tootallnate/once": { diff --git a/webapp/package.json b/webapp/package.json index b90277d..d6d10f3 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -8,7 +8,6 @@ "@mui/material": "^5.15.3", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", - "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", "bootstrap": "^5.3.3", "browserify-zlib": "^0.2.0", @@ -54,6 +53,7 @@ ] }, "devDependencies": { + "@testing-library/user-event": "^14.5.2", "axios-mock-adapter": "^1.22.0", "expect-puppeteer": "^9.0.2", "jest": "^29.3.1", diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js deleted file mode 100644 index 0c8f791..0000000 --- a/webapp/src/App.test.js +++ /dev/null @@ -1,68 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -/*test('renders learn react link', () => { - render(<App />); - const linkElement = screen.getByText(/Welcome to the 2024 edition of the Software Architecture course/i); - expect(linkElement).toBeInTheDocument(); -}); -*/ - -describe('App', () => { - - it('renders login page when not logged in', () => { - render( - <MemoryRouter> - <App /> - </MemoryRouter> - ); - - expect(screen.getByText(/Login/i)).toBeInTheDocument(); - }); - - it('renders home page when logged in', () => { - // Mock local storage to simulate being logged in - const mockLocalStorage = { - getItem: jest.fn(() => 'true'), - setItem: jest.fn(), - clear: jest.fn() - }; - global.localStorage = mockLocalStorage; - - render( - <MemoryRouter> - <App /> - </MemoryRouter> - ); - - expect(screen.getByText(/Home/i)).toBeInTheDocument(); - }); - - it('redirects to login when trying to access stats page without logging in', () => { - render( - <MemoryRouter initialEntries={['/stats']}> - <App /> - </MemoryRouter> - ); - - expect(screen.getByText(/Login/i)).toBeInTheDocument(); - }); - - it('renders stats page when logged in and trying to access stats page', () => { - // Mock local storage to simulate being logged in - const mockLocalStorage = { - getItem: jest.fn(() => 'true'), - setItem: jest.fn(), - clear: jest.fn() - }; - global.localStorage = mockLocalStorage; - - render( - <MemoryRouter initialEntries={['/stats']}> - <App /> - </MemoryRouter> - ); - - expect(screen.getByText(/Estadisticas/i)).toBeInTheDocument(); - }); -}); \ No newline at end of file diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js index af102dc..fa6f20d 100644 --- a/webapp/src/components/Login.test.js +++ b/webapp/src/components/Login.test.js @@ -3,16 +3,25 @@ import { render, fireEvent, screen, waitFor, act } from '@testing-library/react' import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import Login from './Login'; +import userEvent from '@testing-library/user-event' const mockAxios = new MockAdapter(axios); describe('Login component', () => { beforeEach(() => { - mockAxios.reset(); + mockAxios.reset(); }); + it('should render correctly', async () => { + render(<Login isLogged={false} setIsLogged={jest.fn()} username={''} setUsername={jest.fn()}/>); + + expect(screen.getByLabelText(/Username/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Password/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Login/i })).toBeInTheDocument(); + }) + it('should log in successfully', async () => { - render(<Login />); + render(<Login isLogged={false} setIsLogged={jest.fn()} username={'testUser'} setUsername={jest.fn()}/>); const usernameInput = screen.getByLabelText(/Username/i); const passwordInput = screen.getByLabelText(/Password/i); @@ -27,25 +36,33 @@ describe('Login component', () => { fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); fireEvent.click(loginButton); }); + + // Verify that the user information is displayed + expect(screen.getByText(/Login successful/i)).toBeInTheDocument(); + //expect(screen.getByText(/Your account was created on 1\/1\/2024/i)).toBeInTheDocument(); + }); + it('logged in successfully', async () => { + render(<Login isLogged={true} setIsLogged={jest.fn()} username={'testUser'} setUsername={jest.fn()}/>); + // Verify that the user information is displayed - expect(screen.getByText(/Hello testUser!/i)).toBeInTheDocument(); - expect(screen.getByText(/Your account was created on 1\/1\/2024/i)).toBeInTheDocument(); + expect(screen.getByText(/Hello testUser!/i)).toBeInTheDocument(); + expect(screen.getByText(/Your account was created on/i)).toBeInTheDocument(); }); it('should handle error when logging in', async () => { - render(<Login />); + render(<Login isLogged={false} setIsLogged={jest.fn()} username={'testUer'} setUsername={jest.fn()}/>); const usernameInput = screen.getByLabelText(/Username/i); const passwordInput = screen.getByLabelText(/Password/i); const loginButton = screen.getByRole('button', { name: /Login/i }); - // Mock the axios.post request to simulate an error response + mockAxios.onPost('http://localhost:8000/login').reply(401, { error: 'Unauthorized' }); - // Simulate user input - fireEvent.change(usernameInput, { target: { value: 'testUser' } }); - fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); + //Simulate + userEvent.type(usernameInput, 'testUser') + userEvent.type(passwordInput, 'testPassword') // Trigger the login button click fireEvent.click(loginButton); @@ -54,9 +71,5 @@ describe('Login component', () => { await waitFor(() => { expect(screen.getByText(/Error: Unauthorized/i)).toBeInTheDocument(); }); - - // Verify that the user information is not displayed - expect(screen.queryByText(/Hello testUser!/i)).toBeNull(); - expect(screen.queryByText(/Your account was created on/i)).toBeNull(); }); }); diff --git a/webapp/src/components/Pages/Juego.js b/webapp/src/components/Pages/Juego.js index 6f35e59..367a24f 100644 --- a/webapp/src/components/Pages/Juego.js +++ b/webapp/src/components/Pages/Juego.js @@ -22,12 +22,13 @@ const Juego = ({isLogged, username, numPreguntas}) => { const [restartTemporizador, setRestartTemporizador] = useState(false) const [firstRender, setFirstRender] = useState(false); - const[ready, setReady] = useState(false) - const [numPreguntaActual, setNumPreguntaActual] = useState(0) - const [arPreg, setArPreg] = useState([]) + const [finishGame, setFinishGame] = useState(false) + + const [numRespuestasCorrectas, setNumRespuestasCorrectas] = useState(0) + const [numRespuestasIncorrectas, setNumRespuestasIncorrectas] = useState(0) //Variables para la obtencion y modificacion de estadisticas del usuario const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; @@ -44,8 +45,10 @@ const Juego = ({isLogged, username, numPreguntas}) => { //Control de las estadísticas const updateCorrectAnswers = async () => { try { - const response = await axios.get(`${apiEndpoint}/updateCorrectAnswers?username=${username}`); - console.log('Respuesta correcta actualizada con éxito:', response.data); + //const response = await axios.get(`${apiEndpoint}/updateCorrectAnswers?username=${username}`); + const params = {username: {username}, numAnswers: {numRespuestasCorrectas}}; + const response = await axios.get(`${apiEndpoint}/updateCorrectAnswers?params=${params}`); + console.log('Respuestas correctas actualizada con éxito:', response.data); // Realizar otras acciones según sea necesario } catch (error) { console.error('Error al actualizar la respuesta correcta:', error); @@ -55,7 +58,9 @@ const Juego = ({isLogged, username, numPreguntas}) => { const updateIncorrectAnswers = async () => { try { - const response = await axios.get(`${apiEndpoint}/updateIncorrectAnswers?username=${username}`); + //const response = await axios.get(`${apiEndpoint}/updateIncorrectAnswers?username=${username}`); + const params = {username: {username}, numAnswers: {numRespuestasIncorrectas}}; + const response = await axios.get(`${apiEndpoint}/updateIncorrectAnswers?params=${params}`); console.log('Respuesta incorrecta actualizada con éxito:', response.data); } catch (error) { console.error('Error al actualizar la respuesta incorrecta:', error); @@ -124,11 +129,13 @@ const Juego = ({isLogged, username, numPreguntas}) => { if(respuesta == resCorr){ console.log("entro a respuesta correcta") //Aumenta en 1 en las estadisticas de juegos ganado - updateCorrectAnswers(); + setNumRespuestasCorrectas(numRespuestasCorrectas++); + //updateCorrectAnswers(); setVictoria(true) } else{ - updateIncorrectAnswers(); + setNumRespuestasIncorrectas(numRespuestasIncorrectas++); + //updateIncorrectAnswers(); setVictoria(false) } //storeResult(victoria) @@ -205,15 +212,16 @@ const Juego = ({isLogged, username, numPreguntas}) => { console.log("termina descolorear") } - //Función que finaliza la partida (redirigir/mostrar stats...) - function finishGame(){ - updateCompletedGames(); - //TODO - } + //Primer render para un comportamiento diferente + useEffect(() => { + updateCompletedGames() + }, [finishGame]) //Funcion que se llama al hacer click en el boton Siguiente const clickSiguiente = () => { if(numPreguntaActual==numPreguntas){ + setFinishGame(true) + setReady(false) finishGame() return } @@ -226,23 +234,40 @@ const Juego = ({isLogged, username, numPreguntas}) => { setPausarTemporizador(false); } + //Funcion que se llama al hacer click en el boton Siguiente + const clickFinalizar = () => { + //updateCompletedGames(); + updateCorrectAnswers(); + updateIncorrectAnswers(); + //almacenar aqui partida jugada a estadisticas + //y lo que se quiera + } + const handleRestart = () => { setRestartTemporizador(false); // Cambia el estado de restart a false, se llama aqui desde Temporizador.js }; + return ( <Container component="main" maxWidth="xs" sx={{ marginTop: 4 }}> - <div className="numPregunta"> <p> {numPreguntaActual} / {numPreguntas} </p> </div> - <Temporizador restart={restartTemporizador} tiempoInicial={20} tiempoAcabado={cambiarColorBotones} pausa={pausarTemporizador} handleRestart={handleRestart}/> - <h2> {pregunta} </h2> - <div className="button-container"> - <button id="boton1" className="button" onClick={() => botonRespuesta(resFalse[1])}> {resFalse[1]}</button> - <button id="boton2" className="button" onClick={() => botonRespuesta(resFalse[2])}> {resFalse[2]}</button> - <button id="boton3" className="button" onClick={() => botonRespuesta(resFalse[0])}> {resFalse[0]}</button> - <button id="boton4" className="button" onClick={() => botonRespuesta(resFalse[3])}> {resFalse[3]}</button> - </div> - {ready ? <button id="botonSiguiente" className="button" onClick={() =>clickSiguiente()} > SIGUIENTE</button> : <></>} + {ready ? <> + <div className="numPregunta"> <p> {numPreguntaActual} / {numPreguntas} </p> </div> + <Temporizador restart={restartTemporizador} tiempoInicial={20} tiempoAcabado={cambiarColorBotones} pausa={pausarTemporizador} handleRestart={handleRestart}/> + <h2> {pregunta} </h2> + <div className="button-container"> + <button id="boton1" className="button" onClick={() => botonRespuesta(resFalse[1])}> {resFalse[1]}</button> + <button id="boton2" className="button" onClick={() => botonRespuesta(resFalse[2])}> {resFalse[2]}</button> + <button id="boton3" className="button" onClick={() => botonRespuesta(resFalse[0])}> {resFalse[0]}</button> + <button id="boton4" className="button" onClick={() => botonRespuesta(resFalse[3])}> {resFalse[3]}</button> + <button id="botonSiguiente" className="button" onClick={() =>clickSiguiente()} > SIGUIENTE</button> + </div> + </> + : <h2> CARGANDO... </h2>} + {finishGame ? <> + <h2> PARTIDA FINALIZADA </h2> + <button id="botonSiguiente" className="button" onClick={() =>clickFinalizar()} > FINALIZAR PARTIDA</button> + </> : <></>} </Container> ); }; diff --git a/webapp/src/components/Temporizador.test.js b/webapp/src/components/Temporizador.test.js index 1a22c36..3ad7fdd 100644 --- a/webapp/src/components/Temporizador.test.js +++ b/webapp/src/components/Temporizador.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import Temporizador from './Temporizador'; describe('Temporizador', () => { @@ -13,37 +13,41 @@ describe('Temporizador', () => { it('decreases countdown time when not paused', () => { jest.useFakeTimers(); const tiempoInicial = 60; - render(<Temporizador tiempoInicial={tiempoInicial} />); - jest.advanceTimersByTime(1000); + render(<Temporizador tiempoInicial={tiempoInicial} pausa={false} />); + act(() => { + jest.advanceTimersByTime(1000); + }); const updatedCountdownElement = screen.getByText(tiempoInicial - 1); expect(updatedCountdownElement).toBeInTheDocument(); - jest.useRealTimers(); }); it('stops countdown time when paused', () => { jest.useFakeTimers(); const tiempoInicial = 60; render(<Temporizador tiempoInicial={tiempoInicial} pausa={true} />); - jest.advanceTimersByTime(1000); + act(() => { + jest.advanceTimersByTime(1000); + }); const updatedCountdownElement = screen.getByText(tiempoInicial); expect(updatedCountdownElement).toBeInTheDocument(); - jest.useRealTimers(); }); it('restarts countdown time when restart prop changes', () => { jest.useFakeTimers(); const tiempoInicial = 60; const { rerender } = render(<Temporizador tiempoInicial={tiempoInicial} />); - jest.advanceTimersByTime(1000); + act(() => { + jest.advanceTimersByTime(1000); + }); const updatedCountdownElement = screen.getByText(tiempoInicial - 1); expect(updatedCountdownElement).toBeInTheDocument(); // Simulate restart by changing the restart prop - rerender(<Temporizador tiempoInicial={tiempoInicial} restart={true} />); + rerender(<Temporizador tiempoInicial={tiempoInicial} restart={true} + handleRestart={jest.fn()} />); // Countdown should restart const restartedCountdownElement = screen.getByText(tiempoInicial); expect(restartedCountdownElement).toBeInTheDocument(); - jest.useRealTimers(); }); }); \ No newline at end of file