diff --git a/Makefile b/Makefile index 0ddaaf7..c2c7f11 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,5 @@ verify: - @python3 -m compileall -q src tests && python3 -m unittest discover -s tests -q && echo "verification passed" + @python3.13 -m compileall -q src tests app && python3.13 -m unittest discover -s tests -q && echo "verification passed" + +run: + FLASK_APP=app flask run diff --git a/README.md b/README.md index 446f734..7b68822 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # typosaurus-sandbox -Sandbox for Typosaurus end-to-end verification \ No newline at end of file +Sandbox for Typosaurus end-to-end verification + +## Running the web app + +```bash +make run +``` + +The development server starts at http://127.0.0.1:5000. \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8da490d --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,67 @@ +# retoor + +import os + +from flask import Flask +from flask import jsonify +from flask import make_response +from flask import request +from flask.wrappers import Response + +from src.calculator import add +from src.calculator import clamp +from src.calculator import clamp_to_byte +from src.calculator import subtract + +app = Flask(__name__) + + +@app.route('/') +def index() -> Response: + index_path = os.path.join(os.path.dirname(__file__), 'index.html') + with open(index_path) as f: + return make_response(f.read(), 200, {'Content-Type': 'text/html'}) + + +@app.route('/add', methods=['GET']) +def add_route() -> Response: + try: + left = int(request.args['left']) + right = int(request.args['right']) + except (KeyError, TypeError, ValueError): + return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400) + return make_response(jsonify({'result': add(left, right)}), 200) + + +@app.route('/subtract', methods=['GET']) +def subtract_route() -> Response: + try: + left = int(request.args['left']) + right = int(request.args['right']) + except (KeyError, TypeError, ValueError): + return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400) + return make_response(jsonify({'result': subtract(left, right)}), 200) + + +@app.route('/clamp', methods=['GET']) +def clamp_route() -> Response: + try: + value = int(request.args['value']) + low = int(request.args['low']) + high = int(request.args['high']) + except (KeyError, TypeError, ValueError): + return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400) + try: + result = clamp(value, low, high) + except ValueError: + return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400) + return make_response(jsonify({'result': result}), 200) + + +@app.route('/clamp_to_byte', methods=['GET']) +def clamp_to_byte_route() -> Response: + try: + value = int(request.args['value']) + except (KeyError, TypeError, ValueError): + return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400) + return make_response(jsonify({'result': clamp_to_byte(value)}), 200) diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..552818d --- /dev/null +++ b/app/index.html @@ -0,0 +1,69 @@ + + + + + + +Calculator + + +

Calculator

+ +
+Add / Subtract + + + + +
+ +
+Clamp + + + + +
+ +
+Clamp to Byte + + +
+ +

+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..840d434 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask>=3.0,<4.0 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..b1502cd --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,87 @@ +# retoor + +import json +import unittest + +from app import app + + +class TestCalculatorAPI(unittest.TestCase): + + def setUp(self) -> None: + self.client = app.test_client() + + def test_add_success(self) -> None: + response = self.client.get('/add?left=3&right=5') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, {'result': 8}) + + def test_add_missing_param_returns_400(self) -> None: + response = self.client.get('/add?left=3') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_add_invalid_type_returns_400(self) -> None: + response = self.client.get('/add?left=abc&right=5') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_subtract_success(self) -> None: + response = self.client.get('/subtract?left=10&right=3') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, {'result': 7}) + + def test_subtract_missing_param_returns_400(self) -> None: + response = self.client.get('/subtract?right=3') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_subtract_invalid_type_returns_400(self) -> None: + response = self.client.get('/subtract?left=10&right=xyz') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_clamp_success(self) -> None: + response = self.client.get('/clamp?value=5&low=0&high=10') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, {'result': 5}) + + def test_clamp_value_below_low_returns_low(self) -> None: + response = self.client.get('/clamp?value=-5&low=0&high=10') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, {'result': 0}) + + def test_clamp_missing_param_returns_400(self) -> None: + response = self.client.get('/clamp?value=5&low=0') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_clamp_invalid_type_returns_400(self) -> None: + response = self.client.get('/clamp?value=abc&low=0&high=10') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_clamp_low_greater_than_high_returns_400(self) -> None: + response = self.client.get('/clamp?value=5&low=10&high=0') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_clamp_to_byte_success(self) -> None: + response = self.client.get('/clamp_to_byte?value=100') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, {'result': 100}) + + def test_clamp_to_byte_above_255_returns_255(self) -> None: + response = self.client.get('/clamp_to_byte?value=300') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, {'result': 255}) + + def test_clamp_to_byte_missing_param_returns_400(self) -> None: + response = self.client.get('/clamp_to_byte') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json) + + def test_clamp_to_byte_invalid_type_returns_400(self) -> None: + response = self.client.get('/clamp_to_byte?value=abc') + self.assertEqual(response.status_code, 400) + self.assertIn('error', response.json)