Repository: scanapi/scanapi

Issue:
Add ability to use custom templates
related to #15 

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=h1) Report
> Merging [#126](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=desc) into [master](https://codecov.io/gh/scanapi/scanapi/commit/a5a9354a4039db30a93ca8a13022183566185cd9&el=desc) will **increase** coverage by `7.31%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/scanapi/scanapi/pull/126/graphs/tree.svg?width=650&height=150&src=pr&token=xgH1xCg4s4)](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #126      +/-   ##
==========================================
+ Coverage   84.02%   91.33%   +7.31%     
==========================================
  Files          11       11              
  Lines         388      381       -7     
==========================================
+ Hits          326      348      +22     
+ Misses         62       33      -29     
```


| [Impacted Files](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [scanapi/reporter.py](https://codecov.io/gh/scanapi/scanapi/pull/126/diff?src=pr&el=tree#diff-c2NhbmFwaS9yZXBvcnRlci5weQ==) | `100.00% <100.00%> (+72.50%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=footer). Last update [a5a9354...5f0dccf](https://codecov.io/gh/scanapi/scanapi/pull/126?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a33243..de18241 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 - PATCH HTTP method - [#77](https://github.com/scanapi/scanapi/issues/77)
 - Ability to have API spec in multiples files - [#125](https://github.com/scanapi/scanapi/issues/125)
-- CLI `--config-path` option - [#128](https://github.com/scanapi/scanapi/issues/#128)
+- CLI `--config-path` option - [#128](https://github.com/scanapi/scanapi/issues/128)
+- CLI `--template-path` option - [#126](https://github.com/scanapi/scanapi/pull/126)
 
 ### Changed
 - Make markdown report a bit better - [#96](https://github.com/scanapi/scanapi/issues/96)
diff --git a/README.md b/README.md
index 559ef56..48ccd73 100644
--- a/README.md
+++ b/README.md
@@ -12,19 +12,21 @@ A library for **your API** that provides:
 
 ## Contents
 
-* [Requirements](#requirements)
-* [How to install](#how-to-install)
-* [Basic Usage](#basic-usage)
-* [Documentation](#documentation)
-  + [ScanAPI CLI](#scanapi-cli)
-  + [API Specification Keys](#api-specification-keys)
-  + [Environment Variables](#environment-variables)
-  + [Custom Variables](#custom-variables)
-  + [Python Code](#python-code)
-  + [Chaining Requests](#chaining-requests)
-  + [Configuration File](#configuration-file)
-  + [Hiding sensitive information](#hiding-sensitive-information)
-* [Contributing](#contributing)
+- [Contents](#contents)
+- [Requirements](#requirements)
+- [How to install](#how-to-install)
+- [Basic Usage](#basic-usage)
+- [Documentation](#documentation)
+  - [ScanAPI CLI](#scanapi-cli)
+  - [API Specification Keys](#api-specification-keys)
+  - [Environment Variables](#environment-variables)
+  - [Custom Variables](#custom-variables)
+  - [Python Code](#python-code)
+  - [Chaining Requests](#chaining-requests)
+  - [API specification in multiple files](#api-specification-in-multiple-files)
+  - [Configuration File](#configuration-file)
+  - [Hiding sensitive information](#hiding-sensitive-information)
+- [Contributing](#contributing)
 
 ## Requirements
 
@@ -81,7 +83,7 @@ Options:
   -o, --output-path PATH
   -c, --config-path PATH
   -r, --reporter [console|markdown|html]
-  -t, --template TEXT
+  -t, --template PATH
   --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
   --help                          Show this message and exit.
 ```
diff --git a/scanapi/__init__.py b/scanapi/__init__.py
index 1c9e6b3..77157dc 100644
--- a/scanapi/__init__.py
+++ b/scanapi/__init__.py
@@ -17,12 +17,12 @@ from scanapi.config_loader import load_config_file
 
 @click.command()
 @click.option("-s", "--spec-path", "spec_path", type=click.Path(exists=True))
-@click.option("-o", "--output-path", "output_path", type=click.Path(exists=True))
+@click.option("-o", "--output-path", "output_path", type=click.Path())
 @click.option("-c", "--config-path", "config_path", type=click.Path(exists=True))
 @click.option(
     "-r", "--reporter", "reporter", type=click.Choice(["console", "markdown", "html"])
 )
-@click.option("-t", "--template", "template")
+@click.option("-t", "--template", "template", type=click.Path(exists=True))
 @click.option(
     "--log-level",
     "log_level",
diff --git a/scanapi/reporter.py b/scanapi/reporter.py
index 131a898..63e3f82 100644
--- a/scanapi/reporter.py
+++ b/scanapi/reporter.py
@@ -2,7 +2,7 @@
 import logging
 import curlify
 
-from jinja2 import Environment, PackageLoader, select_autoescape
+from jinja2 import Environment, FileSystemLoader, PackageLoader, select_autoescape
 
 from scanapi.settings import settings
 
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
 
 
 class Reporter:
-    def __init__(self, output_path, reporter, template):
+    def __init__(self, output_path, reporter, template=None):
         self.output_path = output_path
         self.reporter = reporter
         self.template = template
@@ -18,11 +18,14 @@ class Reporter:
     def write(self, responses):
         logger.info("Writing documentation")
 
-        # self.hide_headers_info(responses) TODO: Move to RequestNode.run
-        env = Environment(loader=PackageLoader("scanapi", "templates"))
-        env.filters["curlify"] = curlify.to_curl
-        template = env.get_template(f"{self.reporter}.jinja")
-        content = template.render(responses=responses)
+        if self.template:
+            loader = FileSystemLoader(searchpath="./")
+            template_path = self.template
+        else:
+            loader = PackageLoader("scanapi", "templates")
+            template_path = f"{self.reporter}.jinja"
+
+        content = self._render_content(loader, template_path, responses)
 
         if self.reporter == "console":
             print(f"\n{content}")
@@ -30,7 +33,7 @@ class Reporter:
 
         if self.output_path is None:
             outputs = {"html": "scanapi-report.html", "markdown": "scanapi-report.md"}
-            self.output_path = outputs.get(self.reporter)
+            self.output_path = outputs.get(self.reporter) or "scanapi-report"
 
         with open(self.output_path, "w", newline="\n") as doc:
             doc.write(content)
@@ -38,26 +41,8 @@ class Reporter:
         logger.info("The documentation was generated successfully.")
         logger.info(f"It is available at {self.output_path}")
 
-    def hide_headers_info(self, responses):
-        hide_keys = self.hide_keys()
-
-        if not hide_keys:
-            return
-
-        [
-            self.hide_request_headers_info(response.request, hide_keys)
-            for response in responses
-        ]
-
-    def hide_request_headers_info(self, request, hide_keys):
-        request_headers = request.headers
-
-        for key in hide_keys:
-            if key in request_headers:
-                request_headers[key] = "<sensitive_information>"
-
-    def hide_keys(self):
-        if not settings.get("report") or not settings["report"].get("hide"):
-            return
-
-        return settings["report"]["hide"].get("headers")
+    def _render_content(self, loader, template_path, responses):
+        env = Environment(loader=loader)
+        env.filters["curlify"] = curlify.to_curl
+        chosen_template = env.get_template(template_path)
+        return chosen_template.render(responses=responses)


Test patch:
diff --git a/tests/unit/test_reporter.py b/tests/unit/test_reporter.py
new file mode 100644
index 0000000..1848bd8
--- /dev/null
+++ b/tests/unit/test_reporter.py
@@ -0,0 +1,77 @@
+import pytest
+import jinja2
+from scanapi.reporter import Reporter
+
+fake_responses = [
+    {"status_code": 200, "request": {"method": "GET", "url": "http://test.com"}}
+]
+
+
+class TestConsoleReport:
+    @pytest.fixture
+    def mocked_print(self, mocker):
+        return mocker.patch("builtins.print")
+
+    def test_should_print(self, mocker, mocked_print):
+        console_reporter = Reporter(None, "console")
+        console_reporter.write(fake_responses)
+
+        expected_content = "\n".join(
+            (
+                "ScanAPI Report: Console",
+                "=======================",
+                "",
+                "GET http://test.com - 200",
+                "",
+            )
+        )
+
+        mocked_print.assert_called_once_with(f"\n{expected_content}")
+
+
+class TestReporterOtherThanConsole:
+    @pytest.fixture
+    def mocked__render_content(self, mocker):
+        return mocker.patch("scanapi.reporter.Reporter._render_content")
+
+    @pytest.fixture
+    def mocked_open(self, mocker):
+        mock = mocker.mock_open()
+        mocker.patch("builtins.open", mock)
+        return mock
+
+    @pytest.mark.parametrize("reporter_type", ["html", "markdown"])
+    def test_should_write_to_default_output(
+        self, reporter_type, mocker, mocked__render_content, mocked_open
+    ):
+        mocked__render_content.return_value = "ScanAPI Report"
+        reporter = Reporter(None, reporter_type)
+        reporter.write(fake_responses)
+
+        file_extension = {"html": "html", "markdown": "md"}[reporter_type]
+        mocked_open.assert_called_once_with(
+            f"scanapi-report.{file_extension}", "w", newline="\n"
+        )
+        mocked_open().write.assert_called_once_with("ScanAPI Report")
+
+    def test_should_write_to_custom_output(
+        self, mocker, mocked__render_content, mocked_open
+    ):
+        mocked__render_content.return_value = "ScanAPI Report"
+        reporter = Reporter("./custom/report-output.html", "html")
+        reporter.write(fake_responses)
+
+        mocked_open.assert_called_once_with(
+            "./custom/report-output.html", "w", newline="\n"
+        )
+        mocked_open().write.assert_called_once_with("ScanAPI Report")
+
+    def test_should_handle_custom_templates(
+        self, mocker, mocked__render_content, mocked_open
+    ):
+        mocked__render_content.return_value = "ScanAPI Report"
+        reporter = Reporter(None, None, "my-template.html")
+        reporter.write(fake_responses)
+
+        mocked_open.assert_called_once_with("scanapi-report", "w", newline="\n")
+        mocked_open().write.assert_called_once_with("ScanAPI Report")