183 lines
6.2 KiB
Markdown
183 lines
6.2 KiB
Markdown
# 5 — QA (qa) — review
|
|
|
|
**Malli:** `qwen-coder`
|
|
|
|
## System Prompt
|
|
|
|
```
|
|
You are a QA engineer responsible for code review and automated testing.
|
|
|
|
CODE REVIEW CHECKLIST:
|
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
|
3. TYPES: All function parameters have type hints, return types specified
|
|
4. ERRORS: Every db query that can return None has a 404 check
|
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
|
|
|
WHEN REVIEWING:
|
|
- If all checks pass: respond "LGTM"
|
|
- If issues found: list each as "ISSUE: filename.py: description"
|
|
- Be specific and actionable, not vague
|
|
|
|
WHEN WRITING TESTS:
|
|
- ALWAYS import app from main.py: from main import app, get_db
|
|
- ALWAYS import Base from models.py: from models import Base
|
|
- NEVER redefine the app, models, or routes in the test file
|
|
- Use file-based SQLite for test isolation: sqlite:///./test.db
|
|
- Override the get_db dependency to use test database
|
|
- Use TestClient from fastapi.testclient
|
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
|
- Each test should create its own data, not depend on other tests
|
|
```
|
|
|
|
## Syöte
|
|
|
|
```
|
|
You are a QA engineer responsible for code review and automated testing.
|
|
|
|
CODE REVIEW CHECKLIST:
|
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
|
3. TYPES: All function parameters have type hints, return types specified
|
|
4. ERRORS: Every db query that can return None has a 404 check
|
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
|
|
|
WHEN REVIEWING:
|
|
- If all checks pass: respond "LGTM"
|
|
- If issues found: list each as "ISSUE: filename.py: description"
|
|
- Be specific and actionable, not vague
|
|
|
|
WHEN WRITING TESTS:
|
|
- ALWAYS import app from main.py: from main import app, get_db
|
|
- ALWAYS import Base from models.py: from models import Base
|
|
- NEVER redefine the app, models, or routes in the test file
|
|
- Use file-based SQLite for test isolation: sqlite:///./test.db
|
|
- Override the get_db dependency to use test database
|
|
- Use TestClient from fastapi.testclient
|
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
|
- Each test should create its own data, not depend on other tests
|
|
|
|
Review this project code for issues. If everything is correct, respond with "LGTM". Otherwise list issues as "ISSUE: filename.py: description".
|
|
|
|
--- models.py ---
|
|
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import sessionmaker
|
|
from datetime import datetime
|
|
|
|
DATABASE_URL = "sqlite:///./todo.db"
|
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
Base = declarative_base()
|
|
|
|
class Task(Base):
|
|
__tablename__ = "tasks"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
title = Column(String(100), nullable=False)
|
|
description = Column(Text)
|
|
status = Column(String(20), default="pending")
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
--- schemas.py ---
|
|
from pydantic import BaseModel
|
|
|
|
class TaskCreate(BaseModel):
|
|
title: str
|
|
description: str | None = None
|
|
status: str = "pending"
|
|
|
|
class TaskResponse(TaskCreate):
|
|
id: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
--- main.py ---
|
|
from fastapi import FastAPI, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from models import Base, engine, SessionLocal, Task
|
|
from schemas import TaskCreate, TaskResponse
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
app = FastAPI()
|
|
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
|
|
def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
|
db_task = Task(**task.model_dump())
|
|
db.add(db_task)
|
|
db.commit()
|
|
db.refresh(db_task)
|
|
return db_task
|
|
|
|
@app.get("/tasks/", response_model=list[TaskResponse])
|
|
def list_tasks(status: str | None = Query(None), db: Session = Depends(get_db)):
|
|
if status:
|
|
tasks = db.query(Task).filter(Task.status == status).all()
|
|
else:
|
|
tasks = db.query(Task).all()
|
|
return tasks
|
|
|
|
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
|
def get_task(task_id: int, db: Session = Depends(get_db)):
|
|
task = db.query(Task).filter(Task.id == task_id).first()
|
|
if not task:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return task
|
|
|
|
@app.put("/tasks/{task_id}", response_model=TaskResponse)
|
|
def update_task(task_id: int, task: TaskCreate, db: Session = Depends(get_db)):
|
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
|
if not db_task:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
for key, value in task.model_dump().items():
|
|
setattr(db_task, key, value)
|
|
db.commit()
|
|
db.refresh(db_task)
|
|
return db_task
|
|
|
|
@app.delete("/tasks/{task_id}", status_code=204)
|
|
def delete_task(task_id: int, db: Session = Depends(get_db)):
|
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
|
if not db_task:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
db.delete(db_task)
|
|
db.commit()
|
|
|
|
--- pyproject.toml ---
|
|
[project]
|
|
name = "todo-app"
|
|
version = "0.1.0"
|
|
requires-python = ">=3.11"
|
|
dependencies = [
|
|
"fastapi",
|
|
"uvicorn[standard]",
|
|
"sqlalchemy",
|
|
]
|
|
|
|
[project.scripts]
|
|
dev = "uvicorn main:app --reload"
|
|
```
|
|
|
|
## Tulos
|
|
|
|
```
|
|
LGTM
|
|
```
|