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))