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
9 changes: 9 additions & 0 deletions api/booking_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ def update_booking(self, booking_id: int, booking: Booking, token: str) -> reque
cookies={"token": token},
)

# Variante crua de update_booking, para os testes de payload inválido no PUT
# (campos ausentes ou com tipo errado, que não passariam pelo tipo Booking).
def update_booking_raw(self, booking_id: int, payload: dict[str, Any], token: str) -> requests.Response:
return self.session.put(
f"{self.base_url}/booking/{booking_id}",
json=payload,
cookies={"token": token},
)

def partial_update_booking(self, booking_id: int, partial_booking: dict[str, Any], token: str) -> requests.Response:
return self.session.patch(
f"{self.base_url}/booking/{booking_id}",
Expand Down
11 changes: 11 additions & 0 deletions docs/test-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,20 @@ Legenda:
| TC-014 | Remoção | Tentar remover um booking que não existe | Negativo | Baixa | Automatizado | [delete_booking.feature:16](../features/delete_booking.feature#L16) |
| TC-015 | Criação | Tentar criar booking sem um campo obrigatório (bug) | Negativo | Média | Automatizado | [create_booking.feature:13](../features/create_booking.feature#L13) |
| TC-016 | Criação | Criar booking com totalprice em formato inválido | Negativo | Média | Automatizado | [create_booking.feature:24](../features/create_booking.feature#L24) |
| TC-017 | Criação | Criar booking com depositpaid em formato inválido | Negativo | Média | Automatizado | [create_booking.feature:39](../features/create_booking.feature#L39) |
| TC-018 | Criação | Criar booking com checkin em formato inválido | Negativo | Média | Automatizado | [create_booking.feature:49](../features/create_booking.feature#L49) |
| TC-019 | Criação | Criar booking com totalprice negativo | Negativo | Baixa | Automatizado | [create_booking.feature:58](../features/create_booking.feature#L58) |
| TC-020 | Atualização | Tentar atualizar booking com corpo vazio (PUT) | Negativo | Alta | Automatizado | [update_booking.feature:47](../features/update_booking.feature#L47) |
| TC-021 | Atualização | Atualizar booking com totalprice em formato inválido | Negativo | Média | Automatizado | [update_booking.feature:59](../features/update_booking.feature#L59) |
| TC-022 | Atualização | Tentar atualizar booking sem o campo bookingdates | Negativo | Alta | Automatizado | [update_booking.feature:67](../features/update_booking.feature#L67) |
| TC-023 | Atualização | Atualizar parcialmente com lastname em formato inválido | Negativo | Média | Automatizado | [update_booking.feature:77](../features/update_booking.feature#L77) |
| TC-024 | Atualização | Atualizar parcialmente com corpo vazio não altera nada | Funcional | Baixa | Automatizado | [update_booking.feature:87](../features/update_booking.feature#L87) |
| TC-025 | Atualização | Tentar atualizar (PUT/PATCH) um booking inexistente | Negativo | Média | Automatizado | [update_booking.feature:100](../features/update_booking.feature#L100) |

`BookingApiClient.get_booking_ids` também aceita filtro por `checkin`/`checkout`, mas o TC-006 cobre só `firstname`/`lastname`: o filtro por data na API pública de demonstração é conhecido por ser instável, e testá-lo arriscaria um teste flaky em vez de validar um comportamento real.

TC-017 a TC-025 estendem a investigação de payload inválido (iniciada em TC-015/TC-016) para `depositpaid`, `checkin`, `totalprice` negativo e para os verbos PUT/PATCH: PUT valida corretamente campos obrigatórios ausentes (400), mas ainda corrompe silenciosamente um `totalprice` de tipo inválido — o mesmo defeito do TC-016, agora confirmado também nesse verbo; PATCH não valida tipo nenhum; e PUT/PATCH em um id inexistente retornam 405 Method Not Allowed, o mesmo comportamento já visto em DELETE (TC-014). Bugs reais novos (TC-017, TC-018, TC-019, TC-021+TC-016, TC-023, TC-025+TC-014) estão documentados como issues no GitHub.

## Smoke suite

Os fluxos ponta-a-ponta mais críticos (um caminho feliz por operação de CRUD, cobrindo autenticação + criação + consulta + atualização + remoção) estão marcados com a tag `@smoke` (marker `smoke` do pytest): **TC-001, TC-003, TC-004, TC-007, TC-010**.
Expand Down
29 changes: 29 additions & 0 deletions features/create_booking.feature
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,32 @@ Feature: Criação de booking
Scenario: Criar booking com totalprice em formato inválido
When ele cria um booking com totalprice em formato inválido
Then a resposta da API não deve corresponder ao schema esperado

# A API não valida o tipo de depositpaid: qualquer string não vazia é
# coagida para true, então não existe forma de a API rejeitar um valor
# inválido nesse campo. Documentado em
# https://github.com/ThomasTDS/qa-api-python/issues/19.
@TC-017
Scenario: Criar booking com depositpaid em formato inválido
When ele cria um booking com depositpaid em formato inválido
Then o booking deve ser criado com sucesso
And o depositpaid do booking criado deve ser true

# Uma data de check-in com formato inválido não é validada nem rejeitada:
# o valor é processado e gravado corrompido (formato "NaN"), em vez de a
# API retornar 400. Documentado em
# https://github.com/ThomasTDS/qa-api-python/issues/20.
@TC-018
Scenario: Criar booking com checkin em formato inválido
When ele cria um booking com checkin em formato inválido
Then o booking deve ser criado com sucesso
And o checkin do booking criado deve estar corrompido

# Não há validação de regra de negócio para totalprice: valores negativos
# são aceitos normalmente, sem nenhuma restrição. Documentado em
# https://github.com/ThomasTDS/qa-api-python/issues/21.
@TC-019
Scenario: Criar booking com totalprice negativo
When ele cria um booking com totalprice negativo
Then o booking deve ser criado com sucesso
And o totalprice do booking criado deve ser negativo
67 changes: 67 additions & 0 deletions features/update_booking.feature
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,70 @@ Feature: Atualização de booking (PUT e PATCH)
And que ele não possui nenhum token de autenticação
When ele tenta atualizar parcialmente o booking alterando o sobrenome para "NaoDeveriaFuncionar"
Then a resposta deve indicar acesso não autorizado

# PUT valida corretamente a ausência de campos obrigatórios (ao contrário
# do POST de criação, que derruba com 500 no mesmo cenário — ver TC-015):
# aqui a API responde com 400, como esperado.
@TC-020
Scenario: Tentar atualizar booking com corpo vazio
Given que existe um booking criado
And que ele possui um token de autenticação válido
When ele atualiza o booking com corpo vazio
Then a resposta deve indicar requisição inválida

# Mesmo defeito do TC-016 (totalprice de tipo inválido não é validado),
# agora observado também no PUT: a API aceita e grava totalprice como
# null em silêncio. Documentado em
# https://github.com/ThomasTDS/qa-api-python/issues/22, que cobre os
# dois verbos.
@TC-021
Scenario: Atualizar booking com totalprice em formato inválido
Given que existe um booking criado
And que ele possui um token de autenticação válido
When ele atualiza o booking com totalprice em formato inválido
Then o booking deve ser atualizado com sucesso
And o totalprice do booking atualizado deve ser nulo

@TC-022
Scenario: Tentar atualizar booking sem o campo bookingdates
Given que existe um booking criado
And que ele possui um token de autenticação válido
When ele atualiza o booking sem o campo bookingdates
Then a resposta deve indicar requisição inválida

# PATCH não valida o tipo de nenhum campo enviado: um lastname numérico é
# aceito e gravado como número, quebrando o contrato do schema. Documentado
# em https://github.com/ThomasTDS/qa-api-python/issues/23.
@TC-023
Scenario: Atualizar parcialmente o booking com lastname em formato inválido
Given que existe um booking criado
And que ele possui um token de autenticação válido
When ele atualiza parcialmente o booking com lastname em formato inválido
Then o booking deve ser atualizado com sucesso
And o lastname do booking atualizado deve ser o valor numérico enviado

# Comportamento seguro, não um bug: PATCH com corpo vazio não falha nem
# altera nada, documentado aqui como um no-op válido.
@TC-024
Scenario: Atualizar parcialmente o booking com corpo vazio não altera nada
Given que existe um booking criado
And que ele possui um token de autenticação válido
When ele atualiza parcialmente o booking com corpo vazio
Then o booking deve ser atualizado com sucesso
And os dados do booking não devem ter sido alterados

# Mesmo comportamento do TC-014 (DELETE em id inexistente retorna 405),
# agora confirmado também em PUT e PATCH: nenhum dos três verbos retorna
# 404 para um id que nunca existiu. Documentado em
# https://github.com/ThomasTDS/qa-api-python/issues/24, que cobre os
# três verbos.
@TC-025
Scenario Outline: Tentar atualizar um booking inexistente
Given que ele possui um token de autenticação válido
When ele tenta "<verbo>" um booking inexistente
Then a resposta deve indicar que o método não é permitido

Examples:
| verbo |
| atualizar |
| atualizar parcialmente |
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ markers = [
"TC-014: id do test case, ver docs/test-cases.md",
"TC-015: id do test case, ver docs/test-cases.md",
"TC-016: id do test case, ver docs/test-cases.md",
"TC-017: id do test case, ver docs/test-cases.md",
"TC-018: id do test case, ver docs/test-cases.md",
"TC-019: id do test case, ver docs/test-cases.md",
"TC-020: id do test case, ver docs/test-cases.md",
"TC-021: id do test case, ver docs/test-cases.md",
"TC-022: id do test case, ver docs/test-cases.md",
"TC-023: id do test case, ver docs/test-cases.md",
"TC-024: id do test case, ver docs/test-cases.md",
"TC-025: id do test case, ver docs/test-cases.md",
]

[tool.ruff]
Expand Down
134 changes: 134 additions & 0 deletions steps/test_booking_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,66 @@ def resposta_nao_corresponde_ao_schema(context: Context) -> None:
CreateBookingResponse.model_validate(context.last_response.json())


@when("ele cria um booking com depositpaid em formato inválido")
def cria_booking_com_depositpaid_invalido(context: Context) -> None:
payload = {
"firstname": "Fulano",
"lastname": "Ciclano",
"totalprice": 150,
"depositpaid": "sim",
"bookingdates": {"checkin": "2026-01-01", "checkout": "2026-01-05"},
}
context.last_response = context.booking_client.create_booking_raw(payload)
context.booking_id = context.last_response.json().get("bookingid")


@then("o depositpaid do booking criado deve ser true")
def depositpaid_do_booking_criado_deve_ser_true(context: Context) -> None:
assert context.last_response is not None
body = context.last_response.json()
assert body["booking"]["depositpaid"] is True


@when("ele cria um booking com checkin em formato inválido")
def cria_booking_com_checkin_invalido(context: Context) -> None:
payload = {
"firstname": "Fulano",
"lastname": "Ciclano",
"totalprice": 150,
"depositpaid": True,
"bookingdates": {"checkin": "data-invalida", "checkout": "2026-01-05"},
}
context.last_response = context.booking_client.create_booking_raw(payload)
context.booking_id = context.last_response.json().get("bookingid")


@then("o checkin do booking criado deve estar corrompido")
def checkin_do_booking_criado_deve_estar_corrompido(context: Context) -> None:
assert context.last_response is not None
checkin = context.last_response.json()["booking"]["bookingdates"]["checkin"]
assert checkin != "data-invalida"
assert "NaN" in checkin


@when("ele cria um booking com totalprice negativo")
def cria_booking_com_totalprice_negativo(context: Context) -> None:
payload = {
"firstname": "Fulano",
"lastname": "Ciclano",
"totalprice": -150,
"depositpaid": True,
"bookingdates": {"checkin": "2026-01-01", "checkout": "2026-01-05"},
}
context.last_response = context.booking_client.create_booking_raw(payload)
context.booking_id = context.last_response.json().get("bookingid")


@then("o totalprice do booking criado deve ser negativo")
def totalprice_do_booking_criado_deve_ser_negativo(context: Context) -> None:
assert context.last_response is not None
assert context.last_response.json()["booking"]["totalprice"] < 0


# CONSULTA
@when("ele busca o booking pelo id")
def busca_o_booking_pelo_id(context: Context) -> None:
Expand Down Expand Up @@ -236,6 +296,80 @@ def sobrenome_do_booking_deve_ser(context: Context, lastname: str) -> None:
assert body.lastname == lastname


@when("ele atualiza o booking com corpo vazio")
def atualiza_o_booking_com_corpo_vazio(context: Context) -> None:
assert context.booking_id is not None
context.last_response = context.booking_client.update_booking_raw(context.booking_id, {}, context.token or "")


@then("a resposta deve indicar requisição inválida")
def resposta_indica_requisicao_invalida(context: Context) -> None:
assert context.last_response is not None
assert context.last_response.status_code == 400


@when("ele atualiza o booking com totalprice em formato inválido")
def atualiza_o_booking_com_totalprice_invalido(context: Context) -> None:
assert context.booking_data is not None
assert context.booking_id is not None
payload = {**context.booking_data.model_dump(exclude_none=True), "totalprice": "nao-e-numero"}
context.last_response = context.booking_client.update_booking_raw(context.booking_id, payload, context.token or "")


@then("o totalprice do booking atualizado deve ser nulo")
def totalprice_do_booking_atualizado_deve_ser_nulo(context: Context) -> None:
assert context.last_response is not None
assert context.last_response.json()["totalprice"] is None


@when("ele atualiza o booking sem o campo bookingdates")
def atualiza_o_booking_sem_bookingdates(context: Context) -> None:
assert context.booking_data is not None
assert context.booking_id is not None
payload = context.booking_data.model_dump(exclude_none=True)
del payload["bookingdates"]
context.last_response = context.booking_client.update_booking_raw(context.booking_id, payload, context.token or "")


@when("ele atualiza parcialmente o booking com lastname em formato inválido")
def atualiza_parcialmente_o_booking_com_lastname_invalido(context: Context) -> None:
assert context.booking_id is not None
context.last_response = context.booking_client.partial_update_booking(
context.booking_id, {"lastname": 12345}, context.token or ""
)


@then("o lastname do booking atualizado deve ser o valor numérico enviado")
def lastname_do_booking_atualizado_deve_ser_o_valor_numerico(context: Context) -> None:
assert context.last_response is not None
assert context.last_response.json()["lastname"] == 12345


@when("ele atualiza parcialmente o booking com corpo vazio")
def atualiza_parcialmente_o_booking_com_corpo_vazio(context: Context) -> None:
assert context.booking_id is not None
context.last_response = context.booking_client.partial_update_booking(context.booking_id, {}, context.token or "")


@then("os dados do booking não devem ter sido alterados")
def dados_do_booking_nao_devem_ter_sido_alterados(context: Context) -> None:
assert context.last_response is not None
body = Booking.model_validate(context.last_response.json())
assert body == context.booking_data


@when(parsers.parse('ele tenta "{verbo}" um booking inexistente'))
def tenta_verbo_um_booking_inexistente(context: Context, verbo: str) -> None:
if verbo == "atualizar":
context.last_response = context.booking_client.update_booking(
999999999, _default_booking(), context.token or ""
)
else:
context.last_response = context.booking_client.partial_update_booking(
999999999, {"lastname": "Novo"}, context.token or ""
)


# REMOÇÃO
@when("ele remove o booking")
def remove_o_booking(context: Context) -> None:
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/test_booking_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ def test_update_booking_sends_the_full_payload_with_the_auth_token_as_cookie(req
assert requests_mock.last_request.headers["Cookie"] == "token=tok123"


def test_update_booking_raw_sends_the_payload_as_is_with_the_auth_token_as_cookie(requests_mock: Mocker) -> None:
requests_mock.put(f"{BASE_URL}/booking/1", json={})
client = BookingApiClient(requests.Session(), BASE_URL)
payload = {"firstname": "Jane", "totalprice": "nao-e-numero"}

client.update_booking_raw(1, payload, token="tok123")

assert requests_mock.last_request is not None
assert requests_mock.last_request.json() == payload
assert requests_mock.last_request.headers["Cookie"] == "token=tok123"


def test_partial_update_booking_sends_only_the_given_fields(requests_mock: Mocker) -> None:
requests_mock.patch(f"{BASE_URL}/booking/1", json={})
client = BookingApiClient(requests.Session(), BASE_URL)
Expand Down
Loading