diff --git a/htmlproofer/plugin.py b/htmlproofer/plugin.py index 5420840..b1be385 100644 --- a/htmlproofer/plugin.py +++ b/htmlproofer/plugin.py @@ -101,17 +101,18 @@ def on_post_page(self, output_content: str, page: Page, config: Config) -> None: url_status = self.get_url_status(url, page.file.src_path, all_element_ids, self.files, use_directory_urls) - if self.bad_url(url_status) is True: - error = f'invalid url - {url} [{url_status}] [{page.file.src_path}]' - - is_error = self.is_error(self.config, url, url_status) - if self.config['raise_error'] and is_error: - raise PluginError(error) - elif self.config['raise_error_after_finish'] and is_error and not self.invalid_links: - log_error(error) - self.invalid_links = True - if is_error: - log_warning(error) + if self.bad_url(url_status) and self.is_error(self.config, url, url_status): + self.report_invalid_url(url, url_status, page.file.src_path) + + def report_invalid_url(self, url, url_status, src_path): + error = f'invalid url - {url} [{url_status}] [{src_path}]' + if self.config['raise_error']: + raise PluginError(error) + elif self.config['raise_error_after_finish']: + log_error(error) + self.invalid_links = True + else: + log_warning(error) def get_external_url(self, url, scheme, src_path): try: @@ -155,8 +156,12 @@ def get_url_status( # Markdown file, so disable target anchor validation in this case. Examples include: # ../..#BAD_ANCHOR style links to index.html and extra ../ inserted into relative # links. - if not self.is_url_target_valid(url, src_path, files): - return 404 + is_valid = self.is_url_target_valid(url, src_path, files) + url_status = 404 + if not is_valid and self.is_error(self.config, url, url_status): + log_warning(f"Unable to locate source file for: {url}") + return url_status + return 0 return 0 @staticmethod @@ -168,7 +173,7 @@ def is_url_target_valid(url: str, src_path: str, files: Dict[str, File]) -> bool url_target, _, optional_anchor = match.groups() _, extension = os.path.splitext(url_target) if extension == ".html": - # URL is a link to another local Markdown file that may includes an anchor. + # URL is a link to another local Markdown file that may include an anchor. target_markdown = HtmlProoferPlugin.find_target_markdown(url_target, src_path, files) if target_markdown is None: # The corresponding Markdown page was not found. @@ -178,7 +183,6 @@ def is_url_target_valid(url: str, src_path: str, files: Dict[str, File]) -> bool return False elif HtmlProoferPlugin.find_source_file(url_target, src_path, files) is None: return False - return True @staticmethod @@ -204,7 +208,6 @@ def find_source_file(url: str, src_path: str, files: Dict[str, File]) -> Optiona try: return files[search_path] except KeyError: - utils.log.warning(f"Unable to locate source file for: {url}") return None @staticmethod diff --git a/tests/integration/docs/index.md b/tests/integration/docs/index.md index 76ff389..f5e1452 100644 --- a/tests/integration/docs/index.md +++ b/tests/integration/docs/index.md @@ -6,6 +6,7 @@ * [Installation](#installation) * [Acknowledgement](#acknowledge) +* [Browse Tests](../../../tests) ## Installation diff --git a/tests/integration/mkdocs.yml b/tests/integration/mkdocs.yml index f79ce65..700b37a 100644 --- a/tests/integration/mkdocs.yml +++ b/tests/integration/mkdocs.yml @@ -9,7 +9,10 @@ plugins: raise_error_after_finish: True raise_error_excludes: 504: ['*'] - 404: ['https://www.mkdocs.org/user-guide/plugin', - '#acknowledge', - '../index.html#BAD_ANCHOR', - 'page2.html#BAD_ANCHOR'] + 404: [ + 'https://www.mkdocs.org/user-guide/plugin', + '#acknowledge', + '../index.html#BAD_ANCHOR', + 'page2.html#BAD_ANCHOR', + '../../../tests', + ] diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index e060d19..7b70767 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -9,6 +9,7 @@ import pytest from requests import Response +import htmlproofer.plugin from htmlproofer.plugin import HtmlProoferPlugin @@ -316,3 +317,95 @@ def test_get_url_status__local_page_nested(plugin): assert plugin.get_url_status('foo/baz/nested.html#nested-two', 'index.md', set(), files, False) == 0 assert plugin.get_url_status('/index.html', 'foo/baz/sibling.md', set(), files, False) == 0 + + +@patch.object(htmlproofer.plugin, "log_warning", autospec=True) +def test_get_url_status__excluded_non_existing_relative_url__no_warning(log_warning_mock, plugin): + url_status = 404 + url = "non-existing.html" + src_path = "index.md" + files = {} + plugin.config['raise_error_excludes'][url_status] = [url] + + status = plugin.get_url_status(url, src_path, set(), files, False) + + log_warning_mock.assert_not_called() + assert 0 == status + + +@patch.object(htmlproofer.plugin, "log_warning", autospec=True) +def test_get_url_status__excluded_existing_relative_url__no_warning(log_warning_mock, plugin): + url_status = 404 + filename = "existing" + url = f"{filename}.html" + src_path = f"{filename}.md" + existing_page = Mock(spec=Page, markdown='') + files = { + os.path.normpath(file.url): file for file in Files([ + Mock(spec=File, src_path=src_path, dest_path=url, url=url, page=existing_page) + ]) + } + plugin.config['raise_error_excludes'][url_status] = [url] + + status = plugin.get_url_status(url, src_path, set(), files, False) + + log_warning_mock.assert_not_called() + assert 0 == status + + +@patch.object(htmlproofer.plugin, "log_warning", autospec=True) +def test_get_url_status__non_existing_relative_url__warning_and_404(log_warning_mock, plugin): + expected_url_status = 404 + url = "non-existing.html" + src_path = "index.md" + files = {} + + status = plugin.get_url_status(url, src_path, set(), files, False) + + log_warning_mock.assert_called_once() + assert expected_url_status == status + + +def test_report_invalid_url__raise_error__highest_priority(plugin): + plugin.config['raise_error'] = True + plugin.config['raise_error_after_finish'] = True + + with pytest.raises(PluginError): + plugin.report_invalid_url(url='', url_status=404, src_path="") + + +@patch.object(htmlproofer.plugin, "log_error", autospec=True) +@patch.object(htmlproofer.plugin, "log_warning", autospec=True) +def test_report_invalid_url__raise_error__raises_and_no_log(log_warning_mock, log_error_mock, plugin): + plugin.config['raise_error'] = True + + with pytest.raises(PluginError): + plugin.report_invalid_url(url='', url_status=404, src_path="") + log_warning_mock.assert_not_called() + log_error_mock.assert_not_called() + + +@patch.object(htmlproofer.plugin, "log_error", autospec=True) +@patch.object(htmlproofer.plugin, "log_warning", autospec=True) +def test_report_invalid_url__raise_error_after_finish__log_error_is_called(log_warning_mock, log_error_mock, plugin): + plugin.config['raise_error'] = False + plugin.config['raise_error_after_finish'] = True + + plugin.report_invalid_url(url='', url_status=404, src_path="") + + log_warning_mock.assert_not_called() + log_error_mock.assert_called_once() + assert plugin.invalid_links + + +@patch.object(htmlproofer.plugin, "log_error", autospec=True) +@patch.object(htmlproofer.plugin, "log_warning", autospec=True) +def test_report_invalid_url__not_raise_error__only_log_warning_is_called(log_warning_mock, log_error_mock, plugin): + plugin.config['raise_error'] = False + plugin.config['raise_error_after_finish'] = False + + plugin.report_invalid_url(url='', url_status=404, src_path="") + + log_warning_mock.assert_called_once() + log_error_mock.assert_not_called() + assert not plugin.invalid_links