Skip to content

Commit

Permalink
Almost-complete tests including mocking of invalid responses from the…
Browse files Browse the repository at this point in the history
… raire service.
  • Loading branch information
vteague committed Jun 28, 2024
1 parent efc3163 commit c0c1991
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -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<>();

Expand All @@ -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()
Expand Down Expand Up @@ -197,57 +202,79 @@ 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();
LOGGER.debug(String.format("%s %s %s.", prefix, "Error response " + code,
"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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
*/
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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),
Expand All @@ -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);
Expand All @@ -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/";
Expand Down

0 comments on commit c0c1991

Please sign in to comment.