From dd9f6170aa567f0a1170eb60c5003d216615bfa7 Mon Sep 17 00:00:00 2001 From: John-Paul Dakran Date: Fri, 1 Apr 2022 12:04:49 -0700 Subject: [PATCH] Add windows to ci github action --- .github/workflows/ci.yml | 2 +- testing/mocks.py | 21 ++++++++++++++++ tests/audit/analytics_test.py | 6 ++--- tests/audit/audit_test.py | 4 +-- tests/audit/compare_test.py | 4 +-- tests/audit/report_test.py | 6 ++--- tests/core/baseline_test.py | 10 +++++--- tests/core/scan_test.py | 13 +++++----- tests/core/usage/baseline_usage_test.py | 4 +-- tests/core/usage/filters_usage_test.py | 8 +++--- tests/core/usage/plugins_usage_test.py | 10 ++++---- tests/core/usage/scan_usage_test.py | 4 +-- tests/filters/wordlist_filter_test.py | 10 +++++--- tests/main_test.py | 33 ++++++++++++++++--------- tests/plugins/keyword_test.py | 2 +- tests/plugins/private_key_test.py | 5 ++-- tests/pre_commit_hook_test.py | 12 ++++----- 17 files changed, 96 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a00ee8dc..e6cb99bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python: ['3.6', '3.7', '3.8', '3.9'] steps: - uses: actions/checkout@v2 diff --git a/testing/mocks.py b/testing/mocks.py index b97a38cb..a8532565 100644 --- a/testing/mocks.py +++ b/testing/mocks.py @@ -1,5 +1,7 @@ """This is a collection of utility functions for easier, DRY testing.""" import io +import os +import tempfile from collections import defaultdict from contextlib import contextmanager from types import ModuleType @@ -88,3 +90,22 @@ def disable_gibberish_filter() -> Iterator[None]: return_value=False, ): yield + + +@contextmanager +def mock_named_temporary_file( + mode: str = 'w+b', dir: str = None, + suffix: str = None, prefix: str = None, +) -> Iterator[IO[Any]]: + """ + Used to create a mock temporary named file to write baseline files and secret files in + test. To avoid platform differences on how "NamedTemporaryFile" operates, we will perform + the creation and cleanup of the temporary file here. + """ + with tempfile.NamedTemporaryFile( + mode=mode, dir=dir, suffix=suffix, prefix=prefix, delete=False, + ) as f: + yield f + + f.close() + os.unlink(f.name) diff --git a/tests/audit/analytics_test.py b/tests/audit/analytics_test.py index 156a1354..3938ce2f 100644 --- a/tests/audit/analytics_test.py +++ b/tests/audit/analytics_test.py @@ -1,7 +1,6 @@ import json import random import string -import tempfile from contextlib import contextmanager import pytest @@ -11,6 +10,7 @@ from detect_secrets.main import main from detect_secrets.plugins.basic_auth import BasicAuthDetector from testing.factories import potential_secret_factory as original_potential_secret_factory +from testing.mocks import mock_named_temporary_file def potential_secret_factory(**kwargs): @@ -59,7 +59,7 @@ def test_basic_statistics_json(printer): def test_no_divide_by_zero(secret): secrets = SecretsCollection() secrets['file'].add(secret) - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) @@ -84,7 +84,7 @@ def labelled_secrets(): potential_secret_factory(is_secret=False), } - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) diff --git a/tests/audit/audit_test.py b/tests/audit/audit_test.py index b71d5d03..46099c96 100644 --- a/tests/audit/audit_test.py +++ b/tests/audit/audit_test.py @@ -1,6 +1,5 @@ import json import random -import tempfile from typing import List from typing import Optional from unittest import mock @@ -12,6 +11,7 @@ from detect_secrets.main import main from detect_secrets.settings import transient_settings from testing.factories import potential_secret_factory +from testing.mocks import mock_named_temporary_file def test_nothing_to_audit(printer): @@ -166,7 +166,7 @@ def run_logic( :param input: if provided, will automatically quit at the end of input string. otherwise, will assert that no user input is requested. """ - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) diff --git a/tests/audit/compare_test.py b/tests/audit/compare_test.py index 893ac6a4..7eeace19 100644 --- a/tests/audit/compare_test.py +++ b/tests/audit/compare_test.py @@ -1,5 +1,4 @@ import re -import tempfile from contextlib import contextmanager from unittest import mock @@ -11,6 +10,7 @@ from detect_secrets.main import main from detect_secrets.plugins.basic_auth import BasicAuthDetector from testing.factories import potential_secret_factory as original_potential_secret_factory +from testing.mocks import mock_named_temporary_file def potential_secret_factory(secret: str, **kwargs): @@ -139,7 +139,7 @@ def test_fails_when_no_line_number(printer): def run_logic(secretsA: SecretsCollection, secretsB: SecretsCollection): - with tempfile.NamedTemporaryFile() as f, tempfile.NamedTemporaryFile() as g: + with mock_named_temporary_file() as f, mock_named_temporary_file() as g: baseline.save_to_file(secretsA, f.name) baseline.save_to_file(secretsB, g.name) diff --git a/tests/audit/report_test.py b/tests/audit/report_test.py index d544d21e..abdef414 100644 --- a/tests/audit/report_test.py +++ b/tests/audit/report_test.py @@ -1,6 +1,5 @@ import random import string -import tempfile import textwrap from contextlib import contextmanager @@ -14,6 +13,7 @@ from detect_secrets.plugins.basic_auth import BasicAuthDetector from detect_secrets.plugins.jwt import JwtTokenDetector from detect_secrets.settings import transient_settings +from testing.mocks import mock_named_temporary_file url_format = 'http://username:{}@www.example.com/auth' @@ -166,7 +166,7 @@ def count_results(data): @contextmanager def create_file_with_content(content): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write(content.encode()) f.seek(0) yield f.name @@ -187,7 +187,7 @@ def baseline_file(): with create_file_with_content(first_content) as first_file, \ create_file_with_content(second_content) as second_file, \ - tempfile.NamedTemporaryFile() as baseline_file, \ + mock_named_temporary_file() as baseline_file, \ transient_settings({ 'plugins_used': [ {'name': 'BasicAuthDetector'}, diff --git a/tests/core/baseline_test.py b/tests/core/baseline_test.py index 7fbefbac..c8e24a8f 100644 --- a/tests/core/baseline_test.py +++ b/tests/core/baseline_test.py @@ -1,6 +1,7 @@ import json import subprocess import tempfile +from pathlib import Path from unittest import mock import pytest @@ -8,6 +9,7 @@ from detect_secrets.core import baseline from detect_secrets.settings import get_settings from detect_secrets.util.path import get_relative_path_if_in_cwd +from testing.mocks import mock_named_temporary_file @pytest.fixture(autouse=True) @@ -39,8 +41,8 @@ def test_basic_usage(path): secrets = baseline.create(path) assert len(secrets.data.keys()) == 2 - assert len(secrets['test_data/files/file_with_secrets.py']) == 1 - assert len(secrets['test_data/files/tmp/file_with_secrets.py']) == 2 + assert len(secrets[str(Path('test_data/files/file_with_secrets.py'))]) == 1 + assert len(secrets[str(Path('test_data/files/tmp/file_with_secrets.py'))]) == 2 @staticmethod def test_error_when_getting_git_tracked_files(): @@ -59,7 +61,7 @@ def test_non_existent_file(): def test_no_files_in_git_repo(): with tempfile.TemporaryDirectory() as d: # Create a new directory, so scanning is sandboxed. - with tempfile.NamedTemporaryFile(dir=d, suffix='.py') as f: + with mock_named_temporary_file(dir=d, suffix='.py') as f: f.write(b'"2b00042f7481c7b056c4b410d28f33cf"') f.seek(0) @@ -69,7 +71,7 @@ def test_no_files_in_git_repo(): @staticmethod def test_scan_all_files(): - with tempfile.NamedTemporaryFile(dir='test_data/files/tmp', suffix='.py') as f: + with mock_named_temporary_file(dir='test_data/files/tmp', suffix='.py') as f: f.write(b'"2b00042f7481c7b056c4b410d28f33cf"') f.seek(0) diff --git a/tests/core/scan_test.py b/tests/core/scan_test.py index 279f8e4e..566e2e46 100644 --- a/tests/core/scan_test.py +++ b/tests/core/scan_test.py @@ -1,6 +1,6 @@ import os -import tempfile import textwrap +from pathlib import Path import pytest @@ -8,6 +8,7 @@ from detect_secrets.settings import transient_settings from detect_secrets.util import git from detect_secrets.util.path import get_relative_path_if_in_cwd +from testing.mocks import mock_named_temporary_file class TestGetFilesToScan: @@ -53,16 +54,16 @@ def test_handles_each_path_separately(non_tracked_file): @staticmethod def test_handles_multiple_directories(): - directories = ['test_data/short_files', 'test_data/files'] + directories = [Path('test_data/short_files'), Path('test_data/files')] results = list(scan.get_files_to_scan(*directories)) for prefix in directories: - assert len(list(filter(lambda x: x.startswith(prefix), results))) > 1 + assert len(list(filter(lambda x: x.startswith(str(prefix)), results))) > 1 @staticmethod @pytest.fixture(autouse=True, scope='class') def non_tracked_file(): - with tempfile.NamedTemporaryFile( + with mock_named_temporary_file( prefix=os.path.join(git.get_root_directory(), 'test_data/'), ) as f: f.write(b'content does not matter') @@ -74,7 +75,7 @@ def non_tracked_file(): class TestScanFile: @staticmethod def test_handles_broken_yaml_gracefully(): - with tempfile.NamedTemporaryFile(suffix='.yaml') as f: + with mock_named_temporary_file(suffix='.yaml') as f: f.write( textwrap.dedent(""" metadata: @@ -89,7 +90,7 @@ def test_handles_broken_yaml_gracefully(): def test_handles_binary_files_gracefully(): # NOTE: This suffix needs to be something that isn't in the known file types, as determined # by `detect_secrets.util.filetype.determine_file_type`. - with tempfile.NamedTemporaryFile(suffix='.woff2') as f: + with mock_named_temporary_file(suffix='.woff2') as f: f.write(b'\x86') f.seek(0) diff --git a/tests/core/usage/baseline_usage_test.py b/tests/core/usage/baseline_usage_test.py index 39db8a76..c680488d 100644 --- a/tests/core/usage/baseline_usage_test.py +++ b/tests/core/usage/baseline_usage_test.py @@ -1,5 +1,4 @@ import json -import tempfile from contextlib import contextmanager import pytest @@ -7,6 +6,7 @@ from detect_secrets.core.plugins.util import get_mapping_from_secret_type_to_class from detect_secrets.core.usage import ParserBuilder from detect_secrets.settings import get_settings +from testing.mocks import mock_named_temporary_file @pytest.fixture @@ -60,7 +60,7 @@ def test_success(parser): @contextmanager def _mock_file(content: str): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write(content.encode()) f.seek(0) diff --git a/tests/core/usage/filters_usage_test.py b/tests/core/usage/filters_usage_test.py index f7e0a4c2..c4836d9b 100644 --- a/tests/core/usage/filters_usage_test.py +++ b/tests/core/usage/filters_usage_test.py @@ -1,4 +1,3 @@ -import tempfile import uuid import pytest @@ -10,11 +9,12 @@ from detect_secrets.settings import default_settings from detect_secrets.settings import get_settings from detect_secrets.settings import transient_settings +from testing.mocks import mock_named_temporary_file def test_no_verify_overrides_baseline_settings(parser): secrets = SecretsCollection() - with tempfile.NamedTemporaryFile() as f, transient_settings({ + with mock_named_temporary_file() as f, transient_settings({ 'filters_used': [{ 'path': 'detect_secrets.filters.common.is_ignored_due_to_verification_policies', 'min_level': VerifiedResult.UNVERIFIED.value, @@ -30,7 +30,7 @@ def test_no_verify_overrides_baseline_settings(parser): def test_only_verified_overrides_baseline_settings(parser): secrets = SecretsCollection() - with tempfile.NamedTemporaryFile() as f, transient_settings({ + with mock_named_temporary_file() as f, transient_settings({ 'filters_used': [{ 'path': 'detect_secrets.filters.common.is_ignored_due_to_verification_policies', 'min_level': VerifiedResult.UNVERIFIED.value, @@ -134,7 +134,7 @@ def test_module_failure(parser, filepath): def test_disable_filter(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write(f'secret = "{uuid.uuid4()}"'.encode()) # First, make sure that we actually catch it. diff --git a/tests/core/usage/plugins_usage_test.py b/tests/core/usage/plugins_usage_test.py index f1e12dbc..f7bc21b8 100644 --- a/tests/core/usage/plugins_usage_test.py +++ b/tests/core/usage/plugins_usage_test.py @@ -1,6 +1,5 @@ import json import os -import tempfile import pytest @@ -9,6 +8,7 @@ from detect_secrets.core.secrets_collection import SecretsCollection from detect_secrets.core.usage import ParserBuilder from detect_secrets.settings import get_settings +from testing.mocks import mock_named_temporary_file @pytest.fixture @@ -48,7 +48,7 @@ def test_failure(parser, flag, value): @staticmethod def test_precedence_with_only_baseline(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write( json.dumps({ 'version': '0.0.1', @@ -69,7 +69,7 @@ def test_precedence_with_only_baseline(parser): @staticmethod def test_precedence_with_baseline_and_explicit_value(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write( json.dumps({ 'version': '0.0.1', @@ -115,7 +115,7 @@ def test_invalid_classname(parser): @staticmethod def test_precedence_with_baseline(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write( json.dumps({ 'version': '0.0.1', @@ -148,7 +148,7 @@ def test_success(parser): # Ensure it serializes accordingly. parser.parse_args(['-p', 'testing/plugins.py']) - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(SecretsCollection(), f.name) f.seek(0) diff --git a/tests/core/usage/scan_usage_test.py b/tests/core/usage/scan_usage_test.py index 1933db85..3823ee3d 100644 --- a/tests/core/usage/scan_usage_test.py +++ b/tests/core/usage/scan_usage_test.py @@ -1,5 +1,4 @@ import json -import tempfile import pytest @@ -7,6 +6,7 @@ from detect_secrets.core.plugins.util import get_mapping_from_secret_type_to_class from detect_secrets.core.usage import ParserBuilder from detect_secrets.settings import get_settings +from testing.mocks import mock_named_temporary_file @pytest.fixture @@ -15,7 +15,7 @@ def parser(): def test_force_use_all_plugins(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write( json.dumps({ 'version': '0.0.1', diff --git a/tests/filters/wordlist_filter_test.py b/tests/filters/wordlist_filter_test.py index a8fa9177..71073ff4 100644 --- a/tests/filters/wordlist_filter_test.py +++ b/tests/filters/wordlist_filter_test.py @@ -1,6 +1,9 @@ +from pathlib import Path + import pytest from detect_secrets import filters +from detect_secrets.filters.util import compute_file_hash from detect_secrets.settings import get_settings from detect_secrets.settings import transient_settings @@ -17,6 +20,9 @@ def initialize_automaton(): @staticmethod def test_success(): + # Compute file_hash manually due to file path operating system differences + file_hash = compute_file_hash(Path('test_data/word_list.txt')) + # case-insensitivity assert filters.wordlist.should_exclude_secret('testPass') is True @@ -25,9 +31,7 @@ def test_success(): assert get_settings().filters['detect_secrets.filters.wordlist.should_exclude_secret'] == { 'min_length': 8, - - # Manually computed with `sha1sum test_data/word_list.txt` - 'file_hash': '116598304e5b33667e651025bcfed6b9a99484c7', + 'file_hash': file_hash, 'file_name': 'test_data/word_list.txt', } diff --git a/tests/main_test.py b/tests/main_test.py index 4a2bb98e..106d0c4b 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,17 +1,22 @@ import json import os import subprocess +import sys import tempfile from contextlib import contextmanager from contextlib import redirect_stdout +from pathlib import Path from unittest import mock +import pytest + from detect_secrets import main as main_module from detect_secrets.core import baseline from detect_secrets.core.secrets_collection import SecretsCollection from detect_secrets.main import scan_adhoc_string from detect_secrets.settings import transient_settings from testing.mocks import disable_gibberish_filter +from testing.mocks import mock_named_temporary_file from testing.mocks import mock_printer @@ -39,7 +44,7 @@ def test_saves_to_baseline(): secrets = SecretsCollection() old_secrets = baseline.format_for_output(secrets) - with mock_printer(main_module) as printer, tempfile.NamedTemporaryFile() as f: + with mock_printer(main_module) as printer, mock_named_temporary_file() as f: baseline.save_to_file(old_secrets, f.name) f.seek(0) @@ -58,6 +63,10 @@ def test_saves_to_baseline(): assert not printer.message @staticmethod + @pytest.mark.xfail( + sys.version_info < (3, 8) and sys.platform == 'win32', + reason='TemporaryDirectory correct tear-down requires python 3.8 or higher on windows', + ) def test_works_from_different_directory(): with tempfile.TemporaryDirectory() as d: subprocess.call(['git', '-C', d, 'init']) @@ -83,26 +92,28 @@ def test_basic(): @staticmethod def test_restores_line_numbers(): - with tempfile.NamedTemporaryFile('w+') as f: - with redirect_stdout(f): - main_module.main(['scan', '--slim', 'test_data/config.env']) + with mock_named_temporary_file(mode='w+') as f, redirect_stdout(f): + file_a = str(Path('test_data/config.env')) + file_b = str(Path('test_data/config.md')) + + main_module.main(['scan', '--slim', file_a]) f.seek(0) main_module.main([ 'scan', - '--slim', 'test_data/config.md', 'test_data/config.env', + '--slim', file_a, file_b, '--baseline', f.name, ]) f.seek(0) secrets = baseline.load(baseline.load_from_file(f.name)) - # Make sure both old and new files exist - assert secrets.files == {'test_data/config.env', 'test_data/config.md'} + # Make sure both old and new files exist + assert secrets.files == {file_a, file_b} - # Make sure they both have line numbers - assert list(secrets['test_data/config.env'])[0].line_number - assert list(secrets['test_data/config.md'])[0].line_number + # Make sure they both have line numbers + assert list(secrets[file_a])[0].line_number + assert list(secrets[file_b])[0].line_number class TestScanString: @@ -183,7 +194,7 @@ def test_basic(mock_log): for item in output['filters_used'] } - with tempfile.NamedTemporaryFile() as f, mock.patch( + with mock_named_temporary_file() as f, mock.patch( 'detect_secrets.audit.io.get_user_decision', return_value='s', ): diff --git a/tests/plugins/keyword_test.py b/tests/plugins/keyword_test.py index ce8b0748..c9591db5 100644 --- a/tests/plugins/keyword_test.py +++ b/tests/plugins/keyword_test.py @@ -13,7 +13,7 @@ LETTER_SECRET = 'A,.:-¨@*¿?!' SYMBOL_SECRET = ',.:-¨@*¿?!' -LONG_LINE = ''.format(base64.b64encode((str(randint(0, 9)) * 30500).encode())) # noqa: E501 +LONG_LINE = ''.format(base64.b64encode((str(randint(0, 9)) * 24000).encode())) # noqa: E501 CONFIG_TEST_CASES = [ ('password = "{}"'.format(WHITES_SECRET), WHITES_SECRET), diff --git a/tests/plugins/private_key_test.py b/tests/plugins/private_key_test.py index 533ead34..b4a40a39 100644 --- a/tests/plugins/private_key_test.py +++ b/tests/plugins/private_key_test.py @@ -1,9 +1,8 @@ -import tempfile - import pytest from detect_secrets.core.secrets_collection import SecretsCollection from detect_secrets.settings import transient_settings +from testing.mocks import mock_named_temporary_file @pytest.mark.parametrize( @@ -22,7 +21,7 @@ ], ) def test_basic(file_content): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: f.write(file_content.encode()) f.seek(0) diff --git a/tests/pre_commit_hook_test.py b/tests/pre_commit_hook_test.py index 4c44fd7f..1fba4913 100644 --- a/tests/pre_commit_hook_test.py +++ b/tests/pre_commit_hook_test.py @@ -1,5 +1,4 @@ import json -import tempfile from contextlib import contextmanager from functools import partial from typing import List @@ -12,6 +11,7 @@ from detect_secrets.pre_commit_hook import main from detect_secrets.settings import transient_settings from testing.mocks import disable_gibberish_filter +from testing.mocks import mock_named_temporary_file @pytest.fixture(autouse=True) @@ -54,7 +54,7 @@ def test_baseline_filters_out_known_secrets(): assert secrets with disable_gibberish_filter(): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) @@ -68,7 +68,7 @@ def test_baseline_filters_out_known_secrets(): # Remove one arbitrary secret, so that it won't be the full set. secrets.data['test_data/each_secret.py'].pop() - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) @@ -128,7 +128,7 @@ def get_baseline_file(self, formatter=baseline.format_for_output): secrets = SecretsCollection() secrets.scan_file(self.FILENAME) - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: with mock.patch('detect_secrets.core.baseline.VERSION', '0.0.1'): data = formatter(secrets) @@ -143,7 +143,7 @@ class TestLineNumberChanges: FILENAME = 'test_data/files/file_with_secrets.py' def test_modifies_baseline(self, modified_baseline): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file(modified_baseline, f.name) assert_commit_blocked_with_diff_exit_code([ @@ -153,7 +153,7 @@ def test_modifies_baseline(self, modified_baseline): ]) def test_does_not_modify_slim_baseline(self, modified_baseline): - with tempfile.NamedTemporaryFile() as f: + with mock_named_temporary_file() as f: baseline.save_to_file( baseline.format_for_output(modified_baseline, is_slim_mode=True), f.name,