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/";