From 6a6367459ae4e6dfe3eb1509f6eb6f4a5d00b00c Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Mon, 7 Mar 2022 11:42:59 +0100 Subject: [PATCH] Improve test coverage --- .../objects/__tests__/LaoEventBuilder.test.ts | 2 +- .../evoting/components/EventElection.tsx | 12 - .../__tests__/EventElection.test.tsx | 26 +- .../__snapshots__/EventElection.test.tsx.snap | 141 ----- .../features/evoting/hooks/EvotingHooks.ts | 27 + .../hooks/__tests__/EvotingHooks.test.ts | 38 ++ fe1-web/src/features/evoting/hooks/index.ts | 28 +- .../evoting/network/ElectionHandler.ts | 18 +- .../network/__tests__/ElectionHandler.test.ts | 561 ++++++++++++++++-- .../messages/__tests__/CastVote.test.ts | 121 +--- .../messages/__tests__/EndElection.test.ts | 69 +-- .../messages/__tests__/OpenElection.test.ts | 26 +- .../messages/__tests__/SetupElection.test.ts | 85 +-- .../src/features/evoting/objects/Election.ts | 1 - .../objects/__tests__/Election.test.ts | 4 +- .../evoting/objects/__tests__/utils.ts | 156 +++++ 16 files changed, 830 insertions(+), 485 deletions(-) create mode 100644 fe1-web/src/features/evoting/hooks/EvotingHooks.ts create mode 100644 fe1-web/src/features/evoting/hooks/__tests__/EvotingHooks.test.ts create mode 100644 fe1-web/src/features/evoting/objects/__tests__/utils.ts diff --git a/fe1-web/src/features/events/objects/__tests__/LaoEventBuilder.test.ts b/fe1-web/src/features/events/objects/__tests__/LaoEventBuilder.test.ts index 0e0eed1910..1c8a943992 100644 --- a/fe1-web/src/features/events/objects/__tests__/LaoEventBuilder.test.ts +++ b/fe1-web/src/features/events/objects/__tests__/LaoEventBuilder.test.ts @@ -68,7 +68,7 @@ describe('LaoEventBuilder', () => { start: 12345, end: 16345, questions: [question1], - electionStatus: ElectionStatus.FINISHED, + electionStatus: ElectionStatus.TERMINATED, registeredVotes: [registeredVotes], }; expect(eventFromState(election)).toBeInstanceOf(Election); diff --git a/fe1-web/src/features/evoting/components/EventElection.tsx b/fe1-web/src/features/evoting/components/EventElection.tsx index 4ce5f57a53..d580b69a20 100644 --- a/fe1-web/src/features/evoting/components/EventElection.tsx +++ b/fe1-web/src/features/evoting/components/EventElection.tsx @@ -128,18 +128,6 @@ const EventElection = (props: IPropTypes) => { )} ); - case ElectionStatus.FINISHED: - return ( - <> - Election finished - {isOrganizer && ( - - )} - - ); case ElectionStatus.TERMINATED: return ( <> diff --git a/fe1-web/src/features/evoting/components/__tests__/EventElection.test.tsx b/fe1-web/src/features/evoting/components/__tests__/EventElection.test.tsx index 5285f8d6a6..fc396819b2 100644 --- a/fe1-web/src/features/evoting/components/__tests__/EventElection.test.tsx +++ b/fe1-web/src/features/evoting/components/__tests__/EventElection.test.tsx @@ -103,19 +103,6 @@ const terminatedElection = new Election({ registeredVotes: [registeredVote], }); -const finishedElection = new Election({ - lao: mockLaoIdHash, - id: mockElectionId, - name: 'An election', - version: STRINGS.election_version_identifier, - createdAt: TIMESTAMP, - start: TIMESTAMP, - end: CLOSE_TIMESTAMP, - questions: [question], - electionStatus: ElectionStatus.FINISHED, - registeredVotes: [registeredVote], -}); - const resultElection = new Election({ lao: mockLaoIdHash, id: mockElectionId, @@ -160,6 +147,7 @@ beforeAll(() => { onConfirmEventCreation: () => undefined, }); }); + afterEach(() => { warn.mockClear(); }); @@ -205,18 +193,6 @@ describe('EventElection', () => { }); }); - describe('Finished election where the results are not yet available', () => { - it('renders correctly for an organizer', () => { - const component = render().toJSON(); - expect(component).toMatchSnapshot(); - }); - - it('renders correctly for an attendee', () => { - const component = render().toJSON(); - expect(component).toMatchSnapshot(); - }); - }); - describe('Finished election where the results are available', () => { it('renders correctly for an organizer', () => { const component = render().toJSON(); diff --git a/fe1-web/src/features/evoting/components/__tests__/__snapshots__/EventElection.test.tsx.snap b/fe1-web/src/features/evoting/components/__tests__/__snapshots__/EventElection.test.tsx.snap index 9f5c69db57..7ce56051e3 100644 --- a/fe1-web/src/features/evoting/components/__tests__/__snapshots__/EventElection.test.tsx.snap +++ b/fe1-web/src/features/evoting/components/__tests__/__snapshots__/EventElection.test.tsx.snap @@ -1296,147 +1296,6 @@ Array [ ] `; -exports[`EventElection Finished election where the results are not yet available renders correctly for an attendee 1`] = ` -Array [ - - - Start: - - - - End: - - - , - - Election finished - , -] -`; - -exports[`EventElection Finished election where the results are not yet available renders correctly for an organizer 1`] = ` -Array [ - - - Start: - - - - End: - - - , - - Election finished - , - - - - - Terminate Election / Tally Votes - - - - , -] -`; - exports[`EventElection Not started election renders correctly for an attendee 1`] = ` Array [ diff --git a/fe1-web/src/features/evoting/hooks/EvotingHooks.ts b/fe1-web/src/features/evoting/hooks/EvotingHooks.ts new file mode 100644 index 0000000000..9d2b0d189b --- /dev/null +++ b/fe1-web/src/features/evoting/hooks/EvotingHooks.ts @@ -0,0 +1,27 @@ +import { getEvotingConfig } from '../index'; + +export namespace EvotingHooks { + /** + * Gets the current lao id + * @returns The current lao id + */ + export const useCurrentLaoId = () => { + return getEvotingConfig().getCurrentLaoId(); + }; + + /** + * Gets the current lao + * @returns The current lao + */ + export const useCurrentLao = () => { + return getEvotingConfig().getCurrentLao(); + }; + + /** + * Gets the onConfirmEventCreation helper function + * @returns The onConfirmEventCreation function + */ + export const useOnConfirmEventCreation = () => { + return getEvotingConfig().onConfirmEventCreation; + }; +} diff --git a/fe1-web/src/features/evoting/hooks/__tests__/EvotingHooks.test.ts b/fe1-web/src/features/evoting/hooks/__tests__/EvotingHooks.test.ts new file mode 100644 index 0000000000..6c5740ffa4 --- /dev/null +++ b/fe1-web/src/features/evoting/hooks/__tests__/EvotingHooks.test.ts @@ -0,0 +1,38 @@ +import { describe } from '@jest/globals'; +import { configure } from 'features/evoting'; +import { mockLao, mockLaoIdHash, mockMessageRegistry, mockReduxAction } from '__tests__/utils'; +import { EvotingHooks } from '../index'; + +const onConfirmEventCreation = jest.fn(); + +beforeAll(() => { + configure({ + getCurrentLao: () => mockLao, + getCurrentLaoId: () => mockLaoIdHash, + addEvent: () => mockReduxAction, + updateEvent: () => mockReduxAction, + getEventFromId: () => undefined, + messageRegistry: mockMessageRegistry, + onConfirmEventCreation, + }); +}); + +describe('E-Voting hooks', () => { + describe('EvotingHooks.useCurrentLao', () => { + it('should return the current lao', () => { + expect(EvotingHooks.useCurrentLao()).toEqual(mockLao); + }); + }); + + describe('EvotingHooks.useCurrentLaoId', () => { + it('should return the current lao id', () => { + expect(EvotingHooks.useCurrentLaoId()).toEqual(mockLaoIdHash); + }); + }); + + describe('EvotingHooks.useOnConfirmEventCreation', () => { + it('should return the onConfirmEventCreation config option', () => { + expect(EvotingHooks.useOnConfirmEventCreation()).toEqual(onConfirmEventCreation); + }); + }); +}); diff --git a/fe1-web/src/features/evoting/hooks/index.ts b/fe1-web/src/features/evoting/hooks/index.ts index 9d2b0d189b..7b51c1dc96 100644 --- a/fe1-web/src/features/evoting/hooks/index.ts +++ b/fe1-web/src/features/evoting/hooks/index.ts @@ -1,27 +1 @@ -import { getEvotingConfig } from '../index'; - -export namespace EvotingHooks { - /** - * Gets the current lao id - * @returns The current lao id - */ - export const useCurrentLaoId = () => { - return getEvotingConfig().getCurrentLaoId(); - }; - - /** - * Gets the current lao - * @returns The current lao - */ - export const useCurrentLao = () => { - return getEvotingConfig().getCurrentLao(); - }; - - /** - * Gets the onConfirmEventCreation helper function - * @returns The onConfirmEventCreation function - */ - export const useOnConfirmEventCreation = () => { - return getEvotingConfig().onConfirmEventCreation; - }; -} +export * from './EvotingHooks'; diff --git a/fe1-web/src/features/evoting/network/ElectionHandler.ts b/fe1-web/src/features/evoting/network/ElectionHandler.ts index 589d4749b6..d0d476fab7 100644 --- a/fe1-web/src/features/evoting/network/ElectionHandler.ts +++ b/fe1-web/src/features/evoting/network/ElectionHandler.ts @@ -122,17 +122,19 @@ export const handleCastVoteMessage = } const castVoteMsg = msg.messageData as CastVote; + + const election = getEventFromId(castVoteMsg.election) as Election; + if (!election) { + console.warn(makeErr('No active election to register vote ')); + return false; + } + const currentVote: RegisteredVote = { createdAt: castVoteMsg.created_at.valueOf(), sender: msg.sender.valueOf(), votes: castVoteMsg.votes, messageId: msg.message_id.valueOf(), }; - const election = getEventFromId(castVoteMsg.election) as Election; - if (!election) { - console.warn(makeErr('No active election to register vote ')); - return false; - } if (election.registeredVotes.some((votes) => votes.sender === currentVote.sender)) { // Update the vote if the person has already voted before @@ -144,7 +146,7 @@ export const handleCastVoteMessage = } else { election.registeredVotes = [...election.registeredVotes, currentVote]; } - dispatch(updateEvent(lao.id, election.toState())); + dispatch(updateEvent(msg.laoId, election.toState())); return true; }; @@ -210,14 +212,14 @@ export const handleElectionResultMessage = return false; } const electionId = getLastPartOfChannel(msg.channel); - const ElectionResultMsg = msg.messageData as ElectionResult; + const electionResultMessage = msg.messageData as ElectionResult; const election = getEventFromId(electionId) as Election; if (!election) { console.warn(makeErr('No active election for the result')); return false; } - election.questionResult = ElectionResultMsg.questions.map((q) => ({ + election.questionResult = electionResultMessage.questions.map((q) => ({ id: q.id, result: q.result.map((r) => ({ ballotOption: r.ballot_option, count: r.count })), })); diff --git a/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts b/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts index 3e23e579b9..6d7be4975b 100644 --- a/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts +++ b/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts @@ -1,44 +1,47 @@ import 'jest-extended'; import '__tests__/utils/matchers'; import { - mockLaoId, mockLaoIdHash, - mockLaoName, configureTestFeatures, mockKeyPair, mockReduxAction, + mockLao, } from '__tests__/utils'; -import { Hash, Timestamp, Base64UrlData, Signature } from 'core/objects'; +import { Hash, Timestamp, Base64UrlData, Signature, channelFromIds } from 'core/objects'; import { ActionType, MessageData, ObjectType } from 'core/network/jsonrpc/messages'; -import STRINGS from 'resources/strings'; import { dispatch } from 'core/redux'; -import { Election, ElectionStatus, EvotingConfiguration } from 'features/evoting/objects'; -import { handleElectionOpenMessage } from '../ElectionHandler'; +import { + Election, + ElectionState, + ElectionStatus, + EvotingConfiguration, + RegisteredVote, +} from 'features/evoting/objects'; +import { + mockElectionNotStarted, + mockElectionId, + mockElectionOpened, + mockVote1, + mockVote2, + mockRegistedVotesHash, + mockElectionTerminated, + mockElectionResultQuestions, +} from 'features/evoting/objects/__tests__/utils'; +import { subscribeToChannel } from 'core/network'; +import { KeyPairStore } from 'core/keypair'; +import { + handleCastVoteMessage, + handleElectionEndMessage, + handleElectionOpenMessage, + handleElectionResultMessage, + handleElectionSetupMessage, +} from '../ElectionHandler'; +import { CastVote, ElectionResult, EndElection, SetupElection } from '../messages'; +import { OpenElection } from '../messages/OpenElection'; const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 -const CLOSE_TIMESTAMP = new Timestamp(1609542000); // 2nd january 2021 - -const mockElectionId = Hash.fromStringArray( - 'Election', - mockLaoId, - TIMESTAMP.toString(), - mockLaoName, -); - -const election = new Election({ - lao: mockLaoIdHash, - id: mockElectionId, - name: 'An election', - version: STRINGS.election_version_identifier, - createdAt: TIMESTAMP, - start: TIMESTAMP, - end: CLOSE_TIMESTAMP, - questions: [], - electionStatus: ElectionStatus.NOT_STARTED, - registeredVotes: [], -}); const mockMessageData = { receivedAt: TIMESTAMP, @@ -46,17 +49,35 @@ const mockMessageData = { data: Base64UrlData.encode('some data'), sender: mockKeyPair.publicKey, signature: Base64UrlData.encode('some data') as Signature, - channel: '', + channel: `some channel/${mockElectionId.valueOf()}`, message_id: Hash.fromString('some string'), witness_signatures: [], }; +const getMockLao: EvotingConfiguration['getCurrentLao'] = () => mockLao; const getEventFromIdDummy: EvotingConfiguration['getEventFromId'] = () => undefined; const updateEventDummy: EvotingConfiguration['updateEvent'] = () => mockReduxAction; // mocks const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); +// mock channelFromIds and subscribeToChannel (spyOn does not work) +const mockChannelId = 'someChannelId'; + +jest.mock('core/objects', () => { + return { + ...jest.requireActual('core/objects'), + channelFromIds: jest.fn().mockImplementation(() => mockChannelId), + }; +}); + +jest.mock('core/network', () => { + return { + ...jest.requireActual('core/network'), + subscribeToChannel: jest.fn().mockImplementation(() => Promise.resolve()), + }; +}); + beforeAll(() => { configureTestFeatures(); }); @@ -71,6 +92,97 @@ afterEach(() => { }); describe('ElectionHandler', () => { + describe('election#setup', () => { + it('should return false if the object is not "election"', () => { + const addEvent = jest.fn(); + + expect( + handleElectionSetupMessage(addEvent)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.SETUP, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + it('should return false if the action is not "setup"', () => { + const addEvent = jest.fn(); + + expect( + handleElectionSetupMessage(addEvent)({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.ADD, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + + it('should create the election', () => { + let storedElection: ElectionState | undefined; + + const addEvent = jest.fn().mockImplementation((laoId, eventState) => { + storedElection = eventState; + + // Return a redux action, should be an action creator + return mockReduxAction; + }); + + expect(storedElection).toEqual(undefined); + + expect( + handleElectionSetupMessage(addEvent)({ + ...mockMessageData, + messageData: new SetupElection({ + lao: mockLaoIdHash, + id: mockElectionNotStarted.id, + name: mockElectionNotStarted.name, + version: mockElectionNotStarted.version, + created_at: mockElectionNotStarted.createdAt, + start_time: mockElectionNotStarted.start, + end_time: mockElectionNotStarted.end, + questions: mockElectionNotStarted.questions, + }), + }), + ).toBeTrue(); + + // no warning should have been printed + expect(warn).toHaveBeenCalledTimes(0); + + // it should have been subscripted to the election channel + expect(channelFromIds).toHaveBeenCalledTimes(1); + expect(channelFromIds).toHaveBeenCalledWith( + mockElectionNotStarted.lao, + mockElectionNotStarted.id, + ); + + expect(subscribeToChannel).toHaveBeenCalledTimes(1); + expect(subscribeToChannel).toHaveBeenCalledWith(mockChannelId); + + // check whether updateEvent has been called correctly + expect(addEvent.mock.calls[0][0]).toEqual(mockLaoIdHash); + expect(addEvent.mock.calls[0][1]).toHaveProperty('id', mockElectionNotStarted.id.valueOf()); + expect(addEvent.mock.calls[0][1]).toHaveProperty( + 'electionStatus', + ElectionStatus.NOT_STARTED, + ); + expect(addEvent).toHaveBeenCalledTimes(1); + + // check if the status was changed correctly + expect(storedElection?.electionStatus).toEqual(ElectionStatus.NOT_STARTED); + }); + }); + describe('election#open', () => { it('should return false if the object is not "election"', () => { expect( @@ -120,7 +232,7 @@ describe('ElectionHandler', () => { object: ObjectType.ELECTION, action: ActionType.OPEN, election: mockElectionId.valueOf(), - opened_at: TIMESTAMP, + created_at: TIMESTAMP, } as MessageData, }), ).toBeFalse(); @@ -131,7 +243,7 @@ describe('ElectionHandler', () => { }); it('should update the election status', () => { - let storedElection = election.toState(); + let storedElection = mockElectionNotStarted.toState(); const getEventFromId = jest.fn().mockImplementation(() => Election.fromState(storedElection)); const updateEvent = jest.fn().mockImplementation((laoId, eventState) => { @@ -147,15 +259,134 @@ describe('ElectionHandler', () => { handleElectionOpenMessage( getEventFromId, updateEvent, + )({ + ...mockMessageData, + messageData: new OpenElection({ + lao: mockLaoIdHash, + election: mockElectionId, + opened_at: TIMESTAMP, + }), + }), + ).toBeTrue(); + + // no warning should have been printed + expect(warn).toHaveBeenCalledTimes(0); + + // check whether getEventFromId has been called correctly + expect(getEventFromId.mock.calls[0][0]).toEqual(mockElectionId); + expect(getEventFromId).toHaveBeenCalledTimes(1); + + // check whether updateEvent has been called correctly + expect(updateEvent.mock.calls[0][0]).toEqual(mockLaoIdHash); + expect(updateEvent.mock.calls[0][1]).toHaveProperty( + 'id', + mockElectionNotStarted.id.valueOf(), + ); + expect(updateEvent.mock.calls[0][1]).toHaveProperty('electionStatus', ElectionStatus.OPENED); + expect(updateEvent).toHaveBeenCalledTimes(1); + + // check if the status was changed correctly + expect(storedElection.electionStatus).toEqual(ElectionStatus.OPENED); + }); + }); + + describe('election#castVote', () => { + it('should return false if the object is not "election"', () => { + expect( + handleCastVoteMessage( + getMockLao, + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.CAST_VOTE, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + it('should return false if the action is not "cast_vote"', () => { + expect( + handleCastVoteMessage( + getMockLao, + getEventFromIdDummy, + updateEventDummy, )({ ...mockMessageData, messageData: { object: ObjectType.ELECTION, - action: ActionType.OPEN, - election: mockElectionId, + action: ActionType.ADD, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + + it('for organizers should return false if the election has not previously been stored', () => { + // stores the keypair of the mockLao organizer + KeyPairStore.store(mockKeyPair); + + expect( + handleCastVoteMessage( + getMockLao, + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.CAST_VOTE, + election: mockElectionId.valueOf(), opened_at: TIMESTAMP, } as MessageData, }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/election/i); + }); + + it('for organizers should update election.registeredVotes', () => { + // stores the keypair of the mockLao organizer + KeyPairStore.store(mockKeyPair); + + let storedElection = mockElectionOpened.toState(); + + const getEventFromId = jest.fn().mockImplementation(() => Election.fromState(storedElection)); + const updateEvent = jest.fn().mockImplementation((laoId, eventState) => { + storedElection = eventState; + + // Return a redux action, should be an action creator + return mockReduxAction; + }); + + expect(storedElection.electionStatus).toEqual(ElectionStatus.OPENED); + + const castVoteMessage = new CastVote({ + lao: mockLaoIdHash, + election: mockElectionId, + created_at: TIMESTAMP, + votes: [mockVote1, mockVote2], + }); + + expect( + handleCastVoteMessage( + getMockLao, + getEventFromId, + updateEvent, + )({ + ...mockMessageData, + messageData: castVoteMessage, + }), ).toBeTrue(); // no warning should have been printed @@ -167,12 +398,274 @@ describe('ElectionHandler', () => { // check whether updateEvent has been called correctly expect(updateEvent.mock.calls[0][0]).toEqual(mockLaoIdHash); - expect(updateEvent.mock.calls[0][1]).toHaveProperty('id', election.id.valueOf()); + expect(updateEvent.mock.calls[0][1]).toHaveProperty( + 'id', + mockElectionNotStarted.id.valueOf(), + ); expect(updateEvent.mock.calls[0][1]).toHaveProperty('electionStatus', ElectionStatus.OPENED); expect(updateEvent).toHaveBeenCalledTimes(1); - // check if the status was changed correctly + // check whether the status was not changed expect(storedElection.electionStatus).toEqual(ElectionStatus.OPENED); + + // check whether the registered votes have been updated + + const newVote: RegisteredVote = { + createdAt: castVoteMessage.created_at.valueOf(), + sender: mockMessageData.sender.valueOf(), + votes: castVoteMessage.votes, + messageId: mockMessageData.message_id.valueOf(), + }; + + expect(storedElection.registeredVotes).toEqual([ + ...mockElectionOpened.registeredVotes, + newVote, + ]); + }); + }); + + describe('election#end', () => { + it('should return false if the object is not "election"', () => { + expect( + handleElectionEndMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.END, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + it('should return false if the action is not "end"', () => { + expect( + handleElectionEndMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.ADD, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + + it('should return false if the election has not previously been stored', () => { + expect( + handleElectionEndMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.OPEN, + election: mockElectionId.valueOf(), + created_at: TIMESTAMP, + } as MessageData, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/election/i); + }); + + it('should update the election status', () => { + let storedElection = mockElectionOpened.toState(); + + const getEventFromId = jest.fn().mockImplementation(() => Election.fromState(storedElection)); + const updateEvent = jest.fn().mockImplementation((laoId, eventState) => { + storedElection = eventState; + + // Return a redux action, should be an action creator + return mockReduxAction; + }); + + expect(storedElection.electionStatus).toEqual(ElectionStatus.OPENED); + + expect( + handleElectionEndMessage( + getEventFromId, + updateEvent, + )({ + ...mockMessageData, + messageData: new EndElection({ + lao: mockLaoIdHash, + election: mockElectionId, + created_at: TIMESTAMP, + registered_votes: mockRegistedVotesHash, + }), + }), + ).toBeTrue(); + + // no warning should have been printed + expect(warn).toHaveBeenCalledTimes(0); + + // check whether getEventFromId has been called correctly + expect(getEventFromId.mock.calls[0][0]).toEqual(mockElectionId); + expect(getEventFromId).toHaveBeenCalledTimes(1); + + // check whether updateEvent has been called correctly + expect(updateEvent.mock.calls[0][0]).toEqual(mockLaoIdHash); + expect(updateEvent.mock.calls[0][1]).toHaveProperty( + 'id', + mockElectionNotStarted.id.valueOf(), + ); + expect(updateEvent.mock.calls[0][1]).toHaveProperty( + 'electionStatus', + ElectionStatus.TERMINATED, + ); + expect(updateEvent).toHaveBeenCalledTimes(1); + + // check if the status was changed correctly + expect(storedElection.electionStatus).toEqual(ElectionStatus.TERMINATED); + }); + }); + + describe('election#result', () => { + it('should return false if the object is not "election"', () => { + expect( + handleElectionResultMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.RESULT, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + + it('should return false if the action is not "result"', () => { + expect( + handleElectionResultMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.ADD, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/unsupported message/i); + }); + + it('should return false if the message data does not contain a channel', () => { + expect( + handleElectionResultMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + channel: '', + messageData: { + object: ObjectType.ELECTION, + action: ActionType.RESULT, + }, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/No channel/i); + }); + + it('should return false if the election has not previously been stored', () => { + expect( + handleElectionResultMessage( + getEventFromIdDummy, + updateEventDummy, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.OPEN, + election: mockElectionId.valueOf(), + created_at: TIMESTAMP, + } as MessageData, + }), + ).toBeFalse(); + + expect(warn).toHaveBeenCalledTimes(1); + // check if the printed warning message contains substring + expect(warn.mock.calls[0][0]).toMatch(/election/i); + }); + + it('should update the election status and store results', () => { + let storedElection = mockElectionTerminated.toState(); + + const getEventFromId = jest.fn().mockImplementation(() => Election.fromState(storedElection)); + const updateEvent = jest.fn().mockImplementation((laoId, eventState) => { + storedElection = eventState; + + // Return a redux action, should be an action creator + return mockReduxAction; + }); + + expect(storedElection.electionStatus).toEqual(ElectionStatus.TERMINATED); + + expect( + handleElectionResultMessage( + getEventFromId, + updateEvent, + )({ + ...mockMessageData, + messageData: new ElectionResult({ + questions: mockElectionResultQuestions, + }), + }), + ).toBeTrue(); + + // no warning should have been printed + expect(warn).toHaveBeenCalledTimes(0); + + // check whether getEventFromId has been called correctly + expect(getEventFromId.mock.calls[0][0].valueOf()).toEqual(mockElectionId.valueOf()); + expect(getEventFromId).toHaveBeenCalledTimes(1); + + // check whether updateEvent has been called correctly + expect(updateEvent.mock.calls[0][0]).toEqual(mockLaoIdHash); + expect(updateEvent.mock.calls[0][1]).toHaveProperty( + 'id', + mockElectionNotStarted.id.valueOf(), + ); + expect(updateEvent.mock.calls[0][1]).toHaveProperty('electionStatus', ElectionStatus.RESULT); + expect(updateEvent).toHaveBeenCalledTimes(1); + + // check if the status was changed correctly + expect(storedElection.electionStatus).toEqual(ElectionStatus.RESULT); + + // check if the results were correctly stored + expect(storedElection.questionResult).toEqual( + mockElectionResultQuestions.map((q) => ({ + id: q.id, + result: q.result.map((r) => ({ ballotOption: r.ballot_option, count: r.count })), + })), + ); }); }); }); diff --git a/fe1-web/src/features/evoting/network/messages/__tests__/CastVote.test.ts b/fe1-web/src/features/evoting/network/messages/__tests__/CastVote.test.ts index c9ec644c2a..4b6269fdb8 100644 --- a/fe1-web/src/features/evoting/network/messages/__tests__/CastVote.test.ts +++ b/fe1-web/src/features/evoting/network/messages/__tests__/CastVote.test.ts @@ -1,94 +1,23 @@ import 'jest-extended'; import '__tests__/utils/matchers'; -import { mockLaoId, mockLaoIdHash, mockLaoName, configureTestFeatures } from '__tests__/utils'; +import { mockLaoIdHash, configureTestFeatures } from '__tests__/utils'; -import { EventTags, Hash, Timestamp, ProtocolError } from 'core/objects'; +import { Hash, Timestamp, ProtocolError } from 'core/objects'; import { ActionType, ObjectType } from 'core/network/jsonrpc/messages'; import { MessageDataProperties } from 'core/types'; -import STRINGS from 'resources/strings'; -import { Election, ElectionStatus, Question, Vote } from '../../../objects'; +import { + mockElectionId, + mockVote1, + mockVote2, + mockVotes, +} from 'features/evoting/objects/__tests__/utils'; +import { Vote } from '../../../objects'; import { CastVote } from '../CastVote'; // region test data initialization const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 -const VERSION = STRINGS.election_version_identifier; -const CLOSE_TIMESTAMP = new Timestamp(1609542000); // 2nd january 2021 -const TIMESTAMP_BEFORE = new Timestamp(1609445600); - -const electionId: Hash = Hash.fromStringArray( - 'Election', - mockLaoId, - TIMESTAMP.toString(), - mockLaoName, -); - -const mockQuestion1 = 'Mock Question 1'; -const mockQuestion2 = 'Mock Question 2'; -const mockQuestionId1 = Hash.fromStringArray( - EventTags.QUESTION, - electionId.toString(), - mockQuestion1, -); - -const mockQuestionId2 = Hash.fromStringArray( - EventTags.QUESTION, - electionId.toString(), - mockQuestion2, -); - -const mockBallotOptions = ['Ballot Option 1', 'Ballot Option 2']; - -const mockQuestionObject1: Question = { - id: mockQuestionId1.toString(), - question: mockQuestion1, - voting_method: STRINGS.election_method_Plurality, - ballot_options: mockBallotOptions, - write_in: false, -}; - -const mockQuestionObject2: Question = { - id: mockQuestionId2.toString(), - question: mockQuestion2, - voting_method: STRINGS.election_method_Approval, - ballot_options: mockBallotOptions, - write_in: true, -}; - -const election: Election = new Election({ - id: electionId, - lao: mockLaoIdHash, - name: 'An election', - version: VERSION, - createdAt: TIMESTAMP, - start: TIMESTAMP_BEFORE, - end: CLOSE_TIMESTAMP, - questions: [mockQuestionObject1, mockQuestionObject2], - electionStatus: ElectionStatus.OPENED, - registeredVotes: [], - questionResult: [], -}); - -const mockVoteVotes1 = new Set([0]); -const mockVoteVotes2 = new Set([1, 0]); - -const mockVoteId1 = CastVote.computeVoteId(election, 0, mockVoteVotes1); -const mockVoteId2 = CastVote.computeVoteId(election, 1, mockVoteVotes2); - -const mockVoteObject1: Vote = { - id: mockVoteId1.toString(), - question: mockQuestionId1.valueOf(), - vote: [0], -}; - -const mockVoteObject2: Vote = { - id: mockVoteId2.toString(), - question: mockQuestionId2.valueOf(), - vote: [0], -}; - -const mockVotes = [mockVoteObject1]; // In these tests, we should assume that the input to the messages is // just a Partial<> and not a MessageDataProperties<> @@ -97,7 +26,7 @@ const sampleCastVote: Partial = { object: ObjectType.ELECTION, action: ActionType.CAST_VOTE, lao: mockLaoIdHash, - election: electionId, + election: mockElectionId, created_at: TIMESTAMP, votes: mockVotes, }; @@ -106,7 +35,7 @@ const CastVoteJson: string = `{ "object": "${ObjectType.ELECTION}", "action": "${ActionType.CAST_VOTE}", "lao": "${mockLaoIdHash}", - "election": "${electionId}", + "election": "${mockElectionId}", "created_at": ${TIMESTAMP}, "votes": ${JSON.stringify(mockVotes)} }`; @@ -127,9 +56,9 @@ describe('CastVote', () => { object: ObjectType.ELECTION, action: ActionType.CAST_VOTE, lao: mockLaoIdHash, - election: electionId, + election: mockElectionId, created_at: TIMESTAMP, - votes: [mockVoteObject1, mockVoteObject2], + votes: [mockVote1, mockVote2], }; expect(new CastVote(temp)).toBeJsonEqual(temp); }); @@ -144,9 +73,9 @@ describe('CastVote', () => { object: ObjectType.ELECTION, action: ActionType.NOTIFY_ADD, lao: mockLaoIdHash.toString(), - election: electionId.toString(), + election: mockElectionId.toString(), created_at: parseInt(TIMESTAMP.toString(), 10), - votes: [mockVoteObject1, mockVoteObject2], + votes: [mockVote1, mockVote2], }; const createFromJson = () => CastVote.fromJson(obj); expect(createFromJson).toThrow(ProtocolError); @@ -157,9 +86,9 @@ describe('CastVote', () => { object: ObjectType.CHIRP, action: ActionType.CAST_VOTE, lao: mockLaoIdHash.toString(), - election: electionId.toString(), + election: mockElectionId.toString(), created_at: parseInt(TIMESTAMP.toString(), 10), - votes: [mockVoteObject1, mockVoteObject2], + votes: [mockVote1, mockVote2], }; const createFromJson = () => CastVote.fromJson(obj); expect(createFromJson).toThrow(ProtocolError); @@ -172,7 +101,7 @@ describe('CastVote', () => { lao: mockLaoIdHash, election: undefined as unknown as Hash, created_at: TIMESTAMP, - votes: [mockVoteObject1, mockVoteObject2], + votes: [mockVote1, mockVote2], }); expect(createWrongObj).toThrow(ProtocolError); }); @@ -181,9 +110,9 @@ describe('CastVote', () => { const createWrongObj = () => new CastVote({ lao: undefined as unknown as Hash, - election: electionId, + election: mockElectionId, created_at: TIMESTAMP, - votes: [mockVoteObject1, mockVoteObject2], + votes: [mockVote1, mockVote2], }); expect(createWrongObj).toThrow(ProtocolError); }); @@ -192,8 +121,8 @@ describe('CastVote', () => { const createWrongObj = () => new CastVote({ lao: mockLaoIdHash, - election: electionId, - votes: [mockVoteObject1, mockVoteObject2], + election: mockElectionId, + votes: [mockVote1, mockVote2], created_at: undefined as unknown as Timestamp, }); expect(createWrongObj).toThrow(ProtocolError); @@ -203,7 +132,7 @@ describe('CastVote', () => { const createWrongObj = () => new CastVote({ lao: mockLaoIdHash, - election: electionId, + election: mockElectionId, created_at: TIMESTAMP, votes: undefined as unknown as Vote[], }); @@ -215,9 +144,9 @@ describe('CastVote', () => { object: ObjectType.CHIRP, action: ActionType.NOTIFY_ADD, lao: mockLaoIdHash, - election: electionId, + election: mockElectionId, created_at: TIMESTAMP, - votes: [mockVoteObject1, mockVoteObject2], + votes: [mockVote1, mockVote2], } as MessageDataProperties); expect(msg.object).toEqual(ObjectType.ELECTION); expect(msg.action).toEqual(ActionType.CAST_VOTE); diff --git a/fe1-web/src/features/evoting/network/messages/__tests__/EndElection.test.ts b/fe1-web/src/features/evoting/network/messages/__tests__/EndElection.test.ts index 1a987f602e..86ebab6a3b 100644 --- a/fe1-web/src/features/evoting/network/messages/__tests__/EndElection.test.ts +++ b/fe1-web/src/features/evoting/network/messages/__tests__/EndElection.test.ts @@ -2,76 +2,19 @@ import 'jest-extended'; import '__tests__/utils/matchers'; import { mockLaoId, mockLaoIdHash, mockLaoName, configureTestFeatures } from '__tests__/utils'; -import { Hash, Timestamp, ProtocolError, EventTags } from 'core/objects'; +import { Hash, Timestamp, ProtocolError } from 'core/objects'; import { ActionType, ObjectType } from 'core/network/jsonrpc/messages'; import { MessageDataProperties } from 'core/types'; -import { Election, ElectionStatus, Question } from 'features/evoting/objects'; -import STRINGS from 'resources/strings'; +import { + mockElectionOpened, + mockElectionResultHash, +} from 'features/evoting/objects/__tests__/utils'; import { EndElection } from '../EndElection'; // region test data initialization const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 -const CLOSE_TIMESTAMP = new Timestamp(1609542000); // 2nd january 2021 - -const mockElectionId = Hash.fromStringArray( - 'Election', - mockLaoId, - TIMESTAMP.toString(), - mockLaoName, -); - -const mockQuestion = 'Mock Question 1'; -const mockQuestionId = Hash.fromStringArray( - EventTags.QUESTION, - mockElectionId.toString(), - mockQuestion, -); -const mockBallotOptions = ['Ballot Option 1', 'Ballot Option 2']; - -const question: Question = { - id: mockQuestionId.toString(), - question: mockQuestion, - voting_method: STRINGS.election_method_Plurality, - ballot_options: mockBallotOptions, - write_in: false, -}; - -const election = new Election({ - lao: mockLaoIdHash, - id: mockElectionId, - name: 'An election', - version: STRINGS.election_version_identifier, - createdAt: TIMESTAMP, - start: TIMESTAMP, - end: CLOSE_TIMESTAMP, - questions: [question], - electionStatus: ElectionStatus.NOT_STARTED, - registeredVotes: [ - { - createdAt: 0, - messageId: '0', - sender: '', - votes: [ - { id: 'id1', question: 'q1', vote: [0] }, - { id: 'id2', question: 'q2', vote: [0] }, - ], - }, - { - createdAt: 1, - messageId: '1', - sender: '', - votes: [ - { id: 'id3', question: 'q3', vote: [0] }, - { id: 'id4', question: 'q4', vote: [0] }, - ], - }, - ], -}); - -const mockVoteId = 'x'; -const mockElectionResultHash = Hash.fromStringArray(mockVoteId); const electionId: Hash = Hash.fromStringArray( 'Election', @@ -217,7 +160,7 @@ describe('EndElection', () => { describe('computeRegisteredVotesHash', () => { // It seems a bit silly to test this function apart from it not returning an error as // we would simply write the same code here again? - const fn = () => EndElection.computeRegisteredVotesHash(election); + const fn = () => EndElection.computeRegisteredVotesHash(mockElectionOpened); expect(fn).not.toThrow(); expect(fn().valueOf()).toEqual('eYH10agf4Jvfs-rihA-9pG1j0lFPHnYeI9e9Vx-GQ6Q='); }); diff --git a/fe1-web/src/features/evoting/network/messages/__tests__/OpenElection.test.ts b/fe1-web/src/features/evoting/network/messages/__tests__/OpenElection.test.ts index 0333181a26..b0897e233c 100644 --- a/fe1-web/src/features/evoting/network/messages/__tests__/OpenElection.test.ts +++ b/fe1-web/src/features/evoting/network/messages/__tests__/OpenElection.test.ts @@ -1,29 +1,23 @@ import 'jest-extended'; import '__tests__/utils/matchers'; -import { mockLaoId, mockLaoIdHash, mockLaoName, configureTestFeatures } from '__tests__/utils'; +import { mockLaoIdHash, configureTestFeatures } from '__tests__/utils'; import { Hash, Timestamp, ProtocolError } from 'core/objects'; import { ActionType, ObjectType } from 'core/network/jsonrpc/messages'; import { MessageDataProperties } from 'core/types'; +import { mockElectionId } from 'features/evoting/objects/__tests__/utils'; import { OpenElection } from '../OpenElection'; const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 -const electionId: Hash = Hash.fromStringArray( - 'Election', - mockLaoId, - TIMESTAMP.toString(), - mockLaoName, -); - // In these tests, we should assume that the input to the messages is // just a Partial<> and not a MessageDataProperties<> // as this will catch more issues at runtime. (Defensive programming) const sampleOpenElection: Partial = { object: ObjectType.ELECTION, action: ActionType.OPEN, - election: electionId, + election: mockElectionId, lao: mockLaoIdHash, opened_at: TIMESTAMP, }; @@ -31,7 +25,7 @@ const sampleOpenElection: Partial = { const openElectionJson: string = `{ "object": "${ObjectType.ELECTION}", "action": "${ActionType.OPEN}", - "election": "${electionId}", + "election": "${mockElectionId}", "lao": "${mockLaoIdHash}", "opened_at": ${TIMESTAMP} }`; @@ -48,7 +42,7 @@ describe('OpenElection', () => { const temp = { object: ObjectType.ELECTION, action: ActionType.OPEN, - election: electionId, + election: mockElectionId, lao: mockLaoIdHash, opened_at: TIMESTAMP, }; @@ -64,7 +58,7 @@ describe('OpenElection', () => { const obj = { object: ObjectType.ELECTION, action: ActionType.NOTIFY_ADD, - election: electionId.toString(), + election: mockElectionId.toString(), lao: mockLaoIdHash.toString(), opened_at: parseInt(TIMESTAMP.toString(), 10), }; @@ -76,7 +70,7 @@ describe('OpenElection', () => { const obj = { object: ObjectType.CHIRP, action: ActionType.OPEN, - election: electionId.toString(), + election: mockElectionId.toString(), lao: mockLaoIdHash.toString(), opened_at: parseInt(TIMESTAMP.toString(), 10), }; @@ -99,7 +93,7 @@ describe('OpenElection', () => { const createWrongObj = () => new OpenElection({ lao: undefined as unknown as Hash, - election: electionId, + election: mockElectionId, opened_at: TIMESTAMP, }); expect(createWrongObj).toThrow(ProtocolError); @@ -108,7 +102,7 @@ describe('OpenElection', () => { it('should throw an error if opened_at is undefined', () => { const createWrongObj = () => new OpenElection({ - election: electionId, + election: mockElectionId, lao: mockLaoIdHash, opened_at: undefined as unknown as Timestamp, }); @@ -119,7 +113,7 @@ describe('OpenElection', () => { const msg = new OpenElection({ object: ObjectType.CHIRP, action: ActionType.NOTIFY_ADD, - election: electionId, + election: mockElectionId, lao: mockLaoIdHash, opened_at: TIMESTAMP, } as MessageDataProperties); diff --git a/fe1-web/src/features/evoting/network/messages/__tests__/SetupElection.test.ts b/fe1-web/src/features/evoting/network/messages/__tests__/SetupElection.test.ts index bbaa9400aa..fdb6bb92d5 100644 --- a/fe1-web/src/features/evoting/network/messages/__tests__/SetupElection.test.ts +++ b/fe1-web/src/features/evoting/network/messages/__tests__/SetupElection.test.ts @@ -1,12 +1,20 @@ import 'jest-extended'; import '__tests__/utils/matchers'; -import { mockLaoId, mockLaoIdHash, mockLaoName, configureTestFeatures } from '__tests__/utils'; +import { mockLaoIdHash, mockLaoName, configureTestFeatures } from '__tests__/utils'; -import { EventTags, Hash, Timestamp, ProtocolError } from 'core/objects'; +import { Hash, Timestamp, ProtocolError } from 'core/objects'; import { ActionType, ObjectType } from 'core/network/jsonrpc/messages'; import STRINGS from 'resources/strings'; import { MessageDataProperties } from 'core/types'; +import { + mockBallotOptions, + mockElectionId, + mockQuestion1, + mockQuestionObject1, + mockQuestionObject2, + mockQuestions, +} from 'features/evoting/objects/__tests__/utils'; import { Question } from '../../../objects'; import { SetupElection } from '../SetupElection'; @@ -17,54 +25,13 @@ const VERSION = STRINGS.election_version_identifier; const CLOSE_TIMESTAMP = new Timestamp(1609542000); // 2nd january 2021 const TIMESTAMP_BEFORE = new Timestamp(1609445600); -const electionId: Hash = Hash.fromStringArray( - 'Election', - mockLaoId, - TIMESTAMP.toString(), - mockLaoName, -); - -const mockQuestion1: string = 'Mock Question 1'; -const mockQuestion2 = 'Mock Question 2'; - -const mockQuestionId1: Hash = Hash.fromStringArray( - EventTags.QUESTION, - electionId.toString(), - mockQuestion1, -); -const mockQuestionId2 = Hash.fromStringArray( - EventTags.QUESTION, - electionId.toString(), - mockQuestion2, -); - -const mockBallotOptions = ['Ballot Option 1', 'Ballot Option 2']; - -const mockQuestionObject1: Question = { - id: mockQuestionId1.toString(), - question: mockQuestion1, - voting_method: STRINGS.election_method_Plurality, - ballot_options: mockBallotOptions, - write_in: false, -}; - -const mockQuestionObject2: Question = { - id: mockQuestionId2.toString(), - question: mockQuestion2, - voting_method: STRINGS.election_method_Approval, - ballot_options: mockBallotOptions, - write_in: true, -}; - -const mockQuestions = [mockQuestionObject1]; - // In these tests, we should assume that the input to the messages is // just a Partial<> and not a MessageDataProperties<> // as this will catch more issues at runtime. (Defensive programming) const sampleSetupElection: Partial = { object: ObjectType.ELECTION, action: ActionType.SETUP, - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -77,7 +44,7 @@ const sampleSetupElection: Partial = { const setupElectionJson: string = `{ "object": "${ObjectType.ELECTION}", "action": "${ActionType.SETUP}", - "id": "${electionId}", + "id": "${mockElectionId}", "lao": "${mockLaoIdHash}", "name": "${mockLaoName}", "version": "${VERSION}", @@ -101,7 +68,7 @@ describe('SetupElection', () => { const temp = { object: ObjectType.ELECTION, action: ActionType.SETUP, - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -122,7 +89,7 @@ describe('SetupElection', () => { const obj = { object: ObjectType.ELECTION, action: ActionType.NOTIFY_ADD, - id: electionId.toString(), + id: mockElectionId.toString(), lao: mockLaoIdHash.toString(), name: mockLaoName, version: VERSION, @@ -139,7 +106,7 @@ describe('SetupElection', () => { const obj = { object: ObjectType.CHIRP, action: ActionType.SETUP, - id: electionId.toString(), + id: mockElectionId.toString(), lao: mockLaoIdHash.toString(), name: mockLaoName, version: VERSION, @@ -171,7 +138,7 @@ describe('SetupElection', () => { it('should throw an error if lao is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: undefined as unknown as Hash, name: mockLaoName, version: VERSION, @@ -186,7 +153,7 @@ describe('SetupElection', () => { it('should throw an error if name is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: undefined as unknown as string, version: VERSION, @@ -201,7 +168,7 @@ describe('SetupElection', () => { it('should throw an error if version is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: undefined as unknown as string, @@ -216,7 +183,7 @@ describe('SetupElection', () => { it('should throw an error if created_at is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -231,7 +198,7 @@ describe('SetupElection', () => { it('should throw an error if start_time is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -246,7 +213,7 @@ describe('SetupElection', () => { it('should throw an error if end_time is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -261,7 +228,7 @@ describe('SetupElection', () => { it('should throw an error if questions is undefined', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -276,7 +243,7 @@ describe('SetupElection', () => { it('should throw an error if start_time is before created_at', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -291,7 +258,7 @@ describe('SetupElection', () => { it('should throw an error if end_time is before start_time', () => { const createWrongObj = () => new SetupElection({ - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -308,7 +275,7 @@ describe('SetupElection', () => { const msg = new SetupElection({ object: ObjectType.CHIRP, action: ActionType.NOTIFY_ADD, - id: electionId, + id: mockElectionId, lao: mockLaoIdHash, name: mockLaoName, version: VERSION, @@ -332,7 +299,7 @@ describe('SetupElection', () => { write_in: false, }; const wrongValidate = () => { - SetupElection.validateQuestions([wrongQuestion], electionId.valueOf()); + SetupElection.validateQuestions([wrongQuestion], mockElectionId.valueOf()); }; expect(wrongValidate).toThrow(ProtocolError); }); diff --git a/fe1-web/src/features/evoting/objects/Election.ts b/fe1-web/src/features/evoting/objects/Election.ts index 533d24de00..8a95d222b0 100644 --- a/fe1-web/src/features/evoting/objects/Election.ts +++ b/fe1-web/src/features/evoting/objects/Election.ts @@ -10,7 +10,6 @@ export const ELECTION_EVENT_TYPE = 'ELECTION'; export enum ElectionStatus { NOT_STARTED = 'not started', OPENED = 'opened', - FINISHED = 'finished', // When the time is over TERMINATED = 'terminated', // When manually terminated by organizer RESULT = 'result', // When result is available } diff --git a/fe1-web/src/features/evoting/objects/__tests__/Election.test.ts b/fe1-web/src/features/evoting/objects/__tests__/Election.test.ts index 90243c3496..60af0bdd71 100644 --- a/fe1-web/src/features/evoting/objects/__tests__/Election.test.ts +++ b/fe1-web/src/features/evoting/objects/__tests__/Election.test.ts @@ -62,7 +62,7 @@ const initializeData = () => { start: 1520255600, end: 1520275600, questions: [question1, question2], - electionStatus: ElectionStatus.FINISHED, + electionStatus: ElectionStatus.TERMINATED, registeredVotes: [registeredVotes], }; @@ -99,7 +99,7 @@ describe('Election object', () => { end: TIMESTAMP_PAST2.valueOf(), questions: [question1, question2], registeredVotes: [registeredVotes], - electionStatus: ElectionStatus.FINISHED, + electionStatus: ElectionStatus.TERMINATED, }; expect(election.toState()).toStrictEqual(expectedState); }); diff --git a/fe1-web/src/features/evoting/objects/__tests__/utils.ts b/fe1-web/src/features/evoting/objects/__tests__/utils.ts new file mode 100644 index 0000000000..a9432215a1 --- /dev/null +++ b/fe1-web/src/features/evoting/objects/__tests__/utils.ts @@ -0,0 +1,156 @@ +import { EventTags, Hash, Timestamp } from 'core/objects'; +import { CastVote, ElectionResult, EndElection } from 'features/evoting/network/messages'; +import STRINGS from 'resources/strings'; +import { mockLaoId, mockLaoIdHash } from '__tests__/utils'; +import { Election, ElectionStatus, Question, RegisteredVote, Vote } from '../Election'; + +const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 +const VERSION = STRINGS.election_version_identifier; +const CLOSE_TIMESTAMP = new Timestamp(1609542000); // 2nd january 2021 + +export const mockElectionName = 'An election'; + +export const mockElectionId = Hash.fromStringArray( + EventTags.ELECTION, + mockLaoId, + TIMESTAMP.toString(), + mockElectionName, +); + +export const mockQuestion1: string = 'Mock Question 1'; +export const mockQuestion2 = 'Mock Question 2'; + +export const mockQuestionId1: Hash = Hash.fromStringArray( + EventTags.QUESTION, + mockElectionId.toString(), + mockQuestion1, +); +export const mockQuestionId2 = Hash.fromStringArray( + EventTags.QUESTION, + mockElectionId.toString(), + mockQuestion2, +); + +export const mockBallotOption1 = 'Ballot Option 1'; +export const mockBallotOption2 = 'Ballot Option 2'; +export const mockBallotOptions = [mockBallotOption1, mockBallotOption2]; + +export const mockQuestionObject1: Question = { + id: mockQuestionId1.toString(), + question: mockQuestion1, + voting_method: STRINGS.election_method_Plurality, + ballot_options: mockBallotOptions, + write_in: false, +}; + +export const mockQuestionObject2: Question = { + id: mockQuestionId2.toString(), + question: mockQuestion2, + voting_method: STRINGS.election_method_Approval, + ballot_options: mockBallotOptions, + write_in: true, +}; + +export const mockQuestions: Question[] = [mockQuestionObject1, mockQuestionObject2]; + +export const mockRegisteredVotes: RegisteredVote[] = [ + { + createdAt: 0, + messageId: '0', + sender: 'sender 1', + votes: [ + { id: 'id1', question: 'q1', vote: [0] }, + { id: 'id2', question: 'q2', vote: [0] }, + ], + }, + { + createdAt: 1, + messageId: '1', + sender: 'sender 2', + votes: [ + { id: 'id3', question: 'q3', vote: [0] }, + { id: 'id4', question: 'q4', vote: [0] }, + ], + }, +]; + +export const mockElectionNotStarted = new Election({ + lao: mockLaoIdHash, + id: mockElectionId, + name: mockElectionName, + version: VERSION, + createdAt: TIMESTAMP, + start: TIMESTAMP, + end: CLOSE_TIMESTAMP, + questions: mockQuestions, + electionStatus: ElectionStatus.NOT_STARTED, +}); + +export const mockElectionOpened = new Election({ + lao: mockLaoIdHash, + id: mockElectionId, + name: mockElectionName, + version: VERSION, + createdAt: TIMESTAMP, + start: TIMESTAMP, + end: CLOSE_TIMESTAMP, + questions: mockQuestions, + electionStatus: ElectionStatus.OPENED, + registeredVotes: mockRegisteredVotes, +}); + +export const mockElectionTerminated = new Election({ + lao: mockLaoIdHash, + id: mockElectionId, + name: mockElectionName, + version: VERSION, + createdAt: TIMESTAMP, + start: TIMESTAMP, + end: CLOSE_TIMESTAMP, + questions: mockQuestions, + electionStatus: ElectionStatus.TERMINATED, + registeredVotes: mockRegisteredVotes, +}); + +export const mockRegistedVotesHash = EndElection.computeRegisteredVotesHash(mockElectionOpened); + +export const mockVoteVotes1 = new Set([0]); +export const mockVoteVotes2 = new Set([1, 0]); + +export const mockVoteId1 = CastVote.computeVoteId(mockElectionNotStarted, 0, mockVoteVotes1); +export const mockVoteId2 = CastVote.computeVoteId(mockElectionNotStarted, 1, mockVoteVotes2); + +export const mockVote1: Vote = { + id: mockVoteId1.toString(), + question: mockQuestionId1.valueOf(), + vote: [0], +}; + +export const mockVote2: Vote = { + id: mockVoteId2.toString(), + question: mockQuestionId2.valueOf(), + vote: [1], +}; + +export const mockVotes = [mockVote1]; + +export const mockElectionResultHash = Hash.fromStringArray(mockVoteId1.valueOf()); + +// id: string; result: { ballot_option: string; count: number }[] + +export const mockElectionResultQuestions: ElectionResult['questions'] = [ + { + id: mockQuestionId1.valueOf(), + result: [ + { ballot_option: mockBallotOption1, count: 4 }, + { ballot_option: mockBallotOption2, count: 2 }, + ], + }, + { + id: mockQuestionId2.valueOf(), + result: [ + { ballot_option: mockBallotOption1, count: 1 }, + { ballot_option: mockBallotOption2, count: 10 }, + ], + }, +];