diff --git a/src/main/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsService.java b/src/main/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsService.java index 515d03e..91cbc38 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsService.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsService.java @@ -89,6 +89,16 @@ public RaireResultOrError generateAssertions(GenerateAssertionsRequest request) prefix, request.contestName, request.candidates, request.totalAuditableBallots, request.timeLimitSeconds)); + // Check that the contest exists and is all IRV. Otherwise this is an internal error because + // it should be caught before here. + if(contestRepository.findFirstByName(request.contestName).isEmpty() + || !contestRepository.isAllIRV(request.contestName)) { + final String msg = String.format("%s Contest %s does not exist or is not all IRV", prefix, + request.contestName); + logger.error(msg + "Throwing a RaireServiceException."); + throw new RaireServiceException(msg, RaireErrorCode.INTERNAL_ERROR); + } + // Use raire-java to consolidate the votes, collecting all votes with the same ranking // together and representing that collection as a single ranking with an associated number // denoting how many votes with that ranking exist. @@ -103,7 +113,7 @@ public RaireResultOrError generateAssertions(GenerateAssertionsRequest request) c -> cvrContestInfoRepository.getCVRs(c.getContestID(), c.getCountyID())). flatMap(List::stream).toList(); - if(votes.size() > request.totalAuditableBallots){ + if(votes.size() > request.totalAuditableBallots) { final String msg = String.format("%s %d votes present for contest %s but a universe size of " + "%d specified in the assertion generation request. Throwing a RaireServiceException.", prefix, votes.size(), request.contestName, request.totalAuditableBallots); @@ -111,6 +121,13 @@ public RaireResultOrError generateAssertions(GenerateAssertionsRequest request) throw new RaireServiceException(msg, RaireErrorCode.INVALID_TOTAL_AUDITABLE_BALLOTS); } + if(votes.isEmpty()) { + final String msg = String.format("%s No votes present for contest %s.", prefix, + request.contestName); + logger.error(msg + " Throwing a RaireServiceException."); + throw new RaireServiceException(msg, RaireErrorCode.NO_VOTES_PRESENT); + } + logger.debug(String.format("%s Adding all extracted rankings to a consolidator to identify " + "unique rankings and their number.", prefix)); votes.forEach(consolidator::addVoteNames); diff --git a/src/main/java/au/org/democracydevelopers/raireservice/service/RaireServiceException.java b/src/main/java/au/org/democracydevelopers/raireservice/service/RaireServiceException.java index 190116b..4642117 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/service/RaireServiceException.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/service/RaireServiceException.java @@ -150,11 +150,16 @@ public enum RaireErrorCode { WRONG_CANDIDATE_NAMES, /** - * The user has request to retrieve assertions for a contest for which no assertions have + * The user has requested to retrieve assertions for a contest for which no assertions have * been generated. */ NO_ASSERTIONS_PRESENT, + /** + * The user has requested to generate assertions for a contest for which no votes are present. + */ + NO_VOTES_PRESENT, + // Internal errors (that the user can do nothing about) /** diff --git a/src/test/java/au/org/democracydevelopers/raireservice/controller/GenerateAssertionsAPIErrorTests.java b/src/test/java/au/org/democracydevelopers/raireservice/controller/GenerateAssertionsAPIErrorTests.java index aad3bff..4816c4f 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/controller/GenerateAssertionsAPIErrorTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/controller/GenerateAssertionsAPIErrorTests.java @@ -24,7 +24,9 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import au.org.democracydevelopers.raireservice.request.GenerateAssertionsRequest; import au.org.democracydevelopers.raireservice.testUtils; +import java.util.List; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -47,10 +49,11 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra /** * Tests for appropriate responses to bad requests to the generate-assertions endpoint. This class - * automatically fires up the RAIRE Microservice on a random port. Currently, we check for - * proper input validation. + * automatically fires up the RAIRE Microservice on a random port. These sorts of errors are _not_ + * supposed to happen - they indicate programming errors or problems with databases etc. + * Currently, we check for proper input validation and inconsistent input. * The list of tests is similar to GenerateAssertionsRequestTests.java, and also to - * GetAssertionsAPITests.java when the same test is relevant to both endpoints. + * GetAssertionsAPIErrorTests.java when the same test is relevant to both endpoints. * Contests which will be used for validity testing are * preloaded into the database using src/test/resources/data.sql. * Tests include: @@ -74,6 +77,8 @@ public class GenerateAssertionsAPIErrorTests { private final static String baseURL = "http://localhost:"; private final static String generateAssertionsEndpoint = "/raire/generate-assertions"; + private final static List aliceAndBob = List.of("Alice","Bob"); + @LocalServerPort private int port; @@ -132,17 +137,32 @@ public void generateAssertionsWithNonExistentContestIsAnError() { testUtils.log(logger, "generateAssertionsWithNonExistentContestIsAnError"); String url = baseURL + port + generateAssertionsEndpoint; - String requestAsJson = - "{\"timeLimitSeconds\":10.0,\"totalAuditableBallots\":100," - +"\"contestName\":\"NonExistentContest\",\"candidates\":[\"Alice\",\"Bob\"]}"; - - HttpEntity request = new HttpEntity<>(requestAsJson, httpHeaders); + GenerateAssertionsRequest request = new GenerateAssertionsRequest("NonExistentContest", + 100, 10, aliceAndBob); ResponseEntity response = restTemplate.postForEntity(url, request, String.class); assertTrue(response.getStatusCode().is4xxClientError()); assertTrue(StringUtils.containsIgnoreCase(response.getBody(), "No such contest")); } + /** + * The generateAssertions endpoint, called with a valid IRV contest for which no votes are present, + * returns a meaningful error. + */ + @Test + public void generateAssertionsFromNoVotesIsAnError() { + testUtils.log(logger, "generateAssertionsFromNoVotesIsAnError"); + String url = baseURL + port + generateAssertionsEndpoint; + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("No CVR Mayoral", 100, + 10, aliceAndBob); + + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + + assertTrue(response.getStatusCode().is5xxServerError()); + assertTrue(StringUtils.containsIgnoreCase(response.getBody(), "No votes present for contest")); + } + /** * The generateAssertions endpoint, called with a valid plurality contest, * returns a meaningful error. @@ -152,11 +172,8 @@ public void generateAssertionsWithPluralityContestIsAnError() { testUtils.log(logger, "generateAssertionsWithPluralityContestIsAnError"); String url = baseURL + port + generateAssertionsEndpoint; - String requestAsJson = - "{\"timeLimitSeconds\":10.0,\"totalAuditableBallots\":100," - +"\"contestName\":\"Valid Plurality Contest\",\"candidates\":[\"Alice\",\"Bob\"]}"; - - HttpEntity request = new HttpEntity<>(requestAsJson, httpHeaders); + GenerateAssertionsRequest request = new GenerateAssertionsRequest( + "Valid Plurality Contest", 100, 10, aliceAndBob); ResponseEntity response = restTemplate.postForEntity(url, request, String.class); assertTrue(response.getStatusCode().is4xxClientError()); @@ -172,11 +189,8 @@ public void generateAssertionsWithMixedIRVPluralityContestIsAnError() { testUtils.log(logger, "generateAssertionsWithMixedIRVPluralityContestIsAnError"); String url = baseURL + port + generateAssertionsEndpoint; - String requestAsJson = - "{\"timeLimitSeconds\":10.0,\"totalAuditableBallots\":100," - +"\"contestName\":\"Invalid Mixed Contest\",\"candidates\":[\"Alice\",\"Bob\"]}"; - - HttpEntity request = new HttpEntity<>(requestAsJson, httpHeaders); + GenerateAssertionsRequest request = new GenerateAssertionsRequest("Invalid Mixed Contest", + 100,10,aliceAndBob); ResponseEntity response = restTemplate.postForEntity(url, request, String.class); assertTrue(response.getStatusCode().is4xxClientError()); @@ -400,8 +414,6 @@ public void generateAssertionsWithNegativeAuditableBallotsIsAnError() { "Non-positive total auditable ballots")); } - - /** * The generateAssertions endpoint, called with null/missing time limit, returns a meaningful error. */ @@ -478,6 +490,8 @@ public void wrongCandidatesIsAnError() { assertTrue(response.getStatusCode().is5xxServerError()); assertEquals(WRONG_CANDIDATE_NAMES.toString(), response.getHeaders().getFirst("error_code")); + assertTrue(StringUtils.containsIgnoreCase(response.getBody(), + "was not on the list of candidates")); } } \ No newline at end of file diff --git a/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceErrorTests.java b/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceErrorTests.java new file mode 100644 index 0000000..3a59435 --- /dev/null +++ b/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceErrorTests.java @@ -0,0 +1,295 @@ +/* +Copyright 2024 Democracy Developers + +The Raire Service is designed to connect colorado-rla and its associated database to +the raire assertion generation engine (https://github.com/DemocracyDevelopers/raire-java). + +This file is part of raire-service. + +raire-service is free software: you can redistribute it and/or modify it under the terms +of the GNU Affero General Public License as published by the Free Software Foundation, either +version 3 of the License, or (at your option) any later version. + +raire-service is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with +raire-service. If not, see . +*/ + +package au.org.democracydevelopers.raireservice.service; + +import static au.org.democracydevelopers.raireservice.service.RaireServiceException.RaireErrorCode.WRONG_CANDIDATE_NAMES; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import au.org.democracydevelopers.raire.RaireError; +import au.org.democracydevelopers.raireservice.request.GenerateAssertionsRequest; +import au.org.democracydevelopers.raireservice.service.RaireServiceException.RaireErrorCode; +import au.org.democracydevelopers.raireservice.testUtils; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; + +/** + * Tests for appropriate responses to bad input to the generate assertions service. + * These sorts of errors are _not_ supposed to happen - they indicate programming errors or problems + * with databases etc. Currently, we check for invalid and inconsistent input. + * The list of tests is similar to GenerateAssertionsAPIErrorTests.java, when the same test is + * relevant to both services. + * Contests which will be used for validity testing are + * preloaded into the database using src/test/resources/data.sql. + * Tests include: + * - null, missing or whitespace contest name, + * - non-IRV contests, mixed IRV-plurality contests or contests not in the database, + * - null, missing or whitespace candidate names, + * - candidate names that are valid but do not include all the candidates mentioned in votes in the + * database, + * - missing, negative or zero values for numerical inputs (totalAuditableBallots and + * timeLimitSeconds). + */ +@ActiveProfiles("test-containers") +@SpringBootTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +public class GenerateAssertionsServiceErrorTests { + + private static final Logger logger = LoggerFactory.getLogger(GenerateAssertionsServiceErrorTests.class); + + @Autowired + private GenerateAssertionsService generateAssertionsService; + private final static List aliceAndBob = List.of("Alice","Bob"); + private final static List aliceAndBobAndCharlie = List.of("Alice","Bob","Charlie"); + private final static String BallinaOneVote = "Ballina One Vote Contest"; + + /** + * The generateAssertions service, called with a nonexistent contest, returns a meaningful error. + */ + @Test + public void generateAssertionsWithNonExistentContestIsAnError() { + testUtils.log(logger, "generateAssertionsWithNonExistentContestIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("NonExistentContest", 100, + 10, aliceAndBob); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INTERNAL_ERROR, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), + "does not exist or is not all IRV")); + } + + /** + * The generateAssertions service, called with a valid IRV contest for which no votes are present, + * returns a meaningful error. + */ + @Test + public void generateAssertionsFromNoVotesIsAnError() { + testUtils.log(logger, "generateAssertionsFromNoVotesIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("No CVR Mayoral", 100, + 10, aliceAndBob); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.NO_VOTES_PRESENT, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "No votes present for contest")); + } + + /** + * The generateAssertions service, called with a valid plurality contest, returns a meaningful + * error. + */ + @Test + public void generateAssertionsWithPluralityContestIsAnError() { + testUtils.log(logger, "generateAssertionsWithPluralityContestIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest( + "Valid Plurality Contest", 100, 10, aliceAndBob); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INTERNAL_ERROR, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "does not exist or is not all IRV")); + } + + /** + * The generateAssertions service, called with a mixed IRV and non-IRV contest, + * returns a meaningful error. + */ + @Test + public void generateAssertionsWithMixedIRVPluralityContestIsAnError() { + testUtils.log(logger, "generateAssertionsWithMixedIRVPluralityContestIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("Invalid Mixed Contest", + 100,10,aliceAndBob); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INTERNAL_ERROR, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "does not exist or is not all IRV")); + } + + /** + * The generateAssertions service, called with an empty contest name, returns a meaningful error. + */ + @Test + public void generateAssertionsWithEmptyContestNameIsAnError() { + testUtils.log(logger, "generateAssertionsWithEmptyContestNameIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("", + 100,10,aliceAndBob); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INTERNAL_ERROR, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "does not exist or is not all IRV")); + } + + /** + * The generateAssertions service, called with an all-whitespace contest name, returns a + * meaningful error. + */ + @Test + public void generateAssertionsWithWhitespaceContestNameIsAnError() { + testUtils.log(logger, "generateAssertionsWithWhitespaceContestNameIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest(" ", + 100, 10, aliceAndBob); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INTERNAL_ERROR, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "does not exist or is not all IRV")); + } + + /** + * The generateAssertions service, called with an empty candidate list, returns a meaningful error. + */ + @Test + public void generateAssertionsWithEmptyCandidateListIsAnError() { + testUtils.log(logger, "generateAssertionsWithEmptyCandidateListIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("Ballina One Vote Contest", + 100, 10, List.of()); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(WRONG_CANDIDATE_NAMES, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "not on the list of candidates")); + } + + /** + * The generateAssertions service, called with a whitespace candidate name, returns a meaningful + * error. + */ + @Test + public void generateAssertionsWithWhiteSpaceCandidateNameIsAnError() { + testUtils.log(logger, "generateAssertionsWithWhiteSpaceCandidateNameIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest(BallinaOneVote, + 100, 10, List.of("Alice", " ")); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(WRONG_CANDIDATE_NAMES, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "not on the list of candidates")); + } + + /** + * The generateAssertions service, called with zero total auditable ballots, returns a meaningful + * error. + */ + @Test + public void generateAssertionsWithZeroAuditableBallotsIsAnError() { + testUtils.log(logger, "generateAssertionsWithZeroAuditableBallotsIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest(BallinaOneVote, + 0, 10, aliceAndBobAndCharlie); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INVALID_TOTAL_AUDITABLE_BALLOTS, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "universe size")); + } + + /** + * The generateAssertions service, called with negative total auditable ballots, + * returns a meaningful error. + */ + @Test + public void generateAssertionsWithNegativeAuditableBallotsIsAnError() { + testUtils.log(logger, "generateAssertionsWithNegativeAuditableBallotsIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest(BallinaOneVote, + -10, 10, aliceAndBobAndCharlie); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(RaireErrorCode.INVALID_TOTAL_AUDITABLE_BALLOTS, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "universe size")); + } + + /** + * The generateAssertions service, called with zero time limit, returns a meaningful error. + */ + @Test + public void generateAssertionsWithZeroTimeLimitIsAnError() throws RaireServiceException { + testUtils.log(logger, "generateAssertionsWithZeroTimeLimitIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest(BallinaOneVote, + 100, 0, aliceAndBobAndCharlie); + + var response = generateAssertionsService.generateAssertions(request); + assertNull(response.Ok); + assertNotNull(response.Err); + assertInstanceOf(RaireError.InvalidTimeout.class, response.Err); + } + + /** + * The generateAssertions service, called with negative time limit, returns a meaningful error. + */ + @Test + public void generateAssertionsWithNegativeTimeLimitIsAnError() throws RaireServiceException { + testUtils.log(logger, "generateAssertionsWithNegativeTimeLimitIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest(BallinaOneVote, + 100, -50, aliceAndBobAndCharlie); + + var response = generateAssertionsService.generateAssertions(request); + assertNull(response.Ok); + assertNotNull(response.Err); + assertInstanceOf(RaireError.InvalidTimeout.class, response.Err); + } + + /** + * A GenerateAssertions request with a candidate list that is valid, but the votes in the database + * contain at least one candidate who is not in the expected candidate list. This is an error. + */ + @Test + public void wrongCandidatesIsAnError() { + testUtils.log(logger, "wrongCandidatesIsAnError"); + + GenerateAssertionsRequest request = new GenerateAssertionsRequest("Ballina One Vote Contest", + 100, 10, List.of("Alice","Bob","Chuan")); + + RaireServiceException ex = assertThrows(RaireServiceException.class, () -> + generateAssertionsService.generateAssertions(request)); + assertEquals(WRONG_CANDIDATE_NAMES, ex.errorCode); + assertTrue(StringUtils.containsIgnoreCase(ex.getMessage(), "not on the list of candidates")); + } +} \ No newline at end of file diff --git a/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceWickedTests.java b/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceWickedTests.java index 0927a84..555294b 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceWickedTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/service/GenerateAssertionsServiceWickedTests.java @@ -28,7 +28,6 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import au.org.democracydevelopers.raire.RaireError.TimeoutCheckingWinner; import au.org.democracydevelopers.raire.RaireError.TimeoutFindingAssertions; import au.org.democracydevelopers.raire.RaireSolution.RaireResultOrError; -import au.org.democracydevelopers.raireservice.persistence.repository.AssertionRepository; import au.org.democracydevelopers.raireservice.request.GenerateAssertionsRequest; import au.org.democracydevelopers.raireservice.testUtils; import java.util.List; @@ -65,9 +64,6 @@ public class GenerateAssertionsServiceWickedTests { private static final Logger logger = LoggerFactory.getLogger( GenerateAssertionsServiceWickedTests.class); - @Autowired - AssertionRepository assertionRepository; - @Autowired GenerateAssertionsService generateAssertionsService; diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 772d865..37b6034 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -92,3 +92,7 @@ INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) val INSERT INTO cast_vote_record (id, cvr_number, ballot_type, batch_id, county_id, imprinted_id, record_id, record_type, scanner_id) values (15, 15, 'Type 3', 1, 12, '15-1-4', 15, 'UPLOADED', 4); INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (15, 12, '["Alice"]', 999971, 22); INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (15, 12, '["Wendell","Liesel"]', 999972, 23); + +-- Some plurality votes for the Valid Plurality Contest. +INSERT INTO cast_vote_record (id, cvr_number, ballot_type, batch_id, county_id, imprinted_id, record_id, record_type, scanner_id) values (16, 16, 'Type 3', 1, 12, '16-1-4', 16, 'UPLOADED', 4); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (15, 10, '["Alice"]', 999995, 24);