def test_ja_ballots_before_audit_boards_set_up( client: FlaskClient, election_id: str, jurisdiction_ids: List[str], round_1_id: str, ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/ballots" ) ballots = json.loads(rv.data)["ballots"] assert len(ballots) == J1_BALLOTS_ROUND_1 compare_json( ballots[0], { "id": assert_is_id, "auditBoard": None, "batch": { "id": assert_is_id, "name": "1", "tabulator": None }, "position": 12, "status": "NOT_AUDITED", "interpretations": [], }, )
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_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_invalid_risk_limit(client: FlaskClient, election_id: str): # Get the existing data. rv = client.get(f"/election/{election_id}/settings") # Set an invalid state. election = json.loads(rv.data) election["riskLimit"] = -1 # Attempt to write invalid data. rv = put_json(client, f"/election/{election_id}/settings", election) assert rv.status_code == 400, f"unexpected response: {rv.data}" compare_json( json.loads(rv.data), { "errors": [{ "message": "-1 is less than the minimum of 1", "errorType": "Bad Request", }] }, )
def test_invalid_state(client: FlaskClient, election_id: str): # Get the existing data. rv = client.get(f"/election/{election_id}/settings") # Set an invalid state. election = json.loads(rv.data) election["state"] = "XX" # Attempt to write invalid data. rv = put_json(client, f"/election/{election_id}/settings", election) assert rv.status_code == 400, f"unexpected response: {rv.data}" compare_json( json.loads(rv.data), { "errors": [{ "message": asserts_startswith("'XX' is not one of ['AL',"), "errorType": "Bad Request", }] }, )
def test_ab_list_ballots_round_2( client: FlaskClient, election_id: str, jurisdiction_ids: List[str], round_2_id: str, audit_board_round_2_ids: List[str], # pylint: disable=unused-argument ): set_logged_in_user(client, UserType.AUDIT_BOARD, audit_board_round_2_ids[0]) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_2_id}/audit-board/{audit_board_round_2_ids[0]}/ballots" ) ballots = json.loads(rv.data)["ballots"] assert len(ballots) == AB1_BALLOTS_ROUND_2 compare_json( ballots[0], { "id": assert_is_id, "batch": { "id": assert_is_id, "name": "4", "tabulator": None }, "position": 3, "status": "NOT_AUDITED", "interpretations": [], "auditBoard": { "id": assert_is_id, "name": "Audit Board #1" }, }, ) previously_audited_ballots = [ b for b in ballots if b["status"] == "AUDITED" ] assert len(previously_audited_ballots) == 22
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_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_audit_status(client, election_id): ( _url_prefix, contest_id, candidate_id_1, candidate_id_2, _jurisdiction_id, audit_board_id_1, audit_board_id_2, _num_ballots, ) = setup_whole_audit(client, election_id, "Audit Status Test", 10, "1234567890") rv = client.get(f"/election/{election_id}/audit/status") status = json.loads(rv.data) compare_json( status, { "contests": [{ "choices": [ { "id": candidate_id_1, "name": "candidate 1", "numVotes": 48121, }, { "id": candidate_id_2, "name": "candidate 2", "numVotes": 38026, }, ], "id": contest_id, "isTargeted": True, "name": "contest 1", "numWinners": 1, "totalBallotsCast": 86147, "votesAllowed": 1, }], "frozenAt": assert_is_date, "isMultiJurisdiction": False, "jurisdictions": [{ "auditBoards": [ { "id": audit_board_id_1, "members": [ { "affiliation": "REP", "name": "Joe Schmo" }, { "affiliation": "", "name": "Jane Plain" }, ], "name": "Audit Board #1", "passphrase": assert_is_passphrase, }, { "id": audit_board_id_2, "members": [], "name": "audit board #2", "passphrase": assert_is_passphrase, }, ], "ballotManifest": { "file": { "name": "manifest.csv", "uploadedAt": assert_is_date }, "processing": { "status": ProcessingStatus.PROCESSED, "startedAt": assert_is_date, "completedAt": assert_is_date, "error": None, }, "numBallots": 86147, "numBatches": 484, "filename": "manifest.csv", "uploadedAt": assert_is_date, }, "batches": lambda x: x, # pass "contests": [contest_id], "id": assert_is_id, "name": "adams county", }], "name": "Audit Status Test", "online": False, "organizationId": None, "randomSeed": "1234567890", "riskLimit": 10, "rounds": [{ "contests": [{ "endMeasurements": { "isComplete": None, "pvalue": None }, "id": contest_id, "results": {}, "sampleSize": 1035, "sampleSizeOptions": [ { "prob": 0.51, "size": 343, "type": "ASN" }, { "prob": 0.7, "size": 542, "type": None }, { "prob": 0.8, "size": 718, "type": None }, { "prob": 0.9, "size": 1035, "type": None }, ], }], "endedAt": None, "id": assert_is_id, "startedAt": assert_is_date, }], }, )
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_ja_ballots_round_1( client: FlaskClient, election_id: str, jurisdiction_ids: List[str], contest_ids: str, round_1_id: str, audit_board_round_1_ids: List[str], # pylint: disable=unused-argument ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/ballots" ) ballots = json.loads(rv.data)["ballots"] assert len(ballots) == J1_BALLOTS_ROUND_1 compare_json( ballots[0], { "id": assert_is_id, "auditBoard": { "id": assert_is_id, "name": "Audit Board #1" }, "batch": { "id": assert_is_id, "name": "4", "tabulator": None }, "position": 8, "status": "NOT_AUDITED", "interpretations": [], }, ) ballot_with_wrong_status = next( (b for b in ballots if b["status"] != "NOT_AUDITED"), None) assert ballot_with_wrong_status is None assert ballots == sorted( ballots, key=lambda b: (b["auditBoard"]["name"], b["batch"]["name"], b["position"]), ) # Try auditing one ballot set_logged_in_user(client, UserType.AUDIT_BOARD, audit_board_round_1_ids[0]) choice_id = ContestChoice.query.filter_by( contest_id=contest_ids[0]).first().id rv = put_json( client, f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/audit-board/{audit_board_round_1_ids[0]}/ballots/{ballots[0]['id']}", { "status": "AUDITED", "interpretations": [{ "contestId": contest_ids[0], "interpretation": "VOTE", "choiceId": choice_id, "comment": "blah blah blah", }], }, ) assert_ok(rv) set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/ballots" ) ballots = json.loads(rv.data)["ballots"] compare_json( ballots[0], { "id": assert_is_id, "auditBoard": { "id": assert_is_id, "name": "Audit Board #1" }, "batch": { "id": assert_is_id, "name": "4", "tabulator": None }, "position": 8, "status": "AUDITED", "interpretations": [{ "contestId": contest_ids[0], "interpretation": "VOTE", "choiceId": choice_id, "comment": "blah blah blah", }], }, )
def test_audit_boards_list_round_2( client: FlaskClient, election_id: str, jurisdiction_ids: List[str], round_1_id: str, round_2_id: str, ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = post_json( client, f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_2_id}/audit-board", [ { "name": "Audit Board #1" }, { "name": "Audit Board #2" }, { "name": "Audit Board #3" }, ], ) assert_ok(rv) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_2_id}/audit-board", ) audit_boards = json.loads(rv.data) compare_json( audit_boards, { "auditBoards": [ { "id": assert_is_id, "name": "Audit Board #1", "passphrase": assert_is_passphrase, "signedOffAt": None, "currentRoundStatus": { "numSampledBallots": AB1_BALLOTS_ROUND_2, # Some ballots got audited in round 1 and sampled again in round 2 "numAuditedBallots": 22, }, }, { "id": assert_is_id, "name": "Audit Board #2", "passphrase": assert_is_passphrase, "signedOffAt": None, "currentRoundStatus": { "numSampledBallots": AB2_BALLOTS_ROUND_2, "numAuditedBallots": 3, }, }, { "id": assert_is_id, "name": "Audit Board #3", "passphrase": assert_is_passphrase, "signedOffAt": None, "currentRoundStatus": { "numSampledBallots": J1_BALLOTS_ROUND_2 - AB1_BALLOTS_ROUND_2 - AB2_BALLOTS_ROUND_2, "numAuditedBallots": 5, }, }, ] }, ) # Can still access round 1 audit boards rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/audit-board", ) assert rv.status_code == 200
def test_audit_boards_list_two( client: FlaskClient, election_id: str, jurisdiction_ids: List[str], contest_ids: List[str], round_1_id: str, ): set_logged_in_user(client, UserType.JURISDICTION_ADMIN, DEFAULT_JA_EMAIL) rv = post_json( client, f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/audit-board", [{ "name": "Audit Board #1" }, { "name": "Audit Board #2" }], ) assert_ok(rv) rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/audit-board", ) audit_boards = json.loads(rv.data)["auditBoards"] compare_json( audit_boards, [ { "id": assert_is_id, "name": "Audit Board #1", "signedOffAt": None, "passphrase": assert_is_passphrase, "currentRoundStatus": { "numSampledBallots": AB1_BALLOTS_ROUND_1, "numAuditedBallots": 0, }, }, { "id": assert_is_id, "name": "Audit Board #2", "passphrase": assert_is_passphrase, "signedOffAt": None, "currentRoundStatus": { "numSampledBallots": J1_BALLOTS_ROUND_1 - AB1_BALLOTS_ROUND_1, "numAuditedBallots": 0, }, }, ], ) # Fake auditing some ballots audit_board_1 = AuditBoard.query.get(audit_boards[0]["id"]) for ballot in audit_board_1.sampled_ballots[:10]: audit_ballot(ballot, contest_ids[0], Interpretation.BLANK) audit_board_2 = AuditBoard.query.get(audit_boards[1]["id"]) for ballot in audit_board_2.sampled_ballots[:20]: audit_ballot(ballot, contest_ids[0], Interpretation.BLANK) db.session.commit() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/audit-board", ) audit_boards = json.loads(rv.data)["auditBoards"] assert audit_boards[0]["currentRoundStatus"] == { "numSampledBallots": AB1_BALLOTS_ROUND_1, "numAuditedBallots": 10, } assert audit_boards[1]["currentRoundStatus"] == { "numSampledBallots": J1_BALLOTS_ROUND_1 - AB1_BALLOTS_ROUND_1, "numAuditedBallots": 20, } # Finish auditing ballots and sign off audit_board_1 = AuditBoard.query.get(audit_boards[0]["id"]) for ballot in audit_board_1.sampled_ballots[10:]: audit_ballot(ballot, contest_ids[0], Interpretation.BLANK) audit_board_1.signed_off_at = datetime.utcnow() db.session.commit() rv = client.get( f"/election/{election_id}/jurisdiction/{jurisdiction_ids[0]}/round/{round_1_id}/audit-board", ) audit_boards = json.loads(rv.data)["auditBoards"] assert_is_date(audit_boards[0]["signedOffAt"]) assert audit_boards[0]["currentRoundStatus"] == { "numSampledBallots": AB1_BALLOTS_ROUND_1, "numAuditedBallots": AB1_BALLOTS_ROUND_1, } assert audit_boards[1]["signedOffAt"] is None assert audit_boards[1]["currentRoundStatus"] == { "numSampledBallots": J1_BALLOTS_ROUND_1 - AB1_BALLOTS_ROUND_1, "numAuditedBallots": 20, }