Golden example: blog (taso 2, relaatiot Author → Post)
13 testiä, ForeignKey-relaatio, uniikki suomalainen testidata (Aleksis Kivi, Tove Jansson jne). Testattu Docker-kontissa.
This commit is contained in:
110
network-poc/tests/golden-examples/blog/main.py
Normal file
110
network-poc/tests/golden-examples/blog/main.py
Normal file
@@ -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()
|
||||||
45
network-poc/tests/golden-examples/blog/models.py
Normal file
45
network-poc/tests/golden-examples/blog/models.py
Normal file
@@ -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)
|
||||||
37
network-poc/tests/golden-examples/blog/schemas.py
Normal file
37
network-poc/tests/golden-examples/blog/schemas.py
Normal file
@@ -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)
|
||||||
164
network-poc/tests/golden-examples/blog/test_main.py
Normal file
164
network-poc/tests/golden-examples/blog/test_main.py
Normal file
@@ -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"]
|
||||||
Reference in New Issue
Block a user