From 8efbf9629519fad3938c537ae74619ecd28862b5 Mon Sep 17 00:00:00 2001 From: jaakko Date: Tue, 14 Apr 2026 08:03:21 +0300 Subject: [PATCH] =?UTF-8?q?Golden=20example:=20blog=20(taso=202,=20relaati?= =?UTF-8?q?ot=20Author=20=E2=86=92=20Post)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 13 testiä, ForeignKey-relaatio, uniikki suomalainen testidata (Aleksis Kivi, Tove Jansson jne). Testattu Docker-kontissa. --- .../tests/golden-examples/blog/main.py | 110 ++++++++++++ .../tests/golden-examples/blog/models.py | 45 +++++ .../tests/golden-examples/blog/schemas.py | 37 ++++ .../tests/golden-examples/blog/test_main.py | 164 ++++++++++++++++++ 4 files changed, 356 insertions(+) create mode 100644 network-poc/tests/golden-examples/blog/main.py create mode 100644 network-poc/tests/golden-examples/blog/models.py create mode 100644 network-poc/tests/golden-examples/blog/schemas.py create mode 100644 network-poc/tests/golden-examples/blog/test_main.py diff --git a/network-poc/tests/golden-examples/blog/main.py b/network-poc/tests/golden-examples/blog/main.py new file mode 100644 index 0000000..b31c697 --- /dev/null +++ b/network-poc/tests/golden-examples/blog/main.py @@ -0,0 +1,110 @@ +"""FastAPI CRUD — kaksi endpoint-settiä, Author ja Post.""" + +from fastapi import FastAPI, Depends, HTTPException +from sqlalchemy.orm import Session + +from models import SessionLocal, Author, Post +from schemas import AuthorCreate, AuthorResponse, PostCreate, PostResponse + +app = FastAPI() + + +def get_db(): + """Tietokantasessio per pyyntö.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# --- Author --- + + +@app.post("/authors/", response_model=AuthorResponse, status_code=201) +def create_author(item: AuthorCreate, db: Session = Depends(get_db)): + db_item = Author(**item.model_dump()) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + + +@app.get("/authors/", response_model=list[AuthorResponse]) +def list_authors(db: Session = Depends(get_db)): + return db.query(Author).all() + + +@app.get("/authors/{item_id}", response_model=AuthorResponse) +def get_author(item_id: int, db: Session = Depends(get_db)): + item = db.query(Author).filter(Author.id == item_id).first() + if not item: + raise HTTPException(status_code=404, detail="Author not found") + return item + + +@app.put("/authors/{item_id}", response_model=AuthorResponse) +def update_author(item_id: int, item: AuthorCreate, db: Session = Depends(get_db)): + db_item = db.query(Author).filter(Author.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404, detail="Author not found") + for key, value in item.model_dump().items(): + setattr(db_item, key, value) + db.commit() + db.refresh(db_item) + return db_item + + +@app.delete("/authors/{item_id}", status_code=204) +def delete_author(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(Author).filter(Author.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404, detail="Author not found") + db.delete(db_item) + db.commit() + + +# --- Post --- + + +@app.post("/posts/", response_model=PostResponse, status_code=201) +def create_post(item: PostCreate, db: Session = Depends(get_db)): + db_item = Post(**item.model_dump()) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + + +@app.get("/posts/", response_model=list[PostResponse]) +def list_posts(db: Session = Depends(get_db)): + return db.query(Post).all() + + +@app.get("/posts/{item_id}", response_model=PostResponse) +def get_post(item_id: int, db: Session = Depends(get_db)): + item = db.query(Post).filter(Post.id == item_id).first() + if not item: + raise HTTPException(status_code=404, detail="Post not found") + return item + + +@app.put("/posts/{item_id}", response_model=PostResponse) +def update_post(item_id: int, item: PostCreate, db: Session = Depends(get_db)): + db_item = db.query(Post).filter(Post.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404, detail="Post not found") + for key, value in item.model_dump().items(): + setattr(db_item, key, value) + db.commit() + db.refresh(db_item) + return db_item + + +@app.delete("/posts/{item_id}", status_code=204) +def delete_post(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(Post).filter(Post.id == item_id).first() + if not db_item: + raise HTTPException(status_code=404, detail="Post not found") + db.delete(db_item) + db.commit() diff --git a/network-poc/tests/golden-examples/blog/models.py b/network-poc/tests/golden-examples/blog/models.py new file mode 100644 index 0000000..60d343c --- /dev/null +++ b/network-poc/tests/golden-examples/blog/models.py @@ -0,0 +1,45 @@ +"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, ForeignKey-relaatiot.""" + +from datetime import datetime + +from sqlalchemy import String, Text, DateTime, ForeignKey, create_engine +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker + +DATABASE_URL = "sqlite:///./app.db" +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +class Base(DeclarativeBase): + pass + + +class Author(Base): + """Kirjoittaja — nimi, sähköposti ja bio.""" + + __tablename__ = "authors" + + id: Mapped[int] = mapped_column(primary_key=True, index=True) + name: Mapped[str] = mapped_column(String(255)) + email: Mapped[str] = mapped_column(String(255), unique=True) + bio: Mapped[str | None] = mapped_column(Text, default=None) + + posts: Mapped[list["Post"]] = relationship(back_populates="author") + + +class Post(Base): + """Blogipostaus — otsikko, sisältö, kirjoittaja, julkaisuaika ja tila.""" + + __tablename__ = "posts" + + id: Mapped[int] = mapped_column(primary_key=True, index=True) + title: Mapped[str] = mapped_column(String(255)) + content: Mapped[str] = mapped_column(Text) + author_id: Mapped[int] = mapped_column(ForeignKey("authors.id")) + published_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) + status: Mapped[str] = mapped_column(String(20), default="draft") + + author: Mapped["Author"] = relationship(back_populates="posts") + + +Base.metadata.create_all(bind=engine) diff --git a/network-poc/tests/golden-examples/blog/schemas.py b/network-poc/tests/golden-examples/blog/schemas.py new file mode 100644 index 0000000..4e4cebe --- /dev/null +++ b/network-poc/tests/golden-examples/blog/schemas.py @@ -0,0 +1,37 @@ +"""Pydantic v2 -skeemat — Create sisääntulolle, Response vastaukselle.""" + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + + +class AuthorCreate(BaseModel): + """Uuden kirjoittajan luonti. Pakolliset: name, email.""" + + name: str + email: str + bio: str | None = None + + +class AuthorResponse(AuthorCreate): + """Palautettava kirjoittaja — sisältää id:n.""" + + id: int + model_config = ConfigDict(from_attributes=True) + + +class PostCreate(BaseModel): + """Uuden postauksen luonti. Pakolliset: title, content, author_id.""" + + title: str + content: str + author_id: int + published_at: datetime | None = None + status: str = "draft" + + +class PostResponse(PostCreate): + """Palautettava postaus — sisältää id:n.""" + + id: int + model_config = ConfigDict(from_attributes=True) diff --git a/network-poc/tests/golden-examples/blog/test_main.py b/network-poc/tests/golden-examples/blog/test_main.py new file mode 100644 index 0000000..1510720 --- /dev/null +++ b/network-poc/tests/golden-examples/blog/test_main.py @@ -0,0 +1,164 @@ +"""Pytest — TestClient, erillinen test.db, uniikki data per testi.""" + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from main import app, get_db +from models import Base + +test_engine = create_engine( + "sqlite:///./test.db", connect_args={"check_same_thread": False} +) +TestSession = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) +Base.metadata.create_all(bind=test_engine) + + +def override_get_db(): + db = TestSession() + try: + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db +client = TestClient(app) + + +def _create_author(name="Eino Leino", email=None): + """Apufunktio kirjoittajan luomiseen testeissä.""" + if email is None: + email = f"{name.lower().replace(' ', '.')}@example.com" + return client.post( + "/authors/", json={"name": name, "email": email} + ).json() + + +# --- Author-testit --- + + +def test_create_author(): + response = client.post( + "/authors/", + json={"name": "Aleksis Kivi", "email": "aleksis@example.com", "bio": "Suomen kansalliskirjailija"}, + ) + assert response.status_code == 201 + assert response.json()["name"] == "Aleksis Kivi" + assert response.json()["bio"] == "Suomen kansalliskirjailija" + assert "id" in response.json() + + +def test_list_authors(): + _create_author("Minna Canth", "minna.canth@example.com") + response = client.get("/authors/") + assert response.status_code == 200 + assert len(response.json()) >= 1 + + +def test_get_author_by_id(): + created = _create_author("Väinö Linna", "vaino.linna@example.com") + response = client.get(f"/authors/{created['id']}") + assert response.status_code == 200 + assert response.json()["id"] == created["id"] + + +def test_get_author_not_found(): + response = client.get("/authors/99999") + assert response.status_code == 404 + + +def test_update_author(): + created = _create_author("Vanha Nimi", "vanha.nimi@example.com") + response = client.put( + f"/authors/{created['id']}", + json={"name": "Uusi Nimi", "email": "uusi.nimi@example.com"}, + ) + assert response.status_code == 200 + assert response.json()["name"] == "Uusi Nimi" + + +def test_delete_author(): + created = _create_author("Poistettava Kirjailija", "poistettava@example.com") + response = client.delete(f"/authors/{created['id']}") + assert response.status_code == 204 + response = client.get(f"/authors/{created['id']}") + assert response.status_code == 404 + + +# --- Post-testit --- + + +def test_create_post(): + author = _create_author("Tove Jansson", "tove.jansson@example.com") + response = client.post( + "/posts/", + json={"title": "Muumipeikko ja pyrstötähti", "content": "Eräänä aamuna...", "author_id": author["id"]}, + ) + assert response.status_code == 201 + assert response.json()["title"] == "Muumipeikko ja pyrstötähti" + assert response.json()["author_id"] == author["id"] + assert response.json()["status"] == "draft" + + +def test_list_posts(): + author = _create_author("Juhani Aho", "juhani.aho@example.com") + client.post( + "/posts/", + json={"title": "Rautatie", "content": "Junasta kertova novelli.", "author_id": author["id"]}, + ) + response = client.get("/posts/") + assert response.status_code == 200 + assert len(response.json()) >= 1 + + +def test_get_post_by_id(): + author = _create_author("Elias Lönnrot", "elias.lonnrot@example.com") + created = client.post( + "/posts/", + json={"title": "Kalevala", "content": "Vaka vanha Väinämöinen.", "author_id": author["id"]}, + ).json() + response = client.get(f"/posts/{created['id']}") + assert response.status_code == 200 + assert response.json()["id"] == created["id"] + + +def test_get_post_not_found(): + response = client.get("/posts/99999") + assert response.status_code == 404 + + +def test_update_post(): + author = _create_author("Joel Lehtonen", "joel.lehtonen@example.com") + created = client.post( + "/posts/", + json={"title": "Vanha otsikko", "content": "Alkuperäinen teksti.", "author_id": author["id"]}, + ).json() + response = client.put( + f"/posts/{created['id']}", + json={"title": "Päivitetty otsikko", "content": "Muokattu teksti.", "author_id": author["id"], "status": "published"}, + ) + assert response.status_code == 200 + assert response.json()["title"] == "Päivitetty otsikko" + assert response.json()["status"] == "published" + + +def test_delete_post(): + author = _create_author("Aino Kallas", "aino.kallas@example.com") + created = client.post( + "/posts/", + json={"title": "Poistettava postaus", "content": "Tämä poistetaan.", "author_id": author["id"]}, + ).json() + response = client.delete(f"/posts/{created['id']}") + assert response.status_code == 204 + response = client.get(f"/posts/{created['id']}") + assert response.status_code == 404 + + +def test_post_belongs_to_author(): + author = _create_author("Sofi Oksanen", "sofi.oksanen@example.com") + post = client.post( + "/posts/", + json={"title": "Puhdistus", "content": "Romaani Virosta.", "author_id": author["id"]}, + ).json() + assert post["author_id"] == author["id"]