diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..68536bd --- /dev/null +++ b/conftest.py @@ -0,0 +1,9 @@ +import pytest + +import src.main as snapbin + + +@pytest.fixture() +def app(): + snapbin.app.config["TESTING"] = True + return snapbin.app.test_client() diff --git a/src/main.py b/src/main.py index 485e912..996d6db 100644 --- a/src/main.py +++ b/src/main.py @@ -11,7 +11,6 @@ from .utils import strtobool NO_SSL = bool(strtobool(os.environ.get("NO_SSL", "False"))) -URL_PREFIX = os.environ.get("URL_PREFIX", None) HOST_OVERRIDE = os.environ.get("HOST_OVERRIDE", None) TOKEN_SEPARATOR = "~" diff --git a/tests.py b/tests.py index a3af270..8848f48 100644 --- a/tests.py +++ b/tests.py @@ -1,163 +1,137 @@ import re import time -import unittest import uuid -from unittest import TestCase -from unittest import mock -from urllib.parse import unquote + +import pytest from cryptography.fernet import Fernet from freezegun import freeze_time from werkzeug.exceptions import BadRequest -from fakeredis import FakeStrictRedis # noinspection PyPep8Naming import src.main as snapbin -__author__ = 'davedash' + +def test_get_password(): + password = "melatonin overdose 1337!$" + key = snapbin.set_password(password, 30) + assert password == snapbin.get_password(key) + # Assert that we can't look this up a second time. + assert snapbin.get_password(key) == None -class SnapPassTestCase(TestCase): - - @mock.patch('redis.client.StrictRedis', FakeStrictRedis) - def test_get_password(self): - password = "melatonin overdose 1337!$" - key = snapbin.set_password(password, 30) - self.assertEqual(password, snapbin.get_password(key)) - # Assert that we can't look this up a second time. - self.assertIsNone(snapbin.get_password(key)) - - def test_password_is_not_stored_in_plaintext(self): - password = "trustno1" - token = snapbin.set_password(password, 30) - redis_key = token.split(snapbin.TOKEN_SEPARATOR)[0] - stored_password_text = snapbin.redis_client.get(redis_key).decode('utf-8') - self.assertNotIn(password, stored_password_text) - - def test_returned_token_format(self): - password = "trustsome1" - token = snapbin.set_password(password, 30) - token_fragments = token.split(snapbin.TOKEN_SEPARATOR) - self.assertEqual(2, len(token_fragments)) - redis_key, encryption_key = token_fragments - self.assertEqual(32 + len(snapbin.REDIS_PREFIX), len(redis_key)) - try: - Fernet(encryption_key.encode('utf-8')) - except ValueError: - self.fail('the encryption key is not valid') - - def test_encryption_key_is_returned(self): - password = "trustany1" - token = snapbin.set_password(password, 30) - token_fragments = token.split(snapbin.TOKEN_SEPARATOR) - redis_key, encryption_key = token_fragments - stored_password = snapbin.redis_client.get(redis_key) - fernet = Fernet(encryption_key.encode('utf-8')) - decrypted_password = fernet.decrypt(stored_password).decode('utf-8') - self.assertEqual(password, decrypted_password) - - def test_unencrypted_passwords_still_work(self): - unencrypted_password = "trustevery1" - storage_key = uuid.uuid4().hex - snapbin.redis_client.setex(storage_key, 30, unencrypted_password) - retrieved_password = snapbin.get_password(storage_key) - self.assertEqual(unencrypted_password, retrieved_password) - - def test_password_is_decoded(self): - password = "correct horse battery staple" - key = snapbin.set_password(password, 30) - self.assertFalse(isinstance(snapbin.get_password(key), bytes)) - - def test_clean_input(self): - # Test Bad Data - with snapbin.app.test_request_context( - "/", data={'password': 'foo', 'ttl': 'bar'}, method='POST'): - self.assertRaises(BadRequest, snapbin.clean_input) - - # No Password - with snapbin.app.test_request_context( - "/", method='POST'): - self.assertRaises(BadRequest, snapbin.clean_input) - - # No TTL - with snapbin.app.test_request_context( - "/", data={'password': 'foo'}, method='POST'): - self.assertRaises(BadRequest, snapbin.clean_input) - - with snapbin.app.test_request_context( - "/", data={'password': 'foo', 'ttl': 'hour'}, method='POST'): - self.assertEqual((3600, 'foo'), snapbin.clean_input()) - - def test_password_before_expiration(self): - password = 'fidelio' - key = snapbin.set_password(password, 1) - self.assertEqual(password, snapbin.get_password(key)) - - def test_password_after_expiration(self): - password = 'open sesame' - key = snapbin.set_password(password, 1) - time.sleep(1.5) - self.assertIsNone(snapbin.get_password(key)) +def test_password_is_not_stored_in_plaintext(): + password = "trustno1" + token = snapbin.set_password(password, 30) + redis_key = token.split(snapbin.TOKEN_SEPARATOR)[0] + stored_password_text = snapbin.redis_client.get(redis_key).decode("utf-8") + assert stored_password_text not in password -class SnapPassRoutesTestCase(TestCase): - # noinspection PyPep8Naming - def setUp(self): - snapbin.app.config['TESTING'] = True - self.app = snapbin.app.test_client() - - def test_preview_password(self): - password = "I like novelty kitten statues!" - key = snapbin.set_password(password, 30) - rv = self.app.get('/{0}'.format(key)) - self.assertNotIn(password, rv.get_data(as_text=True)) - - def test_show_password(self): - password = "I like novelty kitten statues!" - key = snapbin.set_password(password, 30) - rv = self.app.post('/{0}'.format(key)) - self.assertIn(password, rv.get_data(as_text=True)) - - def test_url_prefix(self): - password = "I like novelty kitten statues!" - snapbin.URL_PREFIX = "/test/prefix" - rv = self.app.post('/', data={'password': password, 'ttl': 'hour'}) - self.assertIn("localhost/test/prefix/", rv.get_data(as_text=True)) - - def test_set_password(self): - with freeze_time("2020-05-08 12:00:00") as frozen_time: - password = 'my name is my passport. verify me.' - rv = self.app.post('/', data={'password': password, 'ttl': 'three days'}) - - html_content = rv.data.decode("ascii") - key = re.search(r'id="password-link" value="http://localhost/([^"]+)', html_content).group(1) - key = unquote(key) - - frozen_time.move_to("2020-05-22 11:59:59") - self.assertEqual(snapbin.get_password(key), password) - - frozen_time.move_to("2020-05-22 12:00:00") - self.assertIsNone(snapbin.get_password(key)) - - def test_set_password_json(self): - with freeze_time("2020-05-08 12:00:00") as frozen_time: - password = 'my name is my passport. verify me.' - rv = self.app.post( - '/', - headers={'Accept': 'application/json'}, - data={'password': password, 'ttl': 'three days'}, - ) - - json_content = rv.get_json() - key = re.search(r'http://localhost/([^"]+)', json_content['link']).group(1) - key = unquote(key) - - frozen_time.move_to("2020-05-22 11:59:59") - self.assertEqual(snapbin.get_password(key), password) - - frozen_time.move_to("2020-05-22 12:00:00") - self.assertIsNone(snapbin.get_password(key)) +def test_returned_token_format(): + password = "trustsome1" + token = snapbin.set_password(password, 30) + token_fragments = token.split(snapbin.TOKEN_SEPARATOR) + assert 2 == len(token_fragments) + redis_key, encryption_key = token_fragments + assert (32 + len(snapbin.REDIS_PREFIX)) == len(redis_key) + try: + Fernet(encryption_key.encode("utf-8")) + except ValueError: + assert False, "the encryption key is not valid" -if __name__ == '__main__': - unittest.main() +def test_encryption_key_is_returned(): + password = "trustany1" + token = snapbin.set_password(password, 30) + token_fragments = token.split(snapbin.TOKEN_SEPARATOR) + redis_key, encryption_key = token_fragments + stored_password = snapbin.redis_client.get(redis_key) + fernet = Fernet(encryption_key.encode("utf-8")) + decrypted_password = fernet.decrypt(stored_password).decode("utf-8") + assert password == decrypted_password + + +def test_unencrypted_passwords_still_work(): + unencrypted_password = "trustevery1" + storage_key = uuid.uuid4().hex + snapbin.redis_client.setex(storage_key, 30, unencrypted_password) + retrieved_password = snapbin.get_password(storage_key) + assert unencrypted_password == retrieved_password + + +def test_password_is_decoded(): + password = "correct horse battery staple" + key = snapbin.set_password(password, 30) + assert not isinstance(snapbin.get_password(key), bytes) + + +def test_clean_input(): + # Test Bad Data + with snapbin.app.test_request_context( + "/", data={"password": "foo", "ttl": "bar"}, method="POST" + ): + with pytest.raises(BadRequest): + snapbin.clean_input() + + # No Password + with snapbin.app.test_request_context("/", method="POST"): + with pytest.raises(BadRequest): + snapbin.clean_input() + + # No TTL + with snapbin.app.test_request_context( + "/", data={"password": "foo", "ttl": ""}, method="POST" + ): + assert (604800, "foo") == snapbin.clean_input() + + with snapbin.app.test_request_context( + "/", data={"password": "foo", "ttl": 3600}, method="POST" + ): + assert (3600, "foo") == snapbin.clean_input() + + +def test_password_before_expiration(): + password = "fidelio" + key = snapbin.set_password(password, 1) + assert password == snapbin.get_password(key) + + +def test_password_after_expiration(): + password = "open sesame" + key = snapbin.set_password(password, 1) + time.sleep(1.5) + assert snapbin.get_password(key) == None + + +def test_preview_password(app): + password = "I like novelty kitten statues!" + key = snapbin.set_password(password, 30) + rv = app.get("/{0}".format(key)) + assert password not in rv.get_data(as_text=True) + + +def test_show_password(app): + password = "I like novelty kitten statues!" + key = snapbin.set_password(password, 30) + rv = app.post("/get-secret", data={"key": key}) + assert password in rv.get_data(as_text=True) + + +def test_set_password_json(app): + with freeze_time("2020-05-08 12:00:00") as frozen_time: + password = "my name is my passport. verify me." + rv = app.post( + "/", + headers={"Accept": "application/json"}, + data={"password": password, "ttl": 3000}, + ) + + json_content = rv.get_json() + key = json_content["key"] + + frozen_time.move_to("2020-05-22 11:59:59") + assert snapbin.get_password(key) == password + + frozen_time.move_to("2020-05-22 12:00:00") + assert snapbin.get_password(key) == None