Skip to content

Commit

Permalink
Merge branch 'release/1.0.1' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
spiftire committed May 7, 2021
2 parents 75af003 + b5e8c6b commit d9f0787
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 32 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "backend",
"version": "0.0.1",
"version": "1.0.1",
"description": "Anovote backend",
"main": "index.js",
"repository": "https://github.com/anovote/backend",
Expand All @@ -24,7 +24,6 @@
"@types/crypto-js": "^4.0.1",
"@types/express": "^4.17.11",
"@types/express-rate-limit": "^5.1.1",
"@types/helmet": "^4.0.0",
"@types/jest": "^26.0.20",
"@types/jsonwebtoken": "^8.5.0",
"@types/morgan": "^1.9.2",
Expand Down
26 changes: 25 additions & 1 deletion src/lib/election/ElectionRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export class ElectionRoom implements IElectionRoom {

private _totalEligibleVoters = 0

private _finishedBallots = new Set()

private _connectedVoters = 0

constructor({
Expand Down Expand Up @@ -96,17 +98,39 @@ export class ElectionRoom implements IElectionRoom {
* Returns true if all eligible voters for the given ballot id has
* voted on the ballot
* @param ballotId the ballot id to check for
* @returns returns true of all eligible voters have voter
* @returns returns true if all eligible voters have voted
*/
haveAllVotedOnBallot(ballotId: number) {
let allVoted = false
const ballotVoteInformation = this.getBallotVoteInformation(ballotId)
if (ballotVoteInformation) {
allVoted = ballotVoteInformation.voters.size === this._totalEligibleVoters

if (allVoted) {
this.addToFinishedBallots(ballotId)
}
}
return allVoted
}

/**
* Increments the finished ballots count if it has not yet been added to the set
* @param ballotId the ballot to add
*/
private addToFinishedBallots(ballotId: number) {
if (!this._finishedBallots.has(ballotId)) {
this._finishedBallots.add(ballotId)
}
}

/**
* Returns the true if all ballots have been voted on.
* @returns returns true if all ballots have been voted on
*/
haveAllBallotsBeenVotedOn() {
return this._ballotVoteInformation.size === this._finishedBallots.size
}

/**
* Returns the vote stats for the provided ballotId or undefined if the ballot information
* does not exists.
Expand Down
95 changes: 76 additions & 19 deletions src/lib/websocket/events/client/voter/submitVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { SocketRoomService } from '@/services/SocketRoomService'
import { VoteService } from '@/services/VoteService'
import { Events } from '@/lib/websocket/events'
import { VoterSocket } from '@/lib/websocket/AnoSocket'
import { Election } from '@/models/Election/ElectionEntity'
import chalk from 'chalk'
import { ElectionRoom } from '@/lib/election/ElectionRoom'

/**
* Submits a vote with the given vote details
Expand All @@ -34,15 +37,7 @@ export const submitVote: EventHandlerAcknowledges<IVote> = async (event) => {
// Todo: verify that a vote has not more candidates than allowed
// Todo: assign points to RANKED votes according to order of candidates
try {
if (!submittedVote.ballot || !submittedVote.voter) {
let typeMissing = ''
if (!submittedVote.ballot) {
typeMissing = 'Ballot id'
} else {
typeMissing = 'voter'
}
throw new BadRequestError({ message: ServerErrorMessage.isMissing(typeMissing) })
}
validateSubmittedVote(submittedVote)

const room = socketRoomService.getRoom(voterSocket.electionCode)

Expand All @@ -56,6 +51,7 @@ export const submitVote: EventHandlerAcknowledges<IVote> = async (event) => {
const ballotId = submittedVote.ballot

let ballot: Ballot | undefined
let election: Election | undefined

// Create vote first so we know it at least inserts into the database
await voteService.create(submittedVote)
Expand All @@ -65,30 +61,91 @@ export const submitVote: EventHandlerAcknowledges<IVote> = async (event) => {

// Check if all voters have voted
const allVoted = room.haveAllVotedOnBallot(ballotId)

if (allVoted) {
const ballotService = new BallotService(database, new ElectionService(database))
ballot = await ballotService.getByIdWithoutOwner(ballotId)
if (ballot) {
ballot.status = BallotStatus.IN_ARCHIVE
// submit the update to the database
await ballotService.update(ballotId, ballot)
// Update room ballot
room.updateVoteInformationBallot(ballot)
}
ballot = await finishBallotForRoom(ballotId, room)
}

const allBallotsVotedOn = room.haveAllBallotsBeenVotedOn()
if (allBallotsVotedOn) {
election = await finishElection(ballot, election)
}

// update with a new vote
if (room.organizerSocketId) {
const organizer = event.server.to(room.organizerSocketId)

if (allVoted && ballot) organizer.emit(Events.server.ballot.update, { ballot })
if (allBallotsVotedOn) organizer.emit(Events.server.election.finish, { election })

organizer.emit(Events.server.vote.newVote, room.getBallotStats(ballotId))
}

logger.info('A vote was submitted')
event.acknowledgement(EventMessage())

if (allVoted && ballot) {
logger.info(`-> ${chalk.redBright(`ballot (${ballot.id})`)} : all votes submitted`)
}

if (allBallotsVotedOn && election) {
logger.info(`-> ${chalk.green(`election (${election.id})`)} : all ballots voted on`)
}
} catch (err) {
logger.error(err)
// Only emit errors that is safe to emit
if (err instanceof BaseError) event.acknowledgement(EventErrorMessage(err))
}
}

/**
* Checks if the submitted vote is missing ballot or a voter. Throws an error if something is missing
* @param submittedVote the vote to validate
*/
function validateSubmittedVote(submittedVote: IVote) {
if (!submittedVote.ballot || !submittedVote.voter) {
let typeMissing = ''
if (!submittedVote.ballot) {
typeMissing = 'Ballot id'
} else {
typeMissing = 'voter'
}
throw new BadRequestError({ message: ServerErrorMessage.isMissing(typeMissing) })
}
}

/**
* Finishes the election when a ballot has all voters added.
* @param ballot the ballot to that have been finished
* @param election the election the ballot is a part of
* @returns the updated election
*/
async function finishElection(ballot: Ballot | undefined, election: Election | undefined) {
const electionService = new ElectionService(database)
if (ballot) {
const entity = await electionService.getElectionById(ballot.election.id)
if (entity) {
// update election as finished, but not close election completely
election = await electionService.markElectionClosed(entity, false)
}
}
return election
}

/**
* Finishes a ballot for a given room
* @param ballotId the ballot id to finish
* @param room the room the ballot belongs to
* @returns
*/
async function finishBallotForRoom(ballotId: number, room: ElectionRoom) {
const ballotService = new BallotService(database, new ElectionService(database))
const ballot = await ballotService.getByIdWithoutOwner(ballotId)
if (ballot) {
ballot.status = BallotStatus.IN_ARCHIVE
// submit the update to the database
await ballotService.update(ballotId, ballot)

room.updateVoteInformationBallot(ballot)
}
return ballot
}
1 change: 1 addition & 0 deletions src/lib/websocket/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const Events = {
election: {
push: 'push_election',
close: 'close_election',
finish: 'finish_election',
voterConnected: 'voter_connected',
voterDisconnected: 'voter_disconnected'
},
Expand Down
2 changes: 1 addition & 1 deletion src/services/ElectionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class ElectionService extends BaseEntityService<Election> implements IHas
return await this.repository.save(updateElection)
}

private async getElectionById(id: number): Promise<Election | undefined> {
async getElectionById(id: number): Promise<Election | undefined> {
if (this.owner) {
const election = await this.manager.findOne(id, {
relations: ['electionOrganizer'],
Expand Down
12 changes: 11 additions & 1 deletion src/services/SocketRoomService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,19 @@ export class SocketRoomService extends BaseEntityService<SocketRoomEntity> {
createRoom(election: Election) {
const room = this._electionRooms.get(election.id)
if (!room) {
const ballotVoteInformation: BallotVoteInformation = new Map()
for (const ballot of election.ballots) {
ballotVoteInformation.set(ballot.id, {
stats: new BallotVoteStats(ballot),
voters: new Set()
})
}
this._electionRooms.set(
election.id,
new ElectionRoom({ totalEligibleVoters: election.eligibleVoters ? election.eligibleVoters.length : 0 })
new ElectionRoom({
totalEligibleVoters: election.eligibleVoters.length,
ballotVoteInformation: ballotVoteInformation
})
)
}
}
Expand Down
132 changes: 132 additions & 0 deletions tests/lib/election/ElectionRoom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,138 @@ it('should return true if all voters have voted on ballot', () => {
expect(electionRoom.haveAllVotedOnBallot(ballot.id)).toBe(true)
})

it('should return true if all ballots have been voted on', () => {
const total = 2
const ballotVoteInformation: BallotVoteInformation = new Map()
const electionRoom = new ElectionRoom({ totalEligibleVoters: total, ballotVoteInformation })

ballotVoteInformation.set(ballot.id, {
stats: new BallotVoteStats(ballot),
voters: new Set()
})

electionRoom.addVote({
ballotId: ballot.id,
voterId: 1,
votes: [{ ballot: 3, candidate: 1, submitted: new Date(), voter: 1 }]
})
electionRoom.addVote({
ballotId: ballot.id,
voterId: 2,
votes: [{ ballot: 3, candidate: 1, submitted: new Date(), voter: 1 }]
})

expect(electionRoom.haveAllVotedOnBallot(ballot.id)).toBeTruthy()

expect(electionRoom.haveAllBallotsBeenVotedOn()).toBeTruthy()
})

it('should return true if all ballots have been voted on even when checked twice', () => {
const secondBallot = new Ballot()
secondBallot.id = 2
secondBallot.candidates = [{ ballot: secondBallot, candidate: 'test', id: 1 }]
secondBallot.createdAt = new Date()
secondBallot.updatedAt = new Date()
secondBallot.title = 'Some title'
secondBallot.description = 'Some description'
secondBallot.displayResultCount = true
secondBallot.type = BallotType.SINGLE
secondBallot.resultDisplayType = BallotResultDisplay.ALL

const total = 2
const ballotVoteInformation: BallotVoteInformation = new Map()
const electionRoom = new ElectionRoom({ totalEligibleVoters: total, ballotVoteInformation })

ballotVoteInformation.set(ballot.id, {
stats: new BallotVoteStats(ballot),
voters: new Set()
})

electionRoom.addVote({
ballotId: ballot.id,
voterId: 1,
votes: [{ ballot: 1, candidate: 1, submitted: new Date(), voter: 1 }]
})
electionRoom.addVote({
ballotId: ballot.id,
voterId: 2,
votes: [{ ballot: 1, candidate: 1, submitted: new Date(), voter: 1 }]
})

ballotVoteInformation.set(secondBallot.id, {
stats: new BallotVoteStats(secondBallot),
voters: new Set()
})

electionRoom.addVote({
ballotId: secondBallot.id,
voterId: 1,
votes: [{ ballot: 2, candidate: 1, submitted: new Date(), voter: 1 }]
})
electionRoom.addVote({
ballotId: secondBallot.id,
voterId: 2,
votes: [{ ballot: 2, candidate: 1, submitted: new Date(), voter: 1 }]
})

electionRoom.haveAllVotedOnBallot(ballot.id)
electionRoom.haveAllVotedOnBallot(secondBallot.id)
electionRoom.haveAllVotedOnBallot(secondBallot.id)

expect(electionRoom.haveAllBallotsBeenVotedOn()).toBeTruthy()
})

it('should return false if less ballots have been voted on', () => {
const secondBallot = new Ballot()
secondBallot.id = 2
secondBallot.candidates = [{ ballot: secondBallot, candidate: 'test', id: 1 }]
secondBallot.createdAt = new Date()
secondBallot.updatedAt = new Date()
secondBallot.title = 'Some title'
secondBallot.description = 'Some description'
secondBallot.displayResultCount = true
secondBallot.type = BallotType.SINGLE
secondBallot.resultDisplayType = BallotResultDisplay.ALL

const total = 2
const ballotVoteInformation: BallotVoteInformation = new Map()
const electionRoom = new ElectionRoom({ totalEligibleVoters: total, ballotVoteInformation })

ballotVoteInformation.set(ballot.id, {
stats: new BallotVoteStats(ballot),
voters: new Set()
})

electionRoom.addVote({
ballotId: ballot.id,
voterId: 1,
votes: [{ ballot: 1, candidate: 1, submitted: new Date(), voter: 1 }]
})
electionRoom.addVote({
ballotId: ballot.id,
voterId: 2,
votes: [{ ballot: 1, candidate: 1, submitted: new Date(), voter: 1 }]
})

ballotVoteInformation.set(secondBallot.id, {
stats: new BallotVoteStats(secondBallot),
voters: new Set()
})

electionRoom.addVote({
ballotId: secondBallot.id,
voterId: 1,
votes: [{ ballot: 2, candidate: 1, submitted: new Date(), voter: 1 }]
})

// missing one vote for secondBallot

electionRoom.haveAllVotedOnBallot(ballot.id)
electionRoom.haveAllVotedOnBallot(secondBallot.id)

expect(electionRoom.haveAllBallotsBeenVotedOn()).toBeFalsy()
})

it('should set connected voters to provided amount', () => {
const electionRoom = new ElectionRoom({ totalEligibleVoters: 1 })
electionRoom.connectedVoters = 3
Expand Down
3 changes: 3 additions & 0 deletions tests/models/SocketRoomEntity.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Ballot } from '@/models/Ballot/BallotEntity'
import { Election } from '@/models/Election/ElectionEntity'
import { ElectionStatus } from '@/models/Election/ElectionStatus'
import { ElectionOrganizer } from '@/models/ElectionOrganizer/ElectionOrganizerEntity'
Expand Down Expand Up @@ -72,6 +73,8 @@ it('should set the socket room status to closed when room is issued close', asyn
election.isAutomatic = true
election.isLocked = false
election.id = -1
election.ballots = []
election.eligibleVoters = []

const socketRoom = repo.create()
socketRoom.id = -1
Expand Down
Loading

0 comments on commit d9f0787

Please sign in to comment.