diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py index c31ca372..9690fed7 100644 --- a/cara/apps/calculator/__init__.py +++ b/cara/apps/calculator/__init__.py @@ -88,8 +88,6 @@ class ConcentrationModel(BaseRequestHandler): try: form = model_generator.FormData.from_dict(requested_model_config) - except (KeyboardInterrupt, SystemExit): - raise except Exception as err: if self.settings.get("debug", False): import traceback @@ -137,7 +135,11 @@ class CompressedCalculatorFormInputs(BaseRequestHandler): def get(self, compressed_args: str): # Convert a base64 zlib encoded shortened URL into a non compressed # URL, and redirect. - args = zlib.decompress(base64.b64decode(compressed_args)).decode() + try: + args = zlib.decompress(base64.b64decode(compressed_args)).decode() + except Exception as err: # noqa + self.set_status(400) + return self.finish("Invalid calculator data: it seems incomplete. Was there an error copying & pasting the URL?") self.redirect(f'/calculator?{args}') @@ -156,7 +158,7 @@ def make_app(debug=False, prefix='/calculator'): calculator_static_dir = Path(__file__).absolute().parent / 'static' urls = [ (r'/?', LandingPage), - (r'/c/(.*)', CompressedCalculatorFormInputs), + (r'/_c/(.*)', CompressedCalculatorFormInputs), (r'/static/(.*)', StaticFileHandler, {'path': static_dir}), (prefix + r'/?', CalculatorForm), (prefix + r'/report', ConcentrationModel), diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index a96571e6..8effd90c 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -77,16 +77,11 @@ def generate_qr_code(prefix, form: FormData): args = urllib.parse.urlencode(form_dict) # Then zlib compress + base64 encode the string. To be inverted by the - # /c/ endpoint. - qr_url = prefix + "/c/" + base64.b64encode( + # /_c/ endpoint. + qr_url = prefix + "/_c/" + base64.b64encode( zlib.compress(args.encode()) ).decode() - - # We show the human-friendly URL when hovering over the link, but for now - # let's keep it the same as the QR URL to ensure everything continues to work - # as expected (it takes more effort to validate QR barcodes than it does links). - # url = prefix + "/calculator/?" + args - url = qr_url + url = prefix + "/calculator?" + args qr = qrcode.QRCode( version=1, @@ -101,6 +96,7 @@ def generate_qr_code(prefix, form: FormData): return { 'image': img2base64(_img2bytes(img)), 'link': url, + 'qr_url': qr_url, } diff --git a/cara/tests/apps/calculator/conftest.py b/cara/tests/apps/calculator/conftest.py new file mode 100644 index 00000000..6e9c4074 --- /dev/null +++ b/cara/tests/apps/calculator/conftest.py @@ -0,0 +1,13 @@ +import pytest + +from cara.apps.calculator import model_generator + + +@pytest.fixture +def baseline_form_data(): + return model_generator.baseline_raw_form_data() + + +@pytest.fixture +def baseline_form(baseline_form_data): + return model_generator.FormData.from_dict(baseline_form_data) diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py index 64ecb0d4..89a3c6e0 100644 --- a/cara/tests/apps/calculator/test_model_generator.py +++ b/cara/tests/apps/calculator/test_model_generator.py @@ -6,15 +6,6 @@ from cara import models from cara import data import numpy as np -@pytest.fixture -def baseline_form_data(): - return model_generator.baseline_raw_form_data() - - -@pytest.fixture -def baseline_form(baseline_form_data): - return model_generator.FormData.from_dict(baseline_form_data) - def test_model_from_dict(baseline_form_data): form = model_generator.FormData.from_dict(baseline_form_data) diff --git a/cara/tests/apps/calculator/test_report_generator.py b/cara/tests/apps/calculator/test_report_generator.py index 14beade5..fb70d70b 100644 --- a/cara/tests/apps/calculator/test_report_generator.py +++ b/cara/tests/apps/calculator/test_report_generator.py @@ -1,19 +1,8 @@ import pytest -from cara.apps.calculator import model_generator from cara.apps.calculator import report_generator -@pytest.fixture -def baseline_form_data(): - return model_generator.baseline_raw_form_data() - - -@pytest.fixture -def baseline_form(baseline_form_data): - return model_generator.FormData.from_dict(baseline_form_data) - - def test_generate_report(baseline_form): model = baseline_form.build_model() @@ -32,4 +21,4 @@ def test_generate_report(baseline_form): ], ) def test_readable_minutes(test_input, expected): - assert report_generator.readable_minutes(test_input) == expected \ No newline at end of file + assert report_generator.readable_minutes(test_input) == expected diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py new file mode 100644 index 00000000..24cb0819 --- /dev/null +++ b/cara/tests/apps/calculator/test_webapp.py @@ -0,0 +1,54 @@ +import pytest + +from cara.apps.calculator import make_app +from cara.apps.calculator.report_generator import generate_qr_code + + +@pytest.fixture +def app(): + return make_app() + + +async def test_homepage(http_server_client): + response = await http_server_client.fetch('/') + assert response.code == 200 + + +async def test_calculator(http_server_client): + # Both with and without a trailing slash. + response = await http_server_client.fetch('/calculator') + assert response.code == 200 + + response = await http_server_client.fetch('/calculator/') + assert response.code == 200 + + +async def test_qrcode_urls(http_server_client, baseline_form): + prefix = 'proto://hostname/prefix' + qr_data = generate_qr_code(prefix, baseline_form) + expected = f'{prefix}/calculator?activity_type=office&air_changes=0.0' + assert qr_data['link'].startswith(expected) + + # We should get a 200 for the link. + response = await http_server_client.fetch(qr_data['link'].replace(prefix, '')) + assert response.code == 200 + + # And a 302 for the QR url itself. The redirected URL should be the same as + # in the link. + assert qr_data['qr_url'].startswith(prefix) + response = await http_server_client.fetch( + qr_data['qr_url'].replace(prefix, ''), + max_redirects=0, + raise_error=False, + ) + assert response.code == 302 + assert response.headers['Location'] == qr_data['link'].replace(prefix, '') + + +async def test_invalid_compressed_url(http_server_client, baseline_form): + response = await http_server_client.fetch( + '/_c/invalid-data', + max_redirects=0, + raise_error=False, + ) + assert response.code == 400 diff --git a/setup.py b/setup.py index 08afec18..0487cf14 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,8 @@ setup( 'all': [req for reqs in REQUIREMENTS.values() for req in reqs], }, package_data={'cara': [ + 'apps/templates/*.j2', + 'apps/calculator/templates/*.j2', 'apps/calculator/*', 'apps/calculator/*/*', 'apps/calculator/*/*/*'