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/src/pages/form/components/ActionButtons/AddVotersButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/AddVotersButton.tsx index e4a1e78b0..72e22f4a1 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/AddVotersButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/AddVotersButton.tsx @@ -1,6 +1,6 @@ import { DocumentAddIcon } from '@heroicons/react/outline'; import { useTranslation } from 'react-i18next'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; import { AuthContext } from 'index'; import { useContext } from 'react'; diff --git a/web/frontend/src/pages/form/components/ActionButtons/CancelButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/CancelButton.tsx index 820028bc3..41f2bbece 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/CancelButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/CancelButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const CancelButton = ({ status, handleCancel, ongoingAction, formID }) => { const { authorization, isLogged } = useContext(AuthContext); diff --git a/web/frontend/src/pages/form/components/ActionButtons/CloseButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/CloseButton.tsx index 916f5af71..4259a016a 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/CloseButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/CloseButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const CloseButton = ({ status, handleClose, ongoingAction, formID }) => { const { authorization, isLogged } = useContext(AuthContext); diff --git a/web/frontend/src/pages/form/components/ActionButtons/CombineButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/CombineButton.tsx index e4a2f7258..7dc5e651d 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/CombineButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/CombineButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const CombineButton = ({ status, handleCombine, ongoingAction, formID }) => { const { t } = useTranslation(); diff --git a/web/frontend/src/pages/form/components/ActionButtons/DecryptButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/DecryptButton.tsx index 65242a181..328a6978e 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/DecryptButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/DecryptButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const DecryptButton = ({ status, handleDecrypt, ongoingAction, formID }) => { const { authorization, isLogged } = useContext(AuthContext); diff --git a/web/frontend/src/pages/form/components/ActionButtons/DeleteButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/DeleteButton.tsx index 937d868d8..fd7712310 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/DeleteButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/DeleteButton.tsx @@ -2,7 +2,7 @@ import { TrashIcon } from '@heroicons/react/outline'; import { AuthContext } from 'index'; import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const DeleteButton = ({ handleDelete, formID }) => { const { t } = useTranslation(); diff --git a/web/frontend/src/pages/form/components/ActionButtons/InitializeButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/InitializeButton.tsx index 7def2696a..9cb4c5281 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/InitializeButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/InitializeButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const InitializeButton = ({ status, handleInitialize, ongoingAction, formID }) => { const { authorization, isLogged } = useContext(AuthContext); diff --git a/web/frontend/src/pages/form/components/ActionButtons/OpenButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/OpenButton.tsx index 77a1b282b..80a48a70d 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/OpenButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/OpenButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const OpenButton = ({ status, handleOpen, ongoingAction, formID }) => { const { t } = useTranslation(); diff --git a/web/frontend/src/pages/form/components/ActionButtons/SetupButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/SetupButton.tsx index 09d6c8e48..f1848c94c 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/SetupButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/SetupButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const SetupButton = ({ status, handleSetup, ongoingAction, formID }) => { const { t } = useTranslation(); diff --git a/web/frontend/src/pages/form/components/ActionButtons/ShuffleButton.tsx b/web/frontend/src/pages/form/components/ActionButtons/ShuffleButton.tsx index 4c537a78f..178b67896 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/ShuffleButton.tsx +++ b/web/frontend/src/pages/form/components/ActionButtons/ShuffleButton.tsx @@ -4,7 +4,7 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { OngoingAction, Status } from 'types/form'; import ActionButton from './ActionButton'; -import { isManager } from './utils'; +import { isManager } from './../../../../utils/auth'; const ShuffleButton = ({ status, handleShuffle, ongoingAction, formID }) => { const { t } = useTranslation(); diff --git a/web/frontend/src/pages/form/components/ActionButtons/utils.tsx b/web/frontend/src/utils/auth.ts similarity index 54% rename from web/frontend/src/pages/form/components/ActionButtons/utils.tsx rename to web/frontend/src/utils/auth.ts index 278336233..c05b218fa 100644 --- a/web/frontend/src/pages/form/components/ActionButtons/utils.tsx +++ b/web/frontend/src/utils/auth.ts @@ -1,4 +1,4 @@ -import { ID } from './../../../../types/configuration'; +import { ID } from './../types/configuration'; export function isManager(formID: ID, authorization: Map, isLogged: boolean) { return ( @@ -9,3 +9,11 @@ export function isManager(formID: ID, authorization: Map, isLo authorization.get(formID).includes('own') // must own the election ); } + +export function isVoter(formID: ID, authorization: Map, isLogged: boolean) { + return ( + isLogged && // must be logged in + authorization.has(formID) && + authorization.get(formID).includes('vote') // must be able to vote in the election + ); +} 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(); + }); +});