Skip to content

Commit 1a32df8

Browse files
authored
Merge pull request #117 from c4dt/99
Non-voter user should not see the ballot
2 parents 9667bff + d4fb1b3 commit 1a32df8

17 files changed

+96
-25
lines changed

web/frontend/src/language/de.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"operationFailure": "Der Vorgang ist fehlgeschlagen. Versuchen Sie, die Seite zu aktualisieren.",
219219
"shuffleFail": "Die Zufallsmischung ist fehlgeschlagen.",
220220
"voteImpossible": "Unmöglich abstimmen",
221-
"notFoundVoteImpossible": "Zurück zur Formulartabelle",
221+
"returnToFormTable": "Zurück zur Formulartabelle",
222222
"voteImpossibleDescription": "Das Formular ist nicht mehr zur Abstimmung geöffnet.",
223223
"yes": "Ja",
224224
"no": "Nein",
@@ -289,6 +289,8 @@
289289
"footerUnknown": "?",
290290
"footerVersion": "version:",
291291
"footerBuild": "build:",
292-
"footerBuildTime": "in:"
292+
"footerBuildTime": "in:",
293+
"voteNotVoter": "Wählen nicht erlaubt.",
294+
"voteNotVoterDescription": "Sie sind nicht wahlberechtigt in dieser Wahl. Falls Sie denken, dass ein Fehler vorliegt, wenden Sie sich bitte an die verantwortliche Stelle."
293295
}
294296
}

web/frontend/src/language/en.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"operationFailure": "The operation failed. Try refreshing the page.",
219219
"shuffleFail": "The shuffle operation failed.",
220220
"voteImpossible": "Vote Impossible",
221-
"notFoundVoteImpossible": "Go back to form table",
221+
"returnToFormTable": "Go back to form table",
222222
"voteImpossibleDescription": "The form is not open for voting anymore.",
223223
"yes": "Yes",
224224
"no": "No",
@@ -290,6 +290,8 @@
290290
"footerUnknown": "?",
291291
"footerVersion": "version:",
292292
"footerBuild": "build:",
293-
"footerBuildTime": "in:"
293+
"footerBuildTime": "in:",
294+
"voteNotVoter": "Voting not allowed.",
295+
"voteNotVoterDescription": "You are not allowed to vote in this form. If you believe this is an error, please contact the responsible of the service."
294296
}
295297
}

web/frontend/src/language/fr.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"operationFailure": "L'opération a échoué. Essayez de rafraichir la page.",
219219
"shuffleFail": "L'opération de mélange a échoué",
220220
"voteImpossible": "Vote Impossible",
221-
"notFoundVoteImpossible": "Retournez à l'onglet des sondages",
221+
"returnToFormTable": "Retournez à l'onglet des sondages",
222222
"voteImpossibleDescription": "Le sondage n'est plus ouvert au vote.",
223223
"yes": "Oui",
224224
"no": "Non",
@@ -289,6 +289,8 @@
289289
"footerUnknown": "?",
290290
"footerVersion": "version:",
291291
"footerBuild": "build:",
292-
"footerBuildTime": "en:"
292+
"footerBuildTime": "en:",
293+
"voteNotVoter": "Interdit de voter.",
294+
"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."
293295
}
294296
}

web/frontend/src/pages/ballot/Show.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { FC, useState } from 'react';
1+
import { FC, useContext, useState } from 'react';
2+
import { AuthContext } from 'index';
3+
import { isVoter } from './../../utils/auth';
24
import { useTranslation } from 'react-i18next';
35
import { useParams } from 'react-router-dom';
46
import kyber from '@dedis/kyber';
@@ -17,7 +19,7 @@ import { useConfiguration } from 'components/utils/useConfiguration';
1719
import { Status } from 'types/form';
1820
import { ballotIsValid } from './components/ValidateAnswers';
1921
import BallotDisplay from './components/BallotDisplay';
20-
import FormClosed from './components/FormClosed';
22+
import FormNotAvailable from './components/FormNotAvailable';
2123
import Loading from 'pages/Loading';
2224
import RedirectToModal from 'components/modal/RedirectToModal';
2325
import { default as i18n } from 'i18next';
@@ -39,6 +41,7 @@ const Ballot: FC = () => {
3941
const [castVoteLoading, setCastVoteLoading] = useState(false);
4042

4143
const navigate = useNavigate();
44+
const { authorization, isLogged } = useContext(AuthContext);
4245

4346
const hexToBytes = (hex: string) => {
4447
const bytes: number[] = [];
@@ -113,6 +116,8 @@ const Ballot: FC = () => {
113116
event.currentTarget.disabled = true;
114117
};
115118

119+
const userIsVoter = isVoter(formID, authorization, isLogged);
120+
116121
return (
117122
<>
118123
<RedirectToModal
@@ -127,7 +132,7 @@ const Ballot: FC = () => {
127132
<Loading />
128133
) : (
129134
<>
130-
{status === Status.Open && (
135+
{status === Status.Open && userIsVoter && (
131136
<div className="w-[60rem] font-sans px-4 pt-8 pb-4">
132137
<div className="pb-2">
133138
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
@@ -165,7 +170,8 @@ const Ballot: FC = () => {
165170
</div>
166171
</div>
167172
)}
168-
{status !== Status.Open && <FormClosed />}
173+
{!userIsVoter && <FormNotAvailable isVoter={false} />}
174+
{status !== Status.Open && <FormNotAvailable isVoter={true} />}
169175
</>
170176
)}
171177
</>

web/frontend/src/pages/ballot/components/FormClosed.tsx web/frontend/src/pages/ballot/components/FormNotAvailable.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
22
import { Link } from 'react-router-dom';
33
import { ROUTE_FORM_INDEX } from 'Routes';
44

5-
export default function FormClosed() {
5+
export default function FormNotAvailable(props) {
66
const { t } = useTranslation();
77

88
return (
@@ -13,15 +13,17 @@ export default function FormClosed() {
1313
<div className="sm:ml-6">
1414
<div className=" sm:border-gray-200 sm:pl-6">
1515
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
16-
{t('voteImpossible')}
16+
{props.isVoter ? t('voteImpossible') : t('voteNotVoter')}
1717
</h1>
18-
<p className="mt-1 text-base text-gray-500">{t('voteImpossibleDescription')}</p>
18+
<p className="mt-1 text-base text-gray-500">
19+
{props.isVoter ? t('voteImpossibleDescription') : t('voteNotVoterDescription')}
20+
</p>
1921
</div>
2022
<div className="mt-10 flex space-x-3 sm:border-l sm:border-transparent sm:pl-6">
2123
<Link
2224
to={ROUTE_FORM_INDEX}
2325
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
24-
{t('notFoundVoteImpossible')}
26+
{t('returnToFormTable')}
2527
</Link>
2628
</div>
2729
</div>

web/frontend/src/pages/form/components/ActionButtons/AddVotersButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DocumentAddIcon } from '@heroicons/react/outline';
22
import { useTranslation } from 'react-i18next';
3-
import { isManager } from './utils';
3+
import { isManager } from './../../../../utils/auth';
44
import { AuthContext } from 'index';
55
import { useContext } from 'react';
66

web/frontend/src/pages/form/components/ActionButtons/CancelButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const CancelButton = ({ status, handleCancel, ongoingAction, formID }) => {
1010
const { authorization, isLogged } = useContext(AuthContext);

web/frontend/src/pages/form/components/ActionButtons/CloseButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const CloseButton = ({ status, handleClose, ongoingAction, formID }) => {
1010
const { authorization, isLogged } = useContext(AuthContext);

web/frontend/src/pages/form/components/ActionButtons/CombineButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const CombineButton = ({ status, handleCombine, ongoingAction, formID }) => {
1010
const { t } = useTranslation();

web/frontend/src/pages/form/components/ActionButtons/DecryptButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const DecryptButton = ({ status, handleDecrypt, ongoingAction, formID }) => {
1010
const { authorization, isLogged } = useContext(AuthContext);

web/frontend/src/pages/form/components/ActionButtons/DeleteButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { TrashIcon } from '@heroicons/react/outline';
22
import { AuthContext } from 'index';
33
import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
5-
import { isManager } from './utils';
5+
import { isManager } from './../../../../utils/auth';
66

77
const DeleteButton = ({ handleDelete, formID }) => {
88
const { t } = useTranslation();

web/frontend/src/pages/form/components/ActionButtons/InitializeButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const InitializeButton = ({ status, handleInitialize, ongoingAction, formID }) => {
1010
const { authorization, isLogged } = useContext(AuthContext);

web/frontend/src/pages/form/components/ActionButtons/OpenButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const OpenButton = ({ status, handleOpen, ongoingAction, formID }) => {
1010
const { t } = useTranslation();

web/frontend/src/pages/form/components/ActionButtons/SetupButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const SetupButton = ({ status, handleSetup, ongoingAction, formID }) => {
1010
const { t } = useTranslation();

web/frontend/src/pages/form/components/ActionButtons/ShuffleButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useContext } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { OngoingAction, Status } from 'types/form';
66
import ActionButton from './ActionButton';
7-
import { isManager } from './utils';
7+
import { isManager } from './../../../../utils/auth';
88

99
const ShuffleButton = ({ status, handleShuffle, ongoingAction, formID }) => {
1010
const { t } = useTranslation();

web/frontend/src/pages/form/components/ActionButtons/utils.tsx web/frontend/src/utils/auth.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ID } from './../../../../types/configuration';
1+
import { ID } from './../types/configuration';
22

33
export function isManager(formID: ID, authorization: Map<String, String[]>, isLogged: boolean) {
44
return (
@@ -9,3 +9,11 @@ export function isManager(formID: ID, authorization: Map<String, String[]>, isLo
99
authorization.get(formID).includes('own') // must own the election
1010
);
1111
}
12+
13+
export function isVoter(formID: ID, authorization: Map<String, String[]>, isLogged: boolean) {
14+
return (
15+
isLogged && // must be logged in
16+
authorization.has(formID) &&
17+
authorization.get(formID).includes('vote') // must be able to vote in the election
18+
);
19+
}

web/frontend/tests/ballot.spec.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect, test } from '@playwright/test';
2+
import { default as i18n } from 'i18next';
3+
import { assertHasFooter, assertHasNavBar, initI18n, logIn, setUp } from './shared';
4+
import { FORMID } from './mocks/shared';
5+
import { SCIPER_ADMIN, SCIPER_OTHER_USER, SCIPER_USER, mockPersonalInfo } from './mocks/api';
6+
import { mockFormsFormID } from './mocks/evoting';
7+
8+
initI18n();
9+
10+
test.beforeEach(async ({ page }) => {
11+
await mockFormsFormID(page, 1);
12+
await logIn(page, SCIPER_ADMIN);
13+
await setUp(page, `/ballot/show/${FORMID}`);
14+
});
15+
16+
test('Assert navigation bar is present', async ({ page }) => {
17+
await assertHasNavBar(page);
18+
});
19+
20+
test('Assert footer is present', async ({ page }) => {
21+
await assertHasFooter(page);
22+
});
23+
24+
test('Assert ballot form is correctly handled for anonymous users, non-voter users and voter users', async ({
25+
page,
26+
}) => {
27+
const castVoteButton = await page.getByRole('button', { name: i18n.t('castVote') });
28+
await test.step('Assert anonymous is redirected to login page', async () => {
29+
await mockPersonalInfo(page);
30+
await page.reload({ waitUntil: 'networkidle' });
31+
await expect(page).toHaveURL('/login');
32+
});
33+
await test.step('Assert non-voter gets page that they are not allowed to vote', async () => {
34+
await logIn(page, SCIPER_OTHER_USER);
35+
await page.goto(`/ballot/show/${FORMID}`, { waitUntil: 'networkidle' });
36+
await expect(page).toHaveURL(`/ballot/show/${FORMID}`);
37+
await expect(castVoteButton).toBeHidden();
38+
await expect(page.getByText(i18n.t('voteNotVoter'))).toBeVisible();
39+
await expect(page.getByText(i18n.t('voteNotVoterDescription'))).toBeVisible();
40+
});
41+
await test.step('Assert voter gets ballot', async () => {
42+
await logIn(page, SCIPER_USER);
43+
await page.goto(`/ballot/show/${FORMID}`, { waitUntil: 'networkidle' });
44+
await expect(page).toHaveURL(`/ballot/show/${FORMID}`);
45+
await expect(castVoteButton).toBeVisible();
46+
await expect(page.getByText(i18n.t('vote'))).toBeVisible();
47+
await expect(page.getByText(i18n.t('voteExplanation'))).toBeVisible();
48+
});
49+
});

0 commit comments

Comments
 (0)