Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add response checks #3

Merged
merged 21 commits into from
Apr 4, 2020
Merged
Show file tree
Hide file tree
Changes from 8 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
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 response check, request={}, " "expected data={}, received={}"
rfunix marked this conversation as resolved.
Show resolved Hide resolved
RESPONSE_STATUS_CODE_CHECK_FAILED_MESSAGE = (
"Failed to status_code check, request={}, " "expected status_code={}, received={}"
rfunix marked this conversation as resolved.
Show resolved Hide resolved
)
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, data=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, data=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, data=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 concurrents coroutines flows
rfunix marked this conversation as resolved.
Show resolved Hide resolved
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] # It's possible, configure envvars for api context
rfunix marked this conversation as resolved.
Show resolved Hide resolved
client_id = "CLIENT_ID" # Envvars names
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
client_id = "CLIENT_ID" # Envvars names
client_id = "CLIENT_ID" # Environment variable definition

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
timeout = 60 # The bloodaxe default timeout value is 10 secs, but it's possible override the default value
timeout = 60 # bloodaxe's default timeout value is 10 secs, but it's possible to override it

save_result = true # Save request result in request name context, the value default is False
rfunix marked this conversation as resolved.
Show resolved Hide resolved
[request.data] # Request data section
client_id = "{{ user_api.client_id }}" # It's possible using the template syntax in request.data
rfunix marked this conversation as resolved.
Show resolved Hide resolved
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}}" # It's possible using the template syntax in request.headers
rfunix marked this conversation as resolved.
Show resolved Hide resolved

[[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 }}" # it's possible using the template syntax in request.params/querystring
rfunix marked this conversation as resolved.
Show resolved Hide resolved
[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" # it's possible using the template syntax in response data checking
rfunix marked this conversation as resolved.
Show resolved Hide resolved
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}}"
2 changes: 1 addition & 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
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