From 79eac65ad8bf0a923243a70574553e7fd9736de5 Mon Sep 17 00:00:00 2001 From: ThomasTDS Date: Thu, 1 Oct 2026 21:56:59 -0300 Subject: [PATCH] =?UTF-8?q?L=C3=AA=20as=20credenciais=20da=20API=20de=20va?= =?UTF-8?q?ri=C3=A1veis=20de=20ambiente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O AuthApiClient recebe usuário e senha pelo construtor, e o conftest os lê de API_USERNAME e API_PASSWORD, com as credenciais públicas da restful-booker como padrão. O TC-001 passa a usar as credenciais configuradas, para continuar válido em outro ambiente. --- README.md | 16 +++++++++++----- api/auth_api_client.py | 18 ++++++++++++++++-- conftest.py | 6 +++++- features/auth.feature | 2 +- steps/test_booking_steps.py | 6 ++++++ tests/unit/test_auth_api_client.py | 19 ++++++++++--------- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b4f1773..3b53ca0 100644 --- a/README.md +++ b/README.md @@ -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 ``` --- @@ -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=`. +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=`. **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. diff --git a/api/auth_api_client.py b/api/auth_api_client.py index 5029771..6a41823 100644 --- a/api/auth_api_client.py +++ b/api/auth_api_client.py @@ -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 diff --git a/conftest.py b/conftest.py index c82f8bf..8a47f81 100644 --- a/conftest.py +++ b/conftest.py @@ -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 diff --git a/features/auth.feature b/features/auth.feature index 5c9d730..75f288e 100644 --- a/features/auth.feature +++ b/features/auth.feature @@ -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 diff --git a/steps/test_booking_steps.py b/steps/test_booking_steps.py index 7c6e40e..4767015 100644 --- a/steps/test_booking_steps.py +++ b/steps/test_booking_steps.py @@ -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) diff --git a/tests/unit/test_auth_api_client.py b/tests/unit/test_auth_api_client.py index d0f7718..a3a574e 100644 --- a/tests/unit/test_auth_api_client.py +++ b/tests/unit/test_auth_api_client.py @@ -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"}