diff --git a/Pipfile b/Pipfile index 8b6c822..e94965a 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ qrcode = "*" pillow = "*" qreader = "*" opencv-contrib-python = "*" +colorama = "*" # for macOS: opencv-contrib-python = "<=4.7.0" # for PYTHON <= 3.7: typing_extensions = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 6fb1428..daf6033 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "beffcba766af29a6a313c019cc98ab27e61c6dd433d02df0917fdb3808b90379" + "sha256": "4cc62fa4427b3a8821438db9164bd5c7c42d4e5f08d3f950c40da381ba7e063e" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,14 @@ ] }, "default": { + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "index": "pypi", + "version": "==0.4.6" + }, "numpy": { "hashes": [ "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9", diff --git a/pyproject.toml b/pyproject.toml index 4cb5f4e..7ff9e69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "opencv-contrib-python<=4.7.0; sys_platform == 'darwin'", "opencv-contrib-python; sys_platform != 'darwin'", "typing_extensions; python_version<='3.7'", + "colorama", ] description = "Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of 'Google Authenticator' app" dynamic = ["version"] diff --git a/requirements.txt b/requirements.txt index 07f99f5..927d4c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ opencv-contrib-python<=4.7.0; sys_platform == 'darwin' opencv-contrib-python; sys_platform != 'darwin' pyzbar typing_extensions; python_version<='3.7' +colorama diff --git a/src/extract_otp_secrets.py b/src/extract_otp_secrets.py index 7bcddaf..b29790c 100644 --- a/src/extract_otp_secrets.py +++ b/src/extract_otp_secrets.py @@ -65,6 +65,7 @@ else: from qrcode import QRCode # type: ignore import protobuf_generated_python.google_auth_pb2 as pb +import colorama try: import cv2 # type: ignore @@ -123,6 +124,7 @@ CAMERA: Final[str] = 'camera' # Global variable declaration verbose: int = 0 quiet: bool = False +colored: bool = True def sys_main() -> None: @@ -134,13 +136,17 @@ def main(sys_args: list[str]) -> None: sys.stdout.close = lambda: None # type: ignore # set encoding to utf-8, needed for Windows try: - sys.stdout.reconfigure(encoding='utf-8') # type: ignore + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') except AttributeError: # '_io.StringIO' object has no attribute 'reconfigure' # StringIO in tests do not have all attributes, ignore it pass args = parse_args(sys_args) + if colored: + colorama.just_fix_windows_console() + otps = extract_otps(args) write_csv(args, otps) write_keepass_csv(args, otps) @@ -148,7 +154,7 @@ def main(sys_args: list[str]) -> None: def parse_args(sys_args: list[str]) -> Args: - global verbose, quiet + global verbose, quiet, colored description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app." if qreader_available: description_text += "\nIf no infiles are provided, the QR codes are interactively captured from the camera." @@ -165,13 +171,14 @@ python extract_otp_secrets.py = < example_export.png""" arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored; b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if qreader_available else '+') if qreader_available: - arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, nargs=1, metavar=('NUMBER')) + arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER')) arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name) arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE')) arg_parser.add_argument('--csv', '-c', help='export csv file or - for stdout', metavar=('FILE')) arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE')) arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true') arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR')) + arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true') output_group = arg_parser.add_mutually_exclusive_group() output_group.add_argument('--verbose', '-v', help='verbose output', action='count') output_group.add_argument('--quiet', '-q', help='no stdout output, except output set by -', action='store_true') @@ -181,6 +188,7 @@ b) image file containing a QR code or = for stdin for an image containing a QR c verbose = args.verbose if args.verbose else 0 quiet = True if args.quiet else False + colored = not args.no_color if verbose: print(f"QReader installed: {qreader_available}") if qreader_available: if verbose > 1: print(f"CV2 version: {cv2.__version__}") @@ -213,7 +221,7 @@ def extract_otps_from_camera(args: Args) -> Otps: qr_mode = QRMode[args.qr] - cam = cv2.VideoCapture(args.camera[0]) + cam = cv2.VideoCapture(args.camera) window_name = "Extract OTP Secrets: Capture QR Codes from Camera" cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE) @@ -224,7 +232,7 @@ def extract_otps_from_camera(args: Args) -> Otps: success, img = cam.read() new_otps_count = 0 if not success: - eprint("ERROR: Failed to capture image") + log_error("Failed to capture image") break if qr_mode in [QRMode.QREADER, QRMode.DEEP_QREADER]: bbox, found = qreader.detect(img) @@ -255,7 +263,7 @@ def extract_otps_from_camera(args: Args) -> Otps: pts = pts.reshape((-1, 1, 2)) cv2.polylines(img, [pts], True, get_color(new_otps_count, otp_url), RECT_THICKNESS) else: - assert False, f"ERROR: Wrong QReader mode {qr_mode.name}" + assert False, f"Wrong QReader mode {qr_mode.name}" cv2.putText(img, f"Mode: {qr_mode.name} (Hit space to change)", START_POS_TEXT, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE) cv2.putText(img, "Hit ESC to quit", tuple(map(add, START_POS_TEXT, FONT_DY)), FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE) @@ -341,14 +349,14 @@ def read_lines_from_text_file(filename: str) -> list[str]: for line in (line.strip() for line in finput): if verbose: print(line) if is_binary(line): - abort("\nBinary input was given in stdin, please use = instead of - as infile argument for images.") + abort("Binary input was given in stdin, please use = instead of - as infile argument for images.") # unfortunately yield line leads to random test fails lines.append(line) if not lines: - eprint(f"WARN: {filename.replace('-', 'stdin')} is empty") + log_warn(f"{filename.replace('-', 'stdin')} is empty") except UnicodeDecodeError: if filename == '-': - abort("\nERROR: Unable to open text file form stdin. " + abort("Unable to open text file form stdin. " "In case you want read an image file from stdin, you must use '=' instead of '-'.") else: # The file is probably an image, process below return [] @@ -367,7 +375,7 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: # pylint: disable=no-member for raw_otp in payload.otp_parameters: new_otps_count += 1 - if verbose: print(f"\n{len(otps) + 1}. Secret Key") + if verbose: print(f"\n{len(otps) + 1}. Secret") secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret) if verbose: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type')) otp_type = get_otp_type_str_from_code(raw_otp.type) @@ -405,17 +413,17 @@ def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls: # Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures if not stdin: - eprint("WARN: stdin is empty") + log_warn("stdin is empty") try: img_array = numpy.frombuffer(stdin, dtype='uint8') except TypeError as e: - abort(f"\nERROR: Cannot read binary stdin buffer. Exception: {e}") + abort(f"Cannot read binary stdin buffer. Exception: {e}") if not img_array.size: return [] img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED) if img is None: - abort(f"\nERROR: Unable to open file for reading.\ninput file: {filename}") + abort(f"Unable to open file for reading.\ninput file: {filename}") qr_mode = QRMode[args.qr] otp_urls: OtpUrls = [] @@ -432,12 +440,12 @@ def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls: qrcodes = zbar.decode(img) otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes] else: - assert False, f"ERROR: Wrong QReader mode {qr_mode.name}" + assert False, f"Wrong QReader mode {qr_mode.name}" if len(otp_urls) == 0: - abort(f"\nERROR: Unable to read QR Code from file.\ninput file: {filename}") + abort(f"Unable to read QR Code from file.\ninput file: {filename}") except Exception as e: - abort(f"\nERROR: Encountered exception '{e}'.\ninput file: {filename}") + abort(f"Encountered exception '{e}'.\ninput file: {filename}") return otp_urls @@ -446,10 +454,10 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M if not otp_url.startswith('otpauth-migration://'): msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}" if source == CAMERA: - eprint(f"\nERROR: {msg}") + log_error(f"{msg}") return None else: - eprint(f"\nWARN: {msg}\nMaybe a wrong file was given") + log_warn(f"{msg}\nMaybe a wrong file was given") parsed_url = urlparse.urlparse(otp_url) if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}") try: @@ -458,7 +466,7 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M params = {} if verbose > 2: print(f"\nDEBUG: querystring params={params}") if 'data' not in params: - eprint(f"\nERROR: could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}") + log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}") return None data_base64 = params['data'][0] if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}") @@ -469,7 +477,7 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M try: payload.ParseFromString(data) except Exception: - abort(f"\nERROR: Cannot decode otpauth-migration migration payload.\n" + abort(f"Cannot decode otpauth-migration migration payload.\n" f"data={data_base64}") if verbose: print(f"\n{i}. Payload Line", payload, sep='\n') @@ -620,7 +628,7 @@ def open_file_or_stdout_for_csv(filename: str) -> TextIO: def check_file_exists(filename: str) -> None: if filename != '-' and not os.path.isfile(filename): - abort(f"\nERROR: Input file provided is non-existent or not a file." + abort(f"Input file provided is non-existent or not a file." f"\ninput file: {filename}") @@ -632,13 +640,21 @@ def is_binary(line: str) -> bool: return True +def log_warn(msg: str) -> None: + eprint(f"{colorama.Fore.RED if colored else ''}\nWARN: {msg}{colorama.Fore.RESET if colored else ''}") + + +def log_error(msg: str) -> None: + eprint(f"{colorama.Fore.RED if colored else ''}\nERROR: {msg}{colorama.Fore.RESET if colored else ''}") + + def eprint(*args: Any, **kwargs: Any) -> None: '''Print to stderr.''' print(*args, file=sys.stderr, **kwargs) -def abort(*args: Any, **kwargs: Any) -> None: - eprint(*args, **kwargs) +def abort(msg: str) -> None: + log_error(msg) sys.exit(1) diff --git a/tests/data/print_verbose_output.txt b/tests/data/print_verbose_output.txt index 675483c..5fc53bf 100644 --- a/tests/data/print_verbose_output.txt +++ b/tests/data/print_verbose_output.txt @@ -43,7 +43,7 @@ batch_size: 1 batch_id: -1320898453 -1. Secret Key +1. Secret OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY @@ -68,7 +68,7 @@ batch_size: 1 batch_id: -2094403140 -2. Secret Key +2. Secret OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY @@ -101,7 +101,7 @@ batch_size: 1 batch_id: -1822886384 -3. Secret Key +3. Secret OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY @@ -109,7 +109,7 @@ Type: totp otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY -4. Secret Key +4. Secret OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY @@ -135,7 +135,7 @@ batch_size: 1 batch_id: -1558849573 -5. Secret Key +5. Secret OTP enum type: OTP_HOTP Name: hotp demo Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY @@ -161,7 +161,7 @@ batch_size: 1 batch_id: -171198419 -6. Secret Key +6. Secret OTP enum type: OTP_TOTP Name: encoding: ¿äÄéÉ? (demo) Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY diff --git a/tests/extract_otp_secrets_img_unit_test.py b/tests/extract_otp_secrets_img_unit_test.py index 05200c1..8682129 100644 --- a/tests/extract_otp_secrets_img_unit_test.py +++ b/tests/extract_otp_secrets_img_unit_test.py @@ -40,7 +40,7 @@ class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_no_qr_code_in_image(self) -> None: with Capturing() as actual_output: with self.assertRaises(SystemExit) as context: - extract_otp_secrets.main(['tests/data/lena_std.tif']) + extract_otp_secrets.main(['-n', 'tests/data/lena_std.tif']) expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: tests/data/lena_std.tif'] @@ -50,7 +50,7 @@ class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_nonexistent_file(self) -> None: with Capturing() as actual_output: with self.assertRaises(SystemExit) as context: - extract_otp_secrets.main(['nonexistent.bmp']) + extract_otp_secrets.main(['-n', 'nonexistent.bmp']) expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: nonexistent.bmp'] @@ -59,7 +59,7 @@ class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_non_image_file(self) -> None: with Capturing() as actual_output: - extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg']) + extract_otp_secrets.main(['-n', 'tests/data/text_masquerading_as_image.jpeg']) expected_output = [ '', diff --git a/tests/extract_otp_secrets_test.py b/tests/extract_otp_secrets_test.py index c7d473c..6a566f4 100644 --- a/tests/extract_otp_secrets_test.py +++ b/tests/extract_otp_secrets_test.py @@ -23,6 +23,7 @@ import io import os import pathlib import sys +import colorama import pytest from pytest_mock import MockerFixture @@ -54,7 +55,7 @@ def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None: def test_extract_non_existent_file(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secrets.main(['non_existent_file.txt']) + extract_otp_secrets.main(['-n', 'non_existent_file.txt']) # Assert captured = capsys.readouterr() @@ -86,25 +87,25 @@ def test_extract_stdin_empty(capsys: pytest.CaptureFixture[str], monkeypatch: py monkeypatch.setattr('sys.stdin', io.StringIO()) # Act - extract_otp_secrets.main(['-']) + extract_otp_secrets.main(['-n', '-']) # Assert captured = capsys.readouterr() assert captured.out == '' - assert captured.err == 'WARN: stdin is empty\n' + assert captured.err == '\nWARN: stdin is empty\n' def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None: if qreader_available: # Act with pytest.raises(SystemExit) as e: - extract_otp_secrets.main(['tests/data/empty_file.txt']) + extract_otp_secrets.main(['-n', 'tests/data/empty_file.txt']) # Assert captured = capsys.readouterr() - expected_stderr = 'WARN: tests/data/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: tests/data/empty_file.txt\n' + expected_stderr = '\nWARN: tests/data/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: tests/data/empty_file.txt\n' assert captured.err == expected_stderr assert captured.out == '' @@ -127,13 +128,13 @@ def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch monkeypatch.setattr('sys.stdin', io.BytesIO()) # Act - extract_otp_secrets.main(['=']) + extract_otp_secrets.main(['-n', '=']) # Assert captured = capsys.readouterr() assert captured.out == '' - assert captured.err == 'WARN: stdin is empty\n' + assert captured.err == '\nWARN: stdin is empty\n' def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None: @@ -354,9 +355,10 @@ def test_normalize_bytes() -> None: 'Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter' +# Generate verbose output: python3.11 src/extract_otp_secrets.py example_export.txt -v -n > tests/data/print_verbose_output.txt def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> None: # Act - extract_otp_secrets.main(['-v', 'example_export.txt']) + extract_otp_secrets.main(['-n', '-v', 'example_export.txt']) # Assert captured = capsys.readouterr() @@ -442,7 +444,7 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(SystemExit) as e: # Act - extract_otp_secrets.main(['-v', '-q', 'example_export.txt']) + extract_otp_secrets.main(['-n', '-v', '-q', 'example_export.txt']) # Assert captured = capsys.readouterr() @@ -457,7 +459,7 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None: def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(SystemExit) as e: # Act - extract_otp_secrets.main(['tests/data/test_export_wrong_data.txt']) + extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_data.txt']) # Assert captured = capsys.readouterr() @@ -475,7 +477,7 @@ data=XXXX def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt']) + extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_content.txt']) # Assert captured = capsys.readouterr() @@ -497,7 +499,7 @@ url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ei def test_one_wrong_file(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt', 'example_export.txt']) + extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_content.txt', 'example_export.txt']) # Assert captured = capsys.readouterr() @@ -517,13 +519,35 @@ url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ei assert captured.err == expected_stderr +def test_one_wrong_file_colored(capsys: pytest.CaptureFixture[str]) -> None: + # Act + extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt', 'example_export.txt']) + + # Assert + captured = capsys.readouterr() + + expected_stderr = f'''{colorama.Fore.RED} +WARN: input is not a otpauth-migration:// url +source: tests/data/test_export_wrong_content.txt +input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. +Maybe a wrong file was given{colorama.Fore.RESET} +{colorama.Fore.RED} +ERROR: could not parse query parameter in input url +source: tests/data/test_export_wrong_content.txt +url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.{colorama.Fore.RESET} +''' + + assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT + assert captured.err == expected_stderr + + def test_one_wrong_line(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: # Arrange monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('tests/data/test_export_wrong_content.txt') + read_file_to_str('example_export.txt'))) # Act - extract_otp_secrets.main(['-']) + extract_otp_secrets.main(['-n', '-']) # Assert captured = capsys.readouterr() @@ -545,7 +569,7 @@ url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ei def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secrets.main(['tests/data/test_export_wrong_prefix.txt']) + extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_prefix.txt']) # Assert captured = capsys.readouterr() @@ -655,12 +679,12 @@ def test_img_qr_reader_from_stdin_wrong_symbol(capsys: pytest.CaptureFixture[str # Act with pytest.raises(SystemExit) as e: - extract_otp_secrets.main(['-']) + extract_otp_secrets.main(['-n', '-']) # Assert captured = capsys.readouterr() - expected_stderr = '\nBinary input was given in stdin, please use = instead of - as infile argument for images.\n' + expected_stderr = '\nERROR: Binary input was given in stdin, please use = instead of - as infile argument for images.\n' assert captured.err == expected_stderr assert captured.out == '' @@ -675,7 +699,7 @@ def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], m # Act with pytest.raises(SystemExit) as e: - extract_otp_secrets.main(['=']) + extract_otp_secrets.main(['-n', '=']) # Assert captured = capsys.readouterr() @@ -692,7 +716,7 @@ def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], m def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secrets.main(['tests/data/lena_std.tif']) + extract_otp_secrets.main(['-n', 'tests/data/lena_std.tif']) # Assert captured = capsys.readouterr() @@ -709,7 +733,7 @@ def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) - def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secrets.main(['nonexistent.bmp']) + extract_otp_secrets.main(['-n', 'nonexistent.bmp']) # Assert captured = capsys.readouterr() @@ -724,7 +748,7 @@ def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> N def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg']) + extract_otp_secrets.main(['-n', 'tests/data/text_masquerading_as_image.jpeg']) # Assert captured = capsys.readouterr() diff --git a/tests/extract_otp_secrets_txt_unit_test.py b/tests/extract_otp_secrets_txt_unit_test.py index 6e1c19b..c3b7fec 100644 --- a/tests/extract_otp_secrets_txt_unit_test.py +++ b/tests/extract_otp_secrets_txt_unit_test.py @@ -175,7 +175,7 @@ Type: totp self.skipTest("Avoid encoding problems") out = io.StringIO() with redirect_stdout(out): - extract_otp_secrets.main(['-v', 'example_export.txt']) + extract_otp_secrets.main(['-n', '-v', 'example_export.txt']) actual_output = out.getvalue() expected_output = read_file_to_str('tests/data/print_verbose_output.txt')