diff --git a/.gitignore b/.gitignore index 8671ca4..bf72bec 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ venv/ !.devcontainer/*.json *.whl build/ +dist/ extract_otp_secret_keys.egg-info/ *.xml pytest-coverage.txt diff --git a/README.md b/README.md index c65125e..e66ea22 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ --- +TODO add src/ +TODO rename extract_otp_secret_keys + Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app. The secret and otp values can be printed and exported to json or csv. The QR codes can be printed or saved as PNG images. @@ -293,6 +296,12 @@ wget https://raw.githubusercontent.com/scito/extract_otp_secret_keys/master/exam python -m extract_otp_secret_keys example_export.txt ``` +### local pip + +``` +pip install -e . +``` + ### pipenv You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secret_keys. @@ -385,6 +394,16 @@ or python -m pytest ``` +#### Hints + +Your tests can run against an installed version after executing pip install . + +Your tests can run against the local copy with an editable install after executing pip install --editable . + +If you don’t use an editable install and are relying on the fact that Python by default puts the current directory in sys.path to import your package, you can execute python -m pytest to execute the tests against the local copy directly, without using pip. + +https://docs.pytest.org/en/7.1.x/explanation/pythonpath.html#pytest-vs-python-m-pytest + ### unittest There are basic [unittest](https://docs.python.org/3.10/library/unittest.html)s, see `test_extract_otp_secret_keys_unittest.py`. @@ -404,7 +423,22 @@ Setup for running the tests in VSCode. 3. Choose unittest or pytest. (pytest is recommended, both are supported) 4. Set ". Root" directory -## Maintenance +## Development + +### Build + +``` +pip install -e . +python src/extract_otp_secret_keys.py + +pip wheel . +# --isolated +# --prefer-binary + +python3.11 -m build --wheel +# = +pip wheel --no-deps . +``` ### Upgrade pip Packages diff --git a/pyproject.toml b/pyproject.toml index 3fc4379..e5b2522 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,4 +54,9 @@ py-modules = ["extract_otp_secret_keys", "protobuf_generated_python.google_auth_ # TODO version = {attr = "extract_otp_secret_keys.VERSION"} [tool.setuptools.package-data] -"*" = ["*.txt", "*.json", "*.png", "*.md"] +"." = ["*.txt", "*.json", "*.csv", "*.png", "*.md"] + +# https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure%3E +# https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#which-import-mode +[tool.pytest.ini_options] +addopts = [ "--import-mode=importlib", ] diff --git a/setup.cfg b/setup.cfg index 696853e..8972bee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,36 +1,5 @@ [metadata] name = extract_otp_secret_keys -version = 1.6.0 -long_description = file: README.md -description = Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app -long_description_content_type = text/markdown -url = https://github.com/scito/extract_otp_secret_keys -author = scito -author_email = info@scito.ch -maintainer = scito -maintainer_email = info@scito.ch -license = GNU General Public License v3 (GPLv3) -keywords = python security json otp csv protobuf qrcode two-factor totp google-authenticator recovery proto3 mfa two-factor-authentication tfa qr-codes otpauth 2fa security-tools - -# https://pypi.org/classifiers/ -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Console - Environment :: X11 Applications :: Qt - Environment :: Win32 (MS Windows) - Topic :: System :: Archiving :: Backup - Topic :: Utilities - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Intended Audience :: End Users/Desktop - Intended Audience :: Developers - Intended Audience :: System Administrators - Programming Language :: Python - Natural Language :: English - License :: OSI Approved :: GNU General Public License v3 (GPLv3) [options] python_requires = >=3.7, <4 @@ -45,21 +14,7 @@ py_modules = extract_otp_secret_keys, protobuf_generated_python.google_auth_pb2 # typing_extensions;python_version<='3.7' packages = find: package_dir = - = . -project_urls = - Bug Reports = https://github.com/scito/extract_otp_secret_keys/issues - Source = https://github.com/scito/extract_otp_secret_keys - -# [options.entry_points] -# console_scripts = -# extract_otp_secret_keys = extract_otp_secret_keys:sys_main + = src [options.packages.find] -where = "." - -[options.package_data] -* = - *.txt - *.json - *.png - *.md +where = src diff --git a/setup.py b/setup.py index 05a37bb..592874f 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ +#!/usr/bin/python3 from setuptools import setup # compatibility with legacy builds or versions of tools that don’t support certain packaging standards -setup( -) +setup() diff --git a/extract_otp_secret_keys.py b/src/extract_otp_secret_keys.py similarity index 100% rename from extract_otp_secret_keys.py rename to src/extract_otp_secret_keys.py diff --git a/protobuf_generated_python/google_auth_pb2.py b/src/protobuf_generated_python/google_auth_pb2.py similarity index 100% rename from protobuf_generated_python/google_auth_pb2.py rename to src/protobuf_generated_python/google_auth_pb2.py diff --git a/protobuf_generated_python/google_auth_pb2.pyi b/src/protobuf_generated_python/google_auth_pb2.pyi similarity index 100% rename from protobuf_generated_python/google_auth_pb2.pyi rename to src/protobuf_generated_python/google_auth_pb2.pyi diff --git a/test/empty_file.txt b/tests/data/empty_file.txt similarity index 100% rename from test/empty_file.txt rename to tests/data/empty_file.txt diff --git a/test/example_export_only_totp.txt b/tests/data/example_export_only_totp.txt similarity index 100% rename from test/example_export_only_totp.txt rename to tests/data/example_export_only_totp.txt diff --git a/test/lena_std.tif b/tests/data/lena_std.tif similarity index 100% rename from test/lena_std.tif rename to tests/data/lena_std.tif diff --git a/test/print_verbose_output.txt b/tests/data/print_verbose_output.txt similarity index 100% rename from test/print_verbose_output.txt rename to tests/data/print_verbose_output.txt diff --git a/test/printqr_output.txt b/tests/data/printqr_output.txt similarity index 100% rename from test/printqr_output.txt rename to tests/data/printqr_output.txt diff --git a/test/test_export_wrong_content.txt b/tests/data/test_export_wrong_content.txt similarity index 100% rename from test/test_export_wrong_content.txt rename to tests/data/test_export_wrong_content.txt diff --git a/test/test_export_wrong_data.txt b/tests/data/test_export_wrong_data.txt similarity index 100% rename from test/test_export_wrong_data.txt rename to tests/data/test_export_wrong_data.txt diff --git a/test/test_export_wrong_prefix.txt b/tests/data/test_export_wrong_prefix.txt similarity index 100% rename from test/test_export_wrong_prefix.txt rename to tests/data/test_export_wrong_prefix.txt diff --git a/test/test_googleauth_export.png b/tests/data/test_googleauth_export.png similarity index 100% rename from test/test_googleauth_export.png rename to tests/data/test_googleauth_export.png diff --git a/test/test_plus_problem_export.txt b/tests/data/test_plus_problem_export.txt similarity index 100% rename from test/test_plus_problem_export.txt rename to tests/data/test_plus_problem_export.txt diff --git a/test/text_masquerading_as_image.jpeg b/tests/data/text_masquerading_as_image.jpeg similarity index 100% rename from test/text_masquerading_as_image.jpeg rename to tests/data/text_masquerading_as_image.jpeg diff --git a/test_extract_otp_secret_keys_pytest.py b/tests/test_extract_otp_secret_keys_pytest.py similarity index 91% rename from test_extract_otp_secret_keys_pytest.py rename to tests/test_extract_otp_secret_keys_pytest.py index 5928833..00bc52a 100644 --- a/test_extract_otp_secret_keys_pytest.py +++ b/tests/test_extract_otp_secret_keys_pytest.py @@ -50,12 +50,12 @@ def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None: def test_extract_non_existent_file(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secret_keys.main(['test/non_existent_file.txt']) + extract_otp_secret_keys.main(['non_existent_file.txt']) # Assert captured = capsys.readouterr() - expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: test/non_existent_file.txt\n' + expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: non_existent_file.txt\n' assert captured.err == expected_stderr assert captured.out == '' @@ -96,12 +96,12 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No if qreader_available: # Act with pytest.raises(SystemExit) as e: - extract_otp_secret_keys.main(['test/empty_file.txt']) + extract_otp_secret_keys.main(['tests/data/empty_file.txt']) # Assert captured = capsys.readouterr() - expected_stderr = 'WARN: test/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: test/empty_file.txt\n' + expected_stderr = 'WARN: tests/data/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: tests/data/empty_file.txt\n' assert captured.err == expected_stderr assert captured.out == '' @@ -109,7 +109,7 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No assert e.type == SystemExit else: # Act - extract_otp_secret_keys.main(['test/empty_file.txt']) + extract_otp_secret_keys.main(['tests/data/empty_file.txt']) # Assert captured = capsys.readouterr() @@ -214,7 +214,7 @@ def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None: '''Two csv files .totp and .htop are generated.''' # Act - extract_otp_secret_keys.main(['-k', '-', 'test/example_export_only_totp.txt']) + extract_otp_secret_keys.main(['-k', '-', 'tests/data/example_export_only_totp.txt']) # Assert expected_totp_csv = read_csv('example_keepass_output.totp.csv') @@ -232,7 +232,7 @@ def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None: def test_single_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None: '''Does not add .totp or .hotp pre-suffix''' # Act - extract_otp_secret_keys.main(['-q', '-k', str(tmp_path / 'test_example_keepass_output.csv'), 'test/example_export_only_totp.txt']) + extract_otp_secret_keys.main(['-q', '-k', str(tmp_path / 'test_example_keepass_output.csv'), 'tests/data/example_export_only_totp.txt']) # Assert expected_totp_csv = read_csv('example_keepass_output.totp.csv') @@ -283,7 +283,7 @@ def test_extract_json_stdout(capsys: pytest.CaptureFixture[str]) -> None: def test_extract_not_encoded_plus(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secret_keys.main(['test/test_plus_problem_export.txt']) + extract_otp_secret_keys.main(['tests/data/test_plus_problem_export.txt']) # Assert captured = capsys.readouterr() @@ -321,7 +321,7 @@ def test_extract_printqr(capsys: pytest.CaptureFixture[str]) -> None: # Assert captured = capsys.readouterr() - expected_stdout = read_file_to_str('test/printqr_output.txt') + expected_stdout = read_file_to_str('tests/data/printqr_output.txt') assert captured.out == expected_stdout assert captured.err == '' @@ -355,7 +355,7 @@ def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> N # Assert captured = capsys.readouterr() - expected_stdout = read_file_to_str('test/print_verbose_output.txt') + expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt') if not qreader_available: expected_stdout = expected_stdout.replace('QReader installed: True', 'QReader installed: False') @@ -377,7 +377,7 @@ def test_extract_debug(capsys: pytest.CaptureFixture[str]) -> None: # Assert captured = capsys.readouterr() - expected_stdout = read_file_to_str('test/print_verbose_output.txt') + expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt') assert len(captured.out) > len(expected_stdout) assert "DEBUG: " in captured.out @@ -450,7 +450,7 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None: def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(SystemExit) as e: # Act - extract_otp_secret_keys.main(['test/test_export_wrong_data.txt']) + extract_otp_secret_keys.main(['tests/data/test_export_wrong_data.txt']) # Assert captured = capsys.readouterr() @@ -469,19 +469,19 @@ data=XXXX def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(SystemExit) as e: # Act - extract_otp_secret_keys.main(['test/test_export_wrong_content.txt']) + extract_otp_secret_keys.main(['tests/data/test_export_wrong_content.txt']) # Assert captured = capsys.readouterr() expected_stderr = ''' WARN: line is not a otpauth-migration:// URL -input: test/test_export_wrong_content.txt +input: 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.' Probably a wrong file was given ERROR: no data query parameter in input URL -input file: test/test_export_wrong_content.txt +input file: 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.' Probably a wrong file was given ''' @@ -494,14 +494,14 @@ Probably a wrong file was given def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secret_keys.main(['test/test_export_wrong_prefix.txt']) + extract_otp_secret_keys.main(['tests/data/test_export_wrong_prefix.txt']) # Assert captured = capsys.readouterr() expected_stderr = ''' WARN: line is not a otpauth-migration:// URL -input: test/test_export_wrong_prefix.txt +input: tests/data/test_export_wrong_prefix.txt line 'QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B' Probably a wrong file was given ''' @@ -526,7 +526,7 @@ def test_add_pre_suffix(capsys: pytest.CaptureFixture[str]) -> None: @pytest.mark.qreader def test_img_qr_reader_from_file_happy_path(capsys: pytest.CaptureFixture[str]) -> None: # Act - extract_otp_secret_keys.main(['test/test_googleauth_export.png']) + extract_otp_secret_keys.main(['tests/data/test_googleauth_export.png']) # Assert captured = capsys.readouterr() @@ -540,9 +540,9 @@ def test_extract_multiple_files_and_mixed(capsys: pytest.CaptureFixture[str]) -> # Act extract_otp_secret_keys.main([ 'example_export.txt', - 'test/test_googleauth_export.png', + 'tests/data/test_googleauth_export.png', 'example_export.txt', - 'test/test_googleauth_export.png']) + 'tests/data/test_googleauth_export.png']) # Assert captured = capsys.readouterr() @@ -555,7 +555,7 @@ def test_extract_multiple_files_and_mixed(capsys: pytest.CaptureFixture[str]) -> def test_img_qr_reader_from_stdin(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # sys.stdin.buffer should be monkey patched, but it does not work - monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png')) + monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('tests/data/test_googleauth_export.png')) # Act extract_otp_secret_keys.main(['=']) @@ -588,7 +588,7 @@ Type: totp def test_img_qr_reader_from_stdin_wrong_symbol(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # sys.stdin.buffer should be monkey patched, but it does not work - monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png')) + monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('tests/data/test_googleauth_export.png')) # Act with pytest.raises(SystemExit) as e: @@ -629,12 +629,12 @@ def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], m def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secret_keys.main(['test/lena_std.tif']) + extract_otp_secret_keys.main(['tests/data/lena_std.tif']) # Assert captured = capsys.readouterr() - expected_stderr = '\nERROR: Unable to read QR Code from file.\ninput file: test/lena_std.tif\n' + expected_stderr = '\nERROR: Unable to read QR Code from file.\ninput file: tests/data/lena_std.tif\n' assert captured.err == expected_stderr assert captured.out == '' @@ -646,12 +646,12 @@ def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) - def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secret_keys.main(['test/nonexistent.bmp']) + extract_otp_secret_keys.main(['nonexistent.bmp']) # Assert captured = capsys.readouterr() - expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: test/nonexistent.bmp\n' + expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: nonexistent.bmp\n' assert captured.err == expected_stderr assert captured.out == '' @@ -662,18 +662,18 @@ def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> N def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None: # Act with pytest.raises(SystemExit) as e: - extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg']) + extract_otp_secret_keys.main(['tests/data/text_masquerading_as_image.jpeg']) # Assert captured = capsys.readouterr() expected_stderr = ''' WARN: line is not a otpauth-migration:// URL -input: test/text_masquerading_as_image.jpeg +input: tests/data/text_masquerading_as_image.jpeg line 'This is just a text file masquerading as an image file.' Probably a wrong file was given ERROR: no data query parameter in input URL -input file: test/text_masquerading_as_image.jpeg +input file: tests/data/text_masquerading_as_image.jpeg line 'This is just a text file masquerading as an image file.' Probably a wrong file was given ''' diff --git a/test_extract_otp_secret_keys_unittest.py b/tests/test_extract_otp_secret_keys_unittest.py similarity index 96% rename from test_extract_otp_secret_keys_unittest.py rename to tests/test_extract_otp_secret_keys_unittest.py index 1025597..6e29a73 100644 --- a/test_extract_otp_secret_keys_unittest.py +++ b/tests/test_extract_otp_secret_keys_unittest.py @@ -123,7 +123,7 @@ Type: totp def test_extract_not_encoded_plus(self) -> None: out = io.StringIO() with redirect_stdout(out): - extract_otp_secret_keys.main(['test/test_plus_problem_export.txt']) + extract_otp_secret_keys.main(['tests/data/test_plus_problem_export.txt']) actual_output = out.getvalue() expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk @@ -155,7 +155,7 @@ Type: totp extract_otp_secret_keys.main(['-p', 'example_export.txt']) actual_output = out.getvalue() - expected_output = read_file_to_str('test/printqr_output.txt') + expected_output = read_file_to_str('tests/data/printqr_output.txt') self.assertEqual(actual_output, expected_output) @@ -174,7 +174,7 @@ Type: totp extract_otp_secret_keys.main(['-v', 'example_export.txt']) actual_output = out.getvalue() - expected_output = read_file_to_str('test/print_verbose_output.txt') + expected_output = read_file_to_str('tests/data/print_verbose_output.txt') self.assertEqual(actual_output, expected_output) @@ -184,7 +184,7 @@ Type: totp extract_otp_secret_keys.main(['-vvv', 'example_export.txt']) actual_output = out.getvalue() - expected_stdout = read_file_to_str('test/print_verbose_output.txt') + expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt') self.assertGreater(len(actual_output), len(expected_stdout)) self.assertTrue("DEBUG: " in actual_output) diff --git a/test_extract_qrcode_unittest.py b/tests/test_extract_qrcode_unittest.py similarity index 84% rename from test_extract_qrcode_unittest.py rename to tests/test_extract_qrcode_unittest.py index 75a4342..eb19dbc 100644 --- a/test_extract_qrcode_unittest.py +++ b/tests/test_extract_qrcode_unittest.py @@ -28,7 +28,7 @@ from utils import Capturing class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_happy_path(self) -> None: with Capturing() as actual_output: - extract_otp_secret_keys.main(['test/test_googleauth_export.png']) + extract_otp_secret_keys.main(['tests/data/test_googleauth_export.png']) expected_output =\ ['Name: Test1:test1@example1.com', 'Secret: JBSWY3DPEHPK3PXP', 'Issuer: Test1', 'Type: totp', '', @@ -40,9 +40,9 @@ class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_no_qr_code_in_image(self) -> None: with Capturing() as actual_output: with self.assertRaises(SystemExit) as context: - extract_otp_secret_keys.main(['test/lena_std.tif']) + extract_otp_secret_keys.main(['tests/data/lena_std.tif']) - expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: test/lena_std.tif'] + expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: tests/data/lena_std.tif'] self.assertEqual(actual_output, expected_output) self.assertEqual(context.exception.code, 1) @@ -50,9 +50,9 @@ class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_nonexistent_file(self) -> None: with Capturing() as actual_output: with self.assertRaises(SystemExit) as context: - extract_otp_secret_keys.main(['test/nonexistent.bmp']) + extract_otp_secret_keys.main(['nonexistent.bmp']) - expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: test/nonexistent.bmp'] + expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: nonexistent.bmp'] self.assertEqual(actual_output, expected_output) self.assertEqual(context.exception.code, 1) @@ -60,17 +60,17 @@ class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_non_image_file(self) -> None: with Capturing() as actual_output: with self.assertRaises(SystemExit) as context: - extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg']) + extract_otp_secret_keys.main(['tests/data/text_masquerading_as_image.jpeg']) expected_output = [ '', 'WARN: line is not a otpauth-migration:// URL', - 'input: test/text_masquerading_as_image.jpeg', + 'input: tests/data/text_masquerading_as_image.jpeg', "line 'This is just a text file masquerading as an image file.'", 'Probably a wrong file was given', '', 'ERROR: no data query parameter in input URL', - 'input file: test/text_masquerading_as_image.jpeg', + 'input file: tests/data/text_masquerading_as_image.jpeg', "line 'This is just a text file masquerading as an image file.'", 'Probably a wrong file was given' ] diff --git a/utils.py b/tests/utils.py similarity index 100% rename from utils.py rename to tests/utils.py