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
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,22 @@ O Codecov também bloqueia o merge do PR (configuração em `codecov.yml`) se o

Um hook de pre-commit (framework `pre-commit`, instalado via `pre-commit install` após o `pip install`) roda `ruff --fix` e `ruff format` nos arquivos staged, e também `mypy .` no projeto inteiro, antes de cada commit — então a maioria dos problemas de lint, formatação ou tipo já é pega localmente antes de chegar no CI.

### Rodar contra outra URL
### Rodar contra outro ambiente

Por padrão os testes apontam para `https://restful-booker.herokuapp.com`. Para rodar contra outro ambiente (ex: uma instância local ou de staging), defina a variável de ambiente `BASE_URL`:
Por padrão os testes apontam para `https://restful-booker.herokuapp.com` e se autenticam com as credenciais públicas da documentação da API (`admin` / `password123`). Para rodar contra outro ambiente (ex: uma instância local ou de staging), defina as variáveis de ambiente:

| Variável | Padrão | Uso |
| -------------- | --------------------------------------- | ------------------------------------ |
| `BASE_URL` | `https://restful-booker.herokuapp.com` | Endereço da API |
| `API_USERNAME` | `admin` | Usuário usado para gerar o token |
| `API_PASSWORD` | `password123` | Senha usada para gerar o token |

```
# PowerShell
$env:BASE_URL="http://localhost:3001"; pytest
$env:BASE_URL="http://localhost:3001"; $env:API_USERNAME="usuario"; $env:API_PASSWORD="senha"; pytest

# bash
BASE_URL=http://localhost:3001 pytest
BASE_URL=http://localhost:3001 API_USERNAME=usuario API_PASSWORD=senha pytest
```

---
Expand All @@ -131,7 +137,7 @@ BASE_URL=http://localhost:3001 pytest
| PATCH | `/booking/:id` | Atualiza parcialmente um booking |
| DELETE | `/booking/:id` | Remove um booking |

Autenticação: `POST /auth` com `{ "username": "admin", "password": "password123" }` retorna um token, enviado nas chamadas de `PUT`, `PATCH` e `DELETE` via cookie `token=<valor>`.
Autenticação: `POST /auth` com `{ "username": "admin", "password": "password123" }` (credenciais públicas da documentação da API, configuráveis por `API_USERNAME` e `API_PASSWORD`) retorna um token, enviado nas chamadas de `PUT`, `PATCH` e `DELETE` via cookie `token=<valor>`.

**Observação de segurança:** `POST /booking` (criação) **não exige autenticação** — qualquer pessoa consegue criar bookings sem token. Isso é uma falha de controle de acesso do próprio app de demonstração, e está coberta explicitamente em [features/create_booking.feature](features/create_booking.feature) como comportamento documentado, não como bug do teste.

Expand Down
18 changes: 16 additions & 2 deletions api/auth_api_client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import requests

from api.base_api_client import BaseApiClient
from api.base_api_client import DEFAULT_TIMEOUT, BaseApiClient
from models.booking import AuthResponse


class AuthApiClient(BaseApiClient):
def __init__(
self,
session: requests.Session,
base_url: str,
username: str,
password: str,
timeout: tuple[float, float] = DEFAULT_TIMEOUT,
) -> None:
super().__init__(session, base_url, timeout)
self.username = username
self.password = password

def create_token(self, username: str, password: str) -> requests.Response:
return self._request("POST", "/auth", json={"username": username, "password": password})

# Token com as credenciais configuradas no client, usado pelos cenários
# que só precisam estar autenticados e pela limpeza ao fim de cada teste.
def get_valid_token(self) -> str:
response = self.create_token("admin", "password123")
response = self.create_token(self.username, self.password)
body = AuthResponse.model_validate_json(response.text)
return body.token
6 changes: 5 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
from models.booking import Booking

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.
API_USERNAME = os.environ.get("API_USERNAME", "admin")
API_PASSWORD = os.environ.get("API_PASSWORD", "password123")


class Context:
def __init__(self, session: requests.Session) -> None:
self.session = session
self.auth_client = AuthApiClient(session, BASE_URL)
self.auth_client = AuthApiClient(session, BASE_URL, API_USERNAME, API_PASSWORD)
self.booking_client = BookingApiClient(session, BASE_URL)
self.token: str | None = None
self.booking_id: int | None = None
Expand Down
2 changes: 1 addition & 1 deletion features/auth.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Feature: Autenticação na API restful-booker

@TC-001 @smoke
Scenario: Gerar token com credenciais válidas
When ele solicita um token com o usuário "admin" e a senha "password123"
When ele solicita um token com credenciais válidas
Then ele deve receber um token de autenticação válido

@TC-002
Expand Down
6 changes: 6 additions & 0 deletions steps/test_booking_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def com_token_de_autenticacao_invalido(context: Context) -> None:
context.token = "token-invalido-qualquer"


@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)
Expand Down
19 changes: 10 additions & 9 deletions tests/unit/test_auth_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,32 @@
BASE_URL = "https://example.test"


def _client() -> AuthApiClient:
return AuthApiClient(requests.Session(), BASE_URL, username="usuario", password="senha")


def test_create_token_posts_the_given_credentials(requests_mock: Mocker) -> None:
requests_mock.post(f"{BASE_URL}/auth", json={"token": "abc123"})
client = AuthApiClient(requests.Session(), BASE_URL)

response = client.create_token("admin", "password123")
response = _client().create_token("outro-usuario", "outra-senha")

assert response.status_code == 200
assert requests_mock.last_request is not None
assert requests_mock.last_request.json() == {"username": "admin", "password": "password123"}
assert requests_mock.last_request.json() == {"username": "outro-usuario", "password": "outra-senha"}


def test_get_valid_token_returns_the_token_from_the_response_body(requests_mock: Mocker) -> None:
requests_mock.post(f"{BASE_URL}/auth", json={"token": "abc123"})
client = AuthApiClient(requests.Session(), BASE_URL)

token = client.get_valid_token()
token = _client().get_valid_token()

assert token == "abc123"


def test_get_valid_token_always_authenticates_as_admin(requests_mock: Mocker) -> None:
def test_get_valid_token_authenticates_with_the_configured_credentials(requests_mock: Mocker) -> None:
requests_mock.post(f"{BASE_URL}/auth", json={"token": "abc123"})
client = AuthApiClient(requests.Session(), BASE_URL)

client.get_valid_token()
_client().get_valid_token()

assert requests_mock.last_request is not None
assert requests_mock.last_request.json() == {"username": "admin", "password": "password123"}
assert requests_mock.last_request.json() == {"username": "usuario", "password": "senha"}
Loading