From c0c199100e21dee0f15e0c758988e8194e25c5f5 Mon Sep 17 00:00:00 2001 From: vteague <vanessafromgit@thinkingcybersecurity.com> Date: Fri, 28 Jun 2024 10:53:37 +1000 Subject: [PATCH] Almost-complete tests including mocking of invalid responses from the raire service. --- .../corla/endpoint/GenerateAssertions.java | 43 ++++++-- .../endpoint/GenerateAssertionsTests.java | 99 +++++++++++++++---- .../corla/util/testUtils.java | 19 +++- 3 files changed, 131 insertions(+), 30 deletions(-) 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 199bd391..b2bbb8cb 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 @@ -127,11 +127,13 @@ public String endpointBody(final Request the_request, final Response the_respons // Get all the IRV contest results. final List<ContestResult> IRVContestResults = AbstractAllIrvEndpoint.getIRVContestResults(); - // try { if (contestName.isBlank()) { // No contest was requested - generate for all. + responseData = generateAllAssertions(IRVContestResults, timeLimitSeconds, raireUrl); } else { + // Generate for the specific contest requested. + responseData = List.of(generateAssertionsUpdateWinners(IRVContestResults, contestName, timeLimitSeconds, raireUrl)); } @@ -155,6 +157,7 @@ public String endpointBody(final Request the_request, final Response the_respons protected List<GenerateAssertionsResponseWithErrors> generateAllAssertions(List<ContestResult> IRVContestResults, double timeLimitSeconds, String raireUrl) { final String prefix = "[generateAllAssertions]"; + LOGGER.debug(String.format("%s %s.", prefix, "Generating assertions for all IRV contests.")); final List<GenerateAssertionsResponseWithErrors> responseData = new ArrayList<>(); @@ -164,12 +167,14 @@ protected List<GenerateAssertionsResponseWithErrors> generateAllAssertions(List< responseData.add(response); } + LOGGER.debug(String.format("%s %s.", prefix, "Completed assertion generation for all IRV contests.")); return responseData; } protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(List<ContestResult> IRVContestResults, String contestName, double timeLimitSeconds, String raireUrl) { final String prefix = "[generateAssertions]"; + LOGGER.debug(String.format("%s %s %s.", prefix, "Generating assertions for contest ", contestName)); try { final ContestResult cr = IRVContestResults.stream() @@ -197,19 +202,23 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L // Interpret the response. final int statusCode = raireResponse.getStatusLine().getStatusCode(); - GenerateAssertionsResponse responseFromRaire = Main.GSON.fromJson(EntityUtils.toString(raireResponse.getEntity()), - GenerateAssertionsResponse.class); if (statusCode == HttpStatus.SC_OK) { // OK response. Update the stored winner and return it. LOGGER.debug(String.format("%s %s.", prefix, "OK response received from RAIRE for " + contestName)); + GenerateAssertionsResponse responseFromRaire = Main.GSON.fromJson(EntityUtils.toString(raireResponse.getEntity()), + GenerateAssertionsResponse.class); + updateWinnersAndLosers(cr, candidates, responseFromRaire.winner); + + LOGGER.debug(String.format("%s %s %s.", prefix, + "Completed assertion generation for contest ", contestName)); return new GenerateAssertionsResponseWithErrors(contestName, responseFromRaire.winner, ""); } else if (raireResponse.containsHeader(RAIRE_ERROR_CODE)) { - // Error response about a specific contest, e.g. "NO_ASSERTIONS_PRESENT". + // Error response about a specific contest, e.g. "TIED_WINNERS". // Return the error, record it. final String code = raireResponse.getFirstHeader(RAIRE_ERROR_CODE).getValue(); @@ -217,37 +226,55 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L "received from RAIRE for " + contestName)); updateWinnersAndLosers(cr, candidates, UNKNOWN_WINNER); + + LOGGER.debug(String.format("%s %s %s.", prefix, + "Error response for assertion generation for contest ", contestName)); return new GenerateAssertionsResponseWithErrors(cr.getContestName(), UNKNOWN_WINNER, code); } else { - // Something went wrong with the connection. Cannot continue. - + // Something went wrong with the connection, e.g. 404. Cannot continue. final String msg = "Connection failure with Raire service. Http code " + statusCode + ". Check the configuration of Raire service url."; LOGGER.error(String.format("%s %s", prefix, msg)); throw new RuntimeException(msg); } + } catch (URISyntaxException | MalformedURLException e) { + // The raire service url is malformed, probably a config error. final String msg = "Bad configuration of Raire service url: " + raireUrl + ". Check your config file."; LOGGER.error(String.format("%s %s %s", prefix, msg, e.getMessage())); throw new RuntimeException(msg); - } catch (NoSuchElementException | NullPointerException e) { + } catch (NoSuchElementException e ) { + // This happens if the contest name is not in the IRVContestResults. final String msg = "Non-existent or non-IRV contest in Generate Assertions request:"; LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage())); throw new RuntimeException(msg + contestName); } catch (JsonSyntaxException e) { + // This happens if the raire service returns something that isn't interpretable as json, + // so gson throws a syntax exception when trying to parse raireResponse. final String msg = "Error interpreting Raire response for contest "; LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage())); - throw new RuntimeException(e); + throw new RuntimeException(msg + contestName); } catch (UnsupportedEncodingException e) { + // This really shouldn't happen, but would happen if the effort to make the + // generateAssertionsRequest as json failed. final String msg = "Error generating request to Raire for contest "; LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage())); throw new RuntimeException(msg + contestName + e.getMessage()); } catch (ClientProtocolException e) { + // This also really shouldn't happen, but would happen if the effort to use the httpClient + // to send a message threw an exception. final String msg = "Error sending request to Raire for contest "; LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage())); throw new RuntimeException(msg + contestName + e.getMessage()); + } catch (NullPointerException e) { + // This also shouldn't happen - it would indicate an unexpected problem such as the httpClient + // returning a null response. + final String msg = "Error requesting or receiving assertions for contest "; + LOGGER.error(String.format("%s %s %s", prefix, msg, contestName)); + throw new RuntimeException(msg + contestName); } catch (IOException e) { + // Generic error that can be thrown by the httpClient if the connection attempt fails. final String msg = "I/O error during generate assertions attempt for contest "; LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage())); throw new RuntimeException(msg + contestName + e.getMessage()); 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 d18d11b3..3bcbad65 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 @@ -77,14 +77,15 @@ public class GenerateAssertionsTests { * Request for Boulder Mayoral '23 */ private final static GenerateAssertionsRequest boulderRequest - = new GenerateAssertionsRequest(boulderMayoral, 100000, 5, + = new GenerateAssertionsRequest(boulderMayoral, bouldMayoralCount, 5, boulderMayoralCandidates.stream().map(Choice::name).toList()); /** - * Mock collected generate assertions response, with Boulder Mayoral '23 and tinyExample1. + * Request for tinyExample1 contest */ - private final static List<GenerateAssertionsResponse> response - = List.of(boulderResponse, tinyIRVResponse); + private final static GenerateAssertionsRequest tinyIRVRequest + = new GenerateAssertionsRequest(tinyIRV, tinyIRVCount, 5, + tinyIRVCandidates.stream().map(Choice::name).toList()); /** * Corla endpoint to be tested. @@ -108,6 +109,23 @@ public class GenerateAssertionsTests { */ private static String baseUrl; + /** + * Bad url, for testing we deal appropriately with the resulting error. + */ + String badEndpoint = "/badUrl"; + + /** + * An endpoint that produces nonsense responses, i.e. valid json but not a valid + * GenerateAssertionsResponse, for testing that we deal appropriately with the resulting error. + */ + String nonsenseResponseEndpoint = "/raire/nonsense-generating-url"; + + /** + * An endpoint that produces nonsense/uninterpretable responses, i.e. not valid json, for testing + * that we deal appropriately with the resulting error. + */ + String invalidResponseEndpoint = "/raire/invalid-json-generating-url"; + /** * GSON for json interpretation. */ @@ -121,29 +139,55 @@ public void initMocks() { MockitoAnnotations.openMocks(this); boulderIRVContestResult.setAuditReason(AuditReason.COUNTY_WIDE_CONTEST); - boulderIRVContestResult.setBallotCount(100000L); + boulderIRVContestResult.setBallotCount((long) bouldMayoralCount); boulderIRVContestResult.setWinners(Set.of("Aaron Brockett")); boulderIRVContestResult.addContests(Set.of(boulderMayoralContest)); tinyIRVContestResult.setAuditReason(AuditReason.COUNTY_WIDE_CONTEST); - tinyIRVContestResult.setBallotCount(10L); + tinyIRVContestResult.setBallotCount((long) tinyIRVCount); tinyIRVContestResult.setWinners(Set.of("Alice")); tinyIRVContestResult.addContests(Set.of(tinyIRVExample)); - // Default raire server. - // baseUrl = "http://localhost:8080"; + // Default raire server. You can instead run the real raire service and set baseUrl accordingly, + // though the tests of invalid/uninterpretable data will fail. wireMockRaireServer.start(); baseUrl = wireMockRaireServer.baseUrl(); + String badUrl = baseUrl + badEndpoint; configureFor("localhost", wireMockRaireServer.port()); + // Mock a proper response to the Boulder Mayoral '23 contest. stubFor(post(urlEqualTo(raireGenerateAssertionsEndpoint)) - // .withMultipartRequestBody( - // aMultipart() - .withRequestBody(equalToJson(gson.toJson(boulderRequest))) - // ) + .withRequestBody(equalToJson(gson.toJson(boulderRequest))) .willReturn(aResponse() .withStatus(HttpStatus.SC_OK) .withHeader("Content-Type", "application/json") .withBody(gson.toJson(boulderResponse)))); + // 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)))); + // Mock a 404 for badUrl. + stubFor(post(urlEqualTo(badUrl)) + .withRequestBody(equalToJson(gson.toJson(tinyIRVRequest))) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_NOT_FOUND))); + // Mock an OK but invalid response from the nonsense endpoint. + // This is just a list of candidates, which should not make sense as a response. + stubFor(post(urlEqualTo(nonsenseResponseEndpoint)) + .withRequestBody(equalToJson(gson.toJson(tinyIRVRequest))) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", "application/json") + .withBody(gson.toJson(tinyIRVCandidates)))); + // Mock an OK response with invalid json. + stubFor(post(urlEqualTo(invalidResponseEndpoint)) + .withRequestBody(equalToJson(gson.toJson(tinyIRVRequest))) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", "application/json") + .withBody("This isn't valid json"))); } @AfterClass @@ -152,11 +196,12 @@ public void closeMocks() { } /** - * Calls the single-contest version for the tinyIRVExample, checks for the right winner. + * Calls the single-contest version of the endpoint for the boulder Mayor '23 example , checks + * for the right winner. */ @Test public void rightBoulderIRVWinner() { - testUtils.log(LOGGER, "rightTinyIRVWinner"); + testUtils.log(LOGGER, "rightBoulderIRVWinner"); GenerateAssertionsResponseWithErrors result = endpoint.generateAssertionsUpdateWinners( mockedIRVContestResults, boulderRequest.contestName, boulderRequest.timeLimitSeconds, @@ -199,17 +244,31 @@ public void nonExistentContestThrowsRuntimeException() { /** * When raire sends an uninterpretable response, an appropriate error message appears. + * This tests a response that is not valid json. */ @Test(expectedExceptions = RuntimeException.class, - expectedExceptionsMessageRegExp = ".*Error interpreting Raire response for contest testContest.*") + expectedExceptionsMessageRegExp = ".*Error interpreting Raire response for contest.*") public void uninterpretableRaireResponseThrowsRuntimeException() { + testUtils.log(LOGGER, "uninterpretableRaireResponseThrowsRuntimeException"); - testUtils.log(LOGGER, "nonExistentContestThrowsRuntimeException"); + endpoint.generateAssertionsUpdateWinners(mockedIRVContestResults, tinyIRV, 5, + baseUrl + invalidResponseEndpoint); + } - endpoint.generateAssertionsUpdateWinners(mockedIRVContestResults, "testContest", 5, - baseUrl + raireGenerateAssertionsEndpoint); + /** + * When raire sends an unexpected response, an appropriate error message appears. + * This tests a response that is valid json, but not the json we were expecting. + */ + @Test(expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = ".*Error interpreting Raire response for contest.*") + public void unexpectedRaireResponseThrowsRuntimeException() { + testUtils.log(LOGGER, "unexpectedRaireResponseThrowsRuntimeException"); + + endpoint.generateAssertionsUpdateWinners(mockedIRVContestResults, tinyIRV, 5, + baseUrl + nonsenseResponseEndpoint); } + /** * When given a bad endpoint, a runtime exception is thrown with an appropriate error message. */ @@ -218,8 +277,8 @@ public void uninterpretableRaireResponseThrowsRuntimeException() { public void badEndpointThrowsRuntimeException() { testUtils.log(LOGGER, "badEndpointThrowsRuntimeException"); - String url = baseUrl + "/badUrl"; - endpoint.generateAllAssertions(mockedIRVContestResults, 5, url); + String badUrl = baseUrl + badEndpoint; + endpoint.generateAllAssertions(mockedIRVContestResults, 5, badUrl); } /** 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 37ddabed..f580f218 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 @@ -39,11 +39,21 @@ public class testUtils { */ public final static String boulderMayoral = "City of Boulder Mayoral Candidates"; + /** + * Count of the universe size for Boulder Mayoral '23. + */ + public final static int bouldMayoralCount = 118669; + /** * Tiny constructed example. */ public final static String tinyIRV = "TinyExample1"; + /** + * Count of the universe size for TinyExample1 + */ + public final static int tinyIRVCount = 10; + /** * Non-existent contest */ @@ -53,6 +63,11 @@ public class testUtils { public final static Choice bob = new Choice("Bob", "", false, false); public final static Choice chuan = new Choice("Chuan", "", false, false); + /** + * Candidates for tinyExample1 + */ + public final static List<Choice> tinyIRVCandidates = List.of(alice, bob, chuan); + public final static List<Choice> boulderMayoralCandidates = List.of( new Choice("Aaron Brockett", "", false, false), new Choice("Nicole Speer", "", false, false), @@ -61,7 +76,7 @@ public class testUtils { ); public final static Contest tinyIRVExample = new Contest(tinyIRV, new County("Arapahoe", 3L), ContestType.IRV.toString(), - List.of(alice, bob, chuan), 3, 1, 0); + tinyIRVCandidates, 3, 1, 0); public final static ContestResult tinyIRVContestResult = new ContestResult(tinyIRV); public final static Contest boulderMayoralContest = new Contest(boulderMayoral, new County("Boulder", 7L), ContestType.IRV.toString(), boulderMayoralCandidates, 4, 1, 0); @@ -73,7 +88,7 @@ public class testUtils { */ public static final DoubleComparator doubleComparator = new DoubleComparator(); - /* + /** * Location of the tiny examples intended to be human-tallyable. */ public static final String TINY_CSV_PATH = "src/test/resources/CSVs/Tiny-IRV-Examples/";