From 9c0b44ecb58bad7c7cae816d9f4511aa58835b89 Mon Sep 17 00:00:00 2001 From: Carine Dengler Date: Wed, 14 Feb 2024 20:32:11 +0100 Subject: [PATCH] fix: non-voter user should not be able to see voting screen --- web/frontend/src/language/de.json | 6 ++- web/frontend/src/language/en.json | 6 ++- web/frontend/src/language/fr.json | 6 ++- web/frontend/src/pages/ballot/Show.tsx | 14 ++++-- .../{FormClosed.tsx => FormNotAvailable.tsx} | 10 ++-- web/frontend/tests/ballot.spec.ts | 49 +++++++++++++++++++ 6 files changed, 77 insertions(+), 14 deletions(-) rename web/frontend/src/pages/ballot/components/{FormClosed.tsx => FormNotAvailable.tsx} (77%) create mode 100644 web/frontend/tests/ballot.spec.ts diff --git a/web/frontend/src/language/de.json b/web/frontend/src/language/de.json index ab1b8ed31..f938a4333 100644 --- a/web/frontend/src/language/de.json +++ b/web/frontend/src/language/de.json @@ -218,7 +218,7 @@ "operationFailure": "Der Vorgang ist fehlgeschlagen. Versuchen Sie, die Seite zu aktualisieren.", "shuffleFail": "Die Zufallsmischung ist fehlgeschlagen.", "voteImpossible": "Unmöglich abstimmen", - "notFoundVoteImpossible": "Zurück zur Formulartabelle", + "returnToFormTable": "Zurück zur Formulartabelle", "voteImpossibleDescription": "Das Formular ist nicht mehr zur Abstimmung geöffnet.", "yes": "Ja", "no": "Nein", @@ -289,6 +289,8 @@ "footerUnknown": "?", "footerVersion": "version:", "footerBuild": "build:", - "footerBuildTime": "in:" + "footerBuildTime": "in:", + "voteNotVoter": "Wählen nicht erlaubt.", + "voteNotVoterDescription": "Sie sind nicht wahlberechtigt in dieser Wahl. Falls Sie denken, dass ein Fehler vorliegt, wenden Sie sich bitte an die verantwortliche Stelle." } } diff --git a/web/frontend/src/language/en.json b/web/frontend/src/language/en.json index 1c5597d1c..7f0a25b6e 100644 --- a/web/frontend/src/language/en.json +++ b/web/frontend/src/language/en.json @@ -218,7 +218,7 @@ "operationFailure": "The operation failed. Try refreshing the page.", "shuffleFail": "The shuffle operation failed.", "voteImpossible": "Vote Impossible", - "notFoundVoteImpossible": "Go back to form table", + "returnToFormTable": "Go back to form table", "voteImpossibleDescription": "The form is not open for voting anymore.", "yes": "Yes", "no": "No", @@ -290,6 +290,8 @@ "footerUnknown": "?", "footerVersion": "version:", "footerBuild": "build:", - "footerBuildTime": "in:" + "footerBuildTime": "in:", + "voteNotVoter": "Voting not allowed.", + "voteNotVoterDescription": "You are not allowed to vote in this form. If you believe this is an error, please contact the responsible of the service." } } diff --git a/web/frontend/src/language/fr.json b/web/frontend/src/language/fr.json index ea517af5c..d783e8c1d 100644 --- a/web/frontend/src/language/fr.json +++ b/web/frontend/src/language/fr.json @@ -218,7 +218,7 @@ "operationFailure": "L'opération a échoué. Essayez de rafraichir la page.", "shuffleFail": "L'opération de mélange a échoué", "voteImpossible": "Vote Impossible", - "notFoundVoteImpossible": "Retournez à l'onglet des sondages", + "returnToFormTable": "Retournez à l'onglet des sondages", "voteImpossibleDescription": "Le sondage n'est plus ouvert au vote.", "yes": "Oui", "no": "Non", @@ -289,6 +289,8 @@ "footerUnknown": "?", "footerVersion": "version:", "footerBuild": "build:", - "footerBuildTime": "en:" + "footerBuildTime": "en:", + "voteNotVoter": "Interdit de voter.", + "voteNotVoterDescription": "Vous n'avez pas le droit de voter dans cette élection. Si vous pensez qu'il s'agit d'une erreur, veuillez contacter le/la reponsable de service." } } diff --git a/web/frontend/src/pages/ballot/Show.tsx b/web/frontend/src/pages/ballot/Show.tsx index 5aa8b1140..c04ebf2ac 100644 --- a/web/frontend/src/pages/ballot/Show.tsx +++ b/web/frontend/src/pages/ballot/Show.tsx @@ -1,4 +1,6 @@ -import { FC, useState } from 'react'; +import { FC, useContext, useState } from 'react'; +import { AuthContext } from 'index'; +import { isVoter } from './../../utils/auth'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import kyber from '@dedis/kyber'; @@ -17,7 +19,7 @@ import { useConfiguration } from 'components/utils/useConfiguration'; import { Status } from 'types/form'; import { ballotIsValid } from './components/ValidateAnswers'; import BallotDisplay from './components/BallotDisplay'; -import FormClosed from './components/FormClosed'; +import FormNotAvailable from './components/FormNotAvailable'; import Loading from 'pages/Loading'; import RedirectToModal from 'components/modal/RedirectToModal'; import { default as i18n } from 'i18next'; @@ -39,6 +41,7 @@ const Ballot: FC = () => { const [castVoteLoading, setCastVoteLoading] = useState(false); const navigate = useNavigate(); + const { authorization, isLogged } = useContext(AuthContext); const hexToBytes = (hex: string) => { const bytes: number[] = []; @@ -113,6 +116,8 @@ const Ballot: FC = () => { event.currentTarget.disabled = true; }; + const userIsVoter = isVoter(formID, authorization, isLogged); + return ( <> { ) : ( <> - {status === Status.Open && ( + {status === Status.Open && userIsVoter && (

@@ -165,7 +170,8 @@ const Ballot: FC = () => {

)} - {status !== Status.Open && } + {!userIsVoter && } + {status !== Status.Open && } )} diff --git a/web/frontend/src/pages/ballot/components/FormClosed.tsx b/web/frontend/src/pages/ballot/components/FormNotAvailable.tsx similarity index 77% rename from web/frontend/src/pages/ballot/components/FormClosed.tsx rename to web/frontend/src/pages/ballot/components/FormNotAvailable.tsx index 79e6e967d..a149694a1 100644 --- a/web/frontend/src/pages/ballot/components/FormClosed.tsx +++ b/web/frontend/src/pages/ballot/components/FormNotAvailable.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { ROUTE_FORM_INDEX } from 'Routes'; -export default function FormClosed() { +export default function FormNotAvailable(props) { const { t } = useTranslation(); return ( @@ -13,15 +13,17 @@ export default function FormClosed() {

- {t('voteImpossible')} + {props.isVoter ? t('voteImpossible') : t('voteNotVoter')}

-

{t('voteImpossibleDescription')}

+

+ {props.isVoter ? t('voteImpossibleDescription') : t('voteNotVoterDescription')} +

- {t('notFoundVoteImpossible')} + {t('returnToFormTable')}
diff --git a/web/frontend/tests/ballot.spec.ts b/web/frontend/tests/ballot.spec.ts new file mode 100644 index 000000000..36e14d83e --- /dev/null +++ b/web/frontend/tests/ballot.spec.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@playwright/test'; +import { default as i18n } from 'i18next'; +import { assertHasFooter, assertHasNavBar, initI18n, logIn, setUp } from './shared'; +import { FORMID } from './mocks/shared'; +import { SCIPER_ADMIN, SCIPER_OTHER_USER, SCIPER_USER, mockPersonalInfo } from './mocks/api'; +import { mockFormsFormID } from './mocks/evoting'; + +initI18n(); + +test.beforeEach(async ({ page }) => { + await mockFormsFormID(page, 1); + await logIn(page, SCIPER_ADMIN); + await setUp(page, `/ballot/show/${FORMID}`); +}); + +test('Assert navigation bar is present', async ({ page }) => { + await assertHasNavBar(page); +}); + +test('Assert footer is present', async ({ page }) => { + await assertHasFooter(page); +}); + +test('Assert ballot form is correctly handled for anonymous users, non-voter users and voter users', async ({ + page, +}) => { + const castVoteButton = await page.getByRole('button', { name: i18n.t('castVote') }); + await test.step('Assert anonymous is redirected to login page', async () => { + await mockPersonalInfo(page); + await page.reload({ waitUntil: 'networkidle' }); + await expect(page).toHaveURL('/login'); + }); + await test.step('Assert non-voter gets page that they are not allowed to vote', async () => { + await logIn(page, SCIPER_OTHER_USER); + await page.goto(`/ballot/show/${FORMID}`, { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(`/ballot/show/${FORMID}`); + await expect(castVoteButton).toBeHidden(); + await expect(page.getByText(i18n.t('voteNotVoter'))).toBeVisible(); + await expect(page.getByText(i18n.t('voteNotVoterDescription'))).toBeVisible(); + }); + await test.step('Assert voter gets ballot', async () => { + await logIn(page, SCIPER_USER); + await page.goto(`/ballot/show/${FORMID}`, { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(`/ballot/show/${FORMID}`); + await expect(castVoteButton).toBeVisible(); + await expect(page.getByText(i18n.t('vote'))).toBeVisible(); + await expect(page.getByText(i18n.t('voteExplanation'))).toBeVisible(); + }); +});