Skip to content

Commit

Permalink
Test that assertion generation is allowed in the initial and PARTIAL_…
Browse files Browse the repository at this point in the history
…AUDIT_INFO_SET states, and not in any other states.
  • Loading branch information
vteague committed Aug 22, 2024
1 parent 06f5a20 commit 55f48d7
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public class GenerateAssertions extends AbstractAllIrvEndpoint {
/**
* Query specifier for contest name.
*/
private static final String CONTEST_NAME = "contestName";
public static final String CONTEST_NAME = "contestName";

/**
* Default time limit for assertion generation.
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,270 @@
/*
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.model.DoSDashboard;
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 assertionGenerationBlockedWhenInWrongASMStateOrComparisonAuditsPresent() {
testUtils.log(LOGGER, "assertionGenerationBlockedWhenInWrongASMStateOrComparisonAuditsPresent");

// 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 @@ -56,7 +56,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 @@ -59,7 +59,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
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,26 @@

import spark.Request;

import java.util.Map;
import java.util.Set;

/**
* A class that can behave like a Spark request, for testing endpoints.
* Note this does _not_ (yet) implement all the functions you might need - it contains only the ones
* that I noticed being used in colorado-rla.
* https://javadoc.io/doc/com.sparkjava/spark-core/2.5.4/spark/Request.html
* <a href="https://javadoc.io/doc/com.sparkjava/spark-core/2.5.4/spark/Request.html">...</a>
*/
public class SparkRequestStub extends Request {

private final String _body;
private final Set<String> _queryParams;
private final Map<String,String> _queryParams;

/**
* Constructor, for use in testing.
* @param body The body of the request.
* @param queryParams The http query parameters.
*/
public SparkRequestStub(final String body, final Set<String> queryParams)
public SparkRequestStub(final String body, final Map<String,String> queryParams)
{
super();

Expand Down Expand Up @@ -69,5 +70,12 @@ public String body()
* Get the query parameters that were set in the constructor.
* @return the query parameters.
*/
public Set<String> queryParams() { return _queryParams;}
public Set<String> queryParams() {return _queryParams.keySet();}

/**
* Get the value of a specific query parameter.
* @param key
* @return
*/
public String queryParams(String key) { return _queryParams.get(key);}
}

0 comments on commit 55f48d7

Please sign in to comment.