Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further work on message validation #1758

Merged
merged 23 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9df081e
Added isValidWriteIn in MessageValidator.kt
Kaz-ookid Feb 23, 2024
01ea364
EncryptedVote validations
Kaz-ookid Feb 23, 2024
5c476b7
EncryptedVote validations : Previous tests now using valid formats
Kaz-ookid Feb 26, 2024
67d5d15
EncryptedVote : added tests for validations
Kaz-ookid Feb 26, 2024
ed2baf8
EncryptedVote : added tests for validations + More previous tests usi…
Kaz-ookid Feb 26, 2024
f5d56d6
ElectionQuestion : added validQuestions in MessageValidator + added v…
Kaz-ookid Feb 26, 2024
151cf30
fixed format
Kaz-ookid Feb 27, 2024
07d495b
ElectionQuestion : added validation at Question creation + added tests
Kaz-ookid Feb 27, 2024
e83a468
ElectionQuestion : removed overlapping Question validations
Kaz-ookid Mar 3, 2024
65a7caf
ElectionQuestion : fixed wrong hashCode concatenation
Kaz-ookid Mar 3, 2024
f88cfaa
MessageValidator : removed unnecessary function isValidWriteIn
Kaz-ookid Mar 3, 2024
fed6cb7
MessageValidator : moved validVotingMethods as a static field
Kaz-ookid Mar 3, 2024
7bbf22c
MessageValidator : forEach instead of useless map use
Kaz-ookid Mar 3, 2024
9768d91
CastVoteTest : actually using defined vote variables to avoid hardcod…
Kaz-ookid Mar 3, 2024
fed176f
formatting
Kaz-ookid Mar 3, 2024
a819878
formatting : run ktfmtFormat
Kaz-ookid Mar 3, 2024
eb92351
ElectionQuestion : Addressing Matteo's comments
Kaz-ookid Mar 3, 2024
2aed118
MessageValidator : Addressing Matteo's comments
Kaz-ookid Mar 3, 2024
a7c0acb
CastVoteTest : actually using defined vote variables to avoid hardcod…
Kaz-ookid Mar 3, 2024
b41f82f
formatting : run ktfmtFormat
Kaz-ookid Mar 3, 2024
a7e5a5f
Merge remote-tracking branch 'origin/work-fe2-maxime-message-validati…
Kaz-ookid Mar 4, 2024
e2d820d
ElectionQuestion : removed useless validation function (validQuestions)
Kaz-ookid Mar 4, 2024
ef01bc7
Merge branch 'master' into work-fe2-maxime-message-validation
Kaz-ookid Mar 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,32 @@ package com.github.dedis.popstellar.model.network.method.message.data.election

import com.github.dedis.popstellar.model.Immutable
import com.github.dedis.popstellar.model.objects.Election
import com.github.dedis.popstellar.utility.MessageValidator.verify
import com.google.gson.annotations.SerializedName
import java.util.Collections
import java.util.Objects

@Immutable
/** Constructor for a data Question, for the election setup */
class ElectionQuestion(electionId: String, question: Question) {
val question: String = question.title
val id: String = Election.generateElectionQuestionId(electionId, question.title)
val question: String
val id: String

@SerializedName(value = "voting_method") val votingMethod: String = question.votingMethod
@SerializedName(value = "voting_method") val votingMethod: String

@SerializedName(value = "ballot_options")
val ballotOptions: List<String> = Collections.unmodifiableList(question.ballotOptions)
@SerializedName(value = "ballot_options") val ballotOptions: List<String>

@SerializedName(value = "write_in") val writeIn: Boolean = question.writeIn
@SerializedName(value = "write_in") val writeIn: Boolean

init {
verify().isNotEmptyBase64(electionId, "election ID")

this.question = question.title
this.id = Election.generateElectionQuestionId(electionId, question.title)
this.votingMethod = question.votingMethod
this.ballotOptions = Collections.unmodifiableList(question.ballotOptions)
this.writeIn = question.writeIn
}

override fun equals(other: Any?): Boolean {
if (this === other) {
Expand Down Expand Up @@ -47,11 +57,25 @@ class ElectionQuestion(electionId: String, question: Question) {
*/
@Immutable
class Question(
val title: String,
val votingMethod: String,
val ballotOptions: List<String>,
val writeIn: Boolean
title: String,
votingMethod: String,
ballotOptions: List<String>,
writeIn: Boolean,
) {
val title: String
val votingMethod: String
val ballotOptions: List<String>
val writeIn: Boolean

init {
verify().validQuestion(title, votingMethod, ballotOptions)

this.title = title
this.votingMethod = votingMethod
this.ballotOptions = Collections.unmodifiableList(ballotOptions)
this.writeIn = writeIn
}

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.dedis.popstellar.model.network.method.message.data.election

import com.github.dedis.popstellar.model.Immutable
import com.github.dedis.popstellar.model.objects.Election
import com.github.dedis.popstellar.utility.MessageValidator.verify
import com.google.gson.annotations.SerializedName
import java.util.Objects

Expand Down Expand Up @@ -29,12 +30,32 @@ class EncryptedVote : Vote {
encryptedVote: String?,
writeInEnabled: Boolean,
encryptedWriteIn: String?,
electionId: String
electionId: String,
) {
// Assumes if write-in is disabled, encryptedVote must be non-null and non-empty. If write-in is
// enabled,
// encryptedVote is ignored, and encryptedWriteIn must be non-null and non-empty.
// TODO: Need to verify the exact intended behavior when write-in is supported.
verify()
.isNotEmptyBase64(questionId, "question ID")
.isNotEmptyBase64(electionId, "election ID")
.apply {
if (writeInEnabled) {
isNotEmptyBase64(encryptedWriteIn, "write-in")
} else {
isNotEmptyBase64(encryptedVote, "encrypted vote")
}
}

this.questionId = questionId
this.id =
Election.generateEncryptedElectionVoteId(
electionId, questionId, encryptedVote, encryptedWriteIn, writeInEnabled)
electionId,
questionId,
encryptedVote,
encryptedWriteIn,
writeInEnabled,
)
this.vote =
if (writeInEnabled) {
null
Expand All @@ -44,6 +65,11 @@ class EncryptedVote : Vote {
}

constructor(id: String, question: String, vote: String) {
verify()
.stringNotEmpty(id, "vote ID")
.stringNotEmpty(question, "question")
.isNotEmptyBase64(vote, "vote")

this.id = id
this.questionId = question
this.vote = vote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.dedis.popstellar.model.objects.Meeting
import com.github.dedis.popstellar.model.objects.Reaction
import com.github.dedis.popstellar.model.objects.security.PublicKey
import com.github.dedis.popstellar.model.qrcode.PoPCHAQRCode
import com.github.dedis.popstellar.ui.lao.event.election.fragments.ElectionSetupFragment
import java.time.Instant
import java.util.Arrays
import java.util.regex.Pattern
Expand Down Expand Up @@ -230,6 +231,38 @@ object MessageValidator {
return this
}

/**
* Helper method to check a single question for validity.
*
* @param title the title of the question
* @param votingMethod the voting method of the question
* @param ballotOptions the ballot options of the question
* @throws IllegalArgumentException if the question does not meet the criteria.
*/
fun validQuestion(
title: String,
votingMethod: String,
ballotOptions: List<String>,
): MessageValidatorBuilder {
stringNotEmpty(title, "question title")
require(votingMethod in validVotingMethods) {
"Unsupported voting method in question: ${title}. Must be one of $validVotingMethods."
}
validBallotOptions(ballotOptions)
return this
}

private fun validBallotOptions(ballotOptions: List<String>?): MessageValidatorBuilder {
requireNotNull(ballotOptions) { "Ballot options cannot be null" }
listNotEmpty(ballotOptions)
require(ballotOptions.size >= 2) { "There must be at least 2 ballot options" }
noListDuplicates(ballotOptions)
ballotOptions.forEach {
stringNotEmpty(it, "ballot option in place " + ballotOptions.indexOf(it))
}
return this
}

fun validUrl(input: String?): MessageValidatorBuilder {
require(input != null && URL_PATTERN.matcher(input).matches()) { "Input is not a url" }
return this
Expand Down Expand Up @@ -297,6 +330,8 @@ object MessageValidator {
private const val VALID_RESPONSE_TYPE = "id_token"
private val REQUIRED_SCOPES = arrayOf("openid", "profile")
private val VALID_RESPONSE_MODES = arrayOf("query", "fragment")

private val validVotingMethods = ElectionSetupFragment.VotingMethods.values().map { it.desc }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.dedis.popstellar.model.network.JsonTestUtils.testData
import com.github.dedis.popstellar.model.objects.Election.Companion.generateElectionQuestionId
import com.github.dedis.popstellar.model.objects.Election.Companion.generateElectionSetupId
import com.github.dedis.popstellar.model.objects.Lao.Companion.generateLaoId
import com.github.dedis.popstellar.model.objects.security.Base64URLData
import com.github.dedis.popstellar.testutils.Base64DataUtils
import com.google.gson.JsonParseException
import java.time.Instant
Expand All @@ -26,15 +27,21 @@ class CastVoteTest {
private val questionId2 = generateElectionQuestionId(electionId, "Question 2")
private val writeInEnabled = false
private val writeIn = "My write in ballot option"
private val vote1 = 1
private val vote2 = 2
private val vote1Base64 = Base64URLData(vote1.toString().toByteArray()).encoded
private val vote2Base64 = Base64URLData(vote2.toString().toByteArray()).encoded

// Set up a open ballot election
private val plainVote1 = PlainVote(questionId1, 1, writeInEnabled, writeIn, electionId)
private val plainVote2 = PlainVote(questionId2, 2, writeInEnabled, writeIn, electionId)
private val plainVote1 = PlainVote(questionId1, vote1, writeInEnabled, writeIn, electionId)
private val plainVote2 = PlainVote(questionId2, vote2, writeInEnabled, writeIn, electionId)
private val plainVotes = listOf<Vote>(plainVote1, plainVote2)

// Set up a secret ballot election
private val encryptedVote1 = EncryptedVote(questionId1, "2", writeInEnabled, writeIn, electionId)
private val encryptedVote2 = EncryptedVote(questionId2, "1", writeInEnabled, writeIn, electionId)
private val encryptedVote1 =
EncryptedVote(questionId1, vote2Base64, writeInEnabled, writeIn, electionId)
private val encryptedVote2 =
EncryptedVote(questionId2, vote1Base64, writeInEnabled, writeIn, electionId)
private val electionEncryptedVotes = listOf<Vote>(encryptedVote1, encryptedVote2)

// Create the cast votes messages
Expand Down Expand Up @@ -71,7 +78,7 @@ class CastVoteTest {

@Test(expected = IllegalArgumentException::class)
fun constructorFailsWithVoteQuestionIdNotBase64Test() {
val invalid = PlainVote("not base 64", 1, writeInEnabled, writeIn, electionId)
val invalid = PlainVote("not base 64", vote1, writeInEnabled, writeIn, electionId)
val invalidVotes = listOf<Vote>(plainVote1, plainVote2, invalid)
CastVote(invalidVotes, electionId, laoId, creation)
}
Expand Down Expand Up @@ -112,7 +119,7 @@ class CastVoteTest {
Assert.assertNotEquals(castEncryptedVote, CastVote(listOf(encryptedVote1), randomId, laoId))
Assert.assertNotEquals(
castEncryptedVote,
CastVote(listOf(encryptedVote1), electionId, randomId)
CastVote(listOf(encryptedVote1), electionId, randomId),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package com.github.dedis.popstellar.model.network.method.message.data.election
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.dedis.popstellar.model.network.JsonTestUtils.testData
import com.github.dedis.popstellar.model.network.method.message.data.election.ElectionQuestion.Question
import com.github.dedis.popstellar.model.objects.Election
import com.github.dedis.popstellar.model.objects.Lao
import com.github.dedis.popstellar.testutils.Base64DataUtils
import com.github.dedis.popstellar.ui.lao.event.election.fragments.ElectionSetupFragment
import com.github.dedis.popstellar.utility.security.HashSHA256.hash
import java.time.Instant
import org.hamcrest.CoreMatchers
Expand All @@ -13,6 +17,61 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ElectionQuestionTest {

private val organizer = Base64DataUtils.generatePublicKey()
private val creation = Instant.now().epochSecond
private val laoId = Lao.generateLaoId(organizer, creation, "name")

private val validElectionId = Election.generateElectionSetupId(laoId, creation, "election name")
private val invalidElectionId = "this is not base64"
private val validQuestionTitle = "Valid Question Title"
private val validVotingMethod = ElectionSetupFragment.VotingMethods.values()[0].desc
private val invalidVotingMethod = "Invalid Voting Method for sure"
private val validBallotOptions = listOf("Option 1", "Option 2")
private val insufficientBallotOptions = listOf("OnlyOneOption")
private val emptyBallotOption = listOf("Option 1", "")
private val duplicateBallotOptions = listOf("Option 1", "Option 1")
private val writeIn = false

private val validQuestion =
Question(validQuestionTitle, validVotingMethod, validBallotOptions, writeIn)

@Test
fun constructorSucceedsWithValidData() {
val electionQuestion = ElectionQuestion(validElectionId, validQuestion)
Assert.assertNotNull(electionQuestion)
}

@Test(expected = IllegalArgumentException::class)
fun constructorFailsWhenElectionIdNotBase64() {
ElectionQuestion(invalidElectionId, validQuestion)
}

@Test(expected = IllegalArgumentException::class)
fun questionConstructorFailsWithInvalidVotingMethod() {
Question(validQuestionTitle, invalidVotingMethod, validBallotOptions, writeIn)
}

@Test(expected = IllegalArgumentException::class)
fun questionConstructorFailsWithInsufficientBallotOptions() {
Question(validQuestionTitle, validVotingMethod, insufficientBallotOptions, writeIn)
}

@Test(expected = IllegalArgumentException::class)
fun questionConstructorFailsWithInvalidBallotOptions() {
Question(validQuestionTitle, validVotingMethod, insufficientBallotOptions, writeIn)
}

@Test(expected = IllegalArgumentException::class)
fun questionConstructorFailsWithDuplicateBallotOptions() {
Question(validQuestionTitle, validVotingMethod, duplicateBallotOptions, writeIn)
}

@Test(expected = IllegalArgumentException::class)
fun questionConstructorFailsWithEmptyBallotOption() {
Question(validQuestionTitle, validVotingMethod, emptyBallotOption, writeIn)
}

@Test
fun electionQuestionGetterReturnsCorrectId() {
// Hash(“Question”||election_id||question)
Expand Down
Loading
Loading