diff --git a/.github/scripts/docker-compose.yml b/.github/scripts/docker-compose.yml index d27c9cb8..1c9bd54e 100644 --- a/.github/scripts/docker-compose.yml +++ b/.github/scripts/docker-compose.yml @@ -26,6 +26,7 @@ services: LIVE_POLL_MYSQL_USER: livepoll LIVE_POLL_MYSQL_PASSWORD: M3uBcPLbM7RmgX4C3wAUej6WPzq886 LIVE_POLL_DEV_URL: localhost:4200 + LIVE_POLL_FRONTEND_URL: localhost:4200 LIVE_POLL_SERVER_URL: localhost:8080 LIVE_POLL_MAIL_HOST: ${LIVE_POLL_MAIL_HOST} LIVE_POLL_MAIL_PORT: ${LIVE_POLL_MAIL_PORT} @@ -41,12 +42,6 @@ services: depends_on: db: condition: service_healthy - healthcheck: - test: "curl --fail --silent localhost:8080/actuator/health | grep UP || exit 1" - interval: 10s - timeout: 5s - retries: 10 - start_period: 40s networks: app-db: {} \ No newline at end of file diff --git a/.github/workflows/ci-with-docker-develop.yml b/.github/workflows/ci-with-docker-develop.yml index 8fb07fa4..08d54c94 100644 --- a/.github/workflows/ci-with-docker-develop.yml +++ b/.github/workflows/ci-with-docker-develop.yml @@ -27,6 +27,7 @@ jobs: LIVE_POLL_MYSQL_USER: ${{ secrets.API_LIVE_POLL_MYSQL_USER }} LIVE_POLL_MYSQL_PASSWORD: ${{ secrets.API_LIVE_POLL_MYSQL_PASSWORD }} LIVE_POLL_DEV_URL: ${{ secrets.API_LIVE_POLL_DEV_URL }} + LIVE_POLL_FRONTEND_URL: ${{ secrets.API_LIVE_POLL_FRONTEND_URL }} LIVE_POLL_SERVER_URL: ${{ secrets.API_LIVE_POLL_SERVER_URL }} LIVE_POLL_MAIL_HOST: ${{ secrets.API_LIVE_POLL_MAIL_HOST }} LIVE_POLL_MAIL_PORT: ${{ secrets.API_LIVE_POLL_MAIL_PORT }} diff --git a/.github/workflows/ci-with-docker.yml b/.github/workflows/ci-with-docker.yml index aeeb2425..22f906c8 100644 --- a/.github/workflows/ci-with-docker.yml +++ b/.github/workflows/ci-with-docker.yml @@ -29,6 +29,7 @@ jobs: LIVE_POLL_MYSQL_USER: ${{ secrets.API_LIVE_POLL_MYSQL_USER }} LIVE_POLL_MYSQL_PASSWORD: ${{ secrets.API_LIVE_POLL_MYSQL_PASSWORD }} LIVE_POLL_DEV_URL: ${{ secrets.API_LIVE_POLL_DEV_URL }} + LIVE_POLL_FRONTEND_URL: ${{ secrets.API_LIVE_POLL_FRONTEND_URL }} LIVE_POLL_SERVER_URL: ${{ secrets.API_LIVE_POLL_SERVER_URL }} LIVE_POLL_MAIL_HOST: ${{ secrets.API_LIVE_POLL_MAIL_HOST }} LIVE_POLL_MAIL_PORT: ${{ secrets.API_LIVE_POLL_MAIL_PORT }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57f98c49..7d6263c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: LIVE_POLL_MYSQL_USER: ${{ secrets.API_LIVE_POLL_MYSQL_USER }} LIVE_POLL_MYSQL_PASSWORD: ${{ secrets.API_LIVE_POLL_MYSQL_PASSWORD }} LIVE_POLL_DEV_URL: ${{ secrets.API_LIVE_POLL_DEV_URL }} + LIVE_POLL_FRONTEND_URL: ${{ secrets.API_LIVE_POLL_FRONTEND_URL }} LIVE_POLL_SERVER_URL: ${{ secrets.API_LIVE_POLL_SERVER_URL }} LIVE_POLL_MAIL_HOST: ${{ secrets.API_LIVE_POLL_MAIL_HOST }} LIVE_POLL_MAIL_PORT: ${{ secrets.API_LIVE_POLL_MAIL_PORT }} @@ -61,9 +62,12 @@ jobs: - name: Install Newman run: sudo npm i -g newman - - - name: API healthcheck - run: curl -sSLf --retry-delay 5 --retry 5 --retry-connrefused --insecure http://example.org > /dev/null + + - name: Test Docker Container listing + run: docker ps + + - name: Wait for API to come online + run: sleep 20s - name: Run Newman tests - run: newman run ./postman/Livepoll.postman_collection.json --iteration-count 3 --folder "Integration Test" --insecure \ No newline at end of file + run: newman run ./postman/Livepoll.postman_collection.json --iteration-count 3 --folder "Integration Test" --insecure diff --git a/env/docker-compose.yml b/env/docker-compose.yml index 0746c656..66d000e8 100644 --- a/env/docker-compose.yml +++ b/env/docker-compose.yml @@ -4,8 +4,8 @@ services: image: mysql:8.0 container_name: live-poll-dev-environment-db-mysql volumes: - - ./volumes/mysql-data:/var/lib/mysql - - ./volumes/mysql-logs:/var/log/mysql + - ~/volumes/mysql-data:/var/lib/mysql + - ~/volumes/mysql-logs:/var/log/mysql networks: - mysql-phpmyadmin ports: diff --git a/pom.xml b/pom.xml index 8bb73542..0ff3a3c8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ org.springframework.boot spring-boot-starter-parent - 2.5.0 + 2.5.1 de.live-poll api - 0.7.0 + 0.8.0 Live-Poll API Backend for Live-Poll @@ -24,7 +24,7 @@ org.springframework.boot spring-boot-starter-quartz - 2.5.0 + 2.5.1 diff --git a/postman/Livepoll.postman_collection.json b/postman/Livepoll.postman_collection.json index 780494bf..ebd72d90 100644 --- a/postman/Livepoll.postman_collection.json +++ b/postman/Livepoll.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "63c95483-1621-437e-8135-14b5c87cd4c8", + "_postman_id": "b83df12e-5482-4bc6-baa9-c95b38afdbb2", "name": "Livepoll", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -947,11 +947,17 @@ " pm.response.to.have.status(201);\r", "});\r", "\r", + "const response = pm.response.json();\r", + "\r", "pm.test(\"Multiple choice item is at position 1\", () => {\r", - " const response = pm.response.json();\r", " pm.expect(response.position).to.equal(1);\r", "});\r", "\r", + "pm.test(\"Answers are in correct order\", () => {\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a orig\");\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b orig\");\r", + "});\r", + "\r", "pm.globals.set(\"poll-item-id-multiple-choice\", pm.response.json().itemId);\r", "pm.globals.set(\"poll-item-id\", pm.response.json().itemId);\r", "" @@ -971,7 +977,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-multiplechoice-question\",\r\n \"selectionOptions\": [\"postman-multiplechoice-answer-1\", \"postman-multiplechoice-answer-2\"]\r\n}", + "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-multiplechoice-question\",\r\n \"selectionOptions\": [\"a orig\", \"b orig\"]\r\n}", "options": { "raw": { "language": "json" @@ -999,13 +1005,80 @@ "listen": "test", "script": { "exec": [ + "// Async operations\r", + "// https://community.postman.com/t/async-operations/24314/3\r", + "\r", + "/**\r", + " * @private\r", + " * @description Internal function to run tasks in series\r", + " * \r", + " * @param {Array} tasks\r", + " * @param {Function} cb\r", + " * @param {Number} currOperation\r", + " * @param {Array} results\r", + " */\r", + "function _series (tasks, cb, currOperation = 0, results = []) {\r", + " // Bail-out condition\r", + " if (currOperation === tasks.length) {\r", + " return cb(null, results);\r", + " }\r", + "\r", + " if (typeof tasks[currOperation] !== 'function') {\r", + " return cb(new Error('asyncSeries: Please provide a function'));\r", + " }\r", + "\r", + " tasks[currOperation]((err, res) => {\r", + " if (err) {\r", + " return cb(err);\r", + " }\r", + "\r", + " results.push(res);\r", + "\r", + " // Recursively call the next task in series till we're done executing all the operations\r", + " return _series(tasks, cb, currOperation + 1, results);\r", + " });\r", + "}\r", + "\r", + "/**\r", + " * @description asyncSeries to execute requests in a series format\r", + " * \r", + " * @param {Array} tasks\r", + " * @param {Function} cb\r", + " */\r", + "function asyncSeries (tasks, cb = () => {}) {\r", + " return _series(tasks, cb);\r", + "}\r", + "\r", + "/////////////////////////////////////////////////////////////////////////////////////////////////////////\r", + "\r", + "\r", "pm.test(\"Status code is 201\", function () {\r", " pm.response.to.have.status(201);\r", "});\r", "\r", - "pm.test(\"Quiz item is at position 2\", () => {\r", - " const response = pm.response.json();\r", - " pm.expect(response.position).to.equal(2);\r", + "const response = pm.response.json();\r", + "\r", + "asyncSeries([\r", + "\r", + " (cb) => pm.test(\"Quiz item is at position 2\", () => {\r", + " pm.expect(response.position).to.equal(2);\r", + "\r", + " cb(\"\", response);\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Answers are in correct order and the first one is correct\", () => {\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a orig\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b orig\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"c orig\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + "\r", + " cb(\"\", response);\r", + " })\r", + "\r", + "], (err, res) => {\r", + " console.log('Series operations resolved (Create Quiz Item)', err, res);\r", "});\r", "\r", "pm.globals.set(\"poll-item-id-quiz\", pm.response.json().itemId);\r", @@ -1013,6 +1086,15 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { @@ -1026,7 +1108,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-quiz-question\",\r\n \"selectionOptions\": [ \"correct option\", \"wrong option\", \"wrong option\"]\r\n}", + "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-quiz-question\",\r\n \"selectionOptions\": [ \"a orig\", \"b orig\", \"c orig\"]\r\n}", "options": { "raw": { "language": "json" @@ -1324,6 +1406,53 @@ "listen": "test", "script": { "exec": [ + "// Async operations\r", + "// https://community.postman.com/t/async-operations/24314/3\r", + "\r", + "/**\r", + " * @private\r", + " * @description Internal function to run tasks in series\r", + " * \r", + " * @param {Array} tasks\r", + " * @param {Function} cb\r", + " * @param {Number} currOperation\r", + " * @param {Array} results\r", + " */\r", + "function _series (tasks, cb, currOperation = 0, results = []) {\r", + " // Bail-out condition\r", + " if (currOperation === tasks.length) {\r", + " return cb(null, results);\r", + " }\r", + "\r", + " if (typeof tasks[currOperation] !== 'function') {\r", + " return cb(new Error('asyncSeries: Please provide a function'));\r", + " }\r", + "\r", + " tasks[currOperation]((err, res) => {\r", + " if (err) {\r", + " return cb(err);\r", + " }\r", + "\r", + " results.push(res);\r", + "\r", + " // Recursively call the next task in series till we're done executing all the operations\r", + " return _series(tasks, cb, currOperation + 1, results);\r", + " });\r", + "}\r", + "\r", + "/**\r", + " * @description asyncSeries to execute requests in a series format\r", + " * \r", + " * @param {Array} tasks\r", + " * @param {Function} cb\r", + " */\r", + "function asyncSeries (tasks, cb = () => {}) {\r", + " return _series(tasks, cb);\r", + "}\r", + "\r", + "/////////////////////////////////////////////////////////////////////////////////////////////////////////\r", + "\r", + "\r", "const baseUrl = pm.environment.get('base-url');\r", "const pollId = pm.globals.get('poll-id');\r", "const cookies = pm.environment.get('cookies');\r", @@ -1338,113 +1467,125 @@ " pm.response.to.have.status(200);\r", "});\r", "\r", + "asyncSeries([\r", "\r", - "// ---------------------- Position -------------------------------\r", - "pm.test(\"Multiple choice item got moved from position 1 to position 3\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/polls/${pollId}/poll-items`,\r", - " method: 'GET',\r", - " header: {\r", - " 'Cookie': cookies\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " // ---------------------- Position -------------------------------\r", + " (cb) => pm.test(\"Multiple choice item got moved from position 1 to position 3\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/polls/${pollId}/poll-items`,\r", + " method: 'GET',\r", + " header: {\r", + " 'Cookie': cookies\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", "\r", - " // Multiple choice item got moved from position 1 to position 3\r", - " pm.expect(response[0].itemId).to.equal(multipleChoiceId);\r", - " pm.expect(response[0].position).to.equal(3);\r", + " // Multiple choice item got moved from position 1 to position 3\r", + " pm.expect(response[0].itemId).to.equal(multipleChoiceId);\r", + " pm.expect(response[0].position).to.equal(3);\r", "\r", - " pm.expect(response[1].itemId).to.equal(quizId);\r", - " pm.expect(response[1].position).to.equal(1);\r", + " pm.expect(response[1].itemId).to.equal(quizId);\r", + " pm.expect(response[1].position).to.equal(1);\r", "\r", - " pm.expect(response[2].itemId).to.equal(openTextId);\r", - " pm.expect(response[2].position).to.equal(2);\r", - " });\r", - "});\r", + " pm.expect(response[2].itemId).to.equal(openTextId);\r", + " pm.expect(response[2].position).to.equal(2);\r", "\r", + " cb(err, response);\r", + " });\r", + " }),\r", "\r", - "// -------------------- Selection options -------------------------------\r", - "pm.test(\"Selection options that already exist are kept\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/poll-items/multiple-choice/${multipleChoiceId}`,\r", - " method: 'PUT',\r", - " header: {\r", - " 'Cookie': cookies,\r", - " 'Content-Type': 'application/json'\r", - " },\r", - " body: {\r", - " mode: 'raw',\r", - " raw: JSON.stringify({\r", - " \"pollId\": `${pollId}`,\r", - " \"question\": \"postman-multiplechoice-question-update\",\r", - " \"position\": 3,\r", - " \"selectionOptions\": [\"postman-multiplechoice-answer-1-update\", \"postman-multiplechoice-answer-2-update\"]\r", - " })\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " // -------------------- Selection options -------------------------------\r", + " (cb) => pm.test(\"Selection options that already exist are kept\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/multiple-choice/${multipleChoiceId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-multiplechoice-question-update\",\r", + " \"position\": 3,\r", + " \"selectionOptions\": [\"a\", \"b\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", "\r", - " // Selection option already existed and are kept\r", - " pm.expect(response.answers[0].selectionOption).to.equal(\"postman-multiplechoice-answer-1-update\");\r", - " pm.expect(response.answers[1].selectionOption).to.equal(\"postman-multiplechoice-answer-2-update\");\r", - " // TODO: test in conjunction with websockets to make sure that answer count also stays the same\r", - " });\r", - "});\r", + " // Selection option already existed and are kept\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " // TODO: test in conjunction with websockets to make sure that answer count also stays the same\r", "\r", - "pm.test(\"New selection options are added (with initial answer count of 0)\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/poll-items/multiple-choice/${multipleChoiceId}`,\r", - " method: 'PUT',\r", - " header: {\r", - " 'Cookie': cookies,\r", - " 'Content-Type': 'application/json'\r", - " },\r", - " body: {\r", - " mode: 'raw',\r", - " raw: JSON.stringify({\r", - " \"pollId\": `${pollId}`,\r", - " \"question\": \"postman-multiplechoice-question-update\",\r", - " \"position\": 3,\r", - " \"selectionOptions\": [\"new-option\", \"postman-multiplechoice-answer-1-update\", \"postman-multiplechoice-answer-2-update\"]\r", - " })\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " cb(err, response);\r", + " });\r", + " }),\r", "\r", - " // New selection option is added at the end\r", - " pm.expect(response.answers[0].selectionOption).to.equal(\"postman-multiplechoice-answer-1-update\");\r", - " pm.expect(response.answers[1].selectionOption).to.equal(\"postman-multiplechoice-answer-2-update\");\r", - " pm.expect(response.answers[2].selectionOption).to.equal(\"new-option\"); // was first item in the request body selectionOptions\r", + " (cb) => pm.test(\"New selection options are added (with initial answer count of 0) & order is guaranteed\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/multiple-choice/${multipleChoiceId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-multiplechoice-question-update\",\r", + " \"position\": 3,\r", + " \"selectionOptions\": [\"c\", \"a\", \"b\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", "\r", - " // Initial answer count must equal 0\r", - " pm.expect(response.answers[2].answerCount).to.equal(0)\r", - " });\r", - "});\r", + " // New selection option is added at the end\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"b\");\r", "\r", - "pm.test(\"Selection option that existed in database but not anymore in update is removed\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/poll-items/multiple-choice/${multipleChoiceId}`,\r", - " method: 'PUT',\r", - " header: {\r", - " 'Cookie': cookies,\r", - " 'Content-Type': 'application/json'\r", - " },\r", - " body: {\r", - " mode: 'raw',\r", - " raw: JSON.stringify({\r", - " \"pollId\": `${pollId}`,\r", - " \"question\": \"postman-multiplechoice-question-update\",\r", - " \"position\": 3,\r", - " \"selectionOptions\": [\"postman-multiplechoice-answer-1-update\", \"postman-multiplechoice-answer-2-update\"]\r", - " })\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " // Initial answer count must equal 0\r", + " pm.expect(response.answers[0].answerCount).to.equal(0);\r", "\r", - " // New selection option is removed and correct answer updated\r", - " pm.expect(response.answers[0].selectionOption).to.equal(\"postman-multiplechoice-answer-1-update\");\r", - " pm.expect(response.answers[1].selectionOption).to.equal(\"postman-multiplechoice-answer-2-update\");\r", - " });\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Selection option that existed in database but not anymore in update is removed\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/multiple-choice/${multipleChoiceId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-multiplechoice-question-update\",\r", + " \"position\": 3,\r", + " \"selectionOptions\": [\"a\", \"b\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + "\r", + " // New selection option is removed and correct answer updated\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + "\r", + " cb(err, response);\r", + " });\r", + " })\r", + "\r", + "], (err, res) => {\r", + " console.log('Series operations resolved (Update multiple choice item)', err, res);\r", "});\r", "" ], @@ -1472,7 +1613,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-multiplechoice-question-update\",\r\n \"position\": 3,\r\n \"selectionOptions\": [\"postman-multiplechoice-answer-1-update\", \"postman-multiplechoice-answer-2-update\"]\r\n}", + "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-multiplechoice-question-update\",\r\n \"position\": 3,\r\n \"selectionOptions\": [\"a\", \"b\"]\r\n}", "options": { "raw": { "language": "json" @@ -1501,6 +1642,53 @@ "listen": "test", "script": { "exec": [ + "// Async operations\r", + "// https://community.postman.com/t/async-operations/24314/3\r", + "\r", + "/**\r", + " * @private\r", + " * @description Internal function to run tasks in series\r", + " * \r", + " * @param {Array} tasks\r", + " * @param {Function} cb\r", + " * @param {Number} currOperation\r", + " * @param {Array} results\r", + " */\r", + "function _series (tasks, cb, currOperation = 0, results = []) {\r", + " // Bail-out condition\r", + " if (currOperation === tasks.length) {\r", + " return cb(null, results);\r", + " }\r", + "\r", + " if (typeof tasks[currOperation] !== 'function') {\r", + " return cb(new Error('asyncSeries: Please provide a function'));\r", + " }\r", + "\r", + " tasks[currOperation]((err, res) => {\r", + " if (err) {\r", + " return cb(err);\r", + " }\r", + "\r", + " results.push(res);\r", + "\r", + " // Recursively call the next task in series till we're done executing all the operations\r", + " return _series(tasks, cb, currOperation + 1, results);\r", + " });\r", + "}\r", + "\r", + "/**\r", + " * @description asyncSeries to execute requests in a series format\r", + " * \r", + " * @param {Array} tasks\r", + " * @param {Function} cb\r", + " */\r", + "function asyncSeries (tasks, cb = () => {}) {\r", + " return _series(tasks, cb);\r", + "}\r", + "\r", + "/////////////////////////////////////////////////////////////////////////////////////////////////////////\r", + "\r", + "\r", "const baseUrl = pm.environment.get('base-url');\r", "const pollId = pm.globals.get('poll-id');\r", "const cookies = pm.environment.get('cookies');\r", @@ -1515,129 +1703,298 @@ " pm.response.to.have.status(200);\r", "});\r", "\r", + "asyncSeries([\r", "\r", - "// ---------------------- Position -------------------------------\r", - "pm.test(\"Quiz item stays at position 1\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/polls/${pollId}/poll-items`,\r", - " method: 'GET',\r", - " header: {\r", - " 'Cookie': cookies\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " // ---------------------- Position -------------------------------\r", + " (cb) => pm.test(\"Quiz item stays at position 1\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/polls/${pollId}/poll-items`,\r", + " method: 'GET',\r", + " header: {\r", + " 'Cookie': cookies\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", "\r", - " // Quiz item stays at position 1\r", - " pm.expect(response[0].itemId).to.equal(multipleChoiceId);\r", - " pm.expect(response[0].position).to.equal(3);\r", + " // Quiz item stays at position 1\r", + " pm.expect(response[0].itemId).to.equal(multipleChoiceId);\r", + " pm.expect(response[0].position).to.equal(3);\r", "\r", - " pm.expect(response[1].itemId).to.equal(quizId);\r", - " pm.expect(response[1].position).to.equal(1);\r", + " pm.expect(response[1].itemId).to.equal(quizId);\r", + " pm.expect(response[1].position).to.equal(1);\r", "\r", - " pm.expect(response[2].itemId).to.equal(openTextId);\r", - " pm.expect(response[2].position).to.equal(2);\r", - " });\r", - "});\r", + " pm.expect(response[2].itemId).to.equal(openTextId);\r", + " pm.expect(response[2].position).to.equal(2);\r", "\r", + " cb(err, response);\r", + " });\r", + " }),\r", "\r", - "// -------------------- Selection options -------------------------------\r", - "pm.test(\"Selection options that already exist are kept\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", - " method: 'PUT',\r", - " header: {\r", - " 'Cookie': cookies,\r", - " 'Content-Type': 'application/json'\r", - " },\r", - " body: {\r", - " mode: 'raw',\r", - " raw: JSON.stringify({\r", + " // -------------------- Selection options -------------------------------\r", + " (cb) => pm.test(\"Selection options that already exist are kept\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [\"a\", \"b\", \"c\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + "\r", + " // Selection option already existed and are kept\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + " // TODO: test in conjunction with websockets to make sure that answer count also stays the same (meaning: it is not reset to 0 !!)\r", + "\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"New selection options are added (with initial answer count of 0) & order is guaranteed\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", " \"pollId\": `${pollId}`,\r", - " \"question\": \"postman-quiz-question-update\",\r", - " \"position\": 1,\r", - " \"selectionOptions\": [ \"correct option update\", \"wrong option update\", \"wrong option update2\"]\r", - " })\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [ \"a\", \"b\", \"d\", \"c\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", "\r", - " // Selection option already existed and are kept\r", - " pm.expect(response.answers[0].selectionOption).to.equal(\"correct option update\");\r", - " pm.expect(response.answers[0].isCorrect).to.be.true;\r", - " pm.expect(response.answers[1].selectionOption).to.equal(\"wrong option update\");\r", - " pm.expect(response.answers[1].isCorrect).to.be.false;\r", - " pm.expect(response.answers[2].selectionOption).to.equal(\"wrong option update2\");\r", - " pm.expect(response.answers[2].isCorrect).to.be.false;\r", - " // TODO: test in conjunction with websockets to make sure that answer count also stays the same\r", - " });\r", - "});\r", + " // New selection option is added (at the end, but no order is guaranteed!)\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"d\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + " pm.expect(response.answers[3].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[3].isCorrect).to.be.false;\r", "\r", - "pm.test(\"New selection options are added (with initial answer count of 0)\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", - " method: 'PUT',\r", - " header: {\r", - " 'Cookie': cookies,\r", - " 'Content-Type': 'application/json'\r", - " },\r", - " body: {\r", - " mode: 'raw',\r", - " raw: JSON.stringify({\r", - " \"pollId\": `${pollId}`,\r", - " \"question\": \"postman-quiz-question-update\",\r", - " \"position\": 1,\r", - " \"selectionOptions\": [ \"correct option update\", \"wrong option update\", \"new wrong option\", \"wrong option update2\"]\r", - " })\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", + " // Initial answer count must equal 0\r", + " pm.expect(response.answers[2].answerCount).to.equal(0);\r", "\r", - " // New selection option is added (at the end, but no order is guaranteed!)\r", - " pm.expect(response.answers[0].selectionOption).to.equal(\"correct option update\");\r", - " pm.expect(response.answers[0].isCorrect).to.be.true;\r", - " pm.expect(response.answers[1].selectionOption).to.equal(\"wrong option update\");\r", - " pm.expect(response.answers[1].isCorrect).to.be.false;\r", - " pm.expect(response.answers[2].selectionOption).to.equal(\"wrong option update2\");\r", - " pm.expect(response.answers[2].isCorrect).to.be.false;\r", - " pm.expect(response.answers[3].selectionOption).to.equal(\"new wrong option\");\r", - " pm.expect(response.answers[3].isCorrect).to.be.false;\r", + " cb(err, response);\r", + " });\r", + " }),\r", "\r", - " // Initial answer count must equal 0\r", - " pm.expect(response.answers[3].answerCount).to.equal(0)\r", - " });\r", - "});\r", + " (cb) => pm.test(\"Selection option that existed in database but not anymore in update is removed\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [\"a\", \"b\", \"c\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + " \r", + " // \"d\" is removed\r", + " // TODO: should only be possible if \"d\" has answerCount == 0\r", + " pm.expect(response.answers.length).to.be.equal(3); // only three elements left\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", "\r", - "pm.test(\"Selection option that existed in database but not anymore in update is removed\", () => {\r", - " pm.sendRequest({\r", - " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", - " method: 'PUT',\r", - " header: {\r", - " 'Cookie': cookies,\r", - " 'Content-Type': 'application/json'\r", - " },\r", - " body: {\r", - " mode: 'raw',\r", - " raw: JSON.stringify({\r", - " \"pollId\": `${pollId}`,\r", - " \"question\": \"postman-quiz-question-update\",\r", - " \"position\": 1,\r", - " \"selectionOptions\": [\"wrong option update\", \"new wrong option\", \"wrong option update2\"]\r", - " })\r", - " }\r", - " }, function (err, response) {\r", - " response = response.json();\r", - " \r", - " // First option is removed. As the first item should always represent the correct option\r", - " // a new item is the correct option now (only possible since the \"correct option update\" had an answer count of 0)\r", - " // Sorted according to their ids:\r", - " pm.expect(response.answers.length).to.be.equal(3); // only three elements left\r", - " pm.expect(response.answers[0].selectionOption).to.equal(\"wrong option update\");\r", - " pm.expect(response.answers[0].isCorrect).to.be.true;\r", - " pm.expect(response.answers[1].selectionOption).to.equal(\"wrong option update2\");\r", - " pm.expect(response.answers[1].isCorrect).to.be.false;\r", - " pm.expect(response.answers[2].selectionOption).to.equal(\"new wrong option\");\r", - " pm.expect(response.answers[2].isCorrect).to.be.false;\r", - " });\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Remove first element, thus the next element is the correct one\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [\"b\", \"c\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + " \r", + " // First option is removed. As the first item should always represent the correct option\r", + " // \"b\" is the correct option now (since b is the first string in the updated list)\r", + " // TODO: should only be possible if \"a\" has answerCount == 0\r", + " pm.expect(response.answers.length).to.be.equal(2); // only three elements left\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + "\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Insert new correct selection option at the beginning\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [\"a\", \"b\", \"c\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + " \r", + " // \"a\" is now the correct selection option\r", + " // TODO: should only be possible if \"b\" has answerCount == 0\r", + " pm.expect(response.answers.length).to.be.equal(3); // only three elements left\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + "\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Choose a new correct answer & change order (1)\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [\"b\", \"c\", \"a\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + " \r", + " // \"b\" is the new correct option\r", + " // TODO: should only be possible if \"a\" has answerCount == 0 (a was the correct option before)\r", + " pm.expect(response.answers.length).to.be.equal(3); // only three elements left\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + "\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Choose a new correct answer & change order (2)\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/quiz/${quizId}`,\r", + " method: 'PUT',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"pollId\": `${pollId}`,\r", + " \"question\": \"postman-quiz-question-update\",\r", + " \"position\": 1,\r", + " \"selectionOptions\": [\"c\", \"b\", \"a\"]\r", + " })\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + " \r", + " // \"c\" is the new correct option\r", + " // TODO: should only be possible if \"b\" has answerCount == 0 (b was the correct option before)\r", + " pm.expect(response.answers.length).to.be.equal(3); // only three elements left\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + "\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + " (cb) => pm.test(\"Order is preserved even for the GET poll item endpoint (1)\", () => {\r", + " pm.sendRequest({\r", + " url: `${baseUrl}/v1/poll-items/${quizId}`,\r", + " method: 'GET',\r", + " header: {\r", + " 'Cookie': cookies,\r", + " 'Content-Type': 'application/json'\r", + " }\r", + " }, function (err, response) {\r", + " response = response.json();\r", + " \r", + " pm.expect(response.answers.length).to.be.equal(3); // only three elements left\r", + " pm.expect(response.answers[0].selectionOption).to.equal(\"c\");\r", + " pm.expect(response.answers[0].isCorrect).to.be.true;\r", + " pm.expect(response.answers[1].selectionOption).to.equal(\"b\");\r", + " pm.expect(response.answers[1].isCorrect).to.be.false;\r", + " pm.expect(response.answers[2].selectionOption).to.equal(\"a\");\r", + " pm.expect(response.answers[2].isCorrect).to.be.false;\r", + "\r", + " cb(err, response);\r", + " });\r", + " }),\r", + "\r", + "], (err, res) => {\r", + " console.log('Series operations resolved (Update quiz item)', err, res);\r", "});\r", "\r", "// TODO: have an item with answer count > 0 and test\r", @@ -1670,7 +2027,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-quiz-question-update\",\r\n \"position\": 1,\r\n \"selectionOptions\": [ \"correct option update\", \"wrong option update\", \"wrong option update2\"]\r\n}", + "raw": "{\r\n \"pollId\": \"{{poll-id}}\",\r\n \"question\": \"postman-quiz-question-update\",\r\n \"position\": 1,\r\n \"selectionOptions\": [ \"a\", \"b\", \"c\"]\r\n}", "options": { "raw": { "language": "json" @@ -1842,8 +2199,8 @@ " headers: 'Cookie:'+pm.environment.get(\"cookies\")\r", " }, function(error, response) {\r", " pm.test(\"Poll item does not exist anymore\", function() {\r", - " pm.expect(response).to.have.property('code', 403)\r", - " pm.expect(response).to.have.property('status', 'Forbidden')\r", + " pm.expect(response).to.have.property('code', 404)\r", + " pm.expect(response).to.have.property('status', 'Not Found')\r", " })\r", "})\r", "\r", diff --git a/src/main/kotlin/de/livepoll/api/LivePollApplication.kt b/src/main/kotlin/de/livepoll/api/LivePollApplication.kt index 4d1ecef0..f1aebfbd 100644 --- a/src/main/kotlin/de/livepoll/api/LivePollApplication.kt +++ b/src/main/kotlin/de/livepoll/api/LivePollApplication.kt @@ -1,7 +1,5 @@ package de.livepoll.api -import de.livepoll.api.entity.db.User -import de.livepoll.api.repository.UserRepository import de.livepoll.api.service.AccountService import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.SpringBootApplication @@ -9,14 +7,14 @@ import org.springframework.boot.runApplication @SpringBootApplication(scanBasePackages = ["de.livepoll.api"]) class LivePollApplication( - private val accountService: AccountService, -): CommandLineRunner { - override fun run(vararg args: String?) { - // Create postman user, when started in testing environment - if (System.getenv("LIVE_POLL_POSTMAN") == "true") accountService.createPostmanAccount() - } + private val accountService: AccountService, +) : CommandLineRunner { + override fun run(vararg args: String?) { + // Create postman user, when started in testing environment + if (System.getenv("LIVE_POLL_POSTMAN") == "true") accountService.createPostmanAccount() + } } fun main(args: Array) { - runApplication(*args) -} \ No newline at end of file + runApplication(*args) +} diff --git a/src/main/kotlin/de/livepoll/api/config/BlockedTokenConfig.kt b/src/main/kotlin/de/livepoll/api/config/BlockedTokenConfig.kt index 616dc498..42610e1c 100644 --- a/src/main/kotlin/de/livepoll/api/config/BlockedTokenConfig.kt +++ b/src/main/kotlin/de/livepoll/api/config/BlockedTokenConfig.kt @@ -9,7 +9,7 @@ import java.util.* @Configuration @EnableScheduling class BlockedTokenConfig( - val blockedTokenRepository: BlockedTokenRepository + val blockedTokenRepository: BlockedTokenRepository ) { @Scheduled(cron = "0 0 12 * * ?") @@ -21,4 +21,4 @@ class BlockedTokenConfig( } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/config/CorsConfig.kt b/src/main/kotlin/de/livepoll/api/config/CorsConfig.kt index ec9b261c..e7f277a1 100644 --- a/src/main/kotlin/de/livepoll/api/config/CorsConfig.kt +++ b/src/main/kotlin/de/livepoll/api/config/CorsConfig.kt @@ -12,18 +12,18 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver class CorsConfig : WebMvcConfigurer { private val allowedOrigins = setOf( - System.getenv("LIVE_POLL_DEV_URL"), - "https://dev.live-poll.de", - "https://www.live-poll.de" + System.getenv("LIVE_POLL_FRONTEND_URL"), + System.getenv("LIVE_POLL_DEV_URL") ) override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") - .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE") - .allowedOrigins(*allowedOrigins.toTypedArray()) - .allowCredentials(true) + .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE") + .allowedOrigins(*allowedOrigins.toTypedArray()) + .allowCredentials(true) } @Bean fun defaultViewResolver() = InternalResourceViewResolver() -} \ No newline at end of file + +} diff --git a/src/main/kotlin/de/livepoll/api/config/QuartzSchedulerConfig.kt b/src/main/kotlin/de/livepoll/api/config/QuartzSchedulerConfig.kt index 2430c63e..4971c3b1 100644 --- a/src/main/kotlin/de/livepoll/api/config/QuartzSchedulerConfig.kt +++ b/src/main/kotlin/de/livepoll/api/config/QuartzSchedulerConfig.kt @@ -6,7 +6,8 @@ import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.ClassPathResource -import org.springframework.scheduling.quartz.* +import org.springframework.scheduling.quartz.SchedulerFactoryBean +import org.springframework.scheduling.quartz.SpringBeanJobFactory import javax.sql.DataSource @Configuration @@ -34,4 +35,4 @@ class QuartzSchedulerConfig { return schedulerFactory } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/config/SecurityConfig.kt b/src/main/kotlin/de/livepoll/api/config/SecurityConfig.kt index 53d39aa6..de2c5d38 100644 --- a/src/main/kotlin/de/livepoll/api/config/SecurityConfig.kt +++ b/src/main/kotlin/de/livepoll/api/config/SecurityConfig.kt @@ -4,6 +4,7 @@ import de.livepoll.api.service.JwtUserDetailsService import de.livepoll.api.util.JwtRequestFilter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.WebSecurity @@ -12,7 +13,6 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.authentication.AuthenticationManager @Configuration @EnableWebSecurity @@ -24,14 +24,15 @@ class SecurityConfig( override fun configure(http: HttpSecurity) { http.cors().and().csrf().disable() .authorizeRequests() - .antMatchers("/v1/account/register").permitAll() - .antMatchers("/v1/account/confirm").permitAll() - .antMatchers("/v1/account/login").permitAll() - .antMatchers("/v1/websocket/**").permitAll() - .antMatchers("/actuator/**").permitAll() - //.antMatchers("/admin").hasRole("ADMIN") // TODO: introduce ROLE_ADMIN authority later on - .anyRequest().authenticated() - .and() + .antMatchers("/v1/account/register").permitAll() + .antMatchers("/v1/account/confirm").permitAll() + .antMatchers("/v1/account/login").permitAll() + .antMatchers("/v1/polls/{id:[\\d]+}").permitAll() + .antMatchers("/v1/websocket/**").permitAll() + .antMatchers("/actuator/**").permitAll() + //.antMatchers("/admin").hasRole("ADMIN") // TODO: introduce ROLE_ADMIN authority later on + .anyRequest().authenticated() + .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter::class.java) diff --git a/src/main/kotlin/de/livepoll/api/config/SwaggerConfig.kt b/src/main/kotlin/de/livepoll/api/config/SwaggerConfig.kt index 975c46a0..2447d778 100644 --- a/src/main/kotlin/de/livepoll/api/config/SwaggerConfig.kt +++ b/src/main/kotlin/de/livepoll/api/config/SwaggerConfig.kt @@ -17,28 +17,28 @@ class SwaggerConfig { @Bean fun api(): Docket = Docket(DocumentationType.OAS_30) - .ignoredParameterTypes(AuthenticationPrincipal::class.java) - .host("api.live-poll.de") - .enableUrlTemplating(true) - .select() - .apis(RequestHandlerSelectors.basePackage("de.livepoll.api")) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiInfo()) + .ignoredParameterTypes(AuthenticationPrincipal::class.java) + .host("api.live-poll.de") + .enableUrlTemplating(true) + .select() + .apis(RequestHandlerSelectors.basePackage("de.livepoll.api")) + .paths(PathSelectors.any()) + .build() + .apiInfo(apiInfo()) @Bean fun uiConfig(): UiConfiguration = UiConfigurationBuilder.builder() - .defaultModelsExpandDepth(-1) - .build() + .defaultModelsExpandDepth(-1) + .build() private fun apiInfo() = ApiInfo( - "Live-Poll API", - "Platform for providing surveys with live display features", - "1.0.1", - "https://chillibits.com/pmapp?p=privacy", - Contact("ChilliBits", "https://www.chillibits.com", "contact@chillibits.com"), - "ODC DbCL v1.0 License", - "https://opendatacommons.org/licenses/dbcl/1.0/", - emptyList() + "Live-Poll API", + "Platform for providing surveys with live display features", + "1.0.1", + "https://chillibits.com/pmapp?p=privacy", + Contact("ChilliBits", "https://www.chillibits.com", "contact@chillibits.com"), + "ODC DbCL v1.0 License", + "https://opendatacommons.org/licenses/dbcl/1.0/", + emptyList() ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/config/WebSocketConfig.kt b/src/main/kotlin/de/livepoll/api/config/WebSocketConfig.kt index cc51e090..7cdb3bf4 100644 --- a/src/main/kotlin/de/livepoll/api/config/WebSocketConfig.kt +++ b/src/main/kotlin/de/livepoll/api/config/WebSocketConfig.kt @@ -12,7 +12,7 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo @Configuration @EnableWebSocketMessageBroker class WebSocketConfig( - private val corsConfig: CorsConfig + private val corsConfig: CorsConfig ) : WebSocketMessageBrokerConfigurer { override fun configureMessageBroker(registry: MessageBrokerRegistry) { @@ -22,9 +22,9 @@ class WebSocketConfig( override fun registerStompEndpoints(registry: StompEndpointRegistry) { registry.addEndpoint("/v1/websocket/enter-poll") - .addInterceptors(HttpHandshakeInterceptor()) - .setHandshakeHandler(CustomWebSocketHandshakeHandler()) - .setAllowedOrigins("*") + .addInterceptors(HttpHandshakeInterceptor()) + .setHandshakeHandler(CustomWebSocketHandshakeHandler()) + .setAllowedOrigins("*") } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/controller/PollController.kt b/src/main/kotlin/de/livepoll/api/controller/PollController.kt index 64e16bfb..6491e579 100644 --- a/src/main/kotlin/de/livepoll/api/controller/PollController.kt +++ b/src/main/kotlin/de/livepoll/api/controller/PollController.kt @@ -36,8 +36,7 @@ class PollController( @ApiOperation(value = "Get poll", tags = ["Poll"]) @GetMapping("/{id}") - fun getPoll(@PathVariable(name = "id") pollId: Long, @AuthenticationPrincipal user: User): PollDtoOut { - accountService.checkAuthorizationByPollId(pollId) + fun getPoll(@PathVariable(name = "id") pollId: Long): PollDtoOut { return pollService.getPoll(pollId) } diff --git a/src/main/kotlin/de/livepoll/api/controller/WebSocketController.kt b/src/main/kotlin/de/livepoll/api/controller/WebSocketController.kt index 6e25414e..7d28c16c 100644 --- a/src/main/kotlin/de/livepoll/api/controller/WebSocketController.kt +++ b/src/main/kotlin/de/livepoll/api/controller/WebSocketController.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Controller @Controller class WebSocketController( - private val webSocketService: WebSocketService + private val webSocketService: WebSocketService ) { @MessageMapping("/{pollItemId}") diff --git a/src/main/kotlin/de/livepoll/api/entity/db/BlockedToken.kt b/src/main/kotlin/de/livepoll/api/entity/db/BlockedToken.kt index f819286b..8b4afa31 100644 --- a/src/main/kotlin/de/livepoll/api/entity/db/BlockedToken.kt +++ b/src/main/kotlin/de/livepoll/api/entity/db/BlockedToken.kt @@ -6,14 +6,14 @@ import javax.persistence.* @Entity @Table(name = "blocked_token") data class BlockedToken( - @Id - @Column(nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long, + @Id + @Column(nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, - @Column(nullable = false) - val token: String, + @Column(nullable = false) + val token: String, - @Column(nullable = false) - val expiryDate: Date -) \ No newline at end of file + @Column(nullable = false) + val expiryDate: Date +) diff --git a/src/main/kotlin/de/livepoll/api/entity/db/MultipleChoiceItemAnswer.kt b/src/main/kotlin/de/livepoll/api/entity/db/MultipleChoiceItemAnswer.kt index 1280cf62..e2c447f4 100644 --- a/src/main/kotlin/de/livepoll/api/entity/db/MultipleChoiceItemAnswer.kt +++ b/src/main/kotlin/de/livepoll/api/entity/db/MultipleChoiceItemAnswer.kt @@ -6,7 +6,7 @@ import javax.persistence.* @Entity @Table(name = "multiple_choice_item_answer") -class MultipleChoiceItemAnswer ( +class MultipleChoiceItemAnswer( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/kotlin/de/livepoll/api/entity/db/Poll.kt b/src/main/kotlin/de/livepoll/api/entity/db/Poll.kt index 76814f4f..ccc7d5c2 100644 --- a/src/main/kotlin/de/livepoll/api/entity/db/Poll.kt +++ b/src/main/kotlin/de/livepoll/api/entity/db/Poll.kt @@ -7,31 +7,31 @@ import javax.persistence.* @Entity @Table(name = "poll") data class Poll( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "poll_id", nullable = false) - var id: Long, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "poll_id", nullable = false) + var id: Long, - @ManyToOne - @JsonIgnore - @JoinColumn(name = "user_id") - var user: User, + @ManyToOne + @JsonIgnore + @JoinColumn(name = "user_id") + var user: User, - @Column(nullable = false) - var name: String, + @Column(nullable = false) + var name: String, - @Column(nullable = true) - var startDate: Date?, + @Column(nullable = true) + var startDate: Date?, - @Column(nullable = true) - var endDate: Date?, + @Column(nullable = true) + var endDate: Date?, - var slug: String, + var slug: String, - @Column(nullable = true) - var currentItem : Long?, + @Column(nullable = true) + var currentItem: Long?, - @JsonIgnore - @OneToMany(mappedBy = "poll", cascade = [CascadeType.ALL], orphanRemoval = true) - var pollItems: MutableList + @JsonIgnore + @OneToMany(mappedBy = "poll", cascade = [CascadeType.ALL], orphanRemoval = true) + var pollItems: MutableList ) diff --git a/src/main/kotlin/de/livepoll/api/entity/db/UserAttr.kt b/src/main/kotlin/de/livepoll/api/entity/db/UserAttr.kt index abce2911..3073ba4c 100644 --- a/src/main/kotlin/de/livepoll/api/entity/db/UserAttr.kt +++ b/src/main/kotlin/de/livepoll/api/entity/db/UserAttr.kt @@ -5,18 +5,18 @@ import javax.persistence.* @Entity @Table(name = "user_attr") data class UserAttr( - @Id - @GeneratedValue(strategy= GenerationType.IDENTITY) - @Column(name="user_attr_id", nullable = false) - var id: Long, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_attr_id", nullable = false) + var id: Long, - @OneToOne - @JoinColumn(name="user_id") - var user: User, + @OneToOne + @JoinColumn(name = "user_id") + var user: User, - @Column(nullable = false) - var key1: String, + @Column(nullable = false) + var key1: String, - @Column(nullable = false) - var value: String -) \ No newline at end of file + @Column(nullable = false) + var value: String +) diff --git a/src/main/kotlin/de/livepoll/api/entity/db/VerificationToken.kt b/src/main/kotlin/de/livepoll/api/entity/db/VerificationToken.kt index 9c3d86f8..869f11f9 100644 --- a/src/main/kotlin/de/livepoll/api/entity/db/VerificationToken.kt +++ b/src/main/kotlin/de/livepoll/api/entity/db/VerificationToken.kt @@ -6,17 +6,17 @@ import javax.persistence.* @Entity @Table(name = "verification_token") data class VerificationToken( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="verification_token_id", nullable = false) - val id: Long, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "verification_token_id", nullable = false) + val id: Long, - @Column(name="token", nullable = false) - val token: String, + @Column(name = "token", nullable = false) + val token: String, - @Column(name="username", nullable = false) - val username: String, + @Column(name = "username", nullable = false) + val username: String, - @Column(name="expiry_date", nullable = false) - var expiryDate: Date -) \ No newline at end of file + @Column(name = "expiry_date", nullable = false) + var expiryDate: Date +) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemDtoOut.kt b/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemDtoOut.kt index e2b7653b..065d6fb9 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemDtoOut.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemDtoOut.kt @@ -1,10 +1,10 @@ package de.livepoll.api.entity.dto class MultipleChoiceItemDtoOut( - itemId: Long, - pollId: Long, - question: String, - position: Int, - type: String, - val answers: List -): PollItemDtoOut(itemId, pollId, question, position, type) + itemId: Long, + pollId: Long, + question: String, + position: Int, + type: String, + val answers: List +) : PollItemDtoOut(itemId, pollId, question, position, type) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemParticipantAnswerDtoIn.kt b/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemParticipantAnswerDtoIn.kt index 46290eb3..31a5801f 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemParticipantAnswerDtoIn.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/MultipleChoiceItemParticipantAnswerDtoIn.kt @@ -3,10 +3,10 @@ package de.livepoll.api.entity.dto import com.fasterxml.jackson.annotation.JsonProperty data class MultipleChoiceItemParticipantAnswerDtoIn( - @JsonProperty("id") - val id: Long, - @JsonProperty("type") - val type: String, - @JsonProperty("selectionOption") - val selectionOption: String -) \ No newline at end of file + @JsonProperty("id") + val id: Long, + @JsonProperty("type") + val type: String, + @JsonProperty("selectionOption") + val selectionOption: String +) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemDtoOut.kt b/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemDtoOut.kt index ddb7ab22..6ea55cc5 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemDtoOut.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemDtoOut.kt @@ -1,10 +1,10 @@ package de.livepoll.api.entity.dto class OpenTextItemDtoOut( - itemId: Long, - pollId: Long, - question: String, - position: Int, - type: String, - val answers: List -): PollItemDtoOut(itemId, pollId, question, position, type) + itemId: Long, + pollId: Long, + question: String, + position: Int, + type: String, + val answers: List +) : PollItemDtoOut(itemId, pollId, question, position, type) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemParticipantAnswerDtoIn.kt b/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemParticipantAnswerDtoIn.kt index 6f1b6744..bf6ae31f 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemParticipantAnswerDtoIn.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/OpenTextItemParticipantAnswerDtoIn.kt @@ -3,10 +3,10 @@ package de.livepoll.api.entity.dto import com.fasterxml.jackson.annotation.JsonProperty data class OpenTextItemParticipantAnswerDtoIn( - @JsonProperty("id") - val id: Long, - @JsonProperty("type") - val type: String, - @JsonProperty("answer") - val answer: String -) \ No newline at end of file + @JsonProperty("id") + val id: Long, + @JsonProperty("type") + val type: String, + @JsonProperty("answer") + val answer: String +) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoIn.kt b/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoIn.kt index 2814a0f3..61587e6e 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoIn.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoIn.kt @@ -3,10 +3,10 @@ package de.livepoll.api.entity.dto import java.util.* data class PollDtoIn( - val name: String, - val startDate: Date?, - val endDate: Date?, - val slug: String?, - val currentItem: Long? + val name: String, + val startDate: Date?, + val endDate: Date?, + val slug: String?, + val currentItem: Long? ) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoOut.kt b/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoOut.kt index 785b20ea..dab44014 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoOut.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/PollDtoOut.kt @@ -3,10 +3,10 @@ package de.livepoll.api.entity.dto import java.util.* data class PollDtoOut( - val id: Long, - val name: String, - val startDate: Date?, - val endDate: Date?, - val slug: String, - var currentItem: Long? + val id: Long, + val name: String, + val startDate: Date?, + val endDate: Date?, + val slug: String, + var currentItem: Long? ) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/PollItemDtoOut.kt b/src/main/kotlin/de/livepoll/api/entity/dto/PollItemDtoOut.kt index 97f1618d..8eaf7909 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/PollItemDtoOut.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/PollItemDtoOut.kt @@ -1,9 +1,9 @@ package de.livepoll.api.entity.dto open class PollItemDtoOut( - val itemId: Long, - val pollId: Long, - val question: String, - val position: Int, - val type: String -) \ No newline at end of file + val itemId: Long, + val pollId: Long, + val question: String, + val position: Int, + val type: String +) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/QuizItemParticipantAnswerDtoIn.kt b/src/main/kotlin/de/livepoll/api/entity/dto/QuizItemParticipantAnswerDtoIn.kt index a86d6477..71a206c4 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/QuizItemParticipantAnswerDtoIn.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/QuizItemParticipantAnswerDtoIn.kt @@ -3,10 +3,10 @@ package de.livepoll.api.entity.dto import com.fasterxml.jackson.annotation.JsonProperty data class QuizItemParticipantAnswerDtoIn( - @JsonProperty("id") - val id: Long, - @JsonProperty("type") - val type: String, - @JsonProperty("selectionOption") - val selectionOption: String -) \ No newline at end of file + @JsonProperty("id") + val id: Long, + @JsonProperty("type") + val type: String, + @JsonProperty("selectionOption") + val selectionOption: String +) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoIn.kt b/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoIn.kt index 1a9e1329..b723e4f9 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoIn.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoIn.kt @@ -1,7 +1,7 @@ package de.livepoll.api.entity.dto data class UserDtoIn( - var username: String, - var email: String, - var password: String, + var username: String, + var email: String, + var password: String, ) diff --git a/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoOut.kt b/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoOut.kt index ceeb8635..997501c0 100644 --- a/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoOut.kt +++ b/src/main/kotlin/de/livepoll/api/entity/dto/UserDtoOut.kt @@ -1,7 +1,7 @@ package de.livepoll.api.entity.dto data class UserDtoOut( - var id: Long, - var username: String, - var email: String + var id: Long, + var username: String, + var email: String ) diff --git a/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationRequest.kt b/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationRequest.kt index 596d030a..648c1af5 100644 --- a/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationRequest.kt +++ b/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationRequest.kt @@ -1,6 +1,6 @@ package de.livepoll.api.entity.jwt data class AuthenticationRequest( - var username: String, - var password: String -) \ No newline at end of file + var username: String, + var password: String +) diff --git a/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationResponse.kt b/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationResponse.kt index 713ec360..a578222e 100644 --- a/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationResponse.kt +++ b/src/main/kotlin/de/livepoll/api/entity/jwt/AuthenticationResponse.kt @@ -1,5 +1,5 @@ package de.livepoll.api.entity.jwt data class AuthenticationResponse( - var jwt: String -) \ No newline at end of file + var jwt: String +) diff --git a/src/main/kotlin/de/livepoll/api/exception/EmailNotConfirmedException.kt b/src/main/kotlin/de/livepoll/api/exception/EmailNotConfirmedException.kt index 4f140590..dd6e9d4a 100644 --- a/src/main/kotlin/de/livepoll/api/exception/EmailNotConfirmedException.kt +++ b/src/main/kotlin/de/livepoll/api/exception/EmailNotConfirmedException.kt @@ -1,5 +1,5 @@ package de.livepoll.api.exception class EmailNotConfirmedException( - override val message: String -) : Exception() \ No newline at end of file + override val message: String +) : Exception() diff --git a/src/main/kotlin/de/livepoll/api/exception/UserExistsException.kt b/src/main/kotlin/de/livepoll/api/exception/UserExistsException.kt index f9d4f4a3..faef576e 100644 --- a/src/main/kotlin/de/livepoll/api/exception/UserExistsException.kt +++ b/src/main/kotlin/de/livepoll/api/exception/UserExistsException.kt @@ -1,5 +1,5 @@ package de.livepoll.api.exception class UserExistsException( - override val message: String -) : Exception() \ No newline at end of file + override val message: String +) : Exception() diff --git a/src/main/kotlin/de/livepoll/api/repository/MultipleChoiceItemRepository.kt b/src/main/kotlin/de/livepoll/api/repository/MultipleChoiceItemRepository.kt index 75f8ac00..0d873a67 100644 --- a/src/main/kotlin/de/livepoll/api/repository/MultipleChoiceItemRepository.kt +++ b/src/main/kotlin/de/livepoll/api/repository/MultipleChoiceItemRepository.kt @@ -4,4 +4,4 @@ import de.livepoll.api.entity.db.MultipleChoiceItem import javax.transaction.Transactional @Transactional -interface MultipleChoiceItemRepository: PollItemRepository \ No newline at end of file +interface MultipleChoiceItemRepository : PollItemRepository diff --git a/src/main/kotlin/de/livepoll/api/repository/UserAttrRepository.kt b/src/main/kotlin/de/livepoll/api/repository/UserAttrRepository.kt index de07a067..3bc2dcd3 100644 --- a/src/main/kotlin/de/livepoll/api/repository/UserAttrRepository.kt +++ b/src/main/kotlin/de/livepoll/api/repository/UserAttrRepository.kt @@ -5,4 +5,4 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface UserAttrRepository: JpaRepository \ No newline at end of file +interface UserAttrRepository : JpaRepository diff --git a/src/main/kotlin/de/livepoll/api/repository/UserRepository.kt b/src/main/kotlin/de/livepoll/api/repository/UserRepository.kt index e2706327..0b5301c9 100644 --- a/src/main/kotlin/de/livepoll/api/repository/UserRepository.kt +++ b/src/main/kotlin/de/livepoll/api/repository/UserRepository.kt @@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface UserRepository: JpaRepository { +interface UserRepository : JpaRepository { // @Query("SELECT u FROM User u WHERE u.username = ?1") fun findByUsername(username: String): User? diff --git a/src/main/kotlin/de/livepoll/api/repository/VerificationTokenRepository.kt b/src/main/kotlin/de/livepoll/api/repository/VerificationTokenRepository.kt index a49240c0..68455469 100644 --- a/src/main/kotlin/de/livepoll/api/repository/VerificationTokenRepository.kt +++ b/src/main/kotlin/de/livepoll/api/repository/VerificationTokenRepository.kt @@ -4,8 +4,8 @@ import de.livepoll.api.entity.db.VerificationToken import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query -interface VerificationTokenRepository: JpaRepository { +interface VerificationTokenRepository : JpaRepository { @Query("SELECT u FROM VerificationToken u WHERE u.token = ?1") fun findByToken(token: String): VerificationToken -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/service/AccountService.kt b/src/main/kotlin/de/livepoll/api/service/AccountService.kt index 2d77f88a..1bd8dcd1 100644 --- a/src/main/kotlin/de/livepoll/api/service/AccountService.kt +++ b/src/main/kotlin/de/livepoll/api/service/AccountService.kt @@ -13,6 +13,7 @@ import de.livepoll.api.util.jwtCookie.CookieCipher import de.livepoll.api.util.jwtCookie.CookieUtil import org.springframework.context.ApplicationEventPublisher import org.springframework.dao.DataAccessException +import org.springframework.dao.EmptyResultDataAccessException import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -86,14 +87,19 @@ class AccountService( * @param token the token string to confirm the new account */ fun confirmAccount(token: String): Boolean { - val verificationToken = verificationTokenRepository.findByToken(token) - if (verificationToken.expiryDate.after(Date())) { - val user = userRepository.findByUsername(verificationToken.username) - user?.isAccountEnabled = true - verificationTokenRepository.delete(verificationToken) - return true + try { + val verificationToken = verificationTokenRepository.findByToken(token) + if (verificationToken.expiryDate.after(Date())) { + val user = userRepository.findByUsername(verificationToken.username) + user?.isAccountEnabled = true + verificationTokenRepository.delete(verificationToken) + return true + } + return false + } catch (ex:EmptyResultDataAccessException) { + return false } - return false + } private fun calculateExpiryDate() = Calendar.getInstance() diff --git a/src/main/kotlin/de/livepoll/api/service/PollItemService.kt b/src/main/kotlin/de/livepoll/api/service/PollItemService.kt index ac4a7ce5..62faec6a 100644 --- a/src/main/kotlin/de/livepoll/api/service/PollItemService.kt +++ b/src/main/kotlin/de/livepoll/api/service/PollItemService.kt @@ -222,7 +222,7 @@ class PollItemService { /** * Update the answers of a poll item according to an update list of selection options (strings). - * This method works in-place and will adjust the poll item answers list. + * This method works in-place and will adjust the poll item answers list. The order is guaranteed. * * Requirements this algorithm has to fulfill: * @@ -236,13 +236,12 @@ class PollItemService { * -> Remove the answer that contained this selection option */ fun updateAnswers(item: PollItemAnswerable, selectionOptionsUpdate: List) { - val selectionOptionsExisting = item.answers.map { it.selectionOption } // --- Example // e indicates: "existing" // u indicates: "update" // selection_option_existing ["Ae", "Be", "Ce"] - // selection_option_update ["Au", "Bu", "Du"] - // selection_option_result ["Ae", "Be", "Du"] + // selection_option_update ["Au", "Du", "Bu"] + // selection_option_result ["Ae", "Du", "Be"] // note that Ce is gone // note that Ae/Be are used in favor of Au/Bu since Ae/Be might include answer counts > 0 @@ -264,35 +263,57 @@ class PollItemService { } } - // --- Removing - // Remove answer in db whose whose selection option is not included in the selection options from the update - // Remove (selection_option_existing \ selection_option_update) - // in the example: remove Ce - item.answers.removeIf { !selectionOptionsUpdate.contains(it.selectionOption) } - - // --- Adding - // Add selection option (wrapped as an answer) to db if it is not included in any answer from the db - // Add (selection_option_update \ selection_option_existing) - // in the example: add Du - val toAddAnswers = selectionOptionsUpdate - .filter { !selectionOptionsExisting.contains(it) } - // wrap selection option string as answerable poll item - .map { - if (item is MultipleChoiceItem) { - MultipleChoiceItemAnswer(0, item, it, 0) - } else if (item is QuizItem) { - QuizItemAnswer(0, item, it, false, 0) // the correct item is updated later - } else { - // Should never happen - throw ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, - "Only multiple choice and quiz items allow for answers" - ) + // --- Build hash map for answers + val existingAnswers: HashMap = HashMap() + for (answer in item.answers) { + // if we deal with a quiz item: + // make sure that correct options are all false here (they get updated later) + if (item is QuizItem) { + (answer as QuizItemAnswer).isCorrect = false + } + + existingAnswers[answer.selectionOption] = answer + } + + // --- Build updated answer list + val answersUpdated: MutableList = mutableListOf() + for (selectionOption in selectionOptionsUpdate) { + if (existingAnswers.containsKey(selectionOption)) { + // Take the existing answer + answersUpdated.add(existingAnswers[selectionOption]!!) + } else { + // Construct a new answer (depending on the item type) + when (item) { + is MultipleChoiceItem -> { + answersUpdated.add(MultipleChoiceItemAnswer(0, item, selectionOption, 0)) + } + is QuizItem -> { + // the correct item is updated later + answersUpdated.add(QuizItemAnswer(0, item, selectionOption, false, 0)) + } + else -> { + // Should never happen + throw ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Only multiple choice and quiz items allow for answers" + ) + } } } + } + + // --- Quiz item: First item should be correct one + if (answersUpdated.size > 0) { + if (item is QuizItem) { + (answersUpdated[0] as QuizItemAnswer).isCorrect = true + } + } + + // --- Override poll item answer list + item.answers.clear() // Don't know why Kotlin wants a Collection here. Might be a bug in Kotlin. // If you know a better approach, please fix this. - item.answers.addAll(toAddAnswers as Collection) + item.answers.addAll(answersUpdated as Collection) } /** @@ -314,8 +335,8 @@ class PollItemService { this.allowMultipleAnswers = pollItem.allowMultipleAnswers this.allowBlankField = pollItem.allowBlankField movePollItem(this.position, pollItem.position, this.poll.pollItems) - pollRepository.saveAndFlush(this.poll) + pollRepository.saveAndFlush(this.poll) return pollItemRepository.saveAndFlush(this).toDtoOut() } } @@ -339,16 +360,11 @@ class PollItemService { .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Poll item not found") } .run { updateAnswers(this, pollItem.selectionOptions) - - // Update correct answer - val newCorrectIndex = this.answers.indexOfFirst { it.selectionOption == pollItem.selectionOptions[0] } - this.answers[newCorrectIndex].isCorrect = true - this.question = pollItem.question movePollItem(this.position, pollItem.position, this.poll.pollItems) - pollRepository.saveAndFlush(this.poll) - return quizItemRepository.saveAndFlush(this).toDtoOut() + pollRepository.saveAndFlush(this.poll) + return pollItemRepository.saveAndFlush(this).toDtoOut() } } @@ -368,9 +384,9 @@ class PollItemService { } this.question = pollItem.question movePollItem(this.position, pollItem.position, this.poll.pollItems) - pollRepository.saveAndFlush(this.poll) - return openTextItemRepository.saveAndFlush(this).toDtoOut() + pollRepository.saveAndFlush(this.poll) + return pollItemRepository.saveAndFlush(this).toDtoOut() } } diff --git a/src/main/kotlin/de/livepoll/api/service/PollService.kt b/src/main/kotlin/de/livepoll/api/service/PollService.kt index 1a8109f2..989d297c 100644 --- a/src/main/kotlin/de/livepoll/api/service/PollService.kt +++ b/src/main/kotlin/de/livepoll/api/service/PollService.kt @@ -7,7 +7,7 @@ import de.livepoll.api.entity.dto.PollDtoOut import de.livepoll.api.entity.dto.PollItemDtoOut import de.livepoll.api.repository.PollRepository import de.livepoll.api.repository.UserRepository -import de.livepoll.api.util.quartz.JobScheduleCrator +import de.livepoll.api.util.quartz.JobScheduleCreator import de.livepoll.api.util.quartz.StartPollPresentationJob import de.livepoll.api.util.quartz.StopPollPresentationJob import de.livepoll.api.util.toDtoOut @@ -29,7 +29,7 @@ class PollService( private val pollItemService: PollItemService, private val webSocketService: WebSocketService, private val schedulerFactory: SchedulerFactoryBean, - private val jobScheduleCrator: JobScheduleCrator + private val jobScheduleCreator: JobScheduleCreator ) { @@ -149,14 +149,25 @@ class PollService( this.name = poll.name this.currentItem = poll.currentItem - if (poll.startDate != null && poll.endDate != null) { - updateScheduledPoll(pollId, poll.startDate, poll.endDate) - this.startDate = poll.startDate - this.endDate = poll.endDate - } else if (poll.startDate == null && poll.endDate == null) { - stopScheduledPoll(pollId) - this.startDate = null - this.endDate = null + try { + if (poll.startDate == null) { + this.startDate = null + stopScheduledPoll("start-poll-$pollId") + } else { + updateScheduledPollStart(pollId, poll.startDate) + this.startDate = poll.startDate + } + + if (poll.endDate == null) { + this.endDate = null + stopScheduledPoll("stop-poll-$pollId") + } else { + updateScheduledPollEnd(pollId, poll.endDate) + this.endDate = poll.endDate + } + } catch (ex: ResponseStatusException) { + pollRepository.saveAndFlush(this) + throw ResponseStatusException(HttpStatus.CONFLICT, ex.message) } if (poll.slug != null && this.slug != poll.slug) { @@ -169,10 +180,10 @@ class PollService( } if (this.currentItem != null) { - webSocketService.sendCurrentItem(this.slug, this.id, this.currentItem) webSocketService.sendItemWithAnswers(this.currentItem!!) } + webSocketService.sendCurrentItem(this.slug, this.id, this.currentItem) return pollRepository.saveAndFlush(this).toDtoOut() } } @@ -215,58 +226,92 @@ class PollService( * * @param pollId the id of the poll that should be scheduled * @param startDate the start date of the poll - * @param stopDate the end date of the poll + * @param endDate the end date of the poll */ - private fun schedulePoll(pollId: Long, startDate: Date, stopDate: Date) { - if (startDate.before(GregorianCalendar.getInstance().time) || stopDate.before(GregorianCalendar.getInstance().time)) { - throw ResponseStatusException( - HttpStatus.CONFLICT, - "Poll was not planned because start or end date is in the past" - ) - } else { - val jobDetailStart = jobScheduleCrator.createJob( - StartPollPresentationJob::class.java, - "start-poll-$pollId", - pollId - ) - val triggerStart = - jobScheduleCrator.createSimpleTrigger("start-poll-trigger-$pollId", startDate) - schedulerFactory.`object`!!.scheduleJob(jobDetailStart, triggerStart) - - val jobDetailStop = jobScheduleCrator.createJob( - StopPollPresentationJob::class.java, - "stop-poll-$pollId", - pollId - ) - val triggerStop = jobScheduleCrator.createSimpleTrigger("stop-poll-trigger-$pollId", stopDate) - schedulerFactory.`object`!!.scheduleJob(jobDetailStop, triggerStop) - } + private fun schedulePoll(pollId: Long, startDate: Date, endDate: Date) { + checkIfDateIsValid(startDate) + checkIfDateIsValid(endDate) + schedulePollStart(pollId, startDate) + schedulePollEnd(pollId, endDate) } /** - * Update the start and end date of an poll which has already been planned. + * Internal method to create start event. * * @param pollId the id of the poll that should be scheduled * @param startDate the start date of the poll - * @param stopDate the end date of the poll */ - fun updateScheduledPoll(pollId: Long, startDate: Date, stopDate: Date) { - if (startDate.before(GregorianCalendar.getInstance().time) || stopDate.before(GregorianCalendar.getInstance().time)) { + private fun schedulePollStart(pollId: Long, startDate: Date) { + val jobDetailStart = jobScheduleCreator.createJob( + StartPollPresentationJob::class.java, + "start-poll-$pollId", + pollId + ) + val triggerStart = + jobScheduleCreator.createSimpleTrigger("start-poll-trigger-$pollId", startDate) + schedulerFactory.`object`!!.scheduleJob(jobDetailStart, triggerStart) + } + + /** + * Internal method to create end event. + * + * @param pollId the id of the poll whose ending should be scheduled + * @param endDate the end date of the poll + */ + private fun schedulePollEnd(pollId: Long, endDate: Date) { + val jobDetailStop = jobScheduleCreator.createJob( + StopPollPresentationJob::class.java, + "stop-poll-$pollId", + pollId + ) + val triggerStop = jobScheduleCreator.createSimpleTrigger("stop-poll-trigger-$pollId", endDate) + schedulerFactory.`object`!!.scheduleJob(jobDetailStop, triggerStop) + } + + /** + * Update the start date of a poll which has already been planned. + * + * @param pollId the id of the poll that should be scheduled + * @param startDate the new start date of the poll + */ + private fun updateScheduledPollStart(pollId: Long, startDate: Date) { + checkIfDateIsValid(startDate) + val jobNameStart = "start-poll-trigger-$pollId" + val triggerStart = jobScheduleCreator.createSimpleTrigger(jobNameStart, startDate) + val returnDate = schedulerFactory.`object`!!.rescheduleJob(TriggerKey.triggerKey(jobNameStart), triggerStart) + if (returnDate == null) { + schedulePollStart(pollId, startDate) + } + } + + /** + * Update the end date of a poll which has already been planned. + * + * @param pollId the id of the poll that should be scheduled + * @param endDate the new end date of the poll + */ + private fun updateScheduledPollEnd(pollId: Long, endDate: Date) { + checkIfDateIsValid(endDate) + val jobNameStop = "stop-poll-trigger-$pollId" + val triggerStop = jobScheduleCreator.createSimpleTrigger(jobNameStop, endDate) + val returnDate = schedulerFactory.`object`!!.rescheduleJob(TriggerKey.triggerKey(jobNameStop), triggerStop) + if (returnDate == null) { + schedulePollEnd(pollId, endDate) + } + } + + /** + * Check that the date is in the future. + * + * @param date the date + * @throws ResponseStatusException an exception is thrown if the date is in the past + */ + private fun checkIfDateIsValid(date: Date) { + if (date.before(GregorianCalendar.getInstance().time)) { throw ResponseStatusException( HttpStatus.CONFLICT, "Poll was not planned because start or end date is in the past" ) - } else { - val jobNameStart = "start-poll-trigger-$pollId" - val jobNameStop = "stop-poll-trigger-$pollId" - val triggerStart = jobScheduleCrator.createSimpleTrigger(jobNameStart, startDate) - val triggerStop = jobScheduleCrator.createSimpleTrigger(jobNameStop, stopDate) - val returnDate = - schedulerFactory.`object`!!.rescheduleJob(TriggerKey.triggerKey(jobNameStart), triggerStart) - schedulerFactory.`object`!!.rescheduleJob(TriggerKey.triggerKey(jobNameStop), triggerStop) - if (returnDate == null) { - schedulePoll(pollId, startDate, stopDate) - } } } @@ -300,11 +345,10 @@ class PollService( /** * Unschedule a poll. * - * @param pollId the id the poll which should be unscheduled + * @param jobkey the name of the job responsible for stopping the scheduled poll */ - fun stopScheduledPoll(pollId: Long) { - schedulerFactory.`object`!!.deleteJob(JobKey("start-poll-$pollId")) - schedulerFactory.`object`!!.deleteJob(JobKey("stop-poll-$pollId")) + fun stopScheduledPoll(jobkey: String) { + schedulerFactory.`object`!!.deleteJob(JobKey(jobkey)) } } diff --git a/src/main/kotlin/de/livepoll/api/util/AccountListener.kt b/src/main/kotlin/de/livepoll/api/util/AccountListener.kt index 5ae49451..64329552 100644 --- a/src/main/kotlin/de/livepoll/api/util/AccountListener.kt +++ b/src/main/kotlin/de/livepoll/api/util/AccountListener.kt @@ -3,10 +3,10 @@ package de.livepoll.api.util import de.livepoll.api.service.AccountService import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.ApplicationListener +import org.springframework.core.io.ClassPathResource import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.MimeMessageHelper import org.springframework.stereotype.Component -import org.springframework.util.ResourceUtils import java.util.* import javax.mail.internet.MimeMessage @@ -60,7 +60,7 @@ class AccountListener : ApplicationListener { ) helper.addInline( "logo", - ResourceUtils.getFile("classpath:logo.png") + ClassPathResource("logo.png") ) javaMailSender.send(mimeMessage) diff --git a/src/main/kotlin/de/livepoll/api/util/CustomModelMapper.kt b/src/main/kotlin/de/livepoll/api/util/CustomModelMapper.kt index 38f77e18..e4d7d67b 100644 --- a/src/main/kotlin/de/livepoll/api/util/CustomModelMapper.kt +++ b/src/main/kotlin/de/livepoll/api/util/CustomModelMapper.kt @@ -2,6 +2,8 @@ package de.livepoll.api.util import de.livepoll.api.entity.db.* import de.livepoll.api.entity.dto.* +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException // --------------------------------------------------- Poll mappers ---------------------------------------------------- @@ -34,8 +36,22 @@ fun MultipleChoiceItemAnswer.toDtoOut(): MultipleChoiceItemAnswerDtoOut { } fun QuizItem.toDtoOut(): QuizItemDtoOut { + // Guarantee that first item is the correct one + val answersSorted: MutableList = this.answers + val correctItemIndex = answersSorted.indexOfFirst { it.isCorrect } + if (correctItemIndex == -1) { + // Should never happen + val msg = "There was no correct item in the list -> data inconsistency (should NEVER happen !!!)" + throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, msg) + } + + if (answersSorted.size > 1) { + // Swap + answersSorted[0] = answersSorted[correctItemIndex].also { answersSorted[correctItemIndex] = answersSorted[0] } + } + return QuizItemDtoOut(this.id, this.poll.id, this.question, this.position, - "quiz", this.answers.map { it.toDtoOut() } + "quiz", answersSorted.map { it.toDtoOut() } ) } diff --git a/src/main/kotlin/de/livepoll/api/util/JwtRequestFilter.kt b/src/main/kotlin/de/livepoll/api/util/JwtRequestFilter.kt index 540defb0..222c1822 100644 --- a/src/main/kotlin/de/livepoll/api/util/JwtRequestFilter.kt +++ b/src/main/kotlin/de/livepoll/api/util/JwtRequestFilter.kt @@ -16,7 +16,7 @@ import javax.servlet.http.HttpServletResponse @Component class JwtRequestFilter( - private val cookieCipher: CookieCipher + private val cookieCipher: CookieCipher ) : OncePerRequestFilter() { @Autowired @@ -27,7 +27,11 @@ class JwtRequestFilter( private val accessTokenCookieName = System.getenv("LIVE_POLL_JWT_AUTH_COOKIE_NAME") - override fun doFilterInternal(httpServletRequest: HttpServletRequest, httpServletResponse: HttpServletResponse, filterChain: FilterChain) { + override fun doFilterInternal( + httpServletRequest: HttpServletRequest, + httpServletResponse: HttpServletResponse, + filterChain: FilterChain + ) { var token: String? = null var userName: String? = null @@ -45,9 +49,9 @@ class JwtRequestFilter( val userDetails: UserDetails = jwtUserDetailsService.loadUserByUsername(userName) if (jwtUtil.validateToken(token, userDetails)) { SecurityContextHolder.getContext().authentication = - UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities).apply { - details = WebAuthenticationDetailsSource().buildDetails(httpServletRequest) - } + UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities).apply { + details = WebAuthenticationDetailsSource().buildDetails(httpServletRequest) + } } } filterChain.doFilter(httpServletRequest, httpServletResponse) diff --git a/src/main/kotlin/de/livepoll/api/util/JwtUtil.kt b/src/main/kotlin/de/livepoll/api/util/JwtUtil.kt index 631380dd..f812f335 100644 --- a/src/main/kotlin/de/livepoll/api/util/JwtUtil.kt +++ b/src/main/kotlin/de/livepoll/api/util/JwtUtil.kt @@ -11,7 +11,7 @@ import java.util.function.Function @Service class JwtUtil( - val blockedTokenRepository: BlockedTokenRepository + val blockedTokenRepository: BlockedTokenRepository ) { private val secret = System.getenv("LIVE_POLL_JWT_SECRET") @@ -20,12 +20,13 @@ class JwtUtil( fun extractExpiration(token: String?): Date = extractClaim(token) { obj: Claims -> obj.expiration } - fun extractClaim(token: String?, claimsResolver: Function) = claimsResolver.apply(extractAllClaims(token)) + fun extractClaim(token: String?, claimsResolver: Function) = + claimsResolver.apply(extractAllClaims(token)) private fun extractAllClaims(token: String?): Claims { return Jwts.parser().setSigningKey(secret) - .parseClaimsJws(token) - .body + .parseClaimsJws(token) + .body } private fun isTokenExpired(token: String?) = extractExpiration(token).before(Date()) @@ -34,14 +35,14 @@ class JwtUtil( private fun createToken(claims: Map, subject: String): String { return Jwts.builder() - .setClaims(claims) - .setSubject(subject).setIssuedAt(Date(System.currentTimeMillis())) - .setExpiration(Date(System.currentTimeMillis() + TOKEN_DURATION * 1000)) - .signWith(SignatureAlgorithm.HS256, secret).compact() + .setClaims(claims) + .setSubject(subject).setIssuedAt(Date(System.currentTimeMillis())) + .setExpiration(Date(System.currentTimeMillis() + TOKEN_DURATION * 1000)) + .signWith(SignatureAlgorithm.HS256, secret).compact() } - fun validateToken(token: String?, userDetails: UserDetails) - = extractUsername(token) == userDetails.username && !isTokenExpired(token) && !isTokenBlocked(token) + fun validateToken(token: String?, userDetails: UserDetails) = + extractUsername(token) == userDetails.username && !isTokenExpired(token) && !isTokenBlocked(token) private fun isTokenBlocked(token: String?): Boolean { blockedTokenRepository.findByToken(token)?.run { @@ -49,4 +50,4 @@ class JwtUtil( } return false } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/OnCreateAccountEvent.kt b/src/main/kotlin/de/livepoll/api/util/OnCreateAccountEvent.kt index 7c411d27..d1a3258f 100644 --- a/src/main/kotlin/de/livepoll/api/util/OnCreateAccountEvent.kt +++ b/src/main/kotlin/de/livepoll/api/util/OnCreateAccountEvent.kt @@ -4,6 +4,6 @@ import de.livepoll.api.entity.db.User import org.springframework.context.ApplicationEvent class OnCreateAccountEvent( - var user: User, - var appUrl: String -): ApplicationEvent(user) \ No newline at end of file + var user: User, + var appUrl: String +) : ApplicationEvent(user) diff --git a/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieCipher.kt b/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieCipher.kt index 7a35ad04..e9e9f523 100644 --- a/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieCipher.kt +++ b/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieCipher.kt @@ -29,7 +29,8 @@ class CookieCipher { try { val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, secretKey) - return Base64.getEncoder().encodeToString(cipher.doFinal(stringToEncrypt.toByteArray(StandardCharsets.UTF_8))) + return Base64.getEncoder() + .encodeToString(cipher.doFinal(stringToEncrypt.toByteArray(StandardCharsets.UTF_8))) } catch (e: Exception) { e.printStackTrace() throw Exception("Error in cookieCipher") @@ -47,4 +48,4 @@ class CookieCipher { throw Exception("Error in cookieCipher") } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieUtil.kt b/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieUtil.kt index 703b88a0..691d850c 100644 --- a/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieUtil.kt +++ b/src/main/kotlin/de/livepoll/api/util/jwtCookie/CookieUtil.kt @@ -6,25 +6,25 @@ import org.springframework.stereotype.Component @Component class CookieUtil( - private val cookieCipher: CookieCipher + private val cookieCipher: CookieCipher ) { private val accessTokenCookieName = System.getenv("LIVE_POLL_JWT_AUTH_COOKIE_NAME") private val isTLSEncrypted = System.getenv("LIVE_POLL_SERVER_URL").startsWith("https://") private val isDevServer = System.getenv("LIVE_POLL_SERVER_URL").contains("localhost") - private val domain = if(isDevServer) "localhost" else "live-poll.de" + private val domain = if (isDevServer) "localhost" else "live-poll.de" fun createAccessTokenCookie(token: String) = buildCookie(cookieCipher.encrypt(token)) fun deleteAccessTokenCookie() = buildCookie("") fun buildCookie(content: String) = ResponseCookie.from(accessTokenCookieName, content) - .maxAge(if (content.isBlank()) 0 else TOKEN_DURATION) - .httpOnly(true) - // Secure is only supported with https - .secure(isTLSEncrypted) - // Use top level domain. Subdomains are included automatically (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) - .domain(domain) - .sameSite("None") - .path("/") - .build() -} \ No newline at end of file + .maxAge(if (content.isBlank()) 0 else TOKEN_DURATION) + .httpOnly(true) + // Secure is only supported with https + .secure(isTLSEncrypted) + // Use top level domain. Subdomains are included automatically (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) + .domain(domain) + .sameSite("None") + .path("/") + .build() +} diff --git a/src/main/kotlin/de/livepoll/api/util/quartz/JobScheduleCrator.kt b/src/main/kotlin/de/livepoll/api/util/quartz/JobScheduleCrator.kt index bb5a8a82..d45040fc 100644 --- a/src/main/kotlin/de/livepoll/api/util/quartz/JobScheduleCrator.kt +++ b/src/main/kotlin/de/livepoll/api/util/quartz/JobScheduleCrator.kt @@ -12,8 +12,8 @@ import java.util.* @Component -class JobScheduleCrator( - private val applicationContext: ApplicationContext +class JobScheduleCreator( + private val applicationContext: ApplicationContext ) { fun createJob(jobClass: Class, jobName: String, pollId: Long): JobDetail { @@ -42,4 +42,4 @@ class JobScheduleCrator( return factoryBean.getObject()!! } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/quartz/StartPollPresentationJob.kt b/src/main/kotlin/de/livepoll/api/util/quartz/StartPollPresentationJob.kt index 1533724b..6e98ac59 100644 --- a/src/main/kotlin/de/livepoll/api/util/quartz/StartPollPresentationJob.kt +++ b/src/main/kotlin/de/livepoll/api/util/quartz/StartPollPresentationJob.kt @@ -1,8 +1,6 @@ package de.livepoll.api.util.quartz -import de.livepoll.api.repository.PollRepository import de.livepoll.api.service.PollService -import de.livepoll.api.service.WebSocketService import org.quartz.Job import org.quartz.JobExecutionContext import org.springframework.beans.factory.annotation.Autowired @@ -21,4 +19,4 @@ class StartPollPresentationJob : Job { pollService.executeStartPoll(context.jobDetail.jobDataMap["pollId"].toString().toLong()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/quartz/StopPollPresentationJob.kt b/src/main/kotlin/de/livepoll/api/util/quartz/StopPollPresentationJob.kt index 76f36aa9..a5e253fe 100644 --- a/src/main/kotlin/de/livepoll/api/util/quartz/StopPollPresentationJob.kt +++ b/src/main/kotlin/de/livepoll/api/util/quartz/StopPollPresentationJob.kt @@ -1,8 +1,6 @@ package de.livepoll.api.util.quartz -import de.livepoll.api.repository.PollRepository import de.livepoll.api.service.PollService -import de.livepoll.api.service.WebSocketService import org.quartz.Job import org.quartz.JobExecutionContext import org.springframework.beans.factory.annotation.Autowired @@ -20,4 +18,4 @@ class StopPollPresentationJob : Job { println("Execute stop poll event with poll-id: " + context.jobDetail.jobDataMap["pollId"].toString()) pollService.executeStopPoll(context.jobDetail.jobDataMap["pollId"].toString().toLong()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/websocket/CustomWebSocketHandshakeHandler.kt b/src/main/kotlin/de/livepoll/api/util/websocket/CustomWebSocketHandshakeHandler.kt index 0120d44f..6b3e172a 100644 --- a/src/main/kotlin/de/livepoll/api/util/websocket/CustomWebSocketHandshakeHandler.kt +++ b/src/main/kotlin/de/livepoll/api/util/websocket/CustomWebSocketHandshakeHandler.kt @@ -10,7 +10,11 @@ private const val ATTR_PRINCIPAL = "_principal_" class CustomWebSocketHandshakeHandler : DefaultHandshakeHandler() { - override fun determineUser(request: ServerHttpRequest, wsHandler: WebSocketHandler, attributes: MutableMap): Principal { + override fun determineUser( + request: ServerHttpRequest, + wsHandler: WebSocketHandler, + attributes: MutableMap + ): Principal { val name: String if (!attributes.containsKey(ATTR_PRINCIPAL)) { name = UUID.randomUUID().toString() @@ -21,4 +25,4 @@ class CustomWebSocketHandshakeHandler : DefaultHandshakeHandler() { return Principal { name } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/websocket/HttpHandshakeInterceptor.kt b/src/main/kotlin/de/livepoll/api/util/websocket/HttpHandshakeInterceptor.kt index d33fb047..1c4323c2 100644 --- a/src/main/kotlin/de/livepoll/api/util/websocket/HttpHandshakeInterceptor.kt +++ b/src/main/kotlin/de/livepoll/api/util/websocket/HttpHandshakeInterceptor.kt @@ -8,7 +8,12 @@ import org.springframework.web.socket.server.HandshakeInterceptor class HttpHandshakeInterceptor : HandshakeInterceptor { - override fun beforeHandshake(request: ServerHttpRequest, response: ServerHttpResponse, webSocketHandler: WebSocketHandler, attributes: MutableMap): Boolean { + override fun beforeHandshake( + request: ServerHttpRequest, + response: ServerHttpResponse, + webSocketHandler: WebSocketHandler, + attributes: MutableMap + ): Boolean { if (request is ServletServerHttpRequest) { val session = request.servletRequest.session attributes["sessionId"] = session.id @@ -20,4 +25,4 @@ class HttpHandshakeInterceptor : HandshakeInterceptor { override fun afterHandshake(p0: ServerHttpRequest, p1: ServerHttpResponse, p2: WebSocketHandler, p3: Exception?) { } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/livepoll/api/util/websocket/SubscribeListener.kt b/src/main/kotlin/de/livepoll/api/util/websocket/SubscribeListener.kt index 479829cc..36e57847 100644 --- a/src/main/kotlin/de/livepoll/api/util/websocket/SubscribeListener.kt +++ b/src/main/kotlin/de/livepoll/api/util/websocket/SubscribeListener.kt @@ -13,10 +13,10 @@ import org.springframework.web.socket.messaging.SessionSubscribeEvent @Component class SubscribeListener( - private val messagingTemplate: SimpMessageSendingOperations, - private val pollRepository: PollRepository, - private val pollItemService: PollItemService, - private val webSocketService: WebSocketService + private val messagingTemplate: SimpMessageSendingOperations, + private val pollRepository: PollRepository, + private val pollItemService: PollItemService, + private val webSocketService: WebSocketService ) : ApplicationListener { @Transactional diff --git a/src/main/resources/quartz.properties b/src/main/resources/quartz.properties index 1a1e4b6a..da1a4988 100644 --- a/src/main/resources/quartz.properties +++ b/src/main/resources/quartz.properties @@ -2,7 +2,6 @@ org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount=2 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true - # JDBCJobStore using JobStoreTX org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate diff --git a/src/test/kotlin/de/livepoll/api/controller/v1/PollItemServiceTest.kt b/src/test/kotlin/de/livepoll/api/controller/v1/PollItemServiceTest.kt index ce116247..e1064398 100644 --- a/src/test/kotlin/de/livepoll/api/controller/v1/PollItemServiceTest.kt +++ b/src/test/kotlin/de/livepoll/api/controller/v1/PollItemServiceTest.kt @@ -18,12 +18,9 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.context.annotation.Bean import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit4.SpringRunner import java.util.* - - @RunWith(SpringRunner::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = [LivePollApplication::class]) @AutoConfigureMockMvc @@ -55,7 +52,7 @@ class PollItemServiceTest { @MockBean private lateinit var quizItemAnswerRepository: QuizItemAnswerRepository - init{ + init { } @@ -133,7 +130,7 @@ class PollItemServiceTest { 0, mockPoll, 0, - "Multiple Choice Question1", + "Multiple Choice Question 1", allowMultipleAnswers = false, allowBlankField = false, answers = mutableListOf() @@ -178,7 +175,7 @@ class PollItemServiceTest { val multipleChoice1 = MultipleChoiceItemDtoOut( 0, mockPoll.id, - "Multiple Choice Question1", + "Multiple Choice Question 1", 0, PollItemType.MULTIPLE_CHOICE.representation, answers1 @@ -252,7 +249,7 @@ class PollItemServiceTest { val answer21 = QuizItemAnswerDtoOut(4, "Option 2-1", false, 0) val answer22 = QuizItemAnswerDtoOut(5, "Option 2-2", true, 0) - val answers2 = listOf(answer21, answer22) + val answers2 = listOf(answer22, answer21) // Shell val quiz1 = QuizItemDtoOut( diff --git a/src/test/kotlin/de/livepoll/api/cucumber/CucumberIntegrationTest.kt b/src/test/kotlin/de/livepoll/api/cucumber/CucumberIntegrationTest.kt index 8f3a15b5..e3a335f1 100644 --- a/src/test/kotlin/de/livepoll/api/cucumber/CucumberIntegrationTest.kt +++ b/src/test/kotlin/de/livepoll/api/cucumber/CucumberIntegrationTest.kt @@ -16,7 +16,6 @@ import org.springframework.http.* import org.springframework.http.client.HttpComponentsClientHttpRequestFactory import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.TestPropertySource import org.springframework.web.client.RestTemplate import org.springframework.web.client.exchange import java.security.cert.X509Certificate @@ -28,7 +27,7 @@ import javax.net.ssl.SSLContext @SpringBootTest(classes = [LivePollApplication::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") class CucumberIntegrationTest( - private val userRepository: UserRepository + private val userRepository: UserRepository ) { @Autowired @@ -62,14 +61,14 @@ class CucumberIntegrationTest( val acceptingTrustStrategy = { chain: Array?, authType: String? -> true } val sslContext: SSLContext = org.apache.http.ssl.SSLContexts.custom() - .loadTrustMaterial(null, acceptingTrustStrategy) - .build() + .loadTrustMaterial(null, acceptingTrustStrategy) + .build() val csf = SSLConnectionSocketFactory(sslContext) val httpClient: CloseableHttpClient = HttpClients.custom() - .setSSLSocketFactory(csf) - .build() + .setSSLSocketFactory(csf) + .build() val requestFactory = HttpComponentsClientHttpRequestFactory() @@ -79,7 +78,17 @@ class CucumberIntegrationTest( protected fun logInWithTestUser(): Pair { if (userRepository.findByUsername(testUserName) == null) { - userRepository.saveAndFlush(User(0, testUserName, "email", passwordEncoder.encode(testUserPassword), true, "ROLE_USER", emptyList())) + userRepository.saveAndFlush( + User( + 0, + testUserName, + "email", + passwordEncoder.encode(testUserPassword), + true, + "ROLE_USER", + emptyList() + ) + ) testUser = userRepository.findByUsername(testUserName) } // https://springbootdev.com/2017/11/21/spring-resttemplate-exchange-method/ @@ -92,9 +101,9 @@ class CucumberIntegrationTest( // make request val responseEntity: ResponseEntity = restTemplate.exchange( - url, - HttpMethod.POST, - requestEntity + url, + HttpMethod.POST, + requestEntity ) // store session cookie (which includes the JWT token) @@ -103,7 +112,10 @@ class CucumberIntegrationTest( return Pair(responseEntity.statusCode, setCookie!!) } - protected final inline fun makeGetRequestWithSessionCookie(url: String, sessionCookie: String): ResponseEntity { + protected final inline fun makeGetRequestWithSessionCookie( + url: String, + sessionCookie: String + ): ResponseEntity { // request body params & headers val headers = HttpHeaders() headers["Cookie"] = sessionCookie @@ -111,9 +123,9 @@ class CucumberIntegrationTest( // make request return restTemplate.exchange( - url, - HttpMethod.GET, - requestEntity + url, + HttpMethod.GET, + requestEntity ) } diff --git a/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/PollStepDefinitions.kt b/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/PollStepDefinitions.kt index eb8c342b..d0497bb2 100644 --- a/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/PollStepDefinitions.kt +++ b/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/PollStepDefinitions.kt @@ -15,8 +15,8 @@ import org.springframework.web.client.exchange import java.sql.Date class PollStepDefinitions( - userRepository: UserRepository, - private val pollRepository: PollRepository + userRepository: UserRepository, + private val pollRepository: PollRepository ) : CucumberIntegrationTest(userRepository) { private val POLL_ENDPOINT = "/v1/polls" @@ -48,9 +48,9 @@ class PollStepDefinitions( // make request val responseEntity: ResponseEntity = restTemplate.exchange( - url, - HttpMethod.POST, - requestEntity + url, + HttpMethod.POST, + requestEntity ) assertThat(responseEntity.statusCode).isEqualTo(HttpStatus.CREATED) @@ -65,7 +65,7 @@ class PollStepDefinitions( fun retrieveMyPolls() { val url = "${SERVER_URL}:$port$POLL_ENDPOINT" val pollResponseEntity = - makeGetRequestWithSessionCookie>(url, SessionCookieUtil.sessionCookie) + makeGetRequestWithSessionCookie>(url, SessionCookieUtil.sessionCookie) assertThat(pollResponseEntity.statusCode).isEqualTo(HttpStatus.OK) assertThat(pollResponseEntity.body).isNotNull for (poll in pollResponseEntity.body!!) { diff --git a/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/UserStepDefinitions.kt b/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/UserStepDefinitions.kt index 56ad5e1b..eaed7de2 100644 --- a/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/UserStepDefinitions.kt +++ b/src/test/kotlin/de/livepoll/api/cucumber/stepdefinitions/UserStepDefinitions.kt @@ -1,9 +1,11 @@ package de.livepoll.api.cucumber.stepdefinitions import de.livepoll.api.cucumber.CucumberIntegrationTest +import de.livepoll.api.entity.db.OpenTextItem import de.livepoll.api.entity.db.Poll import de.livepoll.api.entity.db.PollItem import de.livepoll.api.entity.db.User +import de.livepoll.api.repository.OpenTextItemRepository import de.livepoll.api.repository.PollRepository import de.livepoll.api.repository.UserRepository import io.cucumber.java.en.And @@ -19,12 +21,14 @@ import java.util.* private const val LOGOUT_ENDPOINT = "/v1/account/logout" class UserStepDefinitions( - private val pollRepository: PollRepository, - private val userRepository: UserRepository + private val pollRepository: PollRepository, + private val userRepository: UserRepository, + private val openTextItemRepository: OpenTextItemRepository ) : CucumberIntegrationTest(userRepository) { private val USER_ENDPOINT = "/v1/user" private val POLL_ENDPOINT = "/v1/polls" + private val POLL_ITEM_ENDPOINT = "/v1/poll-items" lateinit var status: HttpStatus var alreadyConfirmed = false @@ -60,14 +64,36 @@ class UserStepDefinitions( @And("I am not authorized to retrieve information about a different user") fun getInfoAboutDifferentUser() { - if(userRepository.findByUsername("different_user") == null){ - userRepository.saveAndFlush(User(1,"different_user", "different_email", passwordEncoder.encode("12345"), true, "ROLE_USER", emptyList())) + if (userRepository.findByUsername("different_user") == null) { + userRepository.saveAndFlush( + User( + 1, + "different_user", + "different_email", + passwordEncoder.encode("12345"), + true, + "ROLE_USER", + emptyList() + ) + ) } - if(pollRepository.findBySlug("different_user_test_slug") == null){ - pollRepository.saveAndFlush(Poll(0, userRepository.findByUsername("different_user")!!, "different_user_test_poll", GregorianCalendar(2021, 5, 2).time, GregorianCalendar(2021, 5, 2).time, "different_user_test_slug", null, emptyList().toMutableList())) + if (pollRepository.findBySlug("different_user_test_slug") == null) { + pollRepository.saveAndFlush( + Poll( + 0, + userRepository.findByUsername("different_user")!!, + "different_user_test_poll", + GregorianCalendar(2021, 5, 2).time, + GregorianCalendar(2021, 5, 2).time, + "different_user_test_slug", + null, + emptyList().toMutableList() + ) + ) } - val id = pollRepository.findBySlug("different_user_test_slug")!!.id - val url = "${SERVER_URL}:$port$POLL_ENDPOINT/${id}" + val poll = pollRepository.findBySlug("different_user_test_slug")!! + val id = openTextItemRepository.saveAndFlush(OpenTextItem(0, poll, "question", 0, mutableListOf())).id + val url = "${SERVER_URL}:$port$POLL_ITEM_ENDPOINT/${id}" try { val pollResponseEntity = makeGetRequestWithSessionCookie(url, SessionCookieUtil.sessionCookie) assertThat(pollResponseEntity.statusCode).isEqualTo(HttpStatus.FORBIDDEN) @@ -86,9 +112,9 @@ class UserStepDefinitions( // make request val logOutResponseEntity: ResponseEntity = restTemplate.exchange( - url, - HttpMethod.PUT, - logOutRequestEntity + url, + HttpMethod.PUT, + logOutRequestEntity ) assertThat(logOutResponseEntity.statusCode).isEqualTo(HttpStatus.OK) } diff --git a/src/test/resources/quartz.properties b/src/test/resources/quartz.properties index 1a1e4b6a..da1a4988 100644 --- a/src/test/resources/quartz.properties +++ b/src/test/resources/quartz.properties @@ -2,7 +2,6 @@ org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount=2 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true - # JDBCJobStore using JobStoreTX org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate