Skip to content

Commit c1f5553

Browse files
authored
Fixed: can push election with date start after date end (#66)
1 parent c849196 commit c1f5553

4 files changed

Lines changed: 91 additions & 4 deletions

File tree

app/crud.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def _create_election_without_candidates_or_grade(
173173
params = election.model_dump()
174174
del params["candidates"]
175175
del params["grades"]
176+
177+
if params.get("date_start") is not None and params.get("date_end") is not None:
178+
if params["date_start"] >= params["date_end"]:
179+
raise errors.ForbiddenError(
180+
"The start date must be before the end date of the election"
181+
)
176182

177183
db_election = models.Election(**params)
178184
db.add(db_election)
@@ -283,6 +289,17 @@ def update_election(
283289
if db_election is None:
284290
raise errors.NotFoundError("elections")
285291

292+
if election.date_start is not None and election.date_end is None and db_election.date_end is not None:
293+
if election.date_start > db_election.date_end:
294+
raise errors.ForbiddenError(
295+
"The start date must be before the end date of the election"
296+
)
297+
elif election.date_end is not None and election.date_start is None:
298+
if election.date_end < db_election.date_start:
299+
raise errors.ForbiddenError(
300+
"The end date must be after the start date of the election"
301+
)
302+
286303
if (
287304
election.restricted is not None
288305
and bool(db_election.restricted) != election.restricted

app/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
async def main():
2929
return {"message": "Hello World"}
3030

31+
@app.exception_handler(schemas.ArgumentsSchemaError)
32+
async def invalid_schema_exception_handler(
33+
request: Request, exc: schemas.ArgumentsSchemaError
34+
):
35+
return JSONResponse(
36+
status_code=422,
37+
content={
38+
"message": f"Validation Error. {exc}",
39+
},
40+
)
3141

3242
@app.exception_handler(errors.NotFoundError)
3343
async def not_found_exception_handler(request: Request, exc: errors.NotFoundError):

app/schemas.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import typing as t
2+
from typing_extensions import Self
23
from datetime import datetime, timedelta, timezone
34
import dateutil.parser
4-
from pydantic import BaseModel, Field, field_validator, ValidationInfo
5+
from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_validator
56
from pydantic_settings import SettingsConfigDict
67
from .settings import settings
78

@@ -121,6 +122,13 @@ class ElectionBase(BaseModel):
121122
restricted: bool = False
122123
auth_for_result: bool = False
123124

125+
@model_validator(mode="after")
126+
def check_dates_order(self) -> Self:
127+
if self.date_start and self.date_end and _parse_date(self.date_start) > _parse_date(self.date_end):
128+
raise ArgumentsSchemaError("date_start must be before or equal to date_end")
129+
130+
return self
131+
124132
@field_validator("date_end", "date_start", mode="before")
125133
@classmethod
126134
def parse_date(cls, value):
@@ -229,6 +237,13 @@ class ElectionUpdate(BaseModel):
229237
candidates: list[CandidateUpdate] | None = None
230238
auth_for_result: bool | None = None
231239

240+
@model_validator(mode="after")
241+
def check_dates_order(self) -> Self:
242+
if self.date_start and self.date_end and _parse_date(self.date_start) > _parse_date(self.date_end):
243+
raise ArgumentsSchemaError(f"date_start must be before or equal to date_end")
244+
245+
return self
246+
232247
@field_validator("date_end", "date_start", mode="before")
233248
@classmethod
234249
def parse_date(cls, value):

app/tests/test_api.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,46 @@ def test_create_election():
111111
data = response.json()
112112
assert data["name"] == body["name"]
113113

114+
def test_start_end_date_are_valid():
115+
# cannot create an election where the start date is after the end date
116+
body = _random_election(2, 2)
117+
body["date_start"] = (datetime.now() + timedelta(days=1)).isoformat()
118+
body["date_end"] = (datetime.now()).isoformat()
119+
response = client.post("/elections", json=body)
120+
assert response.status_code == 422, response.text
121+
122+
body["date_start"] = (datetime.now()).isoformat()
123+
body["date_end"] = (datetime.now() + timedelta(days=1)).isoformat()
124+
response = client.post("/elections", json=body)
125+
assert response.status_code == 200, response.text
126+
election_data = response.json()
127+
del election_data["candidates"]
128+
del election_data["grades"]
129+
del election_data["name"]
130+
del election_data["restricted"]
131+
del election_data["hide_results"]
132+
del election_data["auth_for_result"]
133+
admin_token = election_data["admin"]
134+
election_ref = election_data["ref"]
135+
136+
# update election should not be allowed if the start date is after the end date
137+
election_data["date_start"] = (datetime.now() + timedelta(days=1)).isoformat()
138+
election_data["date_end"] = (datetime.now()).isoformat()
139+
response = client.put("/elections", json=election_data, headers={"Authorization": f"Bearer {admin_token}"})
140+
assert response.status_code == 422, response.text
141+
142+
# update election should be allowed if the start date is before the end date
143+
del election_data["date_start"]
144+
election_data["date_end"] = (datetime.now() - timedelta(days=1)).isoformat()
145+
response = client.put("/elections", json=election_data, headers={"Authorization": f"Bearer {admin_token}"})
146+
assert response.status_code == 403, response.text
147+
148+
# update election should be allowed if the start date is before the end date
149+
del election_data["date_end"]
150+
election_data["date_start"] = (datetime.now() + timedelta(days=2)).isoformat()
151+
response = client.put("/elections", json=election_data, headers={"Authorization": f"Bearer {admin_token}"})
152+
assert response.status_code == 403, response.text
153+
114154

115155
def test_get_election():
116156
body = _random_election(3, 4)
@@ -309,13 +349,14 @@ def test_cannot_create_vote_on_ended_election():
309349
"""
310350
# Create a random election
311351
body = _random_election(10, 5)
352+
body["date_start"] = (datetime.now() - timedelta(days=2)).isoformat()
312353
body["date_end"] = (datetime.now() - timedelta(days=1)).isoformat()
313354
response = client.post("/elections", json=body)
314355
election_data = response.json()
315356
assert response.status_code == 200, election_data
316357
assert len(election_data["invites"]) == 0
317358
election_ref = election_data["ref"]
318-
ballot_token = election_data["admin"]
359+
admin_token = election_data["admin"]
319360

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

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

434+
assert response.status_code == 200, response.json()["date_end"]
435+
436+
print(response.json()["date_end"])
437+
393438
response = client.put(
394439
f"/ballots",
395440
json={"votes": votes},

0 commit comments

Comments
 (0)