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

162 reaudit step not working #169

Merged
merged 6 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -232,13 +232,14 @@ public static boolean reaudit(final CountyDashboard cdb,
final String comment) {

LOGGER.info("[reaudit] cvr: " + cvr.toString());
final CVRAuditInfo cai =
Persistence.getByID(cvr.id(), CVRAuditInfo.class);
final CastVoteRecord oldAcvr = cai.acvr();
if (null == oldAcvr) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could perhaps use DD or Democracy Developers rather than individual initials?

// DemocracyDevelopers: If this cvr has not been audited before, cai will be null.
final CVRAuditInfo cai = Persistence.getByID(cvr.id(), CVRAuditInfo.class);
if (cai == null || cai.acvr() == null) {
LOGGER.error("can't reaudit a cvr that hasn't been audited");
return false;
}
final CastVoteRecord oldAcvr = cai.acvr();

final Integer former_count = unaudit(cdb, cai);
LOGGER.debug("[reaudit] former_count: " + former_count.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,11 +577,17 @@ public static Long maxRevision(final CastVoteRecord cvr) {
q.setString("imprintedId", cvr.imprintedID());

try {
return (Long) q.getSingleResult();
// Make sure not to return null, which causes errors later. 0L is the correct return value if
// there are no prior revisions in the database.
// Some documentation says that getSingleResult() never returns null, but it definitely does.
final Long result = (Long) q.getSingleResult();
return result == null ? 0L : result;
} catch (final PersistenceException e) {
// the DB had a problem!
// TODO: Technically this should probably be an error code?
// TODO: Otherwise there's no way to discern this from a CVR with no revisions?
// VT: Agree. Since 0L is used for "valid; no prior revisions", suggest using a different value
// here (-1?) or throwing an exception.
return 0L;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@
import static au.org.democracydevelopers.corla.util.testUtils.tinyIRV;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertFalse;

import spark.Request;

import javax.transaction.Transactional;
import java.util.HashSet;
import java.util.List;
import java.util.OptionalLong;

import static us.freeandfair.corla.endpoint.Endpoint.AuthorizationType.COUNTY;
import static us.freeandfair.corla.model.CastVoteRecord.RecordType.AUDITOR_ENTERED;
import static us.freeandfair.corla.model.CastVoteRecord.RecordType.REAUDITED;

/**
* Test upload of IRV audit cvrs. Includes tests of both valid and invalid IRV CVRs, and tests that
* the interpreted ballots are properly stored in the database.
* This makes use of TestClassWithAuth to mock authentication as Adams County.
* TODO test reaudits, both IRV and plurality (can they be mixed in one CVR?)
* See <a href="https://github.com/orgs/DemocracyDevelopers/projects/1/views/1?pane=issue&itemId=72434202">...</a>
*/
public class ACVRUploadTests extends TestClassWithAuth {
Expand Down Expand Up @@ -305,6 +307,71 @@ public class ACVRUploadTests extends TestClassWithAuth {
" \"auditBoardIndex\": -1" +
"}";

/**
* 8 & 9. Reaudit of valid IRV vote, for CVR ID 240509; imprinted ID 1-1-1.
*/
private static final String validIRVReauditAsJson = "{" +
" \"cvr_id\": 240509," +
" \"audit_cvr\": {" +
" \"record_type\": \"AUDITOR_ENTERED\"," +
" \"county_id\": 1," +
" \"cvr_number\": 1," +
" \"sequence_number\": 1," +
" \"scanner_id\": 1," +
" \"batch_id\": \"1\"," +
" \"record_id\": 1," +
" \"imprinted_id\": \"1-1-1\"," +
" \"uri\": \"acvr:1:1-1-1\"," +
" \"ballot_type\": \"Ballot 1 - Type 1\"," +
" \"contest_info\": [" +
" {" +
" \"contest\": 240503," +
" \"comment\": \"A comment\"," +
" \"consensus\": \"YES\"," +
" \"choices\": [" +
" \"Alice(1)\"," +
" \"Chuan(2)\"" +
" ]" +
" }" +
" ]" +
" }," +
" \"reaudit\": true," +
" \"comment\": \"\"," +
" \"auditBoardIndex\": -1" +
"}";

/**
* 10. Attempt to reaudit a vote that has not yet been audited - #7, the IRV vote with typos.
* Should cause an error.
*/
private static final String IRVReauditNoPriorUpload = "{" +
" \"cvr_id\": 240515," +
" \"audit_cvr\": {" +
" \"record_type\": \"AUDITOR_ENTERED\"," +
" \"county_id\": 1," +
" \"cvr_number\": 7," +
" \"sequence_number\": 1," +
" \"scanner_id\": 1," +
" \"batch_id\": \"1\"," +
" \"record_id\": 7," +
" \"imprinted_id\": \"1-1-7\"," +
" \"uri\": \"acvr:1:1-1-7\"," +
" \"ballot_type\": \"Ballot 1 - Type 1\"," +
" \"contest_info\": [" +
" {" +
" \"contest\": 240503," +
" \"comment\": \"A comment\"," +
" \"consensus\": \"YES\"," +
" \"choices\": [" +
" ]" +
" }" +
" ]" +
" }," +
" \"reaudit\": true," +
" \"comment\": \"\"," +
" \"auditBoardIndex\": -1" +
"}";

/**
* Database init.
*/
Expand Down Expand Up @@ -338,10 +405,13 @@ public void initMocks() {
* 3. a blank IRV vote,
* 4. a vote with non-IRV choices ("Alice" instead of "Alice(1)"),
* 5. a vote with invalid choices (names not in the list of choices for the contest),
* 6. a vote that doesn't properly correspond to the IDs it should have, and
* 7. an unparseable vote (typos in json data).
* We check that it is accepted and that the right records for CVR and CVRContestInfo are
* stored in the database.
* 6. a vote that doesn't properly correspond to the IDs it should have,
* 7. an unparseable vote (typos in json data),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "market" -> "marked"

* 8. a reaudit (which checks that the superseded upload is marked REAUDITED), and
* 9. a second reaudit with the same data (which checks that the previous reaudit is also now
* marked REAUDITED and that there are now two revisions).
* We check that it is accepted (or rejected if it should be) and that the right records for CVR
* and CVRContestInfo are stored in the database.
*/
@Test
@Transactional
Expand Down Expand Up @@ -386,19 +456,37 @@ void testACVRUploadAndStorage() {
// Expected error messages for malformed upload cvrs.
String malformedACVRMsg = "malformed audit CVR upload";

testErrorResponse(240512L, pluralityIRVAsJson, malformedACVRMsg);
testErrorResponseAndNoMatchingCvr(240512L, pluralityIRVAsJson, malformedACVRMsg);

// Fifth test: upload a vote with IRV choices that are not among the valid candidates. This
// should cause an error.
testErrorResponse(240513L, wrongCandidateNamesIRVAsJson, malformedACVRMsg);
testErrorResponseAndNoMatchingCvr(240513L, wrongCandidateNamesIRVAsJson, malformedACVRMsg);

// Sixth test: upload a vote with IDs that do not correspond properly to the expected CVR.
// This should cause an error.
testErrorResponse(240514L, IRVWithInconsistentIDsAsJson, malformedACVRMsg);
testErrorResponseAndNoMatchingCvr(240514L, IRVWithInconsistentIDsAsJson, malformedACVRMsg);

// Seventh test: upload a vote that has typos preventing json deserialization. This should
// cause an error.
testErrorResponse(240515L, IRVJsonDeserializationFail, malformedACVRMsg);
testErrorResponseAndNoMatchingCvr(240515L, IRVJsonDeserializationFail, malformedACVRMsg);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reaudited ballot

// Eighth test: upload a reaudited ballot.
// Check that the new data successfully replaces the prior upload.
testSuccessResponse(240509L, "1-1-1", validIRVReauditAsJson, List.of("Alice","Chuan"), 3);
// Test that the previous upload, with a vote for Bob and Chuan (validIRVAsJson) is now tagged
// as REAUDITED.
testPreviousAreReaudited(240509L, "1-1-1", List.of("Bob","Chuan"), 1, 1);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"same ballot being reaudited" ?

// Ninth test: upload the same ballot being reaudited, again.
// Check that the new data is identical (it replaces it, but it's the same).
testSuccessResponse(240509L, "1-1-1", validIRVReauditAsJson, List.of("Alice","Chuan"), 3);
// Test that the previous upload (from test 8), with a vote for Alice and Chuan
// (validIRVReauditAsJson) is now tagged as REAUDITED.
// There should be two reaudited ballots now, and the max revision should be 2.
testPreviousAreReaudited(240509L, "1-1-1", List.of("Alice","Chuan"), 2, 2);

// Tenth test: try to "re-"audit something that has not previously been successfully audited. Should throw an error.
testErrorResponseAndNoMatchingCvr(240515L, IRVReauditNoPriorUpload, "CVR has not previously been audited");
}
}

Expand Down Expand Up @@ -427,14 +515,14 @@ private void testIRVBallotInterpretations(final long CvrNum, final String imprin

/**
* Test expected consequences of successful ACVR upload.
* @param CvrId The CVR number (last number of the imprinted ID).
* @param CvrId The CVR ID (of the original UPLOADED CVR).
* @param expectedImprintedId The imprinted id, scanner-batch-record.
* @param CvrAsJson The upload cvr, as a json string.
* @param expectedInterpretedChoices The expected valid interpretation, which should be stored.
* @param expectedACVRs The number of audit CVRs expected in total.
*/
private void testSuccessResponse(final long CvrId, final String expectedImprintedId, final String CvrAsJson,
final List<String> expectedInterpretedChoices, final int expectedACVRs) {
final List<String> expectedInterpretedChoices, final int expectedACVRs) {
final Request request = new SparkRequestStub(CvrAsJson, new HashSet<>());
uploadEndpoint.endpointBody(request, response);

Expand All @@ -443,7 +531,7 @@ private void testSuccessResponse(final long CvrId, final String expectedImprinte
AUDITOR_ENTERED).toList();
assertEquals(acvrs.size(), expectedACVRs);

// There should now be an ACVR with matching cvrId.
// There should now be an AUDITOR_ENTERED ACVR with matching cvrId.
final CastVoteRecord acvr = acvrs.stream().filter(a -> a.getCvrId() == CvrId).findFirst().orElseThrow();
assertEquals(acvr.recordType(), AUDITOR_ENTERED);
// Check that we have the right record: CvrId and Imprinted ID should match.
Expand All @@ -456,14 +544,51 @@ private void testSuccessResponse(final long CvrId, final String expectedImprinte
assertTrue(testUtils.equalStringLists(choices, expectedInterpretedChoices));
}

/**
* Check that some previously-uploaded ballot has been tagged as "REAUDITED". This happens when a new ballot with the
* same CvrId is uploaded with "reaudit = true". The idea is that the most recent REAUDIT is always simply
* of type AUDITOR_ENTERED, but all _previously_ submitted versions of the same ballot are tagged as REAUDITED - these
* are then omitted from discrepancy and risk calculations, because only the most recent upload counts.
* @param CvrId The CVR ID (of the original UPLOADED CVR).
* @param expectedImprintedId The imprinted id, scanner-batch-record.
* @param expectedInterpretedChoices The expected valid interpretation, which should be stored.
* @param expectedReauditedBallots The number of expected REAUDITED ballots with this CvrId.
* @param expectedMaxRevision The revision of the old audit cvr (null for the first audit ballot; 1L for the
* first reaudit, then incrementing by 1 for subsequent reaudits.
*/
private void testPreviousAreReaudited(final long CvrId, final String expectedImprintedId,
final List<String> expectedInterpretedChoices, final long expectedReauditedBallots, final long expectedMaxRevision) {

// Check for the expected number of Reaudited ballots of the required CvrId.
final List<CastVoteRecord> acvrs = CastVoteRecordQueries.getMatching(1L, REAUDITED).filter(a -> a.getCvrId() == CvrId).toList();
assertEquals(acvrs.size(), expectedReauditedBallots);

// Check that we have the right records: Imprinted ID should match.
assertTrue(acvrs.stream().allMatch(cvr -> cvr.imprintedID().equals(expectedImprintedId)));

// Find the maximum revision; check that it matches the expected maximum revision (this should match the number of
// reaudits).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor point: can maxRevision or maxRevisionAcvrs be final?

assertFalse(acvrs.isEmpty());
final OptionalLong maxRevision = acvrs.stream().mapToLong(CastVoteRecord::getRevision).max();
assertEquals(maxRevision, OptionalLong.of(expectedMaxRevision));

// Check that there is one record with the expected revision and it has the expected vote choices.
final List<CastVoteRecord> maxRevisionAcvrs
= acvrs.stream().filter(a -> a.getRevision() == expectedMaxRevision).toList();
assertEquals(maxRevisionAcvrs.size(), 1);
assertTrue(maxRevisionAcvrs.get(0).contestInfoForContestResult(tinyIRV).isPresent());
final List<String> choices = maxRevisionAcvrs.get(0).contestInfoForContestResult(tinyIRV).get().choices();
assertTrue(testUtils.equalStringLists(choices, expectedInterpretedChoices));
}

/**
* Test that submitting the given ACVR to the endpoint produces the given error, and that there are no corresponding
* ACVRs stored in the database afterward.
* @param CvrId The CVR ID (of the original UPLOADED CVR).
* @param CvrAsJson The upload cvr, as a json string.
* @param expectedError The expected error message.
*/
private void testErrorResponse(final long CvrId, final String CvrAsJson, final String expectedError) {
private void testErrorResponseAndNoMatchingCvr(final long CvrId, final String CvrAsJson, final String expectedError) {
final Request request = new SparkRequestStub(CvrAsJson, new HashSet<>());
String errorBody = "";

Expand Down
Loading