Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ qa-api-python/
│ ├── assets/ # Imagens usadas no README
│ └── test-cases.md # Matriz de rastreabilidade dos test cases
├── features/ # Cenários em Gherkin (.feature)
├── steps/ # Implementação dos steps do pytest-bdd (testes de integração, batem na API real)
├── steps/ # Steps do pytest-bdd (testes de integração, batem na API real)
│ ├── test_<feature>.py # Um módulo por arquivo .feature, com os steps exclusivos dele
│ ├── common_steps.py # Steps usados por mais de uma feature (tokens, booking criado, 403, 405)
│ └── booking_data.py # Booking padrão e payloads com campos inválidos
├── tests/
│ └── unit/ # Testes unitários isolados de api/ e models/ (com requests-mock, sem rede)
├── api/ # API Clients (AuthApiClient e BookingApiClient, sobre a base comum BaseApiClient)
Expand Down
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from api.booking_api_client import BookingApiClient
from models.booking import Booking

# Steps compartilhados entre features (tokens, booking criado, 403 e 405).
pytest_plugins = ["steps.common_steps"]

BASE_URL = os.environ.get("BASE_URL", "https://restful-booker.herokuapp.com")
# O padrão são as credenciais públicas da documentação da restful-booker, para
# o projeto rodar sem configuração. Outro ambiente pode usar credenciais próprias.
Expand Down
26 changes: 26 additions & 0 deletions steps/booking_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from datetime import date
from typing import Any

from models.booking import Booking, BookingDates


def default_booking() -> Booking:
return Booking(
firstname="Fulano",
lastname="Ciclano",
totalprice=150,
depositpaid=True,
bookingdates=BookingDates(checkin=date(2026, 1, 1), checkout=date(2026, 1, 5)),
additionalneeds="Breakfast",
)


# Payload cru (sem passar pelo Pydantic) a partir do booking padrão, trocando
# só o que interessa ao cenário: um campo com valor inválido (overrides) ou um
# campo obrigatório ausente (without).
def booking_payload(*, without: str | None = None, **overrides: Any) -> dict[str, Any]:
payload = default_booking().model_dump(mode="json", exclude_none=True)
payload.update(overrides)
if without is not None:
del payload[without]
return payload
40 changes: 40 additions & 0 deletions steps/common_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Steps usados por mais de uma feature. Ficam disponíveis para todos os
# módulos de teste via pytest_plugins, no conftest.py da raiz.
from pytest_bdd import given, then

from conftest import Context
from models.booking import CreateBookingResponse
from steps.booking_data import default_booking


@given("que ele não possui nenhum token de autenticação")
def sem_token_de_autenticacao(context: Context) -> None:
context.token = None


@given("que ele possui um token de autenticação válido")
def com_token_de_autenticacao_valido(context: Context) -> None:
context.token = context.auth_client.get_valid_token()


@given("que ele possui um token de autenticação inválido")
def com_token_de_autenticacao_invalido(context: Context) -> None:
context.token = "token-invalido-qualquer"


@given("que existe um booking criado")
def existe_um_booking_criado(context: Context) -> None:
context.booking_data = default_booking()
response = context.booking_client.create_booking(context.booking_data)
body = CreateBookingResponse.model_validate_json(response.text)
context.booking_id = body.bookingid


@then("a resposta deve indicar acesso não autorizado")
def resposta_indica_acesso_nao_autorizado(context: Context) -> None:
assert context.response.status_code == 403


@then("a resposta deve indicar que o método não é permitido")
def resposta_indica_metodo_nao_permitido(context: Context) -> None:
assert context.response.status_code == 405
29 changes: 29 additions & 0 deletions steps/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pytest_bdd import parsers, scenarios, then, when

from conftest import Context
from models.booking import AuthResponse

scenarios("../features/auth.feature")


@when("ele solicita um token com credenciais válidas")
def solicita_um_token_com_credenciais_validas(context: Context) -> None:
client = context.auth_client
context.last_response = client.create_token(client.username, client.password)


@when(parsers.parse('ele solicita um token com o usuário "{username}" e a senha "{password}"'))
def solicita_um_token(context: Context, username: str, password: str) -> None:
context.last_response = context.auth_client.create_token(username, password)


@then("ele deve receber um token de autenticação válido")
def deve_receber_um_token_valido(context: Context) -> None:
assert context.response.status_code == 200
body = AuthResponse.model_validate_json(context.response.text)
assert body.token


@then("a resposta deve indicar credenciais inválidas")
def resposta_indica_credenciais_invalidas(context: Context) -> None:
assert context.response.json()["reason"] == "Bad credentials"
Loading
Loading