diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da425046..2dd3976b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,16 @@ jobs: node-version: 20 - run: npm --prefix users/authservice ci - run: npm --prefix users/userservice ci + - run: npm --prefix questions/template-questions ci + - run: npm --prefix recordhistory ci - run: npm --prefix gatewayservice ci - run: npm --prefix webapp ci - run: npm --prefix users/authservice test -- --coverage - run: npm --prefix users/userservice test -- --coverage + - run: npm --prefix questions/template-questions test -- --coverage + - run: npm --prefix recordhistory test -- --coverage + - run: npm --prefix gatewayservice 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 38ee5e8c..1fb38c72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,46 @@ on: types: [published] jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm --prefix users/authservice ci + - run: npm --prefix users/userservice ci + - run: npm --prefix questions/template-questions ci + - run: npm --prefix recordhistory ci + - run: npm --prefix gatewayservice ci + - run: npm --prefix webapp ci + - run: npm --prefix users/authservice test -- --coverage + - run: npm --prefix users/userservice test -- --coverage + - run: npm --prefix questions/template-questions test -- --coverage + - run: npm --prefix recordhistory test -- --coverage + - run: npm --prefix gatewayservice test -- --coverage + - run: npm --prefix webapp test -- --coverage + - name: Analyze with SonarCloud + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + e2e-tests: + needs: [unit-tests] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm --prefix users/authservice install + - run: npm --prefix users/userservice install + - run: npm --prefix questions/template-questions install + - run: npm --prefix recordhistory install + - run: npm --prefix gatewayservice install + - run: npm --prefix webapp install + - run: npm --prefix webapp run build + - run: npm --prefix webapp run test:e2e docker-push-webapp: name: Push webapp Docker Image to GitHub Packages runs-on: ubuntu-latest @@ -96,6 +136,10 @@ jobs: packages: write 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 3c6d6b87..668de4f3 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -3,6 +3,11 @@ const axios = require('axios'); const cors = require('cors'); const promBundle = require('express-prom-bundle'); +//libraries required for OpenAPI-Swagger +const swaggerUi = require('swagger-ui-express'); +const fs = require("fs") +const YAML = require('yaml') + const app = express(); const port = 8000; @@ -70,6 +75,31 @@ app.post('/getRecords', async (req, res) => { } }); +app.get('/getAllUsers', async (req, res) => { + try { + const userResponse = await axios.get(userServiceUrl+'/getAllUsers', req.body); + res.json(userResponse.data); + } catch (error) { + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + +// Read the OpenAPI YAML file synchronously +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 00000000..e413af8b --- /dev/null +++ b/gatewayservice/openapi.yaml @@ -0,0 +1,273 @@ +openapi: 3.0.0 +info: + title: Gatewayservice API + description: Gateway OpenAPI specification. + version: 0.2.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 + + paths: + /questions: + post: + summary: Create a question and its answers. + operationId: questions + responses: + '200': + description: Question created successfully. + content: + application/json: + schema: + type: object + properties: + pregunta: + type: string + description: Question text + example: Who was the author of The Hunger Games? + correcta: + type: string + description: Correct answer + example: Suzzanne Collins + incorrectas: + type: array + items: + type: string + example: + - Sarah J. Maas + - Alice Oseman + - Veronica Roth + '400': + description: Failed to create question. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Wikidata error conection. + example: Cannot connect with Wikidata + + /getRecords: + post: + summary: Obtains the records of the user. + operationId: userRecords + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID. + example: student + responses: + '200': + description: Get Records successful. + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + description: Username. + example: student + correctQuestions: + type: number + description: Number of correct questions. + example: 2 + totalQuestions: + type: number + description: Number of questions. + example: 5 + totalTime: + type: number + description: Time spent playing in seconds. + example: 26 + doneAt: + type: string + description: When was the game done. + example: '2024-03-17T20:47:23.935Z' + + + '400': + description: User is not valid or doesn't exist. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Shows the error info.. + example: Invalid user + /getAllUsers: + get: + summary: Obtains the information of all users. + operationId: allUsers + responses: + '200': + description: Get Users successful. + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: Username of the user. + example: student + createdAt: + type: string + description: When the account was created + example: '2024-03-05T15:10:51.313Z' + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json index fc5f2d60..95b84d7b 100644 --- a/gatewayservice/package-lock.json +++ b/gatewayservice/package-lock.json @@ -12,7 +12,10 @@ "axios": "^1.6.5", "cors": "^2.8.5", "express": "^4.18.2", - "express-prom-bundle": "^7.0.0" + "express-openapi": "^12.1.3", + "express-prom-bundle": "^7.0.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.1" }, "devDependencies": { "jest": "^29.7.0", @@ -1277,6 +1280,37 @@ "node": ">= 0.6" } }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1333,7 +1367,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -1474,8 +1507,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/bintrees": { "version": "1.0.2", @@ -1510,7 +1542,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1751,8 +1782,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -1942,6 +1972,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/difunc": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/difunc/-/difunc-0.0.4.tgz", + "integrity": "sha512-zBiL4ALDmviHdoLC0g0G6wVme5bwAow9WfhcZLLopXCAWgg3AEf7RYTs2xugszIGulRHzEVDF/SHl9oyQU07Pw==", + "dependencies": { + "esprima": "^4.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2015,7 +2053,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2121,6 +2158,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-normalize-query-params-middleware": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/express-normalize-query-params-middleware/-/express-normalize-query-params-middleware-0.5.1.tgz", + "integrity": "sha512-KUBjEukYL9KJkrphVX3ZgMHgMTdgaSJe+FIOeWwJIJpCw8UZQPIylt0MYddSyUwbms4LQ8RC4wmavcLUP9uduA==" + }, + "node_modules/express-openapi": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/express-openapi/-/express-openapi-12.1.3.tgz", + "integrity": "sha512-F570dVC5ENSkLu1SpDFPRQ13Y3a/7Udh0rfHyn3O1QrE81fPmlhnAo1JRgoNtbMRJ6goHNymxU1TVSllgFZBlQ==", + "dependencies": { + "express-normalize-query-params-middleware": "^0.5.0", + "openapi-framework": "^12.1.3", + "openapi-types": "^12.1.3" + } + }, "node_modules/express-prom-bundle": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-7.0.0.tgz", @@ -2138,6 +2190,11 @@ "prom-client": ">=15.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2264,11 +2321,18 @@ "node": ">= 0.6" } }, + "node_modules/fs-routes": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/fs-routes/-/fs-routes-12.1.3.tgz", + "integrity": "sha512-Vwxi5StpKj/pgH7yRpNpVFdaZr16z71KNTiYuZEYVET+MfZ31Zkf7oxUmNgyZxptG8BolRtdMP90agIhdyiozg==", + "peerDependencies": { + "glob": ">=7.1.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2349,7 +2413,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2526,7 +2589,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2563,6 +2625,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-dir/-/is-dir-1.0.0.tgz", + "integrity": "sha512-vLwCNpTNkFC5k7SBRxPubhOCryeulkOsSkjbGyZ8eOzZmzMS+hSEO/Kn9ZOVhFNAlRZTFc4ZKql48hESuYUPIQ==" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3328,7 +3395,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3355,6 +3421,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3403,6 +3474,11 @@ "node": ">=8" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3552,7 +3628,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3643,7 +3718,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -3663,6 +3737,97 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-default-setter": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-default-setter/-/openapi-default-setter-12.1.3.tgz", + "integrity": "sha512-wHKwvEuOWwke5WcQn8pyCTXT5WQ+rm9FpJmDeEVECEBWjEyB/MVLYfXi+UQeSHTTu2Tg4VDHHmzbjOqN6hYeLQ==", + "dependencies": { + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-framework": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-framework/-/openapi-framework-12.1.3.tgz", + "integrity": "sha512-p30PHWVXda9gGxm+t/1X2XvEcufW1YhzeDQwc5SsgDnBXt8gkuu1SwrioGJ66wxVYEzfSRTTf/FMLhI49ut8fQ==", + "dependencies": { + "difunc": "0.0.4", + "fs-routes": "^12.1.3", + "glob": "*", + "is-dir": "^1.0.0", + "js-yaml": "^3.10.0", + "openapi-default-setter": "^12.1.3", + "openapi-request-coercer": "^12.1.3", + "openapi-request-validator": "^12.1.3", + "openapi-response-validator": "^12.1.3", + "openapi-schema-validator": "^12.1.3", + "openapi-security-handler": "^12.1.3", + "openapi-types": "^12.1.3", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-jsonschema-parameters": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-12.1.3.tgz", + "integrity": "sha512-aHypKxWHwu2lVqfCIOCZeJA/2NTDiP63aPwuoIC+5ksLK5/IQZ3oKTz7GiaIegz5zFvpMDxDvLR2DMQQSkOAug==", + "dependencies": { + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-request-coercer": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-request-coercer/-/openapi-request-coercer-12.1.3.tgz", + "integrity": "sha512-CT2ZDhBmAZpHhAzHhEN+/J5oMK3Ds99ayLLdXh2Aw1DCcn72EM8VuIGVwG5fSjvkMsgtn7FgltFosHqeM6PRFQ==", + "dependencies": { + "openapi-types": "^12.1.3", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-request-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-request-validator/-/openapi-request-validator-12.1.3.tgz", + "integrity": "sha512-HW1sG00A9Hp2oS5g8CBvtaKvRAc4h5E4ksmuC5EJgmQ+eAUacL7g+WaYCrC7IfoQaZrjxDfeivNZUye/4D8pwA==", + "dependencies": { + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "content-type": "^1.0.4", + "openapi-jsonschema-parameters": "^12.1.3", + "openapi-types": "^12.1.3", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-response-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-12.1.3.tgz", + "integrity": "sha512-beZNb6r1SXAg1835S30h9XwjE596BYzXQFAEZlYAoO2imfxAu5S7TvNFws5k/MMKMCOFTzBXSjapqEvAzlblrQ==", + "dependencies": { + "ajv": "^8.4.0", + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-schema-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-12.1.3.tgz", + "integrity": "sha512-xTHOmxU/VQGUgo7Cm0jhwbklOKobXby+/237EG967+3TQEYJztMgX9Q5UE2taZKwyKPUq0j11dngpGjUuxz1hQ==", + "dependencies": { + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", + "lodash.merge": "^4.6.1", + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-security-handler": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-security-handler/-/openapi-security-handler-12.1.3.tgz", + "integrity": "sha512-25UTAflxqqpjCLrN6rRhINeM1L+MCDixMltiAqtBa9Zz/i7UkWwYwdzqgZY3Cx3vRZElFD09brYxo5VleeP3HQ==", + "dependencies": { + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3753,7 +3918,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3886,6 +4050,14 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", @@ -3953,6 +4125,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4171,8 +4351,7 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/stack-utils": { "version": "2.0.6", @@ -4389,6 +4568,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.15.0.tgz", + "integrity": "sha512-1zd4cNaUayXCWFSdBGNB+CYGISbe7M4FSgPqOjrgqKi1oEZfXzrOrjIHa0jHf5uSDN0X/mXmhFgKR9Jrr+fvqQ==" + }, + "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", @@ -4447,6 +4645,11 @@ "node": ">=0.6" } }, + "node_modules/ts-log": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.5.tgz", + "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4523,6 +4726,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-value-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.2.0.tgz", @@ -4605,8 +4816,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -4636,6 +4846,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 12db5c01..b05ba672 100644 --- a/gatewayservice/package.json +++ b/gatewayservice/package.json @@ -21,7 +21,10 @@ "axios": "^1.6.5", "cors": "^2.8.5", "express": "^4.18.2", - "express-prom-bundle": "^7.0.0" + "express-openapi": "^12.1.3", + "express-prom-bundle": "^7.0.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.1" }, "devDependencies": { "jest": "^29.7.0", diff --git a/questions/template-questions/baseQuestions.json b/questions/template-questions/baseQuestions.json index a85619cd..fe4dde4c 100644 --- a/questions/template-questions/baseQuestions.json +++ b/questions/template-questions/baseQuestions.json @@ -1,28 +1,59 @@ { - "questions":[ - { - "id": "Q123", - "text": "¿Cuál es la capital de Francia?", - "answers": [ - { - "true": "París", - "false1": "Madrid", - "false2": "Berlín", - "false3": "Londres" - } - ] - }, - { - "id": "Q1234", - "text": "¿Cuál es la capital de España?", - "answers": [ - { - "true": "Madrid", - "false1": "Paris", - "false2": "Berlín", - "false3": "Londres" - } - ] - } - ] +"jsonPreg": [ + { + "textStart": "¿Cuál es la capital de ", + "textEnd": "?", + "queryCorrect": "SELECT ?preguntaLabel ?respuestaLabel WHERE {?pregunta wdt:P31 wd:Q6256. ?pregunta wdt:P36 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language '[AUTO_LANGUAGE],es,en'.}}" + }, + { + "textStart": "¿Quién es el director de la película ", + "textEnd": "?", + "queryCorrect": "SELECT DISTINCT ?preguntaLabel ?respuestaLabel WHERE {?pregunta wdt:P31 wd:Q11424. ?pregunta wdt:P57 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language '[AUTO_LANGUAGE],es,en'. }} LIMIT 100" + }, + { + "textStart": "¿Quién es el autor del libro ", + "textEnd": "?", + "queryCorrect": "SELECT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P31 wd:Q7725634. ?pregunta wdt:P50 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". }} LIMIT 100" + }, + { + "textStart": "¿Quién interpreta la canción ", + "textEnd": "?", + "queryCorrect": "SELECT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P31 wd:Q134556. ?pregunta wdt:P175 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". } } LIMIT 100" + }, + { + "textStart": "¿Dónde se encuentra el monumento ", + "textEnd": "?", + "queryCorrect": "SELECT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P31 wd:Q570116. ?pregunta wdt:P17 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". } } LIMIT 100" + }, + { + "textStart": "¿Cuál es la población de ", + "textEnd": "?", + "queryCorrect": "SELECT DISTINCT ?preguntaLabel (CONCAT(REPLACE(STR((ROUND(?respuesta / 1000) * 1000)), \"(\\\\d)(?=(\\\\d{3})+$)\", \"$1.\"), \"\") AS ?respuestaLabel) WHERE { ?pregunta wdt:P31 wd:Q6256. ?pregunta wdt:P1082 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". }}" + } + , + { + "textStart": "¿Cuál es el gentilicio de ", + "textEnd": "?", + "queryCorrect": "SELECT DISTINCT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P31 wd:Q2074737; wdt:P17 wd:Q29; wdt:P1549 ?respuesta. OPTIONAL { ?respuesta rdfs:label ?respuestaLabel. FILTER(LANG(?respuestaLabel) = \"es\") } SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". }}" + } + , + { + "textStart": "¿A qué grupo pertenece ", + "textEnd": "?", + "queryCorrect": "SELECT DISTINCT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P106 wd:Q177220; wdt:P463 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". }} LIMIT 100" + } + , + { + "textStart": "¿Dónde nació el compositor ", + "textEnd": "?", + "queryCorrect": "SELECT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P106 wd:Q36834; wdt:P19 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"es,en\" }} LIMIT 100" + } + , + { + "textStart": "En qué país se encuentra la atracción turística ", + "textEnd": "?", + "queryCorrect": "SELECT ?preguntaLabel ?respuestaLabel WHERE { ?pregunta wdt:P31 wd:Q570116; wdt:P17 ?respuesta. SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],es,en\". }} LIMIT 200" + } + +] } diff --git a/questions/template-questions/question-service.js b/questions/template-questions/question-service.js index f094cd64..ff7fcaac 100644 --- a/questions/template-questions/question-service.js +++ b/questions/template-questions/question-service.js @@ -1,6 +1,11 @@ const express = require('express'); const bodyParser = require('body-parser'); const Wikidata = require('./wikidata-query'); +//import { readFile } from 'fs/promises' + +// leemos el archivo usando top-level await y con +// codificación utf-8 +//const file = await readFile('./baseQuestions.json', 'utf-8') const app = express(); const port = 8004; diff --git a/recordhistory/record-service.js b/recordhistory/record-service.js index 33ae28b3..afde2851 100644 --- a/recordhistory/record-service.js +++ b/recordhistory/record-service.js @@ -45,8 +45,7 @@ app.post('/addRecord', async (req, res) => { app.post('/getRecords', async (req, res) => { try { const username = req.body.username; - - const records = await Record.find({ user_id: username }); + const records = await Record.find({ user_id: username }).sort({doneAt: -1}); res.json(records); } catch (error) { diff --git a/recordhistory/record-service.test.js b/recordhistory/record-service.test.js new file mode 100644 index 00000000..54ae852c --- /dev/null +++ b/recordhistory/record-service.test.js @@ -0,0 +1,79 @@ +const request = require('supertest'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const Record = require('./record-model'); + +let mongoServer; +let app; + +//Record de prueba +const newRecord = { + user_id: 'testuser', + correctQuestions: 8, + totalQuestions: 10, + totalTime: 120, + }; + +// Meter varios records en la base de datos +const records = [ + { + user_id: 'testuser', + correctQuestions: 8, + totalQuestions: 10, + totalTime: 120, + }, + { + user_id: 'testuser', + correctQuestions: 6, + totalQuestions: 12, + totalTime: 90, + }, + ]; + +//Iniciar la conexión con la base de datos +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + process.env.MONGODB_URI = mongoUri; + app = require('./record-service'); + }); + +//Cerrar la conexión con la base de datos +afterAll(async () => { + app.close(); + await mongoServer.stop(); +}); + +//Borrar todos los records antes de cada test +beforeEach(async () => { + await Record.deleteMany(); + }); + +describe('Record Service', () => { + it('Should add a new record to an user using POST /addRecord', async () => { + const response = await request(app).post('/addRecord').send(newRecord); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('user_id', 'testuser'); + }); + + it('Should get all the records of an user using POST /getRecords', async () => { + await Record.insertMany(records); + const response = await request(app).post('/getRecords').send({ username: 'testuser' }); + expect(response.status).toBe(200); + expect(response.body).toHaveLength(2); + //Compruebo que los dos records pertenecen al mismo usuario + expect(response.body[0]).toHaveProperty('user_id', 'testuser'); + expect(response.body[1]).toHaveProperty('user_id', 'testuser'); + + //Compruebo que los records tienen el número de preguntas correctas correcto + expect(response.body[0]).toHaveProperty('correctQuestions', 8); + expect(response.body[1]).toHaveProperty('correctQuestions', 6); + + //Compruebo que los records tienen el número total de preguntas correcto + expect(response.body[0]).toHaveProperty('totalQuestions', 10); + expect(response.body[1]).toHaveProperty('totalQuestions', 12); + + //Compruebo que los records tienen el número total de preguntas correcto + expect(response.body[0]).toHaveProperty('totalTime', 120); + expect(response.body[1]).toHaveProperty('totalTime', 90); + }); +}); \ No newline at end of file diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js index 49a7f612..90777aa3 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -23,12 +23,27 @@ function validateRequiredFields(req, requiredFields) { } } +function validateFieldsNotEmpty(req) { + + if (req.body.username.trim().length === 0) { + throw new Error(`El nombre de usuario no puede estar vacío`); + } + if (req.body.password.trim().length === 0) { + throw new Error(`La contraseña no puede estar vacía`); + } + +} + // Route for user login app.post('/login', async (req, res) => { try { // Check if required fields are present in the request body validateRequiredFields(req, ['username', 'password']); - + try { + validateFieldsNotEmpty(req); + } catch (validationError) { + return res.status(400).json({ error: validationError.message }); + } const { username, password } = req.body; // Find the user by username in the database @@ -42,7 +57,7 @@ app.post('/login', async (req, res) => { // Respond with the token and user information res.json({ token: token, username: username, createdAt: user.createdAt }); } else { - res.status(401).json({ error: 'Invalid credentials' }); + res.status(401).json({ error: 'Credenciales inválidas' }); } } catch (error) { res.status(500).json({ error: 'Internal Server Error' }); diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js index 06b99b9c..f5edb5cc 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -26,11 +26,29 @@ function validateRequiredFields(req, requiredFields) { } } +function validateFieldsNotEmpty(req) { + + if (req.body.username.trim().length === 0) { + throw new Error(`El nombre de usuario no puede estar vacío`); + } + if (req.body.password.trim().length === 0) { + throw new Error(`La contraseña no puede estar vacía`); + } + +} + app.post('/adduser', async (req, res) => { try { // Check if required fields are present in the request body validateRequiredFields(req, ['username', 'password']); + validateFieldsNotEmpty(req); + + const user = await User.findOne({username: req.body.username}); + console.log(user); + if (user) { + throw new Error(`Este nombre de usuario está en uso`); + } // Encrypt the password before saving it const hashedPassword = await bcrypt.hash(req.body.password, 10); @@ -46,6 +64,15 @@ app.post('/adduser', async (req, res) => { } }); + app.get('/getAllUsers', async (req, res) => { + try { + const users = await User.find().select('username createdAt');; + + res.json(users); + } catch (error) { + res.status(400).json({ error: error.message }); + }}); + const server = app.listen(port, () => { console.log(`User Service listening at http://localhost:${port}`); }); diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 7fbcd5fc..94d91b5f 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -16,6 +16,7 @@ "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", "bootstrap": "^5.3.3", + "history": "^5.3.0", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -14163,6 +14164,14 @@ "node": ">= 8" } }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -41849,6 +41858,14 @@ } } }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", diff --git a/webapp/package.json b/webapp/package.json index 58d0ed81..a6faac33 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -11,6 +11,7 @@ "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", "bootstrap": "^5.3.3", + "history": "^5.3.0", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/webapp/src/App.css b/webapp/src/App.css index 776755da..65b89654 100644 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -45,3 +45,20 @@ body { background-image: url('../public/fondo.png'); background-size: cover; } +#btSaberYGanar { + height: 3.5em; + border: none; + padding: 0; + background: none; + cursor: pointer; + margin-left: 45vw; +} + +#btSaberYGanar img { + width: 100%; + height: 100%; +} + +#btLogout { + margin-right: 0.5em; +} \ No newline at end of file diff --git a/webapp/src/App.js b/webapp/src/App.js index 39a287ac..fc2aa789 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -1,8 +1,7 @@ -import React, { useState } from 'react'; + import AddUser from './components/AddUser'; import Login from './components/Login'; import Container from '@mui/material/Container'; -import Typography from '@mui/material/Typography'; import { BrowserRouter as Router, Route, Routes, Link, Navigate } from 'react-router-dom'; import HomeScreen from './components/HomeScreen'; import Game from './components/Game'; @@ -10,27 +9,43 @@ import Welcome from './components/Welcome'; import ImagenA from './LogoSaberYGanar2.png'; import History from './components/History.js'; import './App.css'; - +import { useNavigate } from "react-router-dom"; +import { useEffect } from 'react'; function App() { - const [showLogin, setShowLogin] = useState(true); + const navigate = useNavigate(); + const token = localStorage.getItem('token'); - const handleToggleView = () => { - setShowLogin(!showLogin); + const logout = () => { + localStorage.removeItem('token'); + localStorage.removeItem('numQuestions'); + document.getElementById('btLogout').style.display = 'none'; + navigate("/"); }; - const [isAuthenticated, setAuthenticated] = useState(false); - const handleLogin = () => { - // Lógica para autenticar al usuario (por ejemplo, verificar credenciales) - setAuthenticated(true); + const volverHome = () => { + if (token!=null) { + navigate("/home"); + } + else { + navigate("/"); + } }; + useEffect(() => { + document.getElementById('btLogout').style.display = 'none'; + }, []) + return (
-
); } + + /* function PrivateRoute({ isAuthenticated, ...props }) { return isAuthenticated ? : ; } */ diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js index 5e3b7314..384c5148 100644 --- a/webapp/src/App.test.js +++ b/webapp/src/App.test.js @@ -1,8 +1,22 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen,waitFor } from '@testing-library/react'; +import { BrowserRouter} from 'react-router-dom'; import App from './App'; -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/Welcome to the 2024 edition of the Software Architecture course/i); - expect(linkElement).toBeInTheDocument(); +describe('App component', () => { + + it('renders learn react link',async () => { + render( + + + + ); + + // Esperamos que se muestre el boton de cerrar sesión + await waitFor(() => { + const navElement = screen.getByRole('navigation'); + expect(navElement).toBeInTheDocument(); + expect(screen.getByText(/Cerrar sesión/i)).toBeInTheDocument(); + }); + }); + }); diff --git a/webapp/src/components/AddUser.test.js b/webapp/src/components/AddUser.test.js index 87334886..827d8df5 100644 --- a/webapp/src/components/AddUser.test.js +++ b/webapp/src/components/AddUser.test.js @@ -3,6 +3,7 @@ import { render, fireEvent, screen, waitFor } from '@testing-library/react'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import AddUser from './AddUser'; +import { BrowserRouter} from 'react-router-dom'; const mockAxios = new MockAdapter(axios); @@ -12,7 +13,11 @@ describe('AddUser component', () => { }); it('should add user successfully', async () => { - render(); + render( + + + + ); const usernameInput = screen.getByLabelText(/Username/i); const passwordInput = screen.getByLabelText(/Password/i); @@ -35,7 +40,11 @@ describe('AddUser component', () => { }); it('should handle error when adding user', async () => { - render(); + render( + + + + ); const usernameInput = screen.getByLabelText(/Username/i); const passwordInput = screen.getByLabelText(/Password/i); diff --git a/webapp/src/components/Game.js b/webapp/src/components/Game.js index 3d870648..8bab445e 100644 --- a/webapp/src/components/Game.js +++ b/webapp/src/components/Game.js @@ -4,7 +4,7 @@ import Grid from '@mui/material/Grid'; import axios from 'axios'; import { useEffect } from 'react'; import './Game.css'; -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { jwtDecode } from 'jwt-decode'; const StyledContainer = styled(Container)({ @@ -15,7 +15,7 @@ const StyledContainer = styled(Container)({ const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; -const Game = ({numQuestions}) => { +const Game = () => { const [respuestas, setRespuestas] = useState(Array(4).fill({ data: '', isCorrect: '' })); const [textoPregunta, setTextoPregunta] = useState('Cargando...'); const [preguntasAcertadas, setPreguntasAcertadas] = useState(0); @@ -25,14 +25,10 @@ const Game = ({numQuestions}) => { const [tiempoTotal, setTiempoTotal] = useState(0); var tiempoInicial = 0; var tiempoFinal = 0; - // Obtén el token del almacenamiento local -const token = localStorage.getItem('token'); -// Decodifica el token para obtener la información del usuario -const decoded = jwtDecode(token); + const navigate = useNavigate(); -// Accede al nombre de usuario desde la información decodificada -const username = decoded.username; + const numQuestions = localStorage.getItem('numQuestions'); // Función para iniciar el tiempo const startTime = () => { @@ -176,6 +172,14 @@ const username = decoded.username; const addRecord = async () => { try { + // Obtén el token del almacenamiento local + let token = localStorage.getItem('token'); + + // Decodifica el token para obtener la información del usuario + let decoded = jwtDecode(token); + + // Accede al nombre de usuario desde la información decodificada + let username = decoded.username; //Llamada al post para obtener los resultados de Wikidata await axios.post(`${apiEndpoint}/addRecord`, { user_id: username, @@ -184,16 +188,28 @@ const username = decoded.username; totalTime: tiempoTotal }); - window.location.href = '/home'; + navigate("/home"); } catch (error) { console.log(error.response.data.error); } }; + const checkUserLogin = () => { + let token = localStorage.getItem('token'); + if (token==null) { + navigate("/"); + } + else { + if (numQuestions==null) { + localStorage.setItem('numQuestions', 10); + } + addPregunta(); + } + } useEffect(() => { - addPregunta(); + checkUserLogin(); }, []) return ( @@ -202,7 +218,7 @@ const username = decoded.username;

Has acertado {preguntasAcertadas}/{numQuestions} preguntas en {tiempoTotal} segundos

- +
) : ( @@ -211,7 +227,7 @@ const username = decoded.username;

Pregunta Nº{numPreguntas}

-
{contadorGlobal}
+
{contadorGlobal}
)} diff --git a/webapp/src/components/History.css b/webapp/src/components/History.css index 846063ff..403fd514 100644 --- a/webapp/src/components/History.css +++ b/webapp/src/components/History.css @@ -1,36 +1,42 @@ +.table-container { + overflow-y: auto; + max-height: 20em; + position: relative; /* Necesario para fijar el thead */ +} #table { - width: 100%; - border-collapse: separate; - } - - /* Estilos para las celdas de encabezado */ - th { - background-color: #59b494d1; - color: #333; - padding: 10px; - } - - /* Estilos para las celdas de datos */ - td { - padding: 10px; - } - - /* Estilos para las filas impares */ - tr:nth-child(odd) { - background-color: #bee5c480; - } - - /* Estilos para las filas pares */ - tr:nth-child(even) { - background-color: #d3f0e8ad; - } - - td, th { - font-size: 1em; - } + border-collapse: separate; + width: 100%; +} - h1 { - text-align: center; - } - \ No newline at end of file +#table th, +#table td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +#table th { + background-color: #30ab7ff5; + color: #333; +} + +#table thead th { + position: sticky; + top: 0; + z-index: 1; +} + +/* Estilos para las filas impares */ +#table tbody tr:nth-child(odd) { + background-color: #bee5c480; +} + +/* Estilos para las filas pares */ +#table tbody tr:nth-child(even) { + background-color: #d3f0e8ad; +} + +h1 { + text-align: center; +} \ No newline at end of file diff --git a/webapp/src/components/History.js b/webapp/src/components/History.js index 220e5f70..18eb3a64 100644 --- a/webapp/src/components/History.js +++ b/webapp/src/components/History.js @@ -4,20 +4,22 @@ import { jwtDecode } from 'jwt-decode'; import axios from 'axios'; import './History.css'; import { useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const History = () => { const [historyData, setHistoryData] = useState([]); + const navigate = useNavigate(); + const getHistory = async () => { try { - let result = await axios.post(`${apiEndpoint}/getRecords`, { - username: username, - }); + let token = localStorage.getItem('token'); + let decoded = jwtDecode(token); + let username = decoded.username; + let result = await axios.post(`${apiEndpoint}/getRecords`,{ username: username }); setHistoryData(result.data); - console.log(result.data) } catch (error) { console.log(error.response.data.error); @@ -25,21 +27,30 @@ const History = () => { }; useEffect(() => { - getHistory(); + checkUserLogin(); }, []) - const token = localStorage.getItem('token'); - const decoded = jwtDecode(token); - const username = decoded.username; + const volverHome = () => { - window.location.href = '/home'; - }; + navigate('/home'); + }; + + const checkUserLogin = () => { + let token = localStorage.getItem('token'); + if (token==null) { + navigate("/"); + } + else { + getHistory(); + } + } return (

Historial de partidas



+
@@ -60,7 +71,9 @@ const History = () => { ))}
- +
+

+
) }; diff --git a/webapp/src/components/HomeScreen.css b/webapp/src/components/HomeScreen.css index 7f0ea26a..f9278bc3 100644 --- a/webapp/src/components/HomeScreen.css +++ b/webapp/src/components/HomeScreen.css @@ -5,3 +5,8 @@ flex-direction: column; align-items: center; } + +#formNumPreguntas { + display:flex; + flex-direction: column; +} \ No newline at end of file diff --git a/webapp/src/components/HomeScreen.js b/webapp/src/components/HomeScreen.js index 666a29f8..df780a5e 100644 --- a/webapp/src/components/HomeScreen.js +++ b/webapp/src/components/HomeScreen.js @@ -1,42 +1,72 @@ -import React, { useState } from 'react'; -import { Container} from '@mui/material'; -import Game from './Game'; +import React, { useState,useEffect } from 'react'; +import { Container,Slider,TextField} from '@mui/material'; import './HomeScreen.css'; -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; const HomeScreen = () => { - const [mostrarJuego, setMostrarJuego] = useState(false); - const [defecto, setDefecto] = useState("15"); + const [value, setValue] = useState(15); + const navigate = useNavigate(); + const token = localStorage.getItem('token'); const handleStartButtonClick = () => { - setMostrarJuego(true); - }; - - const changeNumber = (event) => { - setDefecto(event.target.value); + localStorage.setItem('numQuestions', value); + navigate("/game"); }; const handleHistoryButton = () => { - window.location.href = '/history'; + navigate("/history"); + } + const checkUserLogin = () => { + if (token==null) { + navigate("/"); + } } + + useEffect(() => { + checkUserLogin(); + document.getElementById('btLogout').style.display = 'inline-block'; + }, []) + + const handleChange = (event, newValue) => { + setValue(newValue); + }; return (
- {mostrarJuego ? ( - // Muestra otro componente o contenido cuando el juego está iniciado - - ) : ( - // Muestra el contenido inicial con el botón "Jugar"
-
+
- -
- - + + { + const newValue = e.target.value === '' ? 0 : Number(e.target.value); + setValue(Math.min(Math.max(newValue, 1), 30)); + }} + /> + + +
+ +
- )}
) diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js index f694ed68..1b6fee82 100644 --- a/webapp/src/components/Login.js +++ b/webapp/src/components/Login.js @@ -2,17 +2,15 @@ import React, { useState } from 'react'; import axios from 'axios'; import { Container, Typography, TextField, Button, Snackbar } from '@mui/material'; -import HomeScreen from './HomeScreen'; import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; const Login = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); - const [loginSuccess, setLoginSuccess] = useState(false); - const [createdAt, setCreatedAt] = useState(''); const [openSnackbar, setOpenSnackbar] = useState(false); - + const navigate = useNavigate(); const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const loginUser = async () => { @@ -20,13 +18,12 @@ const Login = () => { const response = await axios.post(`${apiEndpoint}/login`, { username, password }); // Extract data from the response - const { createdAt: userCreatedAt, token } = response.data; + const { token } = response.data; // Store the token in localStorage - localStorage.setItem('token', token); - setCreatedAt(userCreatedAt); - setLoginSuccess(true); + localStorage.setItem('token', token); setOpenSnackbar(true); + navigate("/home"); } catch (error) { setError(error.response.data.error); } @@ -38,11 +35,6 @@ const Login = () => { return ( - {loginSuccess ? ( -
- -
- ) : (
@@ -77,8 +69,7 @@ const Login = () => {
- - )} +
); }; diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js index af102dcf..854b18b7 100644 --- a/webapp/src/components/Login.test.js +++ b/webapp/src/components/Login.test.js @@ -1,5 +1,6 @@ import React from 'react'; -import { render, fireEvent, screen, waitFor, act } from '@testing-library/react'; +import { render, fireEvent, screen, waitFor, cleanup } from '@testing-library/react'; +import { BrowserRouter} from 'react-router-dom'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import Login from './Login'; @@ -8,55 +9,74 @@ const mockAxios = new MockAdapter(axios); describe('Login component', () => { beforeEach(() => { + localStorage.clear(); mockAxios.reset(); + cleanup(); }); + +//TEST 1 - Test exitoso de login it('should log in successfully', async () => { - render(); + render( + + + + ); 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 a successful response - mockAxios.onPost('http://localhost:8000/login').reply(200, { createdAt: '2024-01-01T12:34:56Z' }); + // Simulamos la respuesta de la petición axios.post + mockAxios.onPost('http://localhost:8000/login').reply(200, { token: 'testToken' }); + + // Simulamos la entrada de un usuario válido en login + fireEvent.change(usernameInput, { target: { value: 'testUser' } }); + fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); - // Simulate user input - await act(async () => { - fireEvent.change(usernameInput, { target: { value: 'testUser' } }); - fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); - fireEvent.click(loginButton); - }); + // Click en el botón de login + fireEvent.click(loginButton); - // 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(); + // Esperamos que se muestre el SnackBar de loggin exitoso + await waitFor(() => { + expect(screen.getByText(/Login successful/i)).toBeInTheDocument(); + }); + + // Verificar que el token ha sido almacenado en localStorage + expect(localStorage.getItem('token')).toBe('testToken'); + + // Verificar que se ha redirigido a /home + expect(window.location.pathname).toBe('/home'); }); + //TEST 2 - Test fallido de login, las credenciales son incorrectas it('should handle error when logging in', async () => { - render(); + render( + + + + ); 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 + // Simular que ha ocurrido un error en el login 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' } }); + // Simulamos la entrada incorrecta de un usuario + fireEvent.change(usernameInput, { target: { value: 'testUserError' } }); + fireEvent.change(passwordInput, { target: { value: 'testPasswordError' } }); - // Trigger the login button click + // Damos a Login fireEvent.click(loginButton); - // Wait for the error Snackbar to be open + // Texto del Snackbar de error 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(); + // Verificar que no hay token en localStorage + expect(localStorage.getItem('token')).toBeNull(); }); -}); +}); \ No newline at end of file diff --git a/webapp/src/components/Welcome.js b/webapp/src/components/Welcome.js index 89628352..a9223a84 100644 --- a/webapp/src/components/Welcome.js +++ b/webapp/src/components/Welcome.js @@ -1,42 +1,40 @@ -import React, { useState } from 'react'; -import AddUser from './AddUser'; -import Login from './Login'; import './Welcome.css' import Typography from '@mui/material/Typography'; - +import { useNavigate } from "react-router-dom"; const HomeScreen = () => { - const [showLogin, setShowLogin] = useState(false); - const [showSignUp, setShowSignUp] = useState(false); - + const navigate = useNavigate(); const handleLogin = () => { - setShowLogin(true); + navigate("/login"); }; const handleSignUp = () => { - setShowSignUp(true); + navigate("/adduser"); }; const renderButtons = () => { - if (!showLogin && !showSignUp) { return ( <> Tu juego favorito de televisión, ¡ahora en tu ordenador! - Accede a tu cuenta o registrarte para comenzar + Accede a tu cuenta o regístrate para comenzar ↓↓↓

-
- - +
+
+ +
+
+ +
); - } + }; return ( @@ -45,8 +43,6 @@ const HomeScreen = () => {
{renderButtons()}
- {showLogin && } - {showSignUp && }
); };