def manifests(client: FlaskClient, election_id: str, jurisdiction_ids: List[str]): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO(b"Batch Name,Number of Ballots\n" b"1,23\n" b"2,101\n" b"3,122\n" b"4,400"), "manifest.csv", ) }, ) assert_ok(rv) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[1]}/ballot-manifest", data={ "manifest": ( io.BytesIO(b"Batch Name,Number of Ballots\n" b"1,20\n" b"2,10\n" b"3,220\n" b"4,40"), "manifest.csv", ) }, ) assert_ok(rv) bgcompute_update_ballot_manifest_file()
def test_ballot_manifest_upload_bad_csv( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={"manifest": (io.BytesIO(b"not a CSV file"), "random.txt")}, ) assert_ok(rv) bgcompute_update_ballot_manifest_file() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) compare_json( json.loads(rv.data), { "file": {"name": "random.txt", "uploadedAt": assert_is_date,}, "processing": { "status": ProcessingStatus.ERRORED, "startedAt": assert_is_date, "completedAt": assert_is_date, "error": "Please submit a valid CSV file with columns separated by commas.", }, }, )
def test_ballot_manifest_upload_invalid_num_ballots( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO( b"Batch Name,Number of Ballots,Storage Location,Tabulator\n" b"1,not a number,Bin 2,Tabulator 1\n" ), "manifest.csv", ) }, ) assert_ok(rv) bgcompute_update_ballot_manifest_file() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) compare_json( json.loads(rv.data), { "file": {"name": "manifest.csv", "uploadedAt": assert_is_date,}, "processing": { "status": ProcessingStatus.ERRORED, "startedAt": assert_is_date, "completedAt": assert_is_date, "error": "Expected a number in column Number of Ballots, row 1. Got: not a number.", }, }, )
def test_ballot_manifest_replace( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO( b"Batch Name,Number of Ballots,Storage Location,Tabulator\n" b"1,23,Bin 2,Tabulator 1\n" b"12,100,Bin 3,Tabulator 2\n" b"6,0,,\n" ), "manifest.csv", ) }, ) assert_ok(rv) num_files = File.query.count() bgcompute_update_ballot_manifest_file() set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO( b"Batch Name,Number of Ballots,Storage Location,Tabulator\n" b"1,23,Bin 2,Tabulator 1\n" b"12,6,Bin 6,Tabulator 2\n" ), "manifest.csv", ) }, ) assert_ok(rv) # The old file should have been deleted assert File.query.count() == num_files bgcompute_update_ballot_manifest_file() jurisdiction = Jurisdiction.query.get(jurisdiction_ids[0]) assert jurisdiction.manifest_num_batches == 2 assert jurisdiction.manifest_num_ballots == 29 assert len(jurisdiction.batches) == 2 assert jurisdiction.batches[0].name == "1" assert jurisdiction.batches[0].num_ballots == 23 assert jurisdiction.batches[0].storage_location == "Bin 2" assert jurisdiction.batches[0].tabulator == "Tabulator 1" assert jurisdiction.batches[1].name == "12" assert jurisdiction.batches[1].num_ballots == 6 assert jurisdiction.batches[1].storage_location == "Bin 6" assert jurisdiction.batches[1].tabulator == "Tabulator 2"
def test_ballot_manifest_upload_duplicate_batch_name( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO( b"Batch Name,Number of Ballots,Storage Location,Tabulator\n" b"12,23,Bin 2,Tabulator 1\n" b"12,100,Bin 3,Tabulator 2\n" b"6,0,,\n" ), "manifest.csv", ) }, ) assert_ok(rv) bgcompute_update_ballot_manifest_file() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) compare_json( json.loads(rv.data), { "file": {"name": "manifest.csv", "uploadedAt": assert_is_date,}, "processing": { "status": ProcessingStatus.ERRORED, "startedAt": assert_is_date, "completedAt": assert_is_date, "error": "Values in column Batch Name must be unique. Found duplicate value: 12.", }, }, )
def test_ballot_manifest_clear( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO( b"Batch Name,Number of Ballots,Storage Location,Tabulator\n" b"1,23,Bin 2,Tabulator 1\n" ), "manifest.csv", ) }, ) assert_ok(rv) num_files = File.query.count() bgcompute_update_ballot_manifest_file() rv = client.delete( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", ) assert_ok(rv) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) assert json.loads(rv.data) == {"file": None, "processing": None} jurisdiction = Jurisdiction.query.get(jurisdiction_ids[0]) assert jurisdiction.manifest_num_batches is None assert jurisdiction.manifest_num_ballots is None assert jurisdiction.batches == [] assert File.query.count() == num_files - 1
def test_ballot_manifest_upload_missing_field( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): for missing_field in ["Batch Name", "Number of Ballots"]: headers = ["Batch Name", "Number of Ballots", "Storage Location", "Tabulator"] header_row = ",".join(h for h in headers if h != missing_field) set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO(header_row.encode() + b"\n1,2,3"), "manifest.csv", ) }, ) assert_ok(rv) bgcompute_update_ballot_manifest_file() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) compare_json( json.loads(rv.data), { "file": {"name": "manifest.csv", "uploadedAt": assert_is_date,}, "processing": { "status": ProcessingStatus.ERRORED, "startedAt": assert_is_date, "completedAt": assert_is_date, "error": f"Missing required column: {missing_field}.", }, }, )
def test_jurisdictions_list_with_manifest(client: FlaskClient, election_id: str, jurisdiction_ids: List[str]): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) manifest = (b"Batch Name,Number of Ballots\n" b"1,23\n" b"2,101\n" b"3,122\n" b"4,400") rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={"manifest": ( io.BytesIO(manifest), "manifest.csv", )}, ) assert_ok(rv) assert bgcompute_update_ballot_manifest_file() == 1 rv = client.get(f"/election/{election_id}/jurisdiction") jurisdictions = json.loads(rv.data) expected = { "jurisdictions": [ { "id": jurisdiction_ids[0], "name": "J1", "ballotManifest": { "file": { "name": "manifest.csv", "uploadedAt": assert_is_date, }, "processing": { "status": "PROCESSED", "startedAt": assert_is_date, "completedAt": assert_is_date, "error": None, }, "numBallots": 23 + 101 + 122 + 400, "numBatches": 4, }, "currentRoundStatus": None, }, { "id": jurisdiction_ids[1], "name": "J2", "ballotManifest": { "file": None, "processing": None, "numBallots": None, "numBatches": None, }, "currentRoundStatus": None, }, { "id": jurisdiction_ids[2], "name": "J3", "ballotManifest": { "file": None, "processing": None, "numBallots": None, "numBatches": None, }, "currentRoundStatus": None, }, ] } compare_json(jurisdictions, expected) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest/csv" ) assert rv.headers[ "Content-Disposition"] == 'attachment; filename="manifest.csv"' assert rv.data == manifest
def test_duplicate_batch_name(client, election_id, jurisdiction_ids): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO(b"Batch Name,Number of Ballots\n" b"1,23\n" b"1,101\n"), "manifest.csv", ) }, ) assert_ok(rv) bgcompute_update_ballot_manifest_file() rv = client.get(f"/election/{election_id}/jurisdiction") jurisdictions = json.loads(rv.data) expected = { "jurisdictions": [ { "id": jurisdiction_ids[0], "name": "J1", "ballotManifest": { "file": { "name": "manifest.csv", "uploadedAt": assert_is_date, }, "processing": { "status": "ERRORED", "startedAt": assert_is_date, "completedAt": assert_is_date, "error": "Values in column Batch Name must be unique. Found duplicate value: 1.", }, "numBallots": None, "numBatches": None, }, "currentRoundStatus": None, }, { "id": jurisdiction_ids[1], "name": "J2", "ballotManifest": { "file": None, "processing": None, "numBallots": None, "numBatches": None, }, "currentRoundStatus": None, }, { "id": jurisdiction_ids[2], "name": "J3", "ballotManifest": { "file": None, "processing": None, "numBallots": None, "numBatches": None, }, "currentRoundStatus": None, }, ] } compare_json(jurisdictions, expected)
def test_ballot_manifest_upload( client: FlaskClient, election_id: str, jurisdiction_ids: List[str] ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest", data={ "manifest": ( io.BytesIO( b"Batch Name,Number of Ballots,Storage Location,Tabulator\n" b"1,23,Bin 2,Tabulator 1\n" b"12,100,Bin 3,Tabulator 2\n" b"6,0,,\n" ), "manifest.csv", ) }, ) assert_ok(rv) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) compare_json( json.loads(rv.data), { "file": {"name": "manifest.csv", "uploadedAt": assert_is_date,}, "processing": { "status": ProcessingStatus.READY_TO_PROCESS, "startedAt": None, "completedAt": None, "error": None, }, }, ) bgcompute_update_ballot_manifest_file() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/ballot-manifest" ) compare_json( json.loads(rv.data), { "file": {"name": "manifest.csv", "uploadedAt": assert_is_date,}, "processing": { "status": ProcessingStatus.PROCESSED, "startedAt": assert_is_date, "completedAt": assert_is_date, "error": None, }, }, ) jurisdiction = Jurisdiction.query.get(jurisdiction_ids[0]) assert jurisdiction.manifest_num_batches == 3 assert jurisdiction.manifest_num_ballots == 123 assert len(jurisdiction.batches) == 3 assert jurisdiction.batches[0].name == "1" assert jurisdiction.batches[0].num_ballots == 23 assert jurisdiction.batches[0].storage_location == "Bin 2" assert jurisdiction.batches[0].tabulator == "Tabulator 1" assert jurisdiction.batches[1].name == "12" assert jurisdiction.batches[1].num_ballots == 100 assert jurisdiction.batches[1].storage_location == "Bin 3" assert jurisdiction.batches[1].tabulator == "Tabulator 2" assert jurisdiction.batches[2].name == "6" assert jurisdiction.batches[2].num_ballots == 0 assert jurisdiction.batches[2].storage_location is None assert jurisdiction.batches[2].tabulator is None
def test_multi_winner_election(client, election_id): contest_id = str(uuid.uuid4()) candidate_id_1 = str(uuid.uuid4()) candidate_id_2 = str(uuid.uuid4()) candidate_id_3 = str(uuid.uuid4()) rv = post_json( client, f"/election/{election_id}/audit/basic", { "name": "Small Multi-winner Test 2019", "riskLimit": 10, "randomSeed": "a1234567890987654321b", "online": False, "contests": [ { "id": contest_id, "name": "Contest 1", "isTargeted": True, "choices": [ {"id": candidate_id_1, "name": "Candidate 1", "numVotes": 1000}, {"id": candidate_id_2, "name": "Candidate 2", "numVotes": 792}, {"id": candidate_id_3, "name": "Candidate 3", "numVotes": 331}, ], "totalBallotsCast": 2123, "numWinners": 2, "votesAllowed": 1, } ], }, ) assert_ok(rv) rv = client.get(f"/election/{election_id}/audit/status") status = json.loads(rv.data) assert status["name"] == "Small Multi-winner Test 2019" jurisdiction_id = str(uuid.uuid4()) audit_board_id_1 = str(uuid.uuid4()) audit_board_id_2 = str(uuid.uuid4()) rv = post_json( client, f"/election/{election_id}/audit/jurisdictions", { "jurisdictions": [ { "id": jurisdiction_id, "name": "County 1", "contests": [contest_id], "auditBoards": [ { "id": audit_board_id_1, "name": "Audit Board #1", "members": [], }, { "id": audit_board_id_2, "name": "Audit Board #2", "members": [], }, ], } ] }, ) assert_ok(rv) rv = client.post(f"/election/{election_id}/audit/freeze") assert_ok(rv) bgcompute.bgcompute() rv = client.get(f"/election/{election_id}/audit/status") status = json.loads(rv.data) assert len(status["jurisdictions"]) == 1 jurisdiction = status["jurisdictions"][0] assert jurisdiction["name"] == "County 1" assert jurisdiction["auditBoards"][1]["name"] == "Audit Board #2" assert jurisdiction["contests"] == [contest_id] # choose a sample size sample_size_asn = status["rounds"][0]["contests"][0]["sampleSizeOptions"] assert len(sample_size_asn) == 1 sample_size = sample_size_asn[0]["size"] # set the sample_size rv = post_json( client, f"/election/{election_id}/audit/sample-size", {"size": sample_size} ) # upload the manifest data = {} data["manifest"] = (open(small_manifest_file_path, "rb"), "small-manifest.csv") rv = client.put( f"/election/{election_id}/jurisdiction/{jurisdiction_id}/manifest", data=data, content_type="multipart/form-data", ) assert_ok(rv) assert bgcompute.bgcompute_update_ballot_manifest_file() == 1 rv = client.get(f"/election/{election_id}/audit/status") status = json.loads(rv.data) manifest = status["jurisdictions"][0]["ballotManifest"] assert manifest["numBallots"] == 2117 assert manifest["numBatches"] == 10 assert manifest["file"]["name"] == "small-manifest.csv" assert manifest["file"]["uploadedAt"] # get the retrieval list for round 1 rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_id}/1/retrieval-list" ) lines = rv.data.decode("utf-8").split("\r\n") assert ( lines[0] == "Batch Name,Ballot Number,Storage Location,Tabulator,Ticket Numbers,Already Audited,Audit Board" ) assert "attachment" in rv.headers["Content-Disposition"] num_ballots = get_num_ballots_from_retrieval_list(rv) # post results for round 1 num_for_winner = int(num_ballots * 0.61) num_for_winner2 = int(num_ballots * 0.3) num_for_loser = num_ballots - num_for_winner - num_for_winner2 rv = post_json( client, f"/election/{election_id}/jurisdiction/{jurisdiction_id}/1/results", { "contests": [ { "id": contest_id, "results": { candidate_id_1: num_for_winner, candidate_id_2: num_for_winner2, candidate_id_3: num_for_loser, }, } ] }, ) assert_ok(rv) rv = client.get(f"/election/{election_id}/audit/status") status = json.loads(rv.data) round_contest = status["rounds"][0]["contests"][0] assert round_contest["id"] == contest_id assert round_contest["results"][candidate_id_1] == num_for_winner assert round_contest["results"][candidate_id_2] == num_for_winner2 assert round_contest["results"][candidate_id_3] == num_for_loser assert round_contest["endMeasurements"]["isComplete"] assert math.floor(round_contest["endMeasurements"]["pvalue"] * 100) <= 9 rv = client.get(f"/election/{election_id}/report") lines = rv.data.decode("utf-8").splitlines() assert lines[0] == "######## ELECTION INFO ########" assert "attachment" in rv.headers["Content-Disposition"]
def setup_whole_audit(client, election_id, name, risk_limit, random_seed, online=False): contest_id = str(uuid.uuid4()) candidate_id_1 = str(uuid.uuid4()) candidate_id_2 = str(uuid.uuid4()) jurisdiction_id = str(uuid.uuid4()) audit_board_id_1 = str(uuid.uuid4()) audit_board_id_2 = str(uuid.uuid4()) url_prefix = "/election/{}".format(election_id) rv = post_json( client, "{}/audit/basic".format(url_prefix), { "name": name, "riskLimit": risk_limit, "randomSeed": random_seed, "online": online, "contests": [ { "id": contest_id, "name": "contest 1", "isTargeted": True, "choices": [ { "id": candidate_id_1, "name": "candidate 1", "numVotes": 48121, }, { "id": candidate_id_2, "name": "candidate 2", "numVotes": 38026, }, ], "totalBallotsCast": 86147, "numWinners": 1, "votesAllowed": 1, } ], }, ) assert_ok(rv) rv = client.post(f"{url_prefix}/audit/freeze") assert_ok(rv) # before background compute, should be null sample size options rv = client.get("{}/audit/status".format(url_prefix)) status = json.loads(rv.data) assert status["rounds"][0]["contests"][0]["sampleSizeOptions"] is None assert status["online"] == online # after background compute bgcompute.bgcompute() rv = client.get("{}/audit/status".format(url_prefix)) status = json.loads(rv.data) assert len(status["rounds"][0]["contests"][0]["sampleSizeOptions"]) == 4 assert status["randomSeed"] == random_seed assert len(status["contests"]) == 1 assert status["riskLimit"] == risk_limit assert status["name"] == name assert status["contests"][0]["choices"][0]["id"] == candidate_id_1 rv = post_json( client, "{}/audit/jurisdictions".format(url_prefix), { "jurisdictions": [ { "id": jurisdiction_id, "name": "adams county", "contests": [contest_id], "auditBoards": [ { "id": audit_board_id_1, "name": "audit board #1", "members": [], }, { "id": audit_board_id_2, "name": "audit board #2", "members": [], }, ], } ] }, ) assert_ok(rv) rv = client.get("{}/audit/status".format(url_prefix)) status = json.loads(rv.data) assert len(status["jurisdictions"]) == 1 jurisdiction = status["jurisdictions"][0] assert jurisdiction["name"] == "adams county" assert jurisdiction["auditBoards"][1]["name"] == "audit board #2" assert jurisdiction["contests"] == [contest_id] # choose a sample size sample_size_90 = [ option for option in status["rounds"][0]["contests"][0]["sampleSizeOptions"] if option["prob"] == 0.9 ] assert len(sample_size_90) == 1 sample_size = sample_size_90[0]["size"] # set the sample_size rv = post_json( client, "{}/audit/sample-size".format(url_prefix), {"size": sample_size} ) assert_ok(rv) # upload the manifest data = {} data["manifest"] = (open(manifest_file_path, "rb"), "manifest.csv") rv = client.put( "{}/jurisdiction/{}/manifest".format(url_prefix, jurisdiction_id), data=data, content_type="multipart/form-data", ) assert_ok(rv) assert bgcompute.bgcompute_update_ballot_manifest_file() == 1 rv = client.get("{}/audit/status".format(url_prefix)) status = json.loads(rv.data) manifest = status["jurisdictions"][0]["ballotManifest"] assert manifest["numBallots"] == 86147 assert manifest["numBatches"] == 484 assert manifest["file"]["name"] == "manifest.csv" assert manifest["file"]["uploadedAt"] # delete the manifest and make sure that works rv = client.delete( "{}/jurisdiction/{}/manifest".format(url_prefix, jurisdiction_id) ) assert_ok(rv) rv = client.get("{}/audit/status".format(url_prefix)) status = json.loads(rv.data) manifest = status["jurisdictions"][0]["ballotManifest"] assert manifest["file"] is None # upload the manifest again data = {} data["manifest"] = (open(manifest_file_path, "rb"), "manifest.csv") rv = client.put( "{}/jurisdiction/{}/manifest".format(url_prefix, jurisdiction_id), data=data, content_type="multipart/form-data", ) assert_ok(rv) assert bgcompute.bgcompute_update_ballot_manifest_file() == 1 setup_audit_board(client, election_id, jurisdiction_id, audit_board_id_1) # get the retrieval list for round 1 rv = client.get( "{}/jurisdiction/{}/1/retrieval-list".format(url_prefix, jurisdiction_id) ) lines = rv.data.decode("utf-8").splitlines() assert ( lines[0] == "Batch Name,Ballot Number,Storage Location,Tabulator,Ticket Numbers,Already Audited,Audit Board" ) assert len(lines) > 5 assert "attachment" in rv.headers["content-disposition"] num_ballots = get_num_ballots_from_retrieval_list(rv) return ( url_prefix, contest_id, candidate_id_1, candidate_id_2, jurisdiction_id, audit_board_id_1, audit_board_id_2, num_ballots, )