diff --git a/README.md b/README.md index 22eb799..43e6c79 100644 --- a/README.md +++ b/README.md @@ -23,21 +23,19 @@ The secret and otp values can be printed and exported to json or csv. The QR cod ## Program help: arguments and options -
-usage: extract_otp_secret_keys.py [-h] [--verbose] [--quiet] [--saveqr] [--printqr] [--json JSON] [--csv CSV] infile
+
usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--printqr] [--saveqr DIR] [--verbose] [--quiet] infile
 
 positional arguments:
   infile                file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
 
 options:
   -h, --help            show this help message and exit
-  --verbose, -v         verbose output
-  --quiet, -q           no stdout output
-  --saveqr, -s          save QR code(s) as images to the "qr" subfolder (requires qrcode module)
+  --json FILE, -j FILE  export to json file
+  --csv FILE, -c FILE   export to csv file
   --printqr, -p         print QR code(s) as text to the terminal (requires qrcode module)
-  --json JSON, -j JSON  export to json file
-  --csv CSV, -c CSV     export to csv file
-
+ --saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module) + --verbose, -v verbose output + --quiet, -q no stdout output
## Dependencies diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py index 8347879..f5d2766 100644 --- a/extract_otp_secret_keys.py +++ b/extract_otp_secret_keys.py @@ -48,7 +48,7 @@ import sys import csv import json from urllib.parse import parse_qs, urlencode, urlparse, quote -from os import path, mkdir +from os import path, makedirs from re import compile as rcompile import protobuf_generated_python.google_auth_pb2 @@ -70,13 +70,13 @@ def main(sys_args): def parse_args(sys_args): arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored') + arg_parser.add_argument('--json', '-j', help='export to json file', metavar=('FILE')) + arg_parser.add_argument('--csv', '-c', help='export to csv file', 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('--verbose', '-v', help='verbose output', action='count') arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true') - arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the "qr" subfolder (requires qrcode module)', action='store_true') - 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('--json', '-j', help='export to json file') - arg_parser.add_argument('--csv', '-c', help='export to csv file') - arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored') args = arg_parser.parse_args(sys_args) if args.verbose and args.quiet: print("The arguments --verbose and --quite are mutual exclusive.") @@ -176,7 +176,8 @@ def print_otp(otp): def save_qr(otp, args, j): - if not (path.exists('qr')): mkdir('qr') + dir = args.saveqr + if not (path.exists(dir)): makedirs(dir, exist_ok=True) pattern = rcompile(r'[\W_]+') file_otp_name = pattern.sub('', otp['name']) file_otp_issuer = pattern.sub('', otp['issuer']) diff --git a/test_extract_otp_secret_keys_pytest.py b/test_extract_otp_secret_keys_pytest.py index cf9d987..af95551 100644 --- a/test_extract_otp_secret_keys_pytest.py +++ b/test_extract_otp_secret_keys_pytest.py @@ -18,12 +18,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from utils import read_csv, read_json, remove_file, read_file_to_str +from utils import read_csv, read_json, remove_file, remove_dir_with_files, read_file_to_str +from os import path +from pytest import raises import extract_otp_secret_keys -def test_extract_csv(): +def test_extract_csv(capsys): # Arrange cleanup() @@ -36,11 +38,16 @@ def test_extract_csv(): assert actual_csv == expected_csv + captured = capsys.readouterr() + + assert captured.out == '' + assert captured.err == '' + # Clean up cleanup() -def test_extract_json(): +def test_extract_json(capsys): # Arrange cleanup() @@ -53,6 +60,11 @@ def test_extract_json(): assert actual_json == expected_json + captured = capsys.readouterr() + + assert captured.out == '' + assert captured.err == '' + # Clean up cleanup() @@ -134,6 +146,28 @@ def test_extract_printqr(capsys): assert captured.err == '' +def test_extract_saveqr(capsys): + # Arrange + cleanup() + + # Act + extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt']) + + # Assert + captured = capsys.readouterr() + + assert captured.out == '' + assert captured.err == '' + + assert path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png') + assert path.isfile('testout/qr/2-piraspberrypi.png') + assert path.isfile('testout/qr/3-piraspberrypi.png') + assert path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png') + + # Clean up + cleanup() + + def test_extract_verbose(capsys): # Act extract_otp_secret_keys.main(['-v', 'example_export.txt']) @@ -147,6 +181,36 @@ def test_extract_verbose(capsys): assert captured.err == '' +def test_extract_debug(capsys): + # Act + extract_otp_secret_keys.main(['-vv', 'example_export.txt']) + + # Assert + captured = capsys.readouterr() + + expected_stdout = read_file_to_str('test/print_verbose_output.txt') + + assert len(captured.out) > len(expected_stdout) + assert "DEBUG: " in captured.out + assert captured.err == '' + + +def test_extract_help(capsys): + with raises(SystemExit) as pytest_wrapped_e: + # Act + extract_otp_secret_keys.main(['-h']) + + # Assert + captured = capsys.readouterr() + + assert len(captured.out) > 0 + assert "-h, --help" in captured.out and "--verbose, -v" in captured.out + assert captured.err == '' + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + + def cleanup(): remove_file('test_example_output.csv') remove_file('test_example_output.json') + remove_dir_with_files('testout/') diff --git a/test_extract_otp_secret_keys_unittest.py b/test_extract_otp_secret_keys_unittest.py index 869888e..91a6f51 100644 --- a/test_extract_otp_secret_keys_unittest.py +++ b/test_extract_otp_secret_keys_unittest.py @@ -21,7 +21,8 @@ import unittest import io from contextlib import redirect_stdout -from utils import read_csv, read_json, remove_file, Capturing, read_file_to_str +from utils import read_csv, read_json, remove_file, remove_dir_with_files, Capturing, read_file_to_str +from os import path import extract_otp_secret_keys @@ -137,6 +138,14 @@ Type: OTP_TOTP self.assertEqual(actual_output, expected_output) + def test_extract_saveqr(self): + extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt']) + + self.assertTrue(path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png')) + self.assertTrue(path.isfile('testout/qr/2-piraspberrypi.png')) + self.assertTrue(path.isfile('testout/qr/3-piraspberrypi.png')) + self.assertTrue(path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')) + def test_extract_verbose(self): out = io.StringIO() with redirect_stdout(out): @@ -147,6 +156,30 @@ Type: OTP_TOTP self.assertEqual(actual_output, expected_output) + def test_extract_debug(self): + out = io.StringIO() + with redirect_stdout(out): + extract_otp_secret_keys.main(['-vv', 'example_export.txt']) + actual_output = out.getvalue() + + expected_stdout = read_file_to_str('test/print_verbose_output.txt') + + self.assertGreater(len(actual_output), len(expected_stdout)) + self.assertTrue("DEBUG: " in actual_output) + + def test_extract_help(self): + out = io.StringIO() + with redirect_stdout(out): + try: + extract_otp_secret_keys.main(['-h']) + except SystemExit: + pass + + actual_output = out.getvalue() + + self.assertGreater(len(actual_output), 0) + self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output) + def setUp(self): self.cleanup() @@ -156,6 +189,7 @@ Type: OTP_TOTP def cleanup(self): remove_file('test_example_output.csv') remove_file('test_example_output.json') + remove_dir_with_files('testout/') if __name__ == '__main__': diff --git a/utils.py b/utils.py index 3ff4ebc..26ca666 100644 --- a/utils.py +++ b/utils.py @@ -16,6 +16,7 @@ import csv import json import os +import shutil from io import StringIO import sys @@ -38,8 +39,12 @@ with Capturing() as output: sys.stdout = self._stdout -def remove_file(filename): - if os.path.exists(filename): os.remove(filename) +def remove_file(file): + if os.path.isfile(file): os.remove(file) + + +def remove_dir_with_files(dir): + if os.path.exists(dir): shutil.rmtree(dir) def read_csv(filename):