From 9e6c4303c48a0687cb3d56f4117c44ddf302823c Mon Sep 17 00:00:00 2001 From: Camila Maia Date: Tue, 7 Mar 2023 10:35:46 +0100 Subject: [PATCH 1/5] docs: fix changelog release title (#573) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a2ac75..9e8e5fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [2.8.1] - 2023-03-04 +## [2.8.2] - 2023-03-06 ### Fixed - Content field not rendered properly on Chrome [#551](https://github.com/scanapi/scanapi/issues/551) From 7c6bd82690c227b248a9f6efa3dd7e661ac9eac8 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Tue, 7 Mar 2023 17:30:54 +0100 Subject: [PATCH 2/5] refactor(report): cleanup chrome fix (#574) --- scanapi/templates/report.html | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/scanapi/templates/report.html b/scanapi/templates/report.html index 4a0ca211..b7cebf3d 100644 --- a/scanapi/templates/report.html +++ b/scanapi/templates/report.html @@ -687,17 +687,8 @@

Tests Summary

return false; } - function decodeHtmlEntities(str) { - const txt = new DOMParser().parseFromString(str, "text/html"); - return txt.documentElement.textContent; - } - for (const json_body of all_json_bodies) { - let raw_json = json_body.innerText; - if (!raw_json) { - // innerText is empty for hidden elements on Chrome. - raw_json = decodeHtmlEntities(json_body.innerHTML); - } + let raw_json = json_body.textContent; const parsed_json = tryParseJSON(raw_json); if (parsed_json) { From f1d9d96ef670f6592a5ba8b91b08520793dd0d17 Mon Sep 17 00:00:00 2001 From: Ueslei Carvalho Date: Thu, 9 Mar 2023 12:48:53 -0300 Subject: [PATCH 3/5] fix: python -m scanapi command --- CHANGELOG.md | 3 ++ scanapi/__init__.py | 2 +- scanapi/__main__.py | 111 +--------------------------------------- scanapi/cli.py | 110 +++++++++++++++++++++++++++++++++++++++ tests/unit/test_main.py | 2 +- 5 files changed, 117 insertions(+), 111 deletions(-) create mode 100644 scanapi/cli.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e8e5fb0..9455bb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- python -m scanapi command [#501](https://github.com/scanapi/scanapi/issues/501) + ## [2.8.2] - 2023-03-06 ### Fixed - Content field not rendered properly on Chrome [#551](https://github.com/scanapi/scanapi/issues/551) diff --git a/scanapi/__init__.py b/scanapi/__init__.py index 01fa9b6c..698b825d 100644 --- a/scanapi/__init__.py +++ b/scanapi/__init__.py @@ -1,4 +1,4 @@ -from scanapi.__main__ import main +from scanapi.cli import main name = "scanapi" diff --git a/scanapi/__main__.py b/scanapi/__main__.py index 2145c86b..ed26a37d 100644 --- a/scanapi/__main__.py +++ b/scanapi/__main__.py @@ -1,110 +1,3 @@ -import logging +from scanapi.cli import main -import click -import yaml -from pkg_resources import get_distribution -from rich.logging import RichHandler - -from scanapi.exit_code import ExitCode -from scanapi.scan import scan -from scanapi.settings import settings - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - -dist = get_distribution("scanapi") - - -@click.group() -@click.version_option(version=dist.version) -def main(): - """Automated Testing and Documentation for your REST API.""" - - -@main.command(context_settings=CONTEXT_SETTINGS) -@click.argument("spec_path", type=click.Path(exists=True), required=False) -@click.option( - "-o", - "--output-path", - "output_path", - type=click.Path(), - help="Report output path.", -) -@click.option( - "-nr", - "--no-report", - "no_report", - is_flag=True, - help="Run ScanAPI without generating report.", -) -@click.option( - "-b", - "--browser", - "open_browser", - is_flag=True, - help="Open the results file using a browser", -) -@click.option( - "-c", - "--config-path", - "config_path", - type=click.Path(exists=True), - help="Configuration file path.", -) -@click.option( - "-t", - "--template", - "template", - type=click.Path(exists=True), - help="Custom report template path. The template must be a .jinja file.", -) -@click.option( - "-ll", - "--log-level", - "log_level", - type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), - default="INFO", - help="Set the debug logging level for the program.", -) -def run( - spec_path, - output_path, - no_report, - config_path, - template, - log_level, - open_browser, -): - """ - Automated Testing and Documentation for your REST API. - SPEC_PATH argument is the API specification file path. - """ - logging.basicConfig( - level=log_level, - format="%(message)s", - datefmt="[%X]", - handlers=[ - RichHandler( - show_time=False, markup=True, show_path=(log_level == "DEBUG") - ) - ], - ) - logger = logging.getLogger(__name__) - - click_preferences = { - "spec_path": spec_path, - "output_path": output_path, - "no_report": no_report, - "config_path": config_path, - "template": template, - "open_browser": open_browser, - } - - try: - settings.save_preferences(**click_preferences) - except yaml.YAMLError as e: - error_message = "Error loading configuration file." - error_message = "{}\nPyYAML: {}".format(error_message, str(e)) - logger.error(error_message) - raise SystemExit(ExitCode.USAGE_ERROR) - - scan() +main() diff --git a/scanapi/cli.py b/scanapi/cli.py new file mode 100644 index 00000000..c44bc32f --- /dev/null +++ b/scanapi/cli.py @@ -0,0 +1,110 @@ +import logging + +import click +import yaml +from pkg_resources import get_distribution +from rich.logging import RichHandler + +from scanapi.exit_code import ExitCode +from scanapi.scan import scan +from scanapi.settings import settings + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + +dist = get_distribution("scanapi") + + +@click.group() +@click.version_option(version=dist.version) +def main(): + """Automated Testing and Documentation for your REST API.""" + + +@main.command(context_settings=CONTEXT_SETTINGS) +@click.argument("spec_path", type=click.Path(exists=True), required=False) +@click.option( + "-o", + "--output-path", + "output_path", + type=click.Path(), + help="Report output path.", +) +@click.option( + "-nr", + "--no-report", + "no_report", + is_flag=True, + help="Run ScanAPI without generating report.", +) +@click.option( + "-b", + "--browser", + "open_browser", + is_flag=True, + help="Open the results file using a browser", +) +@click.option( + "-c", + "--config-path", + "config_path", + type=click.Path(exists=True), + help="Configuration file path.", +) +@click.option( + "-t", + "--template", + "template", + type=click.Path(exists=True), + help="Custom report template path. The template must be a .jinja file.", +) +@click.option( + "-ll", + "--log-level", + "log_level", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), + default="INFO", + help="Set the debug logging level for the program.", +) +def run( + spec_path, + output_path, + no_report, + config_path, + template, + log_level, + open_browser, +): + """ + Automated Testing and Documentation for your REST API. + SPEC_PATH argument is the API specification file path. + """ + logging.basicConfig( + level=log_level, + format="%(message)s", + datefmt="[%X]", + handlers=[ + RichHandler( + show_time=False, markup=True, show_path=(log_level == "DEBUG") + ) + ], + ) + logger = logging.getLogger(__name__) + + click_preferences = { + "spec_path": spec_path, + "output_path": output_path, + "no_report": no_report, + "config_path": config_path, + "template": template, + "open_browser": open_browser, + } + + try: + settings.save_preferences(**click_preferences) + except yaml.YAMLError as e: + error_message = "Error loading configuration file." + error_message = f"{error_message}\nPyYAML: {e}" + logger.error(error_message) + raise SystemExit(ExitCode.USAGE_ERROR) + + scan() diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 02907eaf..cae010fc 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -4,7 +4,7 @@ from click.testing import CliRunner from pytest import mark -from scanapi.__main__ import run +from scanapi.cli import run log = logging.getLogger(__name__) runner = CliRunner() From 6925c0b84ca5435723f64ad76ae887932cc35bc6 Mon Sep 17 00:00:00 2001 From: Ueslei Carvalho Date: Thu, 16 Mar 2023 06:07:15 -0300 Subject: [PATCH 4/5] feat: improvement to allow the use of differents content types (#575) --- CHANGELOG.md | 3 + scanapi/template_render.py | 1 + scanapi/templates/report.html | 2 +- scanapi/tree/request_node.py | 26 +++++- tests/unit/tree/request_node/test_run.py | 101 +++++++++++++++++++++++ 5 files changed, 128 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9455bb6b..6d7ffe87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Feature +- Enable the use of differents content types [#521](https://github.com/scanapi/scanapi/issues/521) + ### Fixed - python -m scanapi command [#501](https://github.com/scanapi/scanapi/issues/501) diff --git a/scanapi/template_render.py b/scanapi/template_render.py index e4a1f1ed..fbdec59b 100644 --- a/scanapi/template_render.py +++ b/scanapi/template_render.py @@ -8,6 +8,7 @@ def render(template_path, context, is_external=False): env = Environment(loader=loader, autoescape=True) env.filters["curlify"] = curlify2.to_curl env.filters["render_body"] = render_body + env.globals["is_bytes"] = lambda o: isinstance(o, bytes) chosen_template = env.get_template(template_path) return chosen_template.render(**context) diff --git a/scanapi/templates/report.html b/scanapi/templates/report.html index b7cebf3d..a9a0e748 100644 --- a/scanapi/templates/report.html +++ b/scanapi/templates/report.html @@ -536,7 +536,7 @@

Request

{% endif %} - {% if ( request.body and request.body.decode("UTF-8") != "{}" ) %} + {% if ( request.body and is_bytes(request.body) and request.body.decode("UTF-8") !="{}") %}
Body

{{ request|render_body }}

diff --git a/scanapi/tree/request_node.py b/scanapi/tree/request_node.py index 47676b76..cec16561 100644 --- a/scanapi/tree/request_node.py +++ b/scanapi/tree/request_node.py @@ -155,10 +155,7 @@ def run(self): url = self.full_url_path console.print(f"\n- Making request {method} {url}", highlight=False) - session = session_with_retry(self.retry) - response = session.request( - method, - url, + kwargs = dict( headers=self.headers, params=self.params, json=self.body, @@ -166,6 +163,12 @@ def run(self): **self.options, ) + if not self._content_type_is_json(kwargs["headers"]): + kwargs["data"] = kwargs.pop("json") + + session = session_with_retry(self.retry) + response = session.request(method, url, **kwargs) + extras = dict(self.endpoint.spec_vars) extras["response"] = response @@ -214,3 +217,18 @@ def _validate(self): validate_keys( self.spec.keys(), self.ALLOWED_KEYS, self.REQUIRED_KEYS, self.SCOPE ) + + @staticmethod + def _content_type_is_json(headers): + """Check headers for any content-type different than application/json + + Args: + headers dict[str, str]: request headers + + Returns: + bool: False if convent-type is different then application/json + """ + return not any( + k.lower() == "content-type" and v.lower() != "application/json" + for k, v in headers.items() + ) diff --git a/tests/unit/tree/request_node/test_run.py b/tests/unit/tree/request_node/test_run.py index 071d195c..ea4d1350 100644 --- a/tests/unit/tree/request_node/test_run.py +++ b/tests/unit/tree/request_node/test_run.py @@ -62,6 +62,107 @@ def test_calls_request(self, mock_session, mock_time_sleep): "options": {"timeout": 2.3, "verify": False}, } + @mark.context("when has no `Content-Type` header") + @mark.it("should use `json` parameter to send body") + def test_content_type_is_not_defined(self, mock_session): + request = RequestNode( + { + "path": "http://foo.com", + "name": "request_name", + }, + endpoint=EndpointNode( + { + "name": "endpoint_name", + "requests": [{}], + } + ), + ) + + request.run() + + mock_session().request.assert_called_once_with( + request.http_method, + request.full_url_path, + headers=request.headers, + params=request.params, + json=request.body, + allow_redirects=False, + ) + + @mark.context("when `Content-Type` header is `application/json`") + @mark.it("should use `json` parameter to send body") + def test_content_type_is_application_json(self, mock_session): + request = RequestNode( + { + "path": "http://foo.com", + "name": "request_name", + "headers": { + "content-type": "application/json", + "x-foo": "bar", + "x-request-id": "123", + "authorization": "bearer token", + }, + }, + endpoint=EndpointNode( + { + "name": "endpoint_name", + "requests": [{}], + } + ), + ) + + request.run() + + mock_session().request.assert_called_once_with( + request.http_method, + request.full_url_path, + headers=request.headers, + params=request.params, + json=request.body, + allow_redirects=False, + ) + + test_content_types = [ + "multipart/form-data", + "application/x-www-form-urlencoded", + "application/xml", + "text/yaml", + "text/plain", + "foo", + "foo/bar", + ] + + @mark.parametrize("content_type", test_content_types) + @mark.context("when `Content-Type` header isn't `application/json`") + @mark.it("should use `data` parameter to send `body`") + def test_content_type_is_not_application_json( + self, mock_session, content_type + ): + request = RequestNode( + { + "path": "http://foo.com", + "name": "request_name", + "headers": {"content-type": content_type}, + }, + endpoint=EndpointNode( + { + "name": "endpoint_name", + "requests": [{}], + } + ), + ) + + request.run() + + mock_session().request.assert_called_once_with( + request.http_method, + request.full_url_path, + headers=request.headers, + params=request.params, + data=request.body, + allow_redirects=False, + ) + @mark.context("when no_report is False") @mark.it("should call the write_result method") def test_calls_write_result(self, mocker, mock_console_write_result): From 17d0770a0102fc70911ca67b267d45b7774e1237 Mon Sep 17 00:00:00 2001 From: Camila Maia Date: Thu, 16 Mar 2023 16:32:40 +0100 Subject: [PATCH 5/5] Release v2.9.0 (#577) --- CHANGELOG.md | 6 ++++-- Dockerfile | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7ffe87..7cf8423e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.9.0] - 2023-03-16 ### Feature -- Enable the use of differents content types [#521](https://github.com/scanapi/scanapi/issues/521) +- Enable the use of different content types [#521](https://github.com/scanapi/scanapi/issues/521) ### Fixed - python -m scanapi command [#501](https://github.com/scanapi/scanapi/issues/501) @@ -265,7 +266,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix vars interpolation. -[Unreleased]: https://github.com/scanapi/scanapi/compare/v2.8.2...HEAD +[Unreleased]: https://github.com/scanapi/scanapi/compare/v2.9.0...HEAD +[2.9.0]: https://github.com/scanapi/scanapi/compare/v2.8.2...v2.9.0 [2.8.2]: https://github.com/scanapi/scanapi/compare/v2.8.1...v2.8.2 [2.8.1]: https://github.com/scanapi/scanapi/compare/v2.8.0...v2.8.1 [2.8.0]: https://github.com/scanapi/scanapi/compare/v2.7.0...v2.8.0 diff --git a/Dockerfile b/Dockerfile index 27a73d0e..ca99c7f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV PATH="~/.local/bin:${PATH}" RUN pip install pip setuptools --upgrade -RUN pip install scanapi==2.8.2 +RUN pip install scanapi==2.9.0 COPY . /app diff --git a/pyproject.toml b/pyproject.toml index a9cbf752..3c071b8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scanapi" -version = "2.8.2" +version = "2.9.0" description = "Automated Testing and Documentation for your REST API" authors = ["The ScanAPI Organization "] license = "MIT"