From da7cc0b25f4e116427d5d04c7f339f24805773d0 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Thu, 2 Jul 2026 13:16:37 +0300 Subject: [PATCH] Reject blank task titles --- app/schemas/task.py | 36 ++++++++++++++++++++++++++++++++++-- tests/test_tasks.py | 16 +++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/app/schemas/task.py b/app/schemas/task.py index 2fbbd8a..7f9ccd4 100644 --- a/app/schemas/task.py +++ b/app/schemas/task.py @@ -1,7 +1,13 @@ from datetime import datetime from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, field_validator + + +def _clean_optional_text(value: Optional[str]) -> Optional[str]: + if value is None: + return None + return " ".join(value.split()) class TaskBase(BaseModel): @@ -9,6 +15,19 @@ class TaskBase(BaseModel): description: Optional[str] = None completed: bool = False + @field_validator("title") + @classmethod + def title_must_not_be_blank(cls, value: str) -> str: + title = " ".join(value.split()) + if not title: + raise ValueError("Title cannot be blank") + return title + + @field_validator("description") + @classmethod + def normalize_description(cls, value: Optional[str]) -> Optional[str]: + return _clean_optional_text(value) + class TaskCreate(TaskBase): pass @@ -19,6 +38,19 @@ class TaskUpdate(BaseModel): description: Optional[str] = None completed: Optional[bool] = None + @field_validator("title") + @classmethod + def title_must_not_be_blank(cls, value: Optional[str]) -> Optional[str]: + title = _clean_optional_text(value) + if title == "": + raise ValueError("Title cannot be blank") + return title + + @field_validator("description") + @classmethod + def normalize_description(cls, value: Optional[str]) -> Optional[str]: + return _clean_optional_text(value) + class TaskResponse(TaskBase): id: int @@ -26,4 +58,4 @@ class TaskResponse(TaskBase): updated_at: datetime class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 1e56888..06fd336 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -17,6 +17,12 @@ def test_create_task(client): assert "id" in data +def test_create_task_rejects_blank_title(client): + response = client.post("/tasks/", json={"title": " "}) + + assert response.status_code == 422 + + def test_get_tasks(client): client.post("/tasks/", json={"title": "Task 1"}) client.post("/tasks/", json={"title": "Task 2"}) @@ -47,10 +53,18 @@ def test_update_task(client): assert response.json()["completed"] == True +def test_update_task_rejects_blank_title(client): + created = client.post("/tasks/", json={"title": "Old title"}) + task_id = created.json()["id"] + response = client.put(f"/tasks/{task_id}", json={"title": " "}) + + assert response.status_code == 422 + + def test_delete_task(client): created = client.post("/tasks/", json={"title": "To delete"}) task_id = created.json()["id"] response = client.delete(f"/tasks/{task_id}") assert response.status_code == 204 response = client.get(f"/tasks/{task_id}") - assert response.status_code == 404 \ No newline at end of file + assert response.status_code == 404