Example #1
0
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": [],
        },
    )
Example #2
0
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.",
            },
        },
    )
Example #3
0
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.",
            },
        },
    )
Example #4
0
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",
            }]
        },
    )
Example #5
0
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",
            }]
        },
    )
Example #6
0
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
Example #7
0
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.",
            },
        },
    )
Example #8
0
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}.",
                },
            },
        )
Example #9
0
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
Example #10
0
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)
Example #11
0
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,
            }],
        },
    )
Example #12
0
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
Example #13
0
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",
            }],
        },
    )
Example #14
0
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
Example #15
0
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,
    }