Skip to content

Commit 4daaa47

Browse files
committed
feat: Refactor database models and update API endpoints for Aluno and Curso; add test configurations
1 parent 9744390 commit 4daaa47

File tree

10 files changed

+150
-37
lines changed

10 files changed

+150
-37
lines changed

database.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from sqlalchemy import create_engine
2-
from sqlalchemy.ext.declarative import declarative_base
3-
from sqlalchemy.orm import sessionmaker
2+
from sqlalchemy.orm import sessionmaker, declarative_base
43
import os
54

65
# Caminho para o arquivo SQLite (pode ser ajustado conforme necessário)
@@ -12,13 +11,11 @@
1211
)
1312
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
1413

15-
Base = declarative_base()
14+
Base = declarative_base() # type: ignore
1615

1716
def get_db():
1817
db = SessionLocal()
1918
try:
2019
yield db
2120
finally:
2221
db.close()
23-
24-

escola.db

0 Bytes
Binary file not shown.

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
pythonpath = .

requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pytest
2+
httpx

routers/alunos.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import APIRouter, Depends, HTTPException
22
from sqlalchemy.orm import Session
33
from typing import List, Union
4-
from schemas import Aluno
4+
from schemas import Aluno, AlunoBase
55
from models import Aluno as ModelAluno
66
from database import get_db
77

@@ -14,7 +14,7 @@ def read_alunos(db: Session = Depends(get_db)):
1414
1515
"""
1616
alunos = db.query(ModelAluno).all()
17-
return [Aluno.from_orm(aluno) for aluno in alunos]
17+
return [Aluno.model_validate(aluno) for aluno in alunos]
1818

1919
@alunos_router.get("/alunos/{aluno_id}", response_model=Aluno)
2020
def read_aluno(aluno_id: int, db: Session = Depends(get_db)):
@@ -30,24 +30,24 @@ def read_aluno(aluno_id: int, db: Session = Depends(get_db)):
3030
db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first()
3131
if db_aluno is None:
3232
raise HTTPException(status_code=404, detail="Aluno não encontrado")
33-
return Aluno.from_orm(db_aluno)
33+
return Aluno.model_validate(db_aluno)
3434

3535
@alunos_router.post("/alunos", response_model=Aluno)
36-
def create_aluno(aluno: Aluno, db: Session = Depends(get_db)):
36+
def create_aluno(aluno: AlunoBase, db: Session = Depends(get_db)):
3737
"""
3838
Cria um novo aluno com os dados fornecidos.
3939
4040
Args:
41-
aluno: Dados do aluno a ser criado.
41+
aluno: Dados do aluno (sem id) a ser criado.
4242
4343
Returns:
4444
Aluno: aluno criado.
4545
"""
46-
db_aluno = ModelAluno(**aluno.dict(exclude={"id"}))
46+
db_aluno = ModelAluno(**aluno.model_dump())
4747
db.add(db_aluno)
4848
db.commit()
4949
db.refresh(db_aluno)
50-
return Aluno.from_orm(db_aluno)
50+
return Aluno.model_validate(db_aluno)
5151

5252
@alunos_router.put("/alunos/{aluno_id}", response_model=Aluno)
5353
def update_aluno(aluno_id: int, aluno: Aluno, db: Session = Depends(get_db)):
@@ -68,12 +68,12 @@ def update_aluno(aluno_id: int, aluno: Aluno, db: Session = Depends(get_db)):
6868
if db_aluno is None:
6969
raise HTTPException(status_code=404, detail="Aluno não encontrado")
7070

71-
for key, value in aluno.dict(exclude_unset=True).items():
71+
for key, value in aluno.model_dump(exclude_unset=True).items():
7272
setattr(db_aluno, key, value)
7373

7474
db.commit()
7575
db.refresh(db_aluno)
76-
return Aluno.from_orm(db_aluno)
76+
return Aluno.model_validate(db_aluno)
7777

7878
@alunos_router.delete("/alunos/{aluno_id}", response_model=Aluno)
7979
def delete_aluno(aluno_id: int, db: Session = Depends(get_db)):
@@ -93,7 +93,7 @@ def delete_aluno(aluno_id: int, db: Session = Depends(get_db)):
9393
if db_aluno is None:
9494
raise HTTPException(status_code=404, detail="Aluno não encontrado")
9595

96-
aluno_deletado = Aluno.from_orm(db_aluno)
96+
aluno_deletado = Aluno.model_validate(db_aluno)
9797

9898
db.delete(db_aluno)
9999
db.commit()
@@ -120,9 +120,9 @@ def read_aluno_por_nome(nome_aluno: str, db: Session = Depends(get_db)):
120120
raise HTTPException(status_code=404, detail="Nenhum aluno encontrado com esse nome")
121121

122122
if len(db_alunos) == 1: # Retorna um único Aluno se houver apenas uma correspondência
123-
return Aluno.from_orm(db_alunos[0])
123+
return Aluno.model_validate(db_alunos[0])
124124

125-
return [Aluno.from_orm(aluno) for aluno in db_alunos]
125+
return [Aluno.model_validate(aluno) for aluno in db_alunos]
126126

127127
@alunos_router.get("/alunos/email/{email_aluno}", response_model=Aluno)
128128
def read_aluno_por_email(email_aluno: str, db: Session = Depends(get_db)):
@@ -143,4 +143,4 @@ def read_aluno_por_email(email_aluno: str, db: Session = Depends(get_db)):
143143
if db_aluno is None:
144144
raise HTTPException(status_code=404, detail="Nenhum aluno encontrado com esse email")
145145

146-
return Aluno.from_orm(db_aluno)
146+
return Aluno.model_validate(db_aluno)

routers/cursos.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@
1010
@cursos_router.get("/cursos", response_model=List[Curso])
1111
def read_cursos(db: Session = Depends(get_db)):
1212
cursos = db.query(ModelCurso).all()
13-
return [Curso.from_orm(curso) for curso in cursos]
13+
return [Curso.model_validate(curso) for curso in cursos]
1414

1515
@cursos_router.post("/cursos", response_model=Curso)
1616
def create_curso(curso: Curso, db: Session = Depends(get_db)):
17-
db_curso = ModelCurso(**curso.dict(exclude={"id"}))
17+
db_curso = ModelCurso(**curso.model_dump(exclude={"id"}))
1818
db.add(db_curso)
1919
db.commit()
2020
db.refresh(db_curso)
21-
return Curso.from_orm(db_curso)
21+
return Curso.model_validate(db_curso)
2222

2323
@cursos_router.put("/cursos/{codigo_curso}", response_model=Curso)
2424
def update_curso(codigo_curso: str, curso: Curso, db: Session = Depends(get_db)):
2525
db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first()
2626
if db_curso is None:
2727
raise HTTPException(status_code=404, detail="Curso não encontrado")
2828

29-
for key, value in curso.dict(exclude_unset=True, exclude={"id"}).items():
29+
for key, value in curso.model_dump(exclude_unset=True, exclude={"id"}).items():
3030
setattr(db_curso, key, value)
3131

3232
db.commit()
@@ -38,7 +38,7 @@ def read_curso_por_codigo(codigo_curso: str, db: Session = Depends(get_db)):
3838
db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first()
3939
if db_curso is None:
4040
raise HTTPException(status_code=404, detail="Nenhum curso encontrado com esse código")
41-
return Curso.from_orm(db_curso)
41+
return Curso.model_validate(db_curso)
4242

4343

4444
# Não buscar um curso pelo ID nem deletar em nenhuma hipótese

routers/matriculas.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ def create_matricula(matricula: Matricula, db: Session = Depends(get_db)):
1616
if db_aluno is None or db_curso is None:
1717
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Aluno ou Curso não encontrado")
1818

19-
db_matricula = ModelMatricula(**matricula.dict())
19+
db_matricula = ModelMatricula(**matricula.model_dump())
2020
db.add(db_matricula)
2121
db.commit()
2222
db.refresh(db_matricula)
23-
return Matricula.from_orm(db_matricula)
23+
return Matricula.model_validate(db_matricula)
2424

2525

2626

schemas.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,29 @@
1-
from pydantic import BaseModel, EmailStr
1+
from pydantic import BaseModel, EmailStr, ConfigDict
22
from typing import List
33

44
class Matricula(BaseModel):
5+
model_config = ConfigDict(from_attributes=True)
6+
57
aluno_id: int
68
curso_id: int
79

8-
class Config:
9-
from_attributes = True
10-
from_attributes = True
11-
1210
Matriculas = List[Matricula]
1311

14-
class Aluno(BaseModel):
12+
class AlunoBase(BaseModel):
1513
nome: str
1614
email: EmailStr
1715
telefone: str
1816

19-
class Config:
20-
from_attributes = True
17+
class Aluno(AlunoBase):
18+
model_config = ConfigDict(from_attributes=True)
19+
id: int
2120

2221
Alunos = List[Aluno]
2322

2423
class Curso(BaseModel):
24+
model_config = ConfigDict(from_attributes=True)
25+
2526
nome: str
2627
codigo: str
2728
descricao: str
28-
29-
class Config:
30-
from_attributes = True
31-
3229
Cursos = List[Curso]

tests/conftest.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from sqlalchemy import create_engine
4+
from sqlalchemy.orm import sessionmaker
5+
import os
6+
7+
from app import app
8+
from database import Base, get_db
9+
10+
# --- Configuração do Banco de Dados de Teste ---
11+
TEST_DATABASE_URL = "sqlite:///./test_escola.db"
12+
13+
engine = create_engine(
14+
TEST_DATABASE_URL, connect_args={"check_same_thread": False}
15+
)
16+
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
17+
18+
# --- Fixture para gerenciar a criação e destruição do banco de dados ---
19+
@pytest.fixture(scope="session")
20+
def setup_database():
21+
"""
22+
Cria as tabelas do banco de dados de teste antes de todos os testes
23+
e apaga o arquivo do banco de dados depois que todos os testes terminam.
24+
"""
25+
# Garante que o banco de dados de teste antigo seja removido, se existir
26+
if os.path.exists("./test_escola.db"):
27+
os.remove("./test_escola.db")
28+
29+
# Cria todas as tabelas
30+
Base.metadata.create_all(bind=engine)
31+
32+
yield # Aqui é onde os testes são executados
33+
34+
# --- Teardown ---
35+
# Primeiro, garantimos que todas as conexões com o banco de dados sejam fechadas.
36+
# O objeto 'engine' gerencia um pool de conexões, e precisamos descartá-lo para liberar o arquivo.
37+
engine.dispose()
38+
39+
# Apaga o banco de dados de teste após a conclusão de todos os testes
40+
if os.path.exists("./test_escola.db"):
41+
os.remove("./test_escola.db")
42+
43+
# --- Fixture para fornecer um cliente de API com uma sessão de banco de dados isolada ---
44+
@pytest.fixture(scope="function")
45+
def client(setup_database):
46+
"""
47+
Fornece um TestClient que usa uma sessão transacional para cada teste.
48+
Isso garante que cada teste seja executado de forma isolada.
49+
"""
50+
connection = engine.connect()
51+
transaction = connection.begin()
52+
db_session = TestingSessionLocal(bind=connection)
53+
54+
# Sobrescreve a dependência get_db para usar a sessão de teste
55+
app.dependency_overrides[get_db] = lambda: db_session
56+
57+
yield TestClient(app)
58+
59+
# Desfaz a transação, limpando o banco de dados, e fecha a conexão
60+
db_session.close()
61+
transaction.rollback()
62+
connection.close()
63+
app.dependency_overrides.clear()

tests/test_api.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from fastapi.testclient import TestClient
2+
3+
def test_read_main(client: TestClient):
4+
"""Testa se a rota raiz da documentação está acessível."""
5+
response = client.get("/docs")
6+
assert response.status_code == 200
7+
8+
def test_create_and_read_aluno(client: TestClient):
9+
"""Testa a criação de um aluno e a subsequente leitura pelo ID."""
10+
novo_aluno_data = {
11+
"nome": "Aluno Teste E2E",
12+
"email": "teste.e2e@example.com",
13+
"telefone": "11987654321"
14+
}
15+
16+
# 1. Criar o aluno (POST)
17+
response_create = client.post("/alunos", json=novo_aluno_data)
18+
assert response_create.status_code == 200 # Idealmente, seria 201 (Created)
19+
data = response_create.json()
20+
assert data["nome"] == novo_aluno_data["nome"]
21+
assert data["email"] == novo_aluno_data["email"]
22+
assert "id" in data
23+
aluno_id = data["id"]
24+
25+
# 2. Ler o aluno recém-criado pelo ID retornado
26+
response_read = client.get(f"/alunos/{aluno_id}")
27+
assert response_read.status_code == 200
28+
read_data = response_read.json()
29+
assert read_data["nome"] == novo_aluno_data["nome"]
30+
assert read_data["email"] == novo_aluno_data["email"]
31+
assert read_data["id"] == aluno_id
32+
33+
def test_read_alunos(client: TestClient):
34+
"""Testa o endpoint de listagem de alunos, garantindo que ele retorne os dados criados no teste."""
35+
# Como cada teste é isolado, o banco de dados começa vazio.
36+
# Primeiro, criamos alguns dados para o teste.
37+
client.post("/alunos", json={"nome": "Aluno 1", "email": "aluno1@test.com", "telefone": "111"})
38+
client.post("/alunos", json={"nome": "Aluno 2", "email": "aluno2@test.com", "telefone": "222"})
39+
40+
response = client.get("/alunos")
41+
assert response.status_code == 200
42+
data = response.json()
43+
assert isinstance(data, list)
44+
assert len(data) == 2
45+
assert data[0]["nome"] == "Aluno 1"
46+
assert data[1]["nome"] == "Aluno 2"
47+
48+
def test_read_non_existent_aluno(client: TestClient):
49+
"""Testa a busca por um aluno que não existe em um banco de dados vazio."""
50+
response = client.get("/alunos/999999")
51+
assert response.status_code == 404
52+
assert response.json() == {"detail": "Aluno não encontrado"}

0 commit comments

Comments
 (0)