Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ def _create_election_without_candidates_or_grade(
params = election.model_dump()
del params["candidates"]
del params["grades"]

if params.get("date_start") is not None and params.get("date_end") is not None:
if params["date_start"] >= params["date_end"]:
raise errors.ForbiddenError(
"The start date must be before the end date of the election"
)

db_election = models.Election(**params)
db.add(db_election)
Expand Down Expand Up @@ -283,6 +289,17 @@ def update_election(
if db_election is None:
raise errors.NotFoundError("elections")

if election.date_start is not None and election.date_end is None and db_election.date_end is not None:
if election.date_start > db_election.date_end:
raise errors.ForbiddenError(
"The start date must be before the end date of the election"
)
elif election.date_end is not None and election.date_start is None:
if election.date_end < db_election.date_start:
raise errors.ForbiddenError(
"The end date must be after the start date of the election"
)

if (
election.restricted is not None
and bool(db_election.restricted) != election.restricted
Expand Down
10 changes: 10 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
async def main():
return {"message": "Hello World"}

@app.exception_handler(schemas.ArgumentsSchemaError)
async def invalid_schema_exception_handler(
request: Request, exc: schemas.ArgumentsSchemaError
):
return JSONResponse(
status_code=422,
content={
"message": f"Validation Error. {exc}",
},
)

@app.exception_handler(errors.NotFoundError)
async def not_found_exception_handler(request: Request, exc: errors.NotFoundError):
Expand Down
17 changes: 16 additions & 1 deletion app/schemas.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import typing as t
from typing_extensions import Self
from datetime import datetime, timedelta, timezone
import dateutil.parser
from pydantic import BaseModel, Field, field_validator, ValidationInfo
from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_validator
from pydantic_settings import SettingsConfigDict
from .settings import settings

Expand Down Expand Up @@ -121,6 +122,13 @@ class ElectionBase(BaseModel):
restricted: bool = False
auth_for_result: bool = False

@model_validator(mode="after")
def check_dates_order(self) -> Self:
if self.date_start and self.date_end and _parse_date(self.date_start) > _parse_date(self.date_end):
raise ArgumentsSchemaError("date_start must be before or equal to date_end")

return self

@field_validator("date_end", "date_start", mode="before")
@classmethod
def parse_date(cls, value):
Expand Down Expand Up @@ -229,6 +237,13 @@ class ElectionUpdate(BaseModel):
candidates: list[CandidateUpdate] | None = None
auth_for_result: bool | None = None

@model_validator(mode="after")
def check_dates_order(self) -> Self:
if self.date_start and self.date_end and _parse_date(self.date_start) > _parse_date(self.date_end):
raise ArgumentsSchemaError(f"date_start must be before or equal to date_end")

return self

@field_validator("date_end", "date_start", mode="before")
@classmethod
def parse_date(cls, value):
Expand Down
51 changes: 48 additions & 3 deletions app/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,46 @@ def test_create_election():
data = response.json()
assert data["name"] == body["name"]

def test_start_end_date_are_valid():
# cannot create an election where the start date is after the end date
body = _random_election(2, 2)
body["date_start"] = (datetime.now() + timedelta(days=1)).isoformat()
body["date_end"] = (datetime.now()).isoformat()
response = client.post("/elections", json=body)
assert response.status_code == 422, response.text

body["date_start"] = (datetime.now()).isoformat()
body["date_end"] = (datetime.now() + timedelta(days=1)).isoformat()
response = client.post("/elections", json=body)
assert response.status_code == 200, response.text
election_data = response.json()
del election_data["candidates"]
del election_data["grades"]
del election_data["name"]
del election_data["restricted"]
del election_data["hide_results"]
del election_data["auth_for_result"]
admin_token = election_data["admin"]
election_ref = election_data["ref"]

# update election should not be allowed if the start date is after the end date
election_data["date_start"] = (datetime.now() + timedelta(days=1)).isoformat()
election_data["date_end"] = (datetime.now()).isoformat()
response = client.put("/elections", json=election_data, headers={"Authorization": f"Bearer {admin_token}"})
assert response.status_code == 422, response.text

# update election should be allowed if the start date is before the end date
del election_data["date_start"]
election_data["date_end"] = (datetime.now() - timedelta(days=1)).isoformat()
response = client.put("/elections", json=election_data, headers={"Authorization": f"Bearer {admin_token}"})
assert response.status_code == 403, response.text

# update election should be allowed if the start date is before the end date
del election_data["date_end"]
election_data["date_start"] = (datetime.now() + timedelta(days=2)).isoformat()
response = client.put("/elections", json=election_data, headers={"Authorization": f"Bearer {admin_token}"})
assert response.status_code == 403, response.text


def test_get_election():
body = _random_election(3, 4)
Expand Down Expand Up @@ -309,13 +349,14 @@ def test_cannot_create_vote_on_ended_election():
"""
# Create a random election
body = _random_election(10, 5)
body["date_start"] = (datetime.now() - timedelta(days=2)).isoformat()
body["date_end"] = (datetime.now() - timedelta(days=1)).isoformat()
response = client.post("/elections", json=body)
election_data = response.json()
assert response.status_code == 200, election_data
assert len(election_data["invites"]) == 0
election_ref = election_data["ref"]
ballot_token = election_data["admin"]
admin_token = election_data["admin"]

# We create votes using the ID
votes = _generate_votes_from_response("id", election_data)
Expand All @@ -330,7 +371,7 @@ def test_cannot_create_vote_on_ended_election():
response = client.put(
f"/elections",
json={"force_close": True, "date_end":(datetime.now() + timedelta(days=1)).isoformat(), "ref": election_ref},
headers={"Authorization": f"Bearer {ballot_token}"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200, response.json()

Expand Down Expand Up @@ -386,10 +427,14 @@ def test_cannot_update_vote_on_ended_election():
# Test for date_end in the past
response = client.put(
f"/elections",
json={"force_close": False, "date_end":(datetime.now() - timedelta(days=1)).isoformat(), "ref": election_ref},
json={"force_close": False, "date_start":(datetime.now() - timedelta(days=2)).isoformat(), "date_end":(datetime.now() - timedelta(days=1)).isoformat(), "ref": election_ref},
headers={"Authorization": f"Bearer {election_token}"},
)

assert response.status_code == 200, response.json()["date_end"]

print(response.json()["date_end"])

response = client.put(
f"/ballots",
json={"votes": votes},
Expand Down