Repository navigation
Expand file tree
/
Copy pathconftest.py
More file actions
120 lines (97 loc) · 4.51 KB
/
Copy pathconftest.py
File metadata and controls
120 lines (97 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import json
import os
import re
import warnings
from collections.abc import Generator
from pathlib import Path
from typing import Any
import pytest
import requests
from pydantic import ValidationError
from api.auth_api_client import AuthApiClient
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.
API_USERNAME = os.environ.get("API_USERNAME", "admin")
API_PASSWORD = os.environ.get("API_PASSWORD", "password123")
FEATURES_DIR = Path(__file__).parent / "features"
# Cada cenário tem uma tag @TC-XXX na linha logo acima de "Cenário:". Devolve,
# para cada TC, o arquivo .feature e o número da linha do cenário. É a fonte
# usada para registrar os marcadores e para conferir a matriz em docs/.
def tc_scenarios() -> dict[str, tuple[str, int]]:
found: dict[str, tuple[str, int]] = {}
for path in sorted(FEATURES_DIR.glob("*.feature")):
lines = path.read_text(encoding="utf-8").splitlines()
for index, line in enumerate(lines):
for tc in re.findall(r"@(TC-\d{3})\b", line):
assert tc not in found, f"{tc} aparece em mais de um cenário"
found[tc] = (path.name, index + 2)
return found
def pytest_configure(config: pytest.Config) -> None:
for tc in tc_scenarios():
config.addinivalue_line("markers", f"{tc}: id do test case, ver docs/test-cases.md")
class Context:
def __init__(self, session: requests.Session) -> None:
self.session = session
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
self.booking_data: Booking | None = None
self.last_response: requests.Response | None = None
# Atalho para os steps de verificação: falha com mensagem clara se o
# cenário chegou ao Then sem ter feito nenhuma requisição.
@property
def response(self) -> requests.Response:
assert self.last_response is not None, "nenhuma requisição foi feita neste cenário"
return self.last_response
# Cenários sem token mandam o cookie vazio, para testar o bloqueio da API.
@property
def token_or_empty(self) -> str:
return self.token or ""
@pytest.fixture
def context() -> Generator[Context, None, None]:
session = requests.Session()
ctx = Context(session)
yield ctx
if ctx.booking_id is not None:
try:
cleanup_token = ctx.auth_client.get_valid_token()
ctx.booking_client.delete_booking(ctx.booking_id, cleanup_token)
except (requests.RequestException, ValidationError) as error:
# Falha de rede ou de autenticação na limpeza não muda o resultado
# do teste, que já verificou o que precisava, mas deixa o booking
# para trás na API pública. O aviso aparece no resumo do pytest.
# (Se o próprio cenário já removeu o booking, a API só devolve um
# status de erro, sem exceção, e nada é avisado.)
warnings.warn(f"booking {ctx.booking_id} não foi removido na limpeza: {error}", stacklevel=1)
session.close()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[None]) -> Generator[None, Any, None]:
outcome = yield
report: pytest.TestReport = outcome.get_result()
if report.when != "call" or not report.failed:
return
ctx: Context | None = getattr(item, "funcargs", {}).get("context")
if ctx is None or ctx.last_response is None:
return
try:
response = ctx.last_response
try:
body = response.json()
except ValueError:
body = response.text
evidence = {
"url": response.url,
"status": response.status_code,
"reason": response.reason,
"headers": dict(response.headers),
"body": body,
}
report.sections.append(("Evidência da falha", json.dumps(evidence, indent=2, ensure_ascii=False)))
except Exception:
pass # evita mascarar a falha original do teste com um erro na captura de evidência