From b215b78dad2612fb28e98ffa06f0ff01a91f20d5 Mon Sep 17 00:00:00 2001 From: scito Date: Tue, 3 Jan 2023 23:02:46 +0100 Subject: [PATCH] test extract_otps_from_camera() --- src/extract_otp_secrets.py | 72 ++++++++++++------------ tests/extract_otp_secrets_test.py | 92 ++++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 37 deletions(-) diff --git a/src/extract_otp_secrets.py b/src/extract_otp_secrets.py index f3edd7c..7ac9b40 100644 --- a/src/extract_otp_secrets.py +++ b/src/extract_otp_secrets.py @@ -220,42 +220,6 @@ def extract_otps(args: Args) -> Otps: return extract_otps_from_files(args) -def get_color(new_otps_count: int, otp_url: str) -> ColorBGR: - if new_otps_count: - return SUCCESS_COLOR - else: - if otp_url: - return FAILURE_COLOR - else: - return NORMAL_COLOR - - -# TODO use cv2 types if available -def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any: - pts = np.array([raw_pts], np.int32) - pts = pts.reshape((-1, 1, 2)) - cv2.polylines(img, [pts], True, color, BOX_THICKNESS) - return pts - - -# TODO use cv2 types if available -def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None: - window_dim = cv2.getWindowImageRect(WINDOW_NAME) - out_text = text - if opposite_len: - text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS) - actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER - if actual_width >= window_dim[WINDOW_WIDTH]: - out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.' - text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS) - if position == TextPosition.LEFT: - pos = BORDER, START_Y + line_number * FONT_DY - else: - pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY - - cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE) - - def extract_otps_from_camera(args: Args) -> Otps: if verbose: print("Capture QR codes from camera") otp_urls: OtpUrls = [] @@ -325,6 +289,42 @@ def extract_otps_from_camera(args: Args) -> Otps: return otps +def get_color(new_otps_count: int, otp_url: str) -> ColorBGR: + if new_otps_count: + return SUCCESS_COLOR + else: + if otp_url: + return FAILURE_COLOR + else: + return NORMAL_COLOR + + +# TODO use cv2 types if available +def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any: + pts = np.array([raw_pts], np.int32) + pts = pts.reshape((-1, 1, 2)) + cv2.polylines(img, [pts], True, color, BOX_THICKNESS) + return pts + + +# TODO use cv2 types if available +def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None: + window_dim = cv2.getWindowImageRect(WINDOW_NAME) + out_text = text + if opposite_len: + text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS) + actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER + if actual_width >= window_dim[WINDOW_WIDTH]: + out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.' + text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS) + if position == TextPosition.LEFT: + pos = BORDER, START_Y + line_number * FONT_DY + else: + pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY + + cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE) + + def cv2_handle_pressed_keys(qr_mode: QRMode) -> Tuple[bool, QRMode]: key = cv2.waitKey(1) & 0xFF quit = False diff --git a/tests/extract_otp_secrets_test.py b/tests/extract_otp_secrets_test.py index 75df41e..fbecd10 100644 --- a/tests/extract_otp_secrets_test.py +++ b/tests/extract_otp_secrets_test.py @@ -26,7 +26,8 @@ import pathlib import re import sys import time -from typing import Optional +from enum import Enum +from typing import Any, List, Optional, Tuple import colorama import pytest @@ -37,6 +38,12 @@ from utils import (count_files_in_dir, file_exits, read_binary_file_as_stream, import extract_otp_secrets +try: + import cv2 # type: ignore +except ImportError: + # ignore + pass + qreader_available: bool = extract_otp_secrets.qreader_available @@ -494,6 +501,89 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker assert e.type == SystemExit +MockMode = Enum('MockMode', ['REPEAT_FIRST_ENDLESS', 'LOOP_LIST']) + + +class MockCam: + + read_counter: int = 0 + read_files: List[str] = [] + mock_mode: MockMode + + def __init__(self, files: List[str] = ['example_export.png'], mock_mode: MockMode = MockMode.REPEAT_FIRST_ENDLESS): + self.read_files = files + self.image_mode = mock_mode + + def read(self) -> Tuple[bool, Any]: + if self.image_mode == MockMode.REPEAT_FIRST_ENDLESS: + file = self.read_files[0] + elif self.image_mode == MockMode.LOOP_LIST: + file = self.read_files[self.read_counter] + self.read_counter += 1 + + if file: + img = cv2.imread(file) + return True, img + else: + return False, None + + def release(self) -> None: + # ignore + pass + + +@pytest.mark.parametrize("qr_reader", [ + None, + 'ZBAR', + 'QREADER', + 'QREADER_DEEP', + 'CV2', + 'CV2_WECHAT' +]) +def test_extract_otps_from_camera(qr_reader: Optional[str], capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None: + if qreader_available: + # Arrange + mockCam = MockCam() + mocker.patch('cv2.VideoCapture', return_value=mockCam) + mocker.patch('cv2.namedWindow') + mocker.patch('cv2.rectangle') + mocker.patch('cv2.polylines') + mocker.patch('cv2.imshow') + mocker.patch('cv2.getTextSize', return_value=([8, 200], False)) + mocker.patch('cv2.putText') + mocker.patch('cv2.getWindowImageRect', return_value=[0, 0, 640, 480]) + mocker.patch('cv2.waitKey', return_value=27) + mocker.patch('cv2.getWindowProperty', return_value=False) + mocker.patch('cv2.destroyAllWindows') + + args = [] + if qr_reader: + args.append('-Q') + args.append(qr_reader) + # Act + extract_otp_secrets.main(args) + + # Assert + captured = capsys.readouterr() + + assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG + assert captured.err == '' + else: + # Act + with pytest.raises(SystemExit) as e: + extract_otp_secrets.main([]) + + # Assert + captured = capsys.readouterr() + + expected_err_msg = 'error: the following arguments are required: infile' + + assert expected_err_msg in captured.err + assert captured.out == '' + assert e.value.code == 2 + assert e.type == SystemExit + + def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(SystemExit) as e: # Act