diff --git a/tartufo/commands/update_signatures.py b/tartufo/commands/update_signatures.py index dea490b6..230247b5 100644 --- a/tartufo/commands/update_signatures.py +++ b/tartufo/commands/update_signatures.py @@ -10,6 +10,7 @@ Optional, Sequence, Tuple, + Union, ) import click @@ -22,6 +23,18 @@ DeprecationSetT = MutableSet[Sequence[str]] +def unwrap_signature(data: Union[str, MutableMapping[str, str]]) -> str: + """Handles the case where a signature can be a string or a dict + + :param data: The string, or dictionary to pull the signature from + :returns: The unwrapped signature + """ + if isinstance(data, str): + return data + + return data["signature"] + + def scan_local_repo( options: types.GlobalOptions, repo_path: str, @@ -101,13 +114,20 @@ def replace_deprecated_signatures( updated = 0 for old_sig, new_sig in deprecations: - targets = functools.partial(lambda o, s: o == s["signature"], old_sig) + targets = functools.partial(lambda o, s: o == unwrap_signature(s), old_sig) # Iterate all the deprecations and update them everywhere # they are found in the exclude-signatures section of config - for target_signature in filter(targets, config_data["exclude_signatures"]): + for i, target_signature in enumerate(config_data["exclude_signatures"]): + if not targets(target_signature): + continue + updated += 1 click.echo(f"{updated}) {old_sig!r} -> {new_sig!r}") - target_signature["signature"] = new_sig + + if isinstance(target_signature, str): + config_data["exclude_signatures"][i] = new_sig + else: + target_signature["signature"] = new_sig return updated @@ -120,16 +140,18 @@ def write_updated_signatures( :param config_path: The path to the tartufo config file :param config_data: The updated config data """ - with open(str(config_path), "r") as file: - result = tomlkit.loads(file.read()) + with open(str(config_path), "r+") as file: + file_content = file.read() + result = tomlkit.loads(file_content) + file.seek(0) - # Assign the new signatures and write it to the config - result["tool"]["tartufo"]["exclude-signatures"] = config_data[ # type: ignore - "exclude_signatures" - ] + # Assign the new signatures and write it to the config + result["tool"]["tartufo"]["exclude-signatures"] = config_data[ # type: ignore + "exclude_signatures" + ] - with open(str(config_path), "w") as file: file.write(tomlkit.dumps(result)) + file.truncate() def remove_duplicated_entries(config_data: MutableMapping[str, Any]) -> int: @@ -138,17 +160,19 @@ def remove_duplicated_entries(config_data: MutableMapping[str, Any]) -> int: :param config_data: The config data to check for duplicates """ - seen = set() + seen: MutableSet[str] = set() count = 0 - for i, exclude in enumerate(config_data["exclude_signatures"].copy()): - if exclude["signature"] in seen: + for i, exclude in enumerate(config_data["exclude_signatures"]): + signature = unwrap_signature(exclude) + + if signature in seen: # Remove this duplicated signature - del config_data["exclude_signatures"][i] + config_data["exclude_signatures"].pop(i) count += 1 else: # Mark this signature as seen - seen.add(exclude["signature"]) + seen.add(signature) return count @@ -202,7 +226,15 @@ def main( remove_duplicates: bool, ) -> GitRepoScanner: """Update deprecated signatures for a local repository.""" - config_path, config_data = load_config_from_path(pathlib.Path(repo_path)) + try: + config_path, config_data = load_config_from_path(pathlib.Path(repo_path)) + except FileNotFoundError: + util.fail( + util.style_warning("No tartufo config found, exiting..."), + ctx, + code=0, + ) + if not config_data.get("exclude_signatures"): util.fail( util.style_warning("No signatures found in configuration, exiting..."), diff --git a/tests/test_update_signatures.py b/tests/test_update_signatures.py index 5677fd8b..edf0246c 100644 --- a/tests/test_update_signatures.py +++ b/tests/test_update_signatures.py @@ -28,6 +28,17 @@ def test_with_no_signatures_in_config( result.output, "No signatures found in configuration, exiting...\n" ) + @mock.patch("tartufo.commands.update_signatures.load_config_from_path") + def test_with_no_config(self, mock_load_config: mock.MagicMock) -> None: + mock_load_config.side_effect = FileNotFoundError() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli.main, ["update-signatures", "."]) + + mock_load_config.assert_called_once() + self.assertEqual(result.output, "No tartufo config found, exiting...\n") + @mock.patch("tartufo.commands.update_signatures.GitRepoScanner") @mock.patch("tartufo.commands.update_signatures.load_config_from_path") def test_with_no_deprecated_signatures( @@ -280,6 +291,19 @@ def test_found_output_with_no_signatures( mock_scan_local.assert_called_once() self.assertEqual(result.output, "Found 0 deprecated signatures.\n") + def test_replace_deprecated_with_list_of_strings(self) -> None: + deprecations: Set[Sequence[str]] = set() + deprecations.update((("123", "abc"), ("456", "def"))) + config_data = {"exclude_signatures": ["123", "456"]} + expected_result = {"exclude_signatures": ["abc", "def"]} + + count = update_signatures.replace_deprecated_signatures( + deprecations, config_data + ) + + self.assertEqual(count, 2) + self.assertEqual(config_data, expected_result) + def test_remove_duplicated_entries(self) -> None: initial_data = { "exclude_signatures": [