diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java index dfd0e5f2..15bb126a 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java @@ -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 generateAllAssertions(final List 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 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; @@ -223,7 +223,7 @@ protected List generateAllAssertions(final List 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)); @@ -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. */ @@ -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. */ @@ -362,7 +360,7 @@ 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())); } @@ -370,7 +368,7 @@ private boolean assertionGenerationAllowed(final Request the_request) { 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())); diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/ACVRUploadTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/ACVRUploadTests.java index 8abc6c55..70ad8eea 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/ACVRUploadTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/ACVRUploadTests.java @@ -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; @@ -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()); uploadEndpoint.before(request, response); // Before the test, there should be 10 UPLOADED and zero AUDITOR_ENTERED cvrs. @@ -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 expectedInterpretedChoices, final int expectedACVRs) { - final Request request = new SparkRequestStub(CvrAsJson, new HashSet<>()); + final Request request = new SparkRequestStub(CvrAsJson, new HashMap()); uploadEndpoint.endpointBody(request, response); // There should now be expectedACVRs audit cvrs. @@ -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 errorBody = ""; try { diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/EstimateSampleSizesTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/EstimateSampleSizesTests.java index 9798925c..d70100bf 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/EstimateSampleSizesTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/EstimateSampleSizesTests.java @@ -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()); endpoint.before(request, response); // // First test: hit the endpoint before defining the risk limit. Should throw an error. diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java new file mode 100644 index 00000000..816a2d4f --- /dev/null +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java @@ -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 . +*/ + +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 ... + */ +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
mockedMain = Mockito.mockStatic(Main.class); + MockedStatic 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 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)); + } + } + } +} \ No newline at end of file diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java index 7e9e2f8e..e8c6ffb2 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java @@ -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 diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GetAssertionsTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GetAssertionsTests.java index b128ca00..fda1f5b9 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GetAssertionsTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GetAssertionsTests.java @@ -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). diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/SparkRequestStub.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/SparkRequestStub.java index 461a75e3..4beac510 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/SparkRequestStub.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/SparkRequestStub.java @@ -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 + * ... */ public class SparkRequestStub extends Request { private final String _body; - private final Set _queryParams; + private final Map _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 queryParams) + public SparkRequestStub(final String body, final Map queryParams) { super(); @@ -69,5 +70,12 @@ public String body() * Get the query parameters that were set in the constructor. * @return the query parameters. */ - public Set queryParams() { return _queryParams;} + public Set queryParams() {return _queryParams.keySet();} + + /** + * Get the value of a specific query parameter. + * @param key + * @return + */ + public String queryParams(String key) { return _queryParams.get(key);} } diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java index 5384fcf1..c95fe040 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java @@ -22,12 +22,10 @@ package au.org.democracydevelopers.corla.util; import au.org.democracydevelopers.corla.model.ContestType; import org.apache.log4j.Logger; -import us.freeandfair.corla.model.Choice; -import us.freeandfair.corla.model.Contest; -import us.freeandfair.corla.model.ContestResult; -import us.freeandfair.corla.model.County; +import us.freeandfair.corla.model.*; -import java.util.List; +import java.math.BigDecimal; +import java.util.*; /** * This class contains utilities and default data for use in testing.