Skip to content

Commit

Permalink
Merge pull request #3 from rfunix/add_checks
Browse files Browse the repository at this point in the history
Add response checks
  • Loading branch information
rfunix authored Apr 4, 2020
2 parents df40a2f + bef6755 commit d392c26
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 73 deletions.
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,4 @@ Authorization = "{{ get_token.access_token}}"

**Backlog**
---

* Add retry config for request
* Add response status check
* Add response header check
* Add response body check
* Add command to generate imagem diagram flow based in toml file
* Request with 1-n relations
https://github.com/rfunix/bloodaxe/projects/1
86 changes: 63 additions & 23 deletions bloodaxe.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@

SUCCESS = typer.style("success", fg=typer.colors.GREEN, bold=True)
ERROR = typer.style("error", fg=typer.colors.RED, bold=True)
FLOW_ERROR = typer.style("FlowError", bg=typer.colors.RED, fg=typer.colors.WHITE, bold=True)

REQUEST_MESSAGE = "Request {}, name={}, url={}"
START_MESSAGE = "Start bloodaxe, number_of_concurrent_flows={}, duration={} seconds"
RESPONSE_DATA_CHECK_FAILED_MESSAGE = "Failed to check response, request={}, " "expected data={}, received={}"
RESPONSE_STATUS_CODE_CHECK_FAILED_MESSAGE = (
"Status code check failed, request={}, " "expected status_code={}, received={}"
)
SECONDS_MASK = "{0:.2f}"
DEFAULT_TIMEOUT = 10

Expand Down Expand Up @@ -72,66 +77,97 @@ def replace_with_template(context, data):
async def make_get_request(url, timeout, params=None, headers=None, *args, **kwargs):
try:
async with httpx.AsyncClient() as client:
req = await client.get(url, params=params, timeout=timeout, headers=headers)
req.raise_for_status()
resp = await client.get(url, params=params, timeout=timeout, headers=headers)
resp.raise_for_status()
except HTTP_EXCEPTIONS as exc:
raise FlowError(f"An error occurred when make_get_request, exc={exc}")

return req.json()
return resp


async def make_delete_request(url, timeout, params=None, headers=None, *args, **kwargs):
try:
async with httpx.AsyncClient() as client:
req = await client.delete(url, params=params, timeout=timeout, headers=headers)
req.raise_for_status()
resp = await client.delete(url, params=params, timeout=timeout, headers=headers)
resp.raise_for_status()
except HTTP_EXCEPTIONS as exc:
raise FlowError(f"An error occurred when make_delete_request, exc={exc}")

return req.json()
return resp


async def make_put_request(url, data, timeout, headers=None, *args, **kwargs):
try:
async with httpx.AsyncClient() as client:
req = await client.put(url, data=json.dumps(data), timeout=timeout, headers=headers)
req.raise_for_status()
resp = await client.put(url, json=data, timeout=timeout, headers=headers)
resp.raise_for_status()
except HTTP_EXCEPTIONS as exc:
raise FlowError(f"An error occurred when make_put_request, exc={exc}")

return req.json()
return resp


async def make_patch_request(url, data, timeout, headers=None, *args, **kwargs):
try:
async with httpx.AsyncClient() as client:
req = await client.patch(url, data=json.dumps(data), timeout=timeout, headers=headers)
req.raise_for_status()
resp = await client.patch(url, json=data, timeout=timeout, headers=headers)
resp.raise_for_status()
except HTTP_EXCEPTIONS as exc:
raise FlowError(f"An error occurred when make_patch_request, exc={exc}")

return req.json()
return resp


async def make_post_request(url, data, timeout, headers=None, *args, **kwargs):
try:
async with httpx.AsyncClient() as client:
req = await client.post(url, data=json.dumps(data), timeout=timeout, headers=headers)
req.raise_for_status()
resp = await client.post(url, json=data, timeout=timeout, headers=headers)
resp.raise_for_status()
except HTTP_EXCEPTIONS as exc:
raise FlowError(f"An error occurred when make_post_request, exc={exc}")

return req.json()
return resp


async def make_request(url, method, *args, **kwargs):
def check_response_data(request_name, data, expected_data, context):
expected_data = json.loads(replace_with_template(context, expected_data))
error_msg = RESPONSE_DATA_CHECK_FAILED_MESSAGE.format(request_name, expected_data, data)

if data != expected_data:
raise FlowError(error_msg)


def check_response_status_code(request_name, status_code, expected_status_code):
error_msg = RESPONSE_STATUS_CODE_CHECK_FAILED_MESSAGE.format(
request_name, expected_status_code, status_code
)

if status_code != expected_status_code:
raise FlowError(error_msg)


def check_response(request_name, data, status_code, context, response_check=None):
if response_check.get("data"):
check_response_data(request_name, data, response_check["data"], context)
if response_check.get("status_code"):
check_response_status_code(request_name, status_code, response_check["status_code"])


async def make_request(context, name, url, method, response_check=None, *args, **kwargs):
method = method.upper()
try:
func = eval(HTTP_METHODS_FUNC_MAPPING[method])
except KeyError:
raise FlowError(f"An error ocurred when make_request, invalid http method={method}")

return await func(url, *args, **kwargs)
resp = await func(url, *args, **kwargs)
data = resp.json()
status_code = resp.status_code

if response_check:
check_response(name, data, status_code, context, response_check)

return data


def show_metrics(flows, total_time):
Expand Down Expand Up @@ -194,7 +230,7 @@ def make_api_context(api_info):
return context


async def run_flow(toml_data):
async def run_flow(toml_data, verbose):
flow_config = copy.deepcopy(toml_data)
context = make_api_context(flow_config.get("api")) or {}
start_flow_time = time.time()
Expand All @@ -214,12 +250,14 @@ async def run_flow(toml_data):
request["headers"] = generate_request_headers(context, request["headers"])

try:
result = await make_request(**request)
result = await make_request(context, **request)
show_request_message(SUCCESS, request["name"], request["url"])
except FlowError as exc:
show_request_message(ERROR, request["name"], request["url"])
current_flow.error = exc
current_flow.success = False
if verbose:
typer.secho(f"{FLOW_ERROR}: {exc}")
break

if request.get("save_result"):
Expand All @@ -230,7 +268,7 @@ async def run_flow(toml_data):
return current_flow


async def start(toml_data):
async def start(toml_data, verbose):
flows = tuple()
duration = toml_data["configs"]["duration"]
number_of_concurrent_flows = toml_data["configs"]["number_of_concurrent_flows"]
Expand All @@ -249,21 +287,23 @@ async def start(toml_data):
if elapsed_seconds >= duration:
break

results = await asyncio.gather(*[run_flow(toml_data) for _ in range(number_of_concurrent_flows)])
results = await asyncio.gather(
*[run_flow(toml_data, verbose) for _ in range(number_of_concurrent_flows)]
)

flows += tuple(results)

show_metrics(flows, elapsed_seconds)


@app.command()
def main(flow_config_file: Path):
def main(flow_config_file: Path, verbose: bool = False):
try:
toml_data = toml.load(flow_config_file)
except (TypeError, toml.TomlDecodeError):
typer.echo("Invalid toml file")
else:
asyncio.run(start(toml_data))
asyncio.run(start(toml_data, verbose))


if __name__ == "__main__":
Expand Down
48 changes: 27 additions & 21 deletions example.toml
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
[configs]
number_of_concurrent_flows = 10
duration = 60
number_of_concurrent_flows = 10 # Number of concurrent coroutines flows
duration = 60 # Stressing duration

[[api]]
[[api]] # Api context
name = "user_api"
base_url = "http://127.0.0.1:8080"
[api.envvars]
client_id = "CLIENT_ID"
base_url = "http://127.0.0.1:8080" # Base url at the moment, is the unique parameter in api section.
[api.envvars] # Environment variables for given api
client_id = "CLIENT_ID" # Envvars names
client_secret = "CLIENT_SECRET"

[[api]]
name = "any_api"
base_url = "http://127.0.0.1:1010"

[[request]]
name = "get_token"
url = "{{ user_api.base_url }}/token/"
[[request]] # Request context
name = "get_token"
url = "{{ user_api.base_url }}/token/" # Use user_api context to get the base_url
method = "POST"
timeout = 60
save_result = true
[request.data]
client_id = "{{ user_api.client_id }}"
timeout = 60 # The bloodaxe default timeout value is 10 secs, but it's possible override the default value
save_result = true # Save request result in request name context, default value is false
[request.data] # Request data section
client_id = "{{ user_api.client_id }}" # templating syntax is allowed in request.data
client_secret = "{{ user_api.client_secret }}"
[request.headers]
[request.headers] # Request header section
Content-Type = 'application/x-www-form-urlencoded'

[[request]]
Expand All @@ -32,16 +32,16 @@ method = "GET"
timeout = 60
save_result = true
[request.headers]
Authorization = "{{ get_token.access_token}}"
Authorization = "{{ get_token.access_token}}" # templating syntax is allowed in request.headers

[[request]]
name ="get_user_with_params"
url = "{{ user_api.base_url }}/users/"
method = "GET"
timeout = 60
save_result = false
[request.params]
name = "{{ get_user.name }}"
[request.params] # Request params section
name = "{{ get_user.name }}" # templating syntax is allowed in request.params/querystring
[request.headers]
Authorization = "{{ get_token.access_token}}"

Expand All @@ -50,17 +50,23 @@ name = "create_new_user"
url = "{{ user_api.base_url }}/users/"
method = "POST"
[request.data]
firstname = "{{ get_user.firstname }} teste"
lastname = "{{ get_user.Lastname }} teste"
status = "{{ get_user.status }} teste"
firstname = "{{ get_user.firstname }} test"
lastname = "{{ get_user.Lastname }} test"
status = "{{ get_user.status }} test"
[request.headers]
Authorization = "{{ get_token.access_token}}"
[request.response_check] # response_check feature checking response data and status_code
status_code = 201
[request.response_check.data]
firstname = "{{ get_user.firstname }} test" # templating syntax is allowed in response data checks
lastname = "{{ get_user.Lastname }} test"
status = "{{ get_user.status }} test"

[[request]]
name = "create_new_user_with_from_file"
url = "{{ user_api.base_url }}/users/"
method = "PATCH"
[request.data]
from_file = "user.json"
from_file = "user.json" # from_file help you configure request.data
[request.headers]
Authorization = "{{ get_token.access_token}}"
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bloodaxe"
version = "0.1.0"
version = "0.2.0"
description = "bloodaxe is the nice way to testing and metrifying api flows."
authors = ["rfunix <[email protected]>"]
readme = "README.md"
Expand Down Expand Up @@ -32,6 +32,7 @@ pytest-httpserver = "*"
pytest-asyncio = "*"
asynctest = "*"
pytest-env = "*"
pytest-clarity = "^0.3.0-alpha.0"

[tool.black]
line-length = 110
Expand Down
17 changes: 12 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,24 @@ def toml_data():
"method": "POST",
"data": {
"firstname": "{{ get_user.firstname }} test",
"lastname": "{{ get_user.Lastname }} test",
"lastname": "{{ get_user.lastname}} test",
"status": "{{ get_user.status }} test",
},
"response_check": {
"data": {
"firstname": "{{ get_user.firstname }} test",
"lastname": "{{ get_user.lastname }} test",
"status": "{{ get_user.status }} test",
}
},
},
{
"name": "update_user",
"url": "{{ user_api.base_url }}/users/",
"method": "PUT",
"data": {
"firstname": "{{ get_user.firstname }} testx",
"lastname": "{{ get_user.Lastname }} testx",
"lastname": "{{ get_user.lastname }} testx",
"status": "{{ get_user.status }} testx",
},
},
Expand All @@ -56,7 +63,7 @@ def toml_data():
"method": "PATCH",
"data": {
"firstname": "{{ get_user.firstname }} testz",
"lastname": "{{ get_user.Lastname }} testz",
"lastname": "{{ get_user.lastname }} testz",
},
},
{
Expand Down Expand Up @@ -120,13 +127,13 @@ def mocked_secho(mocker):

@pytest.fixture
def get_user_response():
return {"name": "Arnfinn", "lastname": "Bjornsson", "status": "active"}
return {"firstname": "Arnfinn", "lastname": "Bjornsson", "status": "active"}


@pytest.fixture
def post_user_response(get_user_response):
return {
"name": f"{get_user_response['name']} test",
"firstname": f"{get_user_response['firstname']} test",
"lastname": f"{get_user_response['lastname']} test",
"status": f"{get_user_response['status']} test",
}
Loading

0 comments on commit d392c26

Please sign in to comment.