improve handling of wrong urls

- adapt tests
- improve messages for files
- show red box camera
cv2_1
scito 2 years ago
parent 4c0bb8dc61
commit f731530f57

@ -42,6 +42,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations # for compatibility with PYTHON < 3.11 from __future__ import annotations # for compatibility with PYTHON < 3.11
import argparse import argparse
import base64 import base64
import csv import csv
@ -53,16 +54,14 @@ import sys
import urllib.parse as urlparse import urllib.parse as urlparse
from enum import Enum from enum import Enum
from operator import add from operator import add
from typing import Any, Final, List, Optional, TextIO, Tuple, Union
try: try:
from typing import Any, TextIO, TypedDict, Union, List from typing import TypedDict
except ImportError: except ImportError:
from typing import Any, TextIO, Union, List
# PYTHON < 3.8: compatibility # PYTHON < 3.8: compatibility
from typing_extensions import TypedDict from typing_extensions import TypedDict
from qrcode import QRCode # type: ignore from qrcode import QRCode # type: ignore
import protobuf_generated_python.google_auth_pb2 as pb import protobuf_generated_python.google_auth_pb2 as pb
@ -80,11 +79,29 @@ ERROR: Cannot import QReader module. This problem is probably due to the missing
On Linux and macOS libzbar0 must be installed. On Linux and macOS libzbar0 must be installed.
See in README.md for the installation of the libzbar0. See in README.md for the installation of the libzbar0.
Exception: {e}""") Exception: {e}""")
# Types
# PYTHON > 3.9: Final[tuple[int]]
ColorBGR = Tuple[int, int, int] # RGB Color specified as Blue, Green, Red
Point = Tuple[int, int]
# CV2 camera capture constants
NORMAL_COLOR: Final[ColorBGR] = 255, 0, 255
SUCCESS_COLOR: Final[ColorBGR] = 0, 255, 0
FAILURE_COLOR: Final[ColorBGR] = 0, 0, 255
FONT: Final[int] = cv2.FONT_HERSHEY_PLAIN
FONT_SCALE: Final[int] = 1
FONT_THICKNESS: Final[int] = 1
START_POS_TEXT: Final[Point] = 5, 20
FONT_DY: Final[Tuple[int, int]] = 0, cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][1] + 5
FONT_LINE_STYLE: Final[int] = cv2.LINE_AA
RECT_THICKNESS: Final[int] = 5
qreader_available = True qreader_available = True
except ImportError: except ImportError:
qreader_available = False qreader_available = False
# TODO Workaround for PYTHON < 3.10: Union[int, None] used instead of int | None # Workaround for PYTHON < 3.10: Union[int, None] used instead of int | None
# Types # Types
Args = argparse.Namespace Args = argparse.Namespace
@ -97,6 +114,9 @@ Otps = List[Otp]
OtpUrls = List[OtpUrl] OtpUrls = List[OtpUrl]
# Constants
CAMERA: Final[str] = 'camera'
# Global variable declaration # Global variable declaration
verbose: int = 0 verbose: int = 0
quiet: bool = False quiet: bool = False
@ -163,6 +183,16 @@ def extract_otps(args: Args) -> Otps:
return extract_otps_from_files(args) 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
def extract_otps_from_camera(args: Args) -> Otps: def extract_otps_from_camera(args: Args) -> Otps:
if verbose: print("Capture QR codes from camera") if verbose: print("Capture QR codes from camera")
otp_urls: OtpUrls = [] otp_urls: OtpUrls = []
@ -175,15 +205,6 @@ def extract_otps_from_camera(args: Args) -> Otps:
cam = cv2.VideoCapture(args.camera) cam = cv2.VideoCapture(args.camera)
window_name = "Extract OTP Secret Keys: Capture QR Codes from Camera" window_name = "Extract OTP Secret Keys: Capture QR Codes from Camera"
cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE) cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE)
neutral_color = 255, 0, 255
sucess_color = 0, 255, 0
font = cv2.FONT_HERSHEY_PLAIN
font_scale = 1
font_thickness = 1
pos_text = 5, 20
font_dy = 0, cv2.getTextSize("M", font, font_scale, font_thickness)[0][1] + 5
font_line = cv2.LINE_AA
rect_thickness = 5
decoder = QReader() decoder = QReader()
while True: while True:
@ -197,36 +218,37 @@ def extract_otps_from_camera(args: Args) -> Otps:
otp_url = decoder.detect_and_decode(img) otp_url = decoder.detect_and_decode(img)
elif qr_mode == QRMode.QREADER: elif qr_mode == QRMode.QREADER:
otp_url = decoder.decode(img, bbox) if found else None otp_url = decoder.decode(img, bbox) if found else None
if found: new_otps_count = 0
cv2.rectangle(img, (bbox[0], bbox[1]), (bbox[2], bbox[3]), sucess_color if otp_url else neutral_color, rect_thickness)
if otp_url: if otp_url:
extract_otps_from_otp_url(otp_url, otp_urls, otps, args) new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
if found:
cv2.rectangle(img, (bbox[0], bbox[1]), (bbox[2], bbox[3]), get_color(new_otps_count, otp_url), RECT_THICKNESS)
elif qr_mode == QRMode.CV2: elif qr_mode == QRMode.CV2:
for qrcode in zbar.decode(img): for qrcode in zbar.decode(img):
otp_url = qrcode.data.decode('utf-8') otp_url = qrcode.data.decode('utf-8')
pts = numpy.array([qrcode.polygon], numpy.int32) pts = numpy.array([qrcode.polygon], numpy.int32)
pts = pts.reshape((-1, 1, 2)) pts = pts.reshape((-1, 1, 2))
cv2.polylines(img, [pts], True, sucess_color if otp_url else neutral_color, rect_thickness) new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
extract_otps_from_otp_url(otp_url, otp_urls, otps, args) cv2.polylines(img, [pts], True, get_color(new_otps_count, otp_url), RECT_THICKNESS)
else: else:
assert False, f"ERROR: Wrong QReader mode {qr_mode.name}" assert False, f"ERROR: Wrong QReader mode {qr_mode.name}"
cv2.putText(img, f"Mode: {qr_mode.name} (Hit space to change)", pos_text, font, font_scale, neutral_color, font_thickness, font_line) 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, pos_text, font_dy)), font, font_scale, neutral_color, font_thickness, font_line) 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)
window_dim = cv2.getWindowImageRect(window_name) window_dim = cv2.getWindowImageRect(window_name)
qrcodes_text = f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured" qrcodes_text = f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured"
pos_qrcodes_text = window_dim[2] - cv2.getTextSize(qrcodes_text, font, font_scale, font_thickness)[0][0] - 5, pos_text[1] pos_qrcodes_text = window_dim[2] - cv2.getTextSize(qrcodes_text, FONT, FONT_SCALE, FONT_THICKNESS)[0][0] - 5, START_POS_TEXT[1]
cv2.putText(img, qrcodes_text, pos_qrcodes_text, font, font_scale, neutral_color, font_thickness, font_line) cv2.putText(img, qrcodes_text, pos_qrcodes_text, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
otps_text = f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted" otps_text = f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted"
pos_otps_text = window_dim[2] - cv2.getTextSize(otps_text, font, font_scale, font_thickness)[0][0] - 5, pos_text[1] + font_dy[1] pos_otps_text = window_dim[2] - cv2.getTextSize(otps_text, FONT, FONT_SCALE, FONT_THICKNESS)[0][0] - 5, START_POS_TEXT[1] + FONT_DY[1]
cv2.putText(img, otps_text, pos_otps_text, font, font_scale, neutral_color, font_thickness, font_line) cv2.putText(img, otps_text, pos_otps_text, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
cv2.imshow(window_name, img) cv2.imshow(window_name, img)
key = cv2.waitKey(1) & 0xFF key = cv2.waitKey(1) & 0xFF
if key == 27 or key == ord('q') or key == 13: if key == 27 or key == ord('q') or key == 13:
# ESC pressed # ESC or Enter or q pressed
break break
elif key == 32: elif key == 32:
qr_mode = QRMode((qr_mode.value + 1) % len(QRMode)) qr_mode = QRMode((qr_mode.value + 1) % len(QRMode))
@ -241,28 +263,35 @@ def extract_otps_from_camera(args: Args) -> Otps:
return otps return otps
def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> None: # TODO write test
def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> int:
'''Returns -1 if opt_url was already added.'''
if otp_url and verbose: print(otp_url) if otp_url and verbose: print(otp_url)
if otp_url and otp_url not in otp_urls: if not otp_url:
otp_urls.append(otp_url) return 0
extract_otp_from_otp_url(otp_url, otps, len(otp_urls), len(otps), 'camera', args) if otp_url not in otp_urls:
if verbose: print(f"{len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted") new_otps_count = extract_otp_from_otp_url(otp_url, otps, len(otp_urls), CAMERA, args)
if new_otps_count:
otp_urls.append(otp_url)
if verbose: print(f"Extracted {new_otps_count} otp{'s'[:len(otps) != 1]}. {len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted")
return new_otps_count
return -1
def extract_otps_from_files(args: Args) -> Otps: def extract_otps_from_files(args: Args) -> Otps:
otps: Otps = [] otps: Otps = []
i = j = k = 0 files_count = urls_count = otps_count = 0
if verbose: print(f"Input files: {args.infile}") if verbose: print(f"Input files: {args.infile}")
for infile in args.infile: for infile in args.infile:
if verbose: print(f"Processing infile {infile}") if verbose: print(f"Processing infile {infile}")
k += 1 files_count += 1
for line in get_otp_urls_from_file(infile): for line in get_otp_urls_from_file(infile):
if verbose: print(line) if verbose: print(line)
if line.startswith('#') or line == '': continue if line.startswith('#') or line == '': continue
i += 1 urls_count += 1
j = extract_otp_from_otp_url(line, otps, i, j, infile, args) otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args)
if verbose: print(f"{k} infile{'s'[:k != 1]} processed") if verbose: print(f"{files_count} infile{'s'[:files_count != 1]} processed")
return otps return otps
@ -305,13 +334,17 @@ def read_lines_from_text_file(filename: str) -> list[str]:
return lines return lines
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, i: int, j: int, infile: str, args: Args) -> int: def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
payload = get_payload_from_otp_url(otpauth_migration_url, i, infile) payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
if not payload:
return 0
new_otps_count = 0
# pylint: disable=no-member # pylint: disable=no-member
for raw_otp in payload.otp_parameters: for raw_otp in payload.otp_parameters:
j += 1 new_otps_count += 1
if verbose: print(f"\n{j}. Secret Key") if verbose: print(f"\n{len(otps) + 1}. Secret Key")
secret = convert_secret_from_bytes_to_base32_str(raw_otp.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')) 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) otp_type = get_otp_type_str_from_code(raw_otp.type)
@ -330,11 +363,11 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, i: int, j:
if args.printqr: if args.printqr:
print_qr(args, otp_url) print_qr(args, otp_url)
if args.saveqr: if args.saveqr:
save_qr(otp, args, j) save_qr(otp, args, len(otps))
if not quiet: if not quiet:
print() print()
return j return new_otps_count
def convert_img_to_otp_url(filename: str) -> OtpUrls: def convert_img_to_otp_url(filename: str) -> OtpUrls:
@ -369,10 +402,16 @@ def convert_img_to_otp_url(filename: str) -> OtpUrls:
return [decoded_text] return [decoded_text]
def get_payload_from_otp_url(otpauth_migration_url: str, i: int, input_source: str) -> pb.MigrationPayload: # PYTHON >= 3.10 use: pb.MigrationPayload | None
if not otpauth_migration_url.startswith('otpauth-migration://'): def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
eprint(f"\nWARN: line is not a otpauth-migration:// URL\ninput: {input_source}\nline '{otpauth_migration_url}'\nProbably a wrong file was given") if not otp_url.startswith('otpauth-migration://'):
parsed_url = urlparse.urlparse(otpauth_migration_url) msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
if source == CAMERA:
eprint(f"\nERROR: {msg}")
return None
else:
eprint(f"\nWARN: {msg}\nMaybe a wrong file was given")
parsed_url = urlparse.urlparse(otp_url)
if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}") if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}")
try: try:
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True) params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
@ -380,7 +419,8 @@ def get_payload_from_otp_url(otpauth_migration_url: str, i: int, input_source: s
params = {} params = {}
if verbose > 2: print(f"\nDEBUG: querystring params={params}") if verbose > 2: print(f"\nDEBUG: querystring params={params}")
if 'data' not in params: if 'data' not in params:
abort(f"\nERROR: no data query parameter in input URL\ninput file: {input_source}\nline '{otpauth_migration_url}'\nProbably a wrong file was given") eprint(f"\nERROR: could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
return None
data_base64 = params['data'][0] data_base64 = params['data'][0]
if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}") if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}")
data_base64_fixed = data_base64.replace(' ', '+') data_base64_fixed = data_base64.replace(' ', '+')
@ -438,7 +478,7 @@ def save_qr(otp: Otp, args: Args, j: int) -> str:
file_otp_name = pattern.sub('', otp['name']) file_otp_name = pattern.sub('', otp['name'])
file_otp_issuer = pattern.sub('', otp['issuer']) file_otp_issuer = pattern.sub('', otp['issuer'])
save_qr_file(args, otp['url'], f"{dir}/{j}-{file_otp_name}{'-' + file_otp_issuer if file_otp_issuer else ''}.png") save_qr_file(args, otp['url'], f"{dir}/{j}-{file_otp_name}{'-' + file_otp_issuer if file_otp_issuer else ''}.png")
return file_otp_issuer return file_otp_name
def save_qr_file(args: Args, otp_url: OtpUrl, name: str) -> None: def save_qr_file(args: Args, otp_url: OtpUrl, name: str) -> None:

@ -59,24 +59,21 @@ class TestQRImageExtract(unittest.TestCase):
def test_img_qr_reader_non_image_file(self) -> None: def test_img_qr_reader_non_image_file(self) -> None:
with Capturing() as actual_output: with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context: extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg'])
extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg'])
expected_output = [ expected_output = [
'', '',
'WARN: line is not a otpauth-migration:// URL', 'WARN: input is not a otpauth-migration:// url',
'input: tests/data/text_masquerading_as_image.jpeg', 'source: tests/data/text_masquerading_as_image.jpeg',
"line 'This is just a text file masquerading as an image file.'", "input: This is just a text file masquerading as an image file.",
'Probably a wrong file was given', 'Maybe a wrong file was given',
'', '',
'ERROR: no data query parameter in input URL', 'ERROR: could not parse query parameter in input url',
'input file: tests/data/text_masquerading_as_image.jpeg', 'source: tests/data/text_masquerading_as_image.jpeg',
"line 'This is just a text file masquerading as an image file.'", "url: This is just a text file masquerading as an image file.",
'Probably a wrong file was given'
] ]
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
self.assertEqual(context.exception.code, 1)
def setUp(self) -> None: def setUp(self) -> None:
self.cleanup() self.cleanup()

@ -31,7 +31,7 @@ import extract_otp_secrets
from utils import (file_exits, quick_and_dirty_workaround_encoding_problem, from utils import (file_exits, quick_and_dirty_workaround_encoding_problem,
read_binary_file_as_stream, read_csv, read_csv_str, read_binary_file_as_stream, read_csv, read_csv_str,
read_file_to_str, read_json, read_json_str, read_file_to_str, read_json, read_json_str,
replace_escaped_octal_utf8_bytes_with_str) replace_escaped_octal_utf8_bytes_with_str, count_files_in_dir)
qreader_available: bool = extract_otp_secrets.qreader_available qreader_available: bool = extract_otp_secrets.qreader_available
@ -341,6 +341,9 @@ def test_extract_saveqr(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Pa
assert os.path.isfile(tmp_path / '2-piraspberrypi.png') assert os.path.isfile(tmp_path / '2-piraspberrypi.png')
assert os.path.isfile(tmp_path / '3-piraspberrypi.png') assert os.path.isfile(tmp_path / '3-piraspberrypi.png')
assert os.path.isfile(tmp_path / '4-piraspberrypi-raspberrypi.png') assert os.path.isfile(tmp_path / '4-piraspberrypi-raspberrypi.png')
assert os.path.isfile(tmp_path / '5-hotpdemo.png')
assert os.path.isfile(tmp_path / '6-encodingäÄéÉdemo.png')
assert count_files_in_dir(tmp_path) == 6
def test_normalize_bytes() -> None: def test_normalize_bytes() -> None:
@ -467,29 +470,73 @@ data=XXXX
def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None: def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e: # Act
# Act extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt'])
extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt'])
# Assert # Assert
captured = capsys.readouterr() captured = capsys.readouterr()
expected_stderr = ''' expected_stderr = '''
WARN: line is not a otpauth-migration:// URL WARN: input is not a otpauth-migration:// url
input: tests/data/test_export_wrong_content.txt source: tests/data/test_export_wrong_content.txt
line 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.' 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.
Probably a wrong file was given Maybe a wrong file was given
ERROR: no data query parameter in input URL ERROR: could not parse query parameter in input url
input file: tests/data/test_export_wrong_content.txt source: tests/data/test_export_wrong_content.txt
line 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.' 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.
Probably a wrong file was given
''' '''
assert captured.out == '' assert captured.out == ''
assert captured.err == expected_stderr assert captured.err == expected_stderr
assert e.value.code == 1
assert e.type == SystemExit
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'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
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
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.
'''
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(['-'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
WARN: input is not a otpauth-migration:// url
source: -
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
ERROR: could not parse query parameter in input url
source: -
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.
'''
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT
assert captured.err == expected_stderr
def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None: def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None:
@ -500,10 +547,10 @@ def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None:
captured = capsys.readouterr() captured = capsys.readouterr()
expected_stderr = ''' expected_stderr = '''
WARN: line is not a otpauth-migration:// URL WARN: input is not a otpauth-migration:// url
input: tests/data/test_export_wrong_prefix.txt source: tests/data/test_export_wrong_prefix.txt
line 'QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B' input: QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
Probably a wrong file was given Maybe a wrong file was given
''' '''
expected_stdout = '''Name: pi@raspberrypi expected_stdout = '''Name: pi@raspberrypi
@ -661,27 +708,23 @@ def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> N
def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None: def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
with pytest.raises(SystemExit) as e: extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg'])
extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg'])
# Assert # Assert
captured = capsys.readouterr() captured = capsys.readouterr()
expected_stderr = ''' expected_stderr = '''
WARN: line is not a otpauth-migration:// URL WARN: input is not a otpauth-migration:// url
input: tests/data/text_masquerading_as_image.jpeg source: tests/data/text_masquerading_as_image.jpeg
line 'This is just a text file masquerading as an image file.' input: This is just a text file masquerading as an image file.
Probably a wrong file was given Maybe a wrong file was given
ERROR: no data query parameter in input URL ERROR: could not parse query parameter in input url
input file: tests/data/text_masquerading_as_image.jpeg source: tests/data/text_masquerading_as_image.jpeg
line 'This is just a text file masquerading as an image file.' url: This is just a text file masquerading as an image file.
Probably a wrong file was given
''' '''
assert captured.err == expected_stderr assert captured.err == expected_stderr
assert captured.out == '' assert captured.out == ''
assert e.value.code == 1
assert e.type == SystemExit
EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi

@ -27,7 +27,7 @@ from contextlib import redirect_stdout
import extract_otp_secrets import extract_otp_secrets
from utils import (Capturing, read_csv, read_file_to_str, read_json, from utils import (Capturing, read_csv, read_file_to_str, read_json,
remove_dir_with_files, remove_file) remove_dir_with_files, remove_file, count_files_in_dir)
class TestExtract(unittest.TestCase): class TestExtract(unittest.TestCase):
@ -166,6 +166,9 @@ Type: totp
self.assertTrue(os.path.isfile('testout/qr/2-piraspberrypi.png')) self.assertTrue(os.path.isfile('testout/qr/2-piraspberrypi.png'))
self.assertTrue(os.path.isfile('testout/qr/3-piraspberrypi.png')) self.assertTrue(os.path.isfile('testout/qr/3-piraspberrypi.png'))
self.assertTrue(os.path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')) self.assertTrue(os.path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
self.assertTrue(os.path.isfile('testout/qr/5-hotpdemo.png'))
self.assertTrue(os.path.isfile('testout/qr/6-encodingäÄéÉdemo.png'))
self.assertEqual(count_files_in_dir('testout/qr'), 6)
def test_extract_verbose(self) -> None: def test_extract_verbose(self) -> None:
if sys.implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.") if sys.implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.")

@ -134,3 +134,7 @@ def replace_escaped_octal_utf8_bytes_with_str(str: str) -> str:
def quick_and_dirty_workaround_encoding_problem(str: str) -> str: def quick_and_dirty_workaround_encoding_problem(str: str) -> str:
return re.sub(r'name: "encoding: .*$', '', str, flags=re.MULTILINE) return re.sub(r'name: "encoding: .*$', '', str, flags=re.MULTILINE)
def count_files_in_dir(path: PathLike) -> int:
return len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name))])

Loading…
Cancel
Save