Skip to content

Commit

Permalink
Merge pull request #187 from DemocracyDevelopers/block-assertion-rege…
Browse files Browse the repository at this point in the history
…neration-after-audit-starts

Block assertion regeneration after audit starts
  • Loading branch information
vteague authored Aug 29, 2024
2 parents 9fc6362 + 895972d commit 3bd231c
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,14 @@ public String endpointBody(final Request the_request, final Response the_respons
* @param raireUrl the url where the raire-service is running.
*/
protected List<GenerateAssertionsResponse> generateAllAssertions(final List<ContestResult> IRVContestResults,
final double timeLimitSeconds, final String raireUrl) {
final double timeLimitSeconds, final String raireUrl) {
final String prefix = "[generateAllAssertions]";
LOGGER.debug(String.format("%s %s.", prefix, "Generating assertions for all IRV contests"));

// Iterate through all IRV Contests, sending a request to the raire-service for each one's assertions
final List<GenerateAssertionsResponse> responseData = IRVContestResults.stream().map(
r -> generateAssertionsUpdateWinners(IRVContestResults, r.getContestName(), timeLimitSeconds, raireUrl)
).toList();
).toList();

LOGGER.debug(String.format("%s %s.", prefix, "Completed assertion generation for all IRV contests"));
return responseData;
Expand All @@ -223,7 +223,7 @@ protected List<GenerateAssertionsResponse> generateAllAssertions(final List<Cont
* winner but may instead be UNKNOWN_WINNER and an error message.
*/
protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List<ContestResult> IRVContestResults,
final String contestName, final double timeLimitSeconds, final String raireUrl) {
final String contestName, final double timeLimitSeconds, final String raireUrl) {
final String prefix = "[generateAssertionsUpdateWinners]";
LOGGER.debug(String.format("%s %s %s.", prefix, "Generating assertions for contest ", contestName));

Expand Down Expand Up @@ -322,7 +322,6 @@ protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List<
* Validates the parameters of a request. For this endpoint, the query parameters are optional,
* but if the contest is present it should be non-null, and if a time limit is present it should
* be positive.
*
* @param the_request the request sent to the endpoint.
* @return true if the request's query parameters are valid.
*/
Expand All @@ -349,7 +348,6 @@ protected boolean validateParameters(final Request the_request) {
* PARTIAL_AUDIT_INFO_SET state, otherwise it is not.
* This function also checks that there are no ComparisonAudits in the database, though this should
* always be true in the required states.
*
* @param the_request the endpoint request.
* @return true if we are in the right state and there are no ComparisonAudits in the database.
*/
Expand All @@ -362,15 +360,15 @@ private boolean assertionGenerationAllowed(final Request the_request) {
// Check that we're in either the initial state or the PARTIAL_AUDIT_INFO_SET state.
final boolean allowedState
= (dashboardASM.isInInitialState() || dashboardASM.currentState().equals(PARTIAL_AUDIT_INFO_SET));
if (!allowedState) {
if(!allowedState) {
LOGGER.debug(String.format("%s %s %s from illegal state %s.", prefix, errorMsg,
the_request.queryParams(CONTEST_NAME), dashboardASM.currentState()));
}

final boolean noComparisonAudits = ComparisonAuditQueries.count() == 0;

// Check that there are no ComparisonAudits in the database (which should not happen given the state).
if (!noComparisonAudits) {
if(!noComparisonAudits) {
LOGGER.debug(String.format("%s %s %s %s with %d ComparisonAudits in the database.", prefix, errorMsg,
the_request.queryParams(CONTEST_NAME), dashboardASM.currentState().toString(),
ComparisonAuditQueries.count()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import spark.Request;

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

Expand Down Expand Up @@ -425,7 +425,7 @@ void testACVRUploadAndStorage() {
startTheRound();

// We seem to need a dummy request to run before.
final Request request = new SparkRequestStub("", new HashSet<>());
final Request request = new SparkRequestStub("", new HashMap<String,String>());
uploadEndpoint.before(request, response);

// Before the test, there should be 10 UPLOADED and zero AUDITOR_ENTERED cvrs.
Expand Down Expand Up @@ -523,7 +523,7 @@ private void testIRVBallotInterpretations(final long CvrNum, final String imprin
*/
private void testSuccessResponse(final long CvrId, final String expectedImprintedId, final String CvrAsJson,
final List<String> expectedInterpretedChoices, final int expectedACVRs) {
final Request request = new SparkRequestStub(CvrAsJson, new HashSet<>());
final Request request = new SparkRequestStub(CvrAsJson, new HashMap<String,String>());
uploadEndpoint.endpointBody(request, response);

// There should now be expectedACVRs audit cvrs.
Expand Down Expand Up @@ -589,7 +589,7 @@ private void testPreviousAreReaudited(final long CvrId, final String expectedImp
* @param expectedError The expected error message.
*/
private void testErrorResponseAndNoMatchingCvr(final long CvrId, final String CvrAsJson, final String expectedError) {
final Request request = new SparkRequestStub(CvrAsJson, new HashSet<>());
final Request request = new SparkRequestStub(CvrAsJson, new HashMap<String,String>());
String errorBody = "";

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ void basicEstimatedSampleSizesPluralityAndIRV() {
mockedMain.when(Main::authentication).thenReturn(auth);

// We seem to need a dummy request to run before.
final Request request = new SparkRequestStub("", new HashSet<>());
final Request request = new SparkRequestStub("", new HashMap<String,String>());
endpoint.before(request, response);

// // First test: hit the endpoint before defining the risk limit. Should throw an error.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
Democracy Developers IRV extensions to colorado-rla.
@copyright 2024 Colorado Department of State
These IRV extensions are designed to connect to a running instance of the raire
service (https://github.com/DemocracyDevelopers/raire-service), in order to
generate assertions that can be audited using colorado-rla.
The colorado-rla IRV extensions are 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.
The colorado-rla IRV extensions are 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 <https://www.gnu.org/licenses/>.
*/

package au.org.democracydevelopers.corla.endpoint;

import au.org.democracydevelopers.corla.communication.requestToRaire.GenerateAssertionsRequest;
import au.org.democracydevelopers.corla.communication.responseFromRaire.GenerateAssertionsResponse;
import au.org.democracydevelopers.corla.util.SparkRequestStub;
import au.org.democracydevelopers.corla.util.TestClassWithAuth;
import au.org.democracydevelopers.corla.util.testUtils;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.google.gson.Gson;
import org.apache.http.HttpStatus;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.ext.ScriptUtils;
import org.testcontainers.jdbc.JdbcDatabaseDelegate;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import spark.HaltException;
import spark.Request;
import us.freeandfair.corla.Main;
import us.freeandfair.corla.asm.*;
import us.freeandfair.corla.controller.ContestCounter;
import us.freeandfair.corla.model.AuditReason;
import us.freeandfair.corla.model.Choice;
import us.freeandfair.corla.model.ContestResult;
import us.freeandfair.corla.persistence.Persistence;

import javax.transaction.Transactional;
import java.util.*;

import static au.org.democracydevelopers.corla.endpoint.AbstractAllIrvEndpoint.RAIRE_URL;
import static au.org.democracydevelopers.corla.endpoint.GenerateAssertions.CONTEST_NAME;
import static au.org.democracydevelopers.corla.util.testUtils.*;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.testng.Assert.*;
import static us.freeandfair.corla.asm.ASMEvent.DoSDashboardEvent.*;
import static us.freeandfair.corla.endpoint.Endpoint.AuthorizationType.STATE;

/**
* Test the GetAssertions endpoint via the API.
* This currently tests that the assertion generation request is accepted and blocked in the right
* circumstances.
* TODO This really isn't a completely comprehensive set of tests yet.
* See <a href="https://github.com/DemocracyDevelopers/colorado-rla/issues/125">...</a>
*/
public class GenerateAssertionsAPITests extends TestClassWithAuth {

/**
* Class-wide logger.
*/
private static final Logger LOGGER = LogManager.getLogger(GenerateAssertionsAPITests.class);

/**
* Container for the mock-up database.
*/
private final static PostgreSQLContainer<?> postgres = createTestContainer();

/**
* The Generate Assertions endpoint.
*/
private final GenerateAssertions endpoint = new GenerateAssertions();

/**
* Mock response for tinyExample1 contest
*/
private final static GenerateAssertionsResponse tinyIRVResponse
= new GenerateAssertionsResponse(tinyIRV, true, false);

/**
* Request for tinyExample1 contest
*/
private final static GenerateAssertionsRequest tinyIRVRequest
= new GenerateAssertionsRequest(tinyIRV, tinyIRVCount, 5,
tinyIRVCandidates.stream().map(Choice::name).toList());

/**
* Raire endpoint for getting assertions.
*/
private final String raireGenerateAssertionsEndpoint = "/raire/generate-assertions";

/**
* Wiremock server for mocking the raire service.
* (Note the default of 8080 clashes with the raire-service default, so this is different.)
*/
private final WireMockServer wireMockRaireServer = new WireMockServer(8110);

/**
* Base url - this is set up to use the wiremock server, but could be set here to wherever you have the
* raire-service running to test with that directly.
*/
private static String baseUrl;

/**
* The Properties that will be mocked in Main, specifically for the RAIRE_URL.
*/
private static final Properties mockProperties = new Properties();

/**
* GSON for json interpretation.
*/
private final static Gson gson = new Gson();

/**
* Database init.
*/
@BeforeClass
public static void beforeAll() {
postgres.start();
Persistence.setProperties(createHibernateProperties(postgres));

var s = Persistence.openSession();
s.beginTransaction();

final var containerDelegate = new JdbcDatabaseDelegate(postgres, "");
// Used to initialize the database, particularly to set the ASM state to the DOS_INITIAL_STATE.
ScriptUtils.runInitScript(containerDelegate, "SQL/co-counties.sql");
}

/**
* Initialise mocked objects prior to the first test.
*/
@BeforeClass
public void initMocks() {

// Mock successful auth as a state admin.
MockitoAnnotations.openMocks(this);
mockAuth("State test 1", 1L, STATE);

tinyIRVContestResult.setAuditReason(AuditReason.COUNTY_WIDE_CONTEST);
tinyIRVContestResult.setBallotCount((long) tinyIRVCount);
tinyIRVContestResult.setWinners(Set.of("Alice"));
tinyIRVContestResult.addContests(Set.of(tinyIRVExample));

// Default raire server. You can instead run the real raire service and set baseUrl accordingly.
// Of course you have to have appropriate contests in the database.
wireMockRaireServer.start();
baseUrl = wireMockRaireServer.baseUrl();
configureFor("localhost", wireMockRaireServer.port());

// Mock the above-initialized URL for the RAIRE_URL property in Main.
mockProperties.setProperty(RAIRE_URL, baseUrl);

// Mock a proper response to the IRV TinyExample1 contest.
stubFor(post(urlEqualTo(raireGenerateAssertionsEndpoint))
.withRequestBody(equalToJson(gson.toJson(tinyIRVRequest)))
.willReturn(aResponse()
.withStatus(HttpStatus.SC_OK)
.withHeader("Content-Type", "application/json")
.withBody(gson.toJson(tinyIRVResponse))));
}

@AfterClass
public void closeMocks() {
wireMockRaireServer.stop();
}

/**
* Simple test that the assertion generation request is made when in
* ASM initial state and ASM PARTIAL_AUDIT_INFO_SET states, and not in later states.
*/
@Test
@Transactional
void assertionGenerationBlockedWhenInWrongASMState() {
testUtils.log(LOGGER, "assertionGenerationBlockedWhenInWrongASMState");

// Mock the main class; mock its auth as the mocked state admin auth.
try (MockedStatic<Main> mockedMain = Mockito.mockStatic(Main.class);
MockedStatic<ContestCounter> mockedCounter = Mockito.mockStatic(ContestCounter.class)) {

// Mock auth.
mockedMain.when(Main::authentication).thenReturn(auth);

// Mock properties, particularly the RAIRE URL.
mockedMain.when(Main::properties).thenReturn(mockProperties);

// Mock non-empty contest response (one IRV contest).
List<ContestResult> mockedContestResults = List.of(tinyIRVContestResult);
mockedCounter.when(ContestCounter::countAllContests).thenReturn(mockedContestResults);

// We seem to need a dummy request to run before.
final Request request = new SparkRequestStub("", Map.of(CONTEST_NAME, tinyIRV));
endpoint.before(request, response);

// First test: check that the GenerateAssertions endpoint works when in the initial state
// (which is set up initially in the database).

DoSDashboardASM doSDashboardASM = ASMUtilities.asmFor(DoSDashboardASM.class, DoSDashboardASM.IDENTITY);
assertTrue(doSDashboardASM.isInInitialState());

String errorBody = "";
try {
endpoint.endpointBody(request, response);
endpoint.after(request, response);
} catch (HaltException e) {
errorBody = "Error: " + e.body();
}
// There should be no error when the ASM is in the initial state.
assertEquals(errorBody, "");

// Now transition to PARTIAL_AUDIT_INFO_SET
doSDashboardASM.stepEvent(PARTIAL_AUDIT_INFO_EVENT);
ASMUtilities.save(doSDashboardASM);

errorBody = "";
try {
endpoint.endpointBody(request, response);
endpoint.after(request, response);
} catch (HaltException e) {
errorBody = "Error: " + e.body();
}
// There should be still be no error when the ASM is in the PARTIAL_AUDIT_INFO_SET state.
assertEquals(errorBody, "");

final String expectedError = "Assertion generation not allowed in current state.";

// Now transition to COMPLETE_AUDIT_INFO_SET and other, subsequent, states, in which
// assertion generation is expected to throw an error. Check that it does.
for (ASMEvent.DoSDashboardEvent event : List.of(
COMPLETE_AUDIT_INFO_EVENT,
DOS_START_ROUND_EVENT,
AUDIT_EVENT,
DOS_ROUND_COMPLETE_EVENT,
// DoS can start another round after the earlier round is complete.
DOS_START_ROUND_EVENT,
DOS_COUNTY_AUDIT_COMPLETE_EVENT,
DOS_AUDIT_COMPLETE_EVENT,
PUBLISH_AUDIT_REPORT_EVENT
)) {
doSDashboardASM.stepEvent(event);
ASMUtilities.save(doSDashboardASM);

// In this state, assertion generation attempts should throw an error.
errorBody = "";
try {
endpoint.endpointBody(request, response);
endpoint.after(request, response);
} catch (HaltException e) {
errorBody = "Error: " + e.body();
}
assertTrue(errorBody.contains(expectedError));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
* the assertions.
* Includes tests that AbstractAllIrvEndpoint::getIRVContestResults returns the correct values and
* throws the correct exceptions.
* TODO This really isn't a completely comprehensive set of tests yet. We also need:
* TODO VT: This really isn't a completely comprehensive set of tests yet. We also need:
* - API testing
* - Testing that the service throws appropriate exceptions if the raire service connection isn't set up properly.
* - More thorough tests of assertion generation for known cases, e.g. examples from NSW and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
* the assertions.
* Includes tests that AbstractAllIrvEndpoint::getIRVContestResults returns the correct values and
* throws the correct exceptions.
* TODO This really isn't a completely comprehensive set of tests yet. We also need:
* TODO VT: This really isn't a completely comprehensive set of tests yet. We also need:
* - API testing
* - Testing for retrieving the data from the zip.
* - More comprehensive testing of filename sanitization (from contest names).
Expand Down
Loading

0 comments on commit 3bd231c

Please sign in to comment.