From fc4be50d6a294aa53e44cb5d2cb3f326c63826b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Sabatti=C3=A9?= Date: Wed, 4 Jun 2025 19:49:09 +0200 Subject: [PATCH] Fixed: ballot endpoints does not care about date_start allowing using to send vote before the election starts --- app/crud.py | 10 +++++++++ app/schemas.py | 2 +- app/tests/test_api.py | 52 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/crud.py b/app/crud.py index 9aaf44a..d60e733 100644 --- a/app/crud.py +++ b/app/crud.py @@ -367,6 +367,7 @@ def create_ballot(db: Session, ballot: schemas.BallotCreate) -> schemas.BallotGe db_election = _check_public_election(db, ballot.election_ref) election = schemas.ElectionGet.model_validate(db_election) + _check_election_is_started(db_election) _check_election_is_not_ended(db_election) _check_items_in_election( @@ -406,6 +407,14 @@ def _check_public_election(db: Session, election_ref: str): ) return db_election +def _check_election_is_started(election: models.Election): + """ + Check that the election is started. + If it is not, raise an error. + """ + if election.date_start is not None and election.date_start > datetime.now(): + raise errors.ForbiddenError("The election has not started yet. You can not create votes") + def _check_election_is_not_ended(election: models.Election): """ Check that the election is not ended. @@ -453,6 +462,7 @@ def update_ballot( if db_election is None: raise errors.NotFoundError("elections") + _check_election_is_started(db_election) _check_election_is_not_ended(db_election) if len(ballot.votes) != len(vote_ids): diff --git a/app/schemas.py b/app/schemas.py index 054174d..df46aef 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -115,7 +115,7 @@ class ElectionBase(BaseModel): name: Name description: Description = "" ref: Ref = "" - date_start: datetime | int | str = Field(default_factory=_utc_now) + date_start: datetime | int | str | None = Field(default_factory=_utc_now) date_end: datetime | int | str | None = Field(default_factory=_in_a_long_time) hide_results: bool = True restricted: bool = False diff --git a/app/tests/test_api.py b/app/tests/test_api.py index 4ad6d17..f77a963 100644 --- a/app/tests/test_api.py +++ b/app/tests/test_api.py @@ -59,6 +59,7 @@ class RandomElection(t.TypedDict): hide_results: bool num_voters: int date_end: t.Optional[str] + date_start: t.Optional[str] def _random_election(num_candidates: int, num_grades: int) -> RandomElection: @@ -71,6 +72,7 @@ def _random_election(num_candidates: int, num_grades: int) -> RandomElection: candidates = [{"name": _random_string(10)} for i in range(num_candidates)] name = _random_string(10) return { + "date_start":None, "candidates": candidates, "grades": grades, "name": name, @@ -394,6 +396,56 @@ def test_cannot_update_vote_on_ended_election(): assert response.status_code == 403, response.json() +## TODO: cannot change start_date if a people vote; +## +def test_cannot_create_vote_on_unstarted_election(): + """ + On an unstarted election, we are not allowed to create new votes + """ + # Create a random election + body = _random_election(10, 5) + body["date_start"] = (datetime.now() + timedelta(days=1)).isoformat() + body["date_end"] = (datetime.now() + timedelta(days=2)).isoformat() + response = client.post("/elections", json=body) + data = response.json() + assert response.status_code == 200, data + election_ref = data["ref"] + + # We create votes using the ID + votes = _generate_votes_from_response("id", data) + response = client.post( + f"/ballots", + json={"votes": votes, "election_ref": election_ref}, + ) + data = response.json() + assert response.status_code == 403, data + +def test_cannot_update_vote_on_unstarted_election(): + """ + On an unstarted election, we are not allowed to create new votes + """ + # Create a random election + body = _random_election(10, 5) + body["restricted"] = True + body["num_voters"] = 1 + body["date_start"] = (datetime.now() + timedelta(days=1)).isoformat() + body["date_end"] = (datetime.now() + timedelta(days=2)).isoformat() + response = client.post("/elections", json=body) + data = response.json() + assert response.status_code == 200, data + election_ref = data["ref"] + tokens = data["invites"] + assert len(tokens) == 1 + + # We create votes using the ID + votes = _generate_votes_from_response("id", data) + response = client.put( + f"/ballots", + json={"votes": votes, "election_ref": election_ref}, + headers={"Authorization": f"Bearer {tokens[0]}"}, + ) + data = response.json() + assert response.status_code == 403, data def test_cannot_create_vote_on_restricted_election(): """