diff --git a/README.md b/README.md index 1ae0864..8006362 100644 --- a/README.md +++ b/README.md @@ -84,24 +84,23 @@ devbox shell ### PyTest -There are basic pytests, see `test_extract_otp_secret_keys_pytest.py`. +There are basic [pytest](https://pytest.org)s, see `test_extract_otp_secret_keys_pytest.py`. Run tests: ``` -pytest unittest +pytest ``` or - ``` python -m pytest ``` ### unittest -There are basic unit tests, see `test_extract_otp_secret_keys_unittest.py`. +There are basic [unittest](https://docs.python.org/3.10/library/unittest.html)s, see `test_extract_otp_secret_keys_unittest.py`. -Run unit tests: +Run tests: ``` python -m unittest diff --git a/test/printqr_output.txt b/test/printqr_output.txt new file mode 100644 index 0000000..81d5beb --- /dev/null +++ b/test/printqr_output.txt @@ -0,0 +1,106 @@ +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: OTP_TOTP +                                              +                                              +    █▀▀▀▀▀█  ▄▀▄▄█ █▀  ▀▀▀▀▀█  ▄▄ █▀▀▀▀▀█     +    █ ███ █ ███▀ ▀█ █▄ ▀ ▀▄▀█▄▄ █ █ ███ █     +    █ ▀▀▀ █ ██▄▄ ██  ▀█▀█▄▄▀▄ ▄▄█ █ ▀▀▀ █     +    ▀▀▀▀▀▀▀ █ █ █▄▀ █ ▀▄▀▄█ █ ▀ █ ▀▀▀▀▀▀▀     +    █▄█▀▀█▀ ▄▀▀ ▄▀██▄▀ ▄█▄█▄ ▄▄   ██▀▀█▄      +    ▀  █▀▀▀  ██▀█   ▄▀▀▄ ██   ▄▀▄█ █ ▄▄▀█     +    ▄█▀█ ▄▀█▄▄   ▄▀▄▄▀ ▄█▄█▄ ▀    ▀ ▀▄▀▀█     +    ▀▄ ▄▄▄▀▄█▄██▀▄██▀ █▄█▄ ▄▄▀▄█  ▄ █▀ ▀      +    ▄ ▄█ ▀▀▄▄ ▄▄▀█▄█▀▀▄██▄▀▄▀▀ █▀█ █▄███      +    ▀▀▄▀  ▀  ▀▄▀▀█ █▀▀█ █▄    █▄██ ▀█▀▀█▀     +     ▀█▄██▀▀█  ▄▀█▀███▄▄▀▄█▄ ▀▄▀██▀▀▀▄ ▀▄     +      █▄█ ▀   ▀▀▀▄▄▄  ▀ ▀█▄ ▄▀▀█▄██ ▄  ▀▄     +    █▀ ▀  ▀▄ █▀▀█▀▀█▄ ██▄▄█▄ ▀▀▀▄█▀▀▀ ▀ ▀     +    █ █▀▄▄▀█▀█▀▀▀ ▀ ▄  ▄ ▄ ▄▄ ▀▀▀▀█▄▄▄ █▀     +    ▀ ▀▀ ▀▀ ▄  █▀▀▀  █ ▄▄▄█▄▀█▄▀█▀▀▀█ ▀▀▀     +    █▀▀▀▀▀█ ▄▀█▀█  ███▄  ▄▄▄█ ▄ █ ▀ █  ▄      +    █ ███ █ █▄█▄▄▀█▄▀ ▄▄▄ █▄ ▀█▀███▀█ ▀▀      +    █ ▀▀▀ █ ▀█▄▄▄█▀█▀ ▄  █▄▄▄   █  ▀ ██ █     +    ▀▀▀▀▀▀▀ ▀  ▀ ▀▀▀▀▀▀ ▀ ▀  ▀▀ ▀ ▀▀ ▀ ▀▀     +                                              +                                              + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: OTP_TOTP +                                          +                                          +    █▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█     +    █ ███ █ ▄ ▀▀▀ ▀▀█▀█▀▄ ▄█▀ █ ███ █     +    █ ▀▀▀ █ █▀▄▄▄█▄▄▀ ▄ ▄ ▀▄▄ █ ▀▀▀ █     +    ▀▀▀▀▀▀▀ █ █▄▀ ▀ ▀▄█ ▀▄█ █ ▀▀▀▀▀▀▀     +    ▀▄▄▄█▄▀██▄█▀█ ▄███   █ █ ▀▀▀▀█▄▄▀     +     ▄▀   ▀  █ █▀█▀▀█▀   ▄█▀██▀█▀▀ █▄     +    █▀█ ▄▀▀▀ ███▄█▄  ▀ █▀▄█▀▀█▀▀▀█▄▀▀     +    ▀▄▀▄▄ ▀▄▄ ▀▀▄█▀██▄▄ █▄ ▄  █▀█  █▄     +    ▀██▄█▄▀█ ▄▄ ▀▀▄▄█▀ ▀▄▀█▄▀█▀▄▀ ▄       +    ▀▄▄▀▄▀▀▀▄ █  ▀█▀█▄▄ ▄█ █▄  ▀█▀ █▄     +    ▄█ ▄  ▀ ▀ ▄ ▀█ ▄ ▀▄█ ▀▄▄█ ▄█▀ ▄▄      +      ▄█▀▄▀▀ ██ ▄  ▀ █ █▀██ ███ █ ▀█▄     +    ▀▀▀  ▀▀ ▄██▀█▀███▀▄ ▄▀▀██▀▀▀█▀▄ ▀     +    █▀▀▀▀▀█ ▀█▀▄██▀█ ▀█ █ ▄ █ ▀ █  ▄      +    █ ███ █ ▀█▄▄█▀▀▄  ▀▀▄▄ ▄▀████▀▄█      +    █ ▀▀▀ █  ▄ █ █▀▀▀▄  ▄█▀▄▀ ▀ █▀▀       +    ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀ ▀  ▀▀ ▀▀ ▀▀    ▀     +                                          +                                          + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: OTP_TOTP +                                          +                                          +    █▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█     +    █ ███ █ ▄ ▀▀▀ ▀▀█▀█▀▄ ▄█▀ █ ███ █     +    █ ▀▀▀ █ █▀▄▄▄█▄▄▀ ▄ ▄ ▀▄▄ █ ▀▀▀ █     +    ▀▀▀▀▀▀▀ █ █▄▀ ▀ ▀▄█ ▀▄█ █ ▀▀▀▀▀▀▀     +    ▀▄▄▄█▄▀██▄█▀█ ▄███   █ █ ▀▀▀▀█▄▄▀     +     ▄▀   ▀  █ █▀█▀▀█▀   ▄█▀██▀█▀▀ █▄     +    █▀█ ▄▀▀▀ ███▄█▄  ▀ █▀▄█▀▀█▀▀▀█▄▀▀     +    ▀▄▀▄▄ ▀▄▄ ▀▀▄█▀██▄▄ █▄ ▄  █▀█  █▄     +    ▀██▄█▄▀█ ▄▄ ▀▀▄▄█▀ ▀▄▀█▄▀█▀▄▀ ▄       +    ▀▄▄▀▄▀▀▀▄ █  ▀█▀█▄▄ ▄█ █▄  ▀█▀ █▄     +    ▄█ ▄  ▀ ▀ ▄ ▀█ ▄ ▀▄█ ▀▄▄█ ▄█▀ ▄▄      +      ▄█▀▄▀▀ ██ ▄  ▀ █ █▀██ ███ █ ▀█▄     +    ▀▀▀  ▀▀ ▄██▀█▀███▀▄ ▄▀▀██▀▀▀█▀▄ ▀     +    █▀▀▀▀▀█ ▀█▀▄██▀█ ▀█ █ ▄ █ ▀ █  ▄      +    █ ███ █ ▀█▄▄█▀▀▄  ▀▀▄▄ ▄▀████▀▄█      +    █ ▀▀▀ █  ▄ █ █▀▀▀▄  ▄█▀▄▀ ▀ █▀▀       +    ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀ ▀  ▀▀ ▀▀ ▀▀    ▀     +                                          +                                          + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: OTP_TOTP +                                              +                                              +    █▀▀▀▀▀█  ▄▀▄▄█ █▀  ▀▀▀▀▀█  ▄▄ █▀▀▀▀▀█     +    █ ███ █ ███▀ ▀█ █▄ ▀ ▀▄▀█▄▄ █ █ ███ █     +    █ ▀▀▀ █ ██▄▄ ██  ▀█▀█▄▄▀▄ ▄▄█ █ ▀▀▀ █     +    ▀▀▀▀▀▀▀ █ █ █▄▀ █ ▀▄▀▄█ █ ▀ █ ▀▀▀▀▀▀▀     +    █▄█▀▀█▀ ▄▀▀ ▄▀██▄▀ ▄█▄█▄ ▄▄   ██▀▀█▄      +    ▀  █▀▀▀  ██▀█   ▄▀▀▄ ██   ▄▀▄█ █ ▄▄▀█     +    ▄█▀█ ▄▀█▄▄   ▄▀▄▄▀ ▄█▄█▄ ▀    ▀ ▀▄▀▀█     +    ▀▄ ▄▄▄▀▄█▄██▀▄██▀ █▄█▄ ▄▄▀▄█  ▄ █▀ ▀      +    ▄ ▄█ ▀▀▄▄ ▄▄▀█▄█▀▀▄██▄▀▄▀▀ █▀█ █▄███      +    ▀▀▄▀  ▀  ▀▄▀▀█ █▀▀█ █▄    █▄██ ▀█▀▀█▀     +     ▀█▄██▀▀█  ▄▀█▀███▄▄▀▄█▄ ▀▄▀██▀▀▀▄ ▀▄     +      █▄█ ▀   ▀▀▀▄▄▄  ▀ ▀█▄ ▄▀▀█▄██ ▄  ▀▄     +    █▀ ▀  ▀▄ █▀▀█▀▀█▄ ██▄▄█▄ ▀▀▀▄█▀▀▀ ▀ ▀     +    █ █▀▄▄▀█▀█▀▀▀ ▀ ▄  ▄ ▄ ▄▄ ▀▀▀▀█▄▄▄ █▀     +    ▀ ▀▀ ▀▀ ▄  █▀▀▀  █ ▄▄▄█▄▀█▄▀█▀▀▀█ ▀▀▀     +    █▀▀▀▀▀█ ▄▀█▀█  ███▄  ▄▄▄█ ▄ █ ▀ █  ▄      +    █ ███ █ █▄█▄▄▀█▄▀ ▄▄▄ █▄ ▀█▀███▀█ ▀▀      +    █ ▀▀▀ █ ▀█▄▄▄█▀█▀ ▄  █▄▄▄   █  ▀ ██ █     +    ▀▀▀▀▀▀▀ ▀  ▀ ▀▀▀▀▀▀ ▀ ▀  ▀▀ ▀ ▀▀ ▀ ▀▀     +                                              +                                              + diff --git a/test_extract_otp_secret_keys_pytest.py b/test_extract_otp_secret_keys_pytest.py index 87e8cf9..8caa9cb 100644 --- a/test_extract_otp_secret_keys_pytest.py +++ b/test_extract_otp_secret_keys_pytest.py @@ -18,7 +18,7 @@ # 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 +from utils import read_csv, read_json, remove_file, read_file_to_str import extract_otp_secret_keys @@ -47,6 +47,7 @@ def test_extract_json(): # Act extract_otp_secret_keys.main(['-q', '-j', 'test_example_output.json', 'example_export.txt']) + # Assert expected_json = read_json('example_output.json') actual_json = read_json('test_example_output.json') @@ -56,6 +57,50 @@ def test_extract_json(): cleanup() +def test_extract_stdout(capsys): + # Act + extract_otp_secret_keys.main(['example_export.txt']) + + # Assert + captured = capsys.readouterr() + + expected_stdout = '''Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: OTP_TOTP + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: OTP_TOTP + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: OTP_TOTP + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: OTP_TOTP + +''' + + assert captured.out == expected_stdout + assert captured.err == '' + + +def test_extract_printqr(capsys): + # Act + extract_otp_secret_keys.main(['-p', 'example_export.txt']) + + # Assert + captured = capsys.readouterr() + + expected_stdout = read_file_to_str('test/printqr_output.txt') + + assert captured.out == expected_stdout + assert captured.err == '' + + def cleanup(): remove_file('test_example_output.csv') remove_file('test_example_output.json') diff --git a/test_extract_otp_secret_keys_unittest.py b/test_extract_otp_secret_keys_unittest.py index 5e03120..f153257 100644 --- a/test_extract_otp_secret_keys_unittest.py +++ b/test_extract_otp_secret_keys_unittest.py @@ -19,7 +19,9 @@ # along with this program. If not, see . import unittest -from utils import read_csv, read_json, remove_file +import io +from contextlib import redirect_stdout +from utils import read_csv, read_json, remove_file, Capturing, read_file_to_str import extract_otp_secret_keys @@ -42,6 +44,70 @@ class TestExtract(unittest.TestCase): self.assertEqual(actual_json, expected_json) + def test_extract_stdout_1(self): + with Capturing() as output: + extract_otp_secret_keys.main(['example_export.txt']) + + expected_output = [ + 'Name: pi@raspberrypi', + 'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY', + 'Issuer: raspberrypi', + 'Type: OTP_TOTP', + '', + 'Name: pi@raspberrypi', + 'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY', + 'Type: OTP_TOTP', + '', + 'Name: pi@raspberrypi', + 'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY', + 'Type: OTP_TOTP', + '', + 'Name: pi@raspberrypi', + 'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY', + 'Issuer: raspberrypi', + 'Type: OTP_TOTP', + '' + ] + self.assertEqual(output, expected_output) + + # Ref for capturing https://stackoverflow.com/a/40984270 + def test_extract_stdout_2(self): + out = io.StringIO() + with redirect_stdout(out): + extract_otp_secret_keys.main(['example_export.txt']) + actual_output = out.getvalue() + + expected_output = '''Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: OTP_TOTP + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: OTP_TOTP + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: OTP_TOTP + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: OTP_TOTP + +''' + self.assertEqual(actual_output, expected_output) + + def test_extract_printqr(self): + out = io.StringIO() + with redirect_stdout(out): + extract_otp_secret_keys.main(['-p', 'example_export.txt']) + actual_output = out.getvalue() + + expected_output = read_file_to_str('test/printqr_output.txt') + + self.assertEqual(actual_output, expected_output) + def setUp(self): self.cleanup() diff --git a/utils.py b/utils.py index 19f4437..3ff4ebc 100644 --- a/utils.py +++ b/utils.py @@ -16,6 +16,27 @@ import csv import json import os +from io import StringIO +import sys + + +# Ref. https://stackoverflow.com/a/16571630 +class Capturing(list): + '''Capture stdout and stderr +Usage: +with Capturing() as output: + print("Output") +''' + def __enter__(self): + self._stdout = sys.stdout + sys.stdout = self._stringio = StringIO() + return self + + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio # free up some memory + sys.stdout = self._stdout + def remove_file(filename): if os.path.exists(filename): os.remove(filename) @@ -35,3 +56,14 @@ def read_json(filename): """Returns a list or a dictionary.""" with open(filename, "r") as infile: return json.load(infile) + + +def read_file_to_list(filename): + """Returns a list of lines.""" + with open(filename, "r") as infile: + return infile.readlines() + + +def read_file_to_str(filename): + """Returns a str.""" + return "".join(read_file_to_list(filename))