diff --git a/src/example/note/handler.py b/src/example/note/handler.py index 5e06bb9..01c7a31 100644 --- a/src/example/note/handler.py +++ b/src/example/note/handler.py @@ -10,9 +10,13 @@ from .use_case import ( CreateNoteInput, CreateNoteUseCase, + DeleteNoteInput, + DeleteNoteUseCase, GetNoteUseCase, ListNotesInput, ListNotesUseCase, + UpdateNoteInput, + UpdateNoteUseCase, ) router = APIRouter(prefix="/notes", tags=["notes"]) @@ -23,10 +27,17 @@ class CreateNoteBody(BaseModel): body: str +class UpdateNoteBody(BaseModel): + title: str + body: str + + def make_note_router( list_use_case: ListNotesUseCase, get_use_case: GetNoteUseCase, create_use_case: CreateNoteUseCase, + update_use_case: UpdateNoteUseCase, + delete_use_case: DeleteNoteUseCase, ) -> APIRouter: @router.get("") async def list_notes(request: Request) -> JSONResponse: @@ -59,4 +70,19 @@ async def create_note(body: CreateNoteBody) -> JSONResponse: {"id": note.id, "title": note.title, "body": note.body}, status_code=201 ) + @router.put("/{note_id}") + async def update_note(note_id: int, body: UpdateNoteBody) -> JSONResponse: + errors: list[ValidationError] = [] + if not body.title.strip(): + errors.append(ValidationError("title", "Title must not be empty.", "required")) + if errors: + raise ValidationException(errors) + + note = update_use_case.execute(UpdateNoteInput(note_id, body.title, body.body)) + return JSONResponse({"id": note.id, "title": note.title, "body": note.body}) + + @router.delete("/{note_id}", status_code=204) + async def delete_note(note_id: int) -> None: + delete_use_case.execute(DeleteNoteInput(note_id)) + return router diff --git a/src/example/note/repository.py b/src/example/note/repository.py index b6bbbe3..7391211 100644 --- a/src/example/note/repository.py +++ b/src/example/note/repository.py @@ -15,6 +15,12 @@ def find_by_id(self, note_id: int) -> Note | None: ... @abstractmethod def save(self, title: str, body: str) -> Note: ... + @abstractmethod + def update(self, note_id: int, title: str, body: str) -> Note | None: ... + + @abstractmethod + def delete(self, note_id: int) -> bool: ... + @abstractmethod def count(self) -> int: ... @@ -39,5 +45,18 @@ def save(self, title: str, body: str) -> Note: self._next_id += 1 return note + def update(self, note_id: int, title: str, body: str) -> Note | None: + if note_id not in self._store: + return None + updated = Note(id=note_id, title=title, body=body) + self._store[note_id] = updated + return updated + + def delete(self, note_id: int) -> bool: + if note_id not in self._store: + return False + del self._store[note_id] + return True + def count(self) -> int: return len(self._store) diff --git a/src/example/note/use_case.py b/src/example/note/use_case.py index c6fa665..7a767c4 100644 --- a/src/example/note/use_case.py +++ b/src/example/note/use_case.py @@ -59,3 +59,36 @@ def execute(self, note_id: int) -> Note: if note is None: raise NoteNotFoundException(note_id) return note + + +@dataclass(frozen=True) +class UpdateNoteInput: + note_id: int + title: str + body: str + + +class UpdateNoteUseCase: + def __init__(self, repository: NoteRepositoryInterface) -> None: + self._repository = repository + + def execute(self, input_: UpdateNoteInput) -> Note: + note = self._repository.update(input_.note_id, input_.title, input_.body) + if note is None: + raise NoteNotFoundException(input_.note_id) + return note + + +@dataclass(frozen=True) +class DeleteNoteInput: + note_id: int + + +class DeleteNoteUseCase: + def __init__(self, repository: NoteRepositoryInterface) -> None: + self._repository = repository + + def execute(self, input_: DeleteNoteInput) -> None: + deleted = self._repository.delete(input_.note_id) + if not deleted: + raise NoteNotFoundException(input_.note_id) diff --git a/tests/example/note/test_list_notes.py b/tests/example/note/test_list_notes.py index 06d329b..5e78436 100644 --- a/tests/example/note/test_list_notes.py +++ b/tests/example/note/test_list_notes.py @@ -42,6 +42,47 @@ def test_get_nonexistent_note_returns_404() -> None: assert r.status_code == 404 +def test_update_note_returns_200() -> None: + client = _client() + r = client.post("/notes", json={"title": "Old", "body": "Old body"}) + note_id = r.json()["id"] + + r2 = client.put(f"/notes/{note_id}", json={"title": "New", "body": "New body"}) + assert r2.status_code == 200 + assert r2.json()["title"] == "New" + assert r2.json()["body"] == "New body" + + +def test_update_nonexistent_note_returns_404() -> None: + r = _client().put("/notes/9999", json={"title": "T", "body": "B"}) + assert r.status_code == 404 + + +def test_update_note_empty_title_returns_422() -> None: + client = _client() + r = client.post("/notes", json={"title": "T", "body": "B"}) + note_id = r.json()["id"] + r2 = client.put(f"/notes/{note_id}", json={"title": "", "body": "B"}) + assert r2.status_code == 422 + + +def test_delete_note_returns_204() -> None: + client = _client() + r = client.post("/notes", json={"title": "T", "body": "B"}) + note_id = r.json()["id"] + + r2 = client.delete(f"/notes/{note_id}") + assert r2.status_code == 204 + + r3 = client.get(f"/notes/{note_id}") + assert r3.status_code == 404 + + +def test_delete_nonexistent_note_returns_404() -> None: + r = _client().delete("/notes/9999") + assert r.status_code == 404 + + def test_health() -> None: r = _client().get("/health") assert r.status_code == 200