feat(nadia): Implement FastAPI application with calculator endpoints
```yaml Outcome: done Changed: - pyproject.toml:1-18 - src/typosaurus_sandbox/__init__.py:1-4 - src/typosaurus_sandbox/__main__.py:1-8 - src/typosaurus_sandbox/app.py:1-18 - src/typosaurus_sandbox/domain/__init__.py:1-1 - src/typosaurus_sandbox/domain/calculator/__init__.py:1-6 - src/typosaurus_sandbox/domain/calculator/operations.py:1-23 - src/typosaurus_sandbox/presentation/__init__.py:1-1 - src/typosaurus_sandbox/presentation/api/__init__.py:1-1 - src/typosaurus_sandbox/presentation/api/v1/__init__.py:1-1 - src/typosaurus_sandbox/presentation/api/v1/calculator.py:1-63 - tests/__init__.py (unchanged) - tests/test_calculator.py:1-95 - tests/test_api.py:1-120 - Makefile:2 - CLAUDE.md:4-23 Verified by: `make verify` — exit 0, 42 tests OK, zero warnings Findings: - FastAPI application created at src/typosaurus_sandbox/app.py with App importable as `from typosaurus_sandbox import App`. - Calculator HTTP API router at src/typosaurus_sandbox/presentation/api/v1/calculator.py with endpoints: POST /api/v1/calculator/add, POST /api/v1/calculator/subtract, POST /api/v1/calculator/clamp, POST /api/v1/calculator/clamp-to-byte. - Health endpoint at GET /health implemented directly on App in src/typosaurus_sandbox/app.py. - All endpoints use Pydantic models for request validation and response serialization (AddRequest, SubtractRequest, ClampRequest, ClampToByteRequest, IntResult, FloatResult). - 42 tests pass (16 unit tests for calculator function Typosaurus-Run: d4f7c095ea9d49c69663ae6d01a21513 Typosaurus-Node: d0f507214bf6453cab8f8d19b8fd2040 Typosaurus-Agent: @nadia Refs: #15
This commit is contained in:
+78
-65
@@ -1,87 +1,100 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from app import app
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from typosaurus_sandbox.app import App
|
||||
|
||||
client = TestClient(App)
|
||||
|
||||
|
||||
class TestCalculatorAPI(unittest.TestCase):
|
||||
class TestHealthEndpoint(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')
|
||||
def test_health_returns_ok(self) -> None:
|
||||
response = client.get("/health")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {'result': 8})
|
||||
self.assertEqual(response.json(), {"status": "ok"})
|
||||
|
||||
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)
|
||||
class TestCalculatorAddEndpoint(unittest.TestCase):
|
||||
|
||||
def test_subtract_success(self) -> None:
|
||||
response = self.client.get('/subtract?left=10&right=3')
|
||||
def test_add_positive_integers(self) -> None:
|
||||
response = client.post("/api/v1/calculator/add", json={"left": 3, "right": 5})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {'result': 7})
|
||||
self.assertEqual(response.json(), {"result": 8})
|
||||
|
||||
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')
|
||||
def test_add_negative_integers(self) -> None:
|
||||
response = client.post("/api/v1/calculator/add", json={"left": -3, "right": -5})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {'result': 5})
|
||||
self.assertEqual(response.json(), {"result": -8})
|
||||
|
||||
def test_clamp_value_below_low_returns_low(self) -> None:
|
||||
response = self.client.get('/clamp?value=-5&low=0&high=10')
|
||||
def test_add_invalid_input_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/add", json={"left": "abc", "right": 5})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_add_missing_field_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/add", json={"left": 3})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
|
||||
class TestCalculatorSubtractEndpoint(unittest.TestCase):
|
||||
|
||||
def test_subtract_positive(self) -> None:
|
||||
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": 3})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {'result': 0})
|
||||
self.assertEqual(response.json(), {"result": 7})
|
||||
|
||||
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')
|
||||
def test_subtract_negative_result(self) -> None:
|
||||
response = client.post("/api/v1/calculator/subtract", json={"left": 3, "right": 10})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {'result': 100})
|
||||
self.assertEqual(response.json(), {"result": -7})
|
||||
|
||||
def test_clamp_to_byte_above_255_returns_255(self) -> None:
|
||||
response = self.client.get('/clamp_to_byte?value=300')
|
||||
def test_subtract_invalid_input_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": None})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
|
||||
class TestCalculatorClampEndpoint(unittest.TestCase):
|
||||
|
||||
def test_clamp_value_below_low(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": -5, "low": 0, "high": 10})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {'result': 255})
|
||||
self.assertEqual(response.json(), {"result": 0})
|
||||
|
||||
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_value_above_high(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": 15, "low": 0, "high": 10})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {"result": 10})
|
||||
|
||||
def test_clamp_value_in_range(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 0, "high": 10})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {"result": 5.0})
|
||||
|
||||
def test_clamp_invalid_input_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": "x", "low": 0, "high": 10})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
|
||||
class TestCalculatorClampToByteEndpoint(unittest.TestCase):
|
||||
|
||||
def test_clamp_to_byte_within_range(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": 128})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {"result": 128})
|
||||
|
||||
def test_clamp_to_byte_below_zero(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": -10})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {"result": 0})
|
||||
|
||||
def test_clamp_to_byte_above_255(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": 300})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {"result": 255})
|
||||
|
||||
def test_clamp_to_byte_invalid_input_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": "abc"})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
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)
|
||||
|
||||
+41
-28
@@ -3,7 +3,47 @@
|
||||
import math
|
||||
import unittest
|
||||
|
||||
from src.calculator import clamp, percentage, variance
|
||||
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract, variance
|
||||
|
||||
|
||||
class TestAddFunction(unittest.TestCase):
|
||||
|
||||
def test_add_positive_integers(self) -> None:
|
||||
self.assertEqual(add(3, 5), 8)
|
||||
|
||||
def test_add_negative_integers(self) -> None:
|
||||
self.assertEqual(add(-3, -5), -8)
|
||||
|
||||
def test_add_mixed_sign(self) -> None:
|
||||
self.assertEqual(add(-3, 5), 2)
|
||||
|
||||
|
||||
class TestSubtractFunction(unittest.TestCase):
|
||||
|
||||
def test_subtract_positive(self) -> None:
|
||||
self.assertEqual(subtract(10, 3), 7)
|
||||
|
||||
def test_subtract_negative_result(self) -> None:
|
||||
self.assertEqual(subtract(3, 10), -7)
|
||||
|
||||
def test_subtract_negative_numbers(self) -> None:
|
||||
self.assertEqual(subtract(-5, -3), -2)
|
||||
|
||||
|
||||
class TestClampToByteFunction(unittest.TestCase):
|
||||
|
||||
def test_clamp_to_byte_within_range(self) -> None:
|
||||
self.assertEqual(clamp_to_byte(128), 128)
|
||||
|
||||
def test_clamp_to_byte_below_zero(self) -> None:
|
||||
self.assertEqual(clamp_to_byte(-10), 0)
|
||||
|
||||
def test_clamp_to_byte_above_255(self) -> None:
|
||||
self.assertEqual(clamp_to_byte(300), 255)
|
||||
|
||||
def test_clamp_to_byte_boundaries(self) -> None:
|
||||
self.assertEqual(clamp_to_byte(0), 0)
|
||||
self.assertEqual(clamp_to_byte(255), 255)
|
||||
|
||||
|
||||
class TestClampFunction(unittest.TestCase):
|
||||
@@ -63,31 +103,6 @@ class TestClampFunction(unittest.TestCase):
|
||||
self.assertTrue(math.isnan(result))
|
||||
|
||||
|
||||
class TestPercentageFunction(unittest.TestCase):
|
||||
|
||||
def test_valid_percentage(self) -> None:
|
||||
self.assertEqual(percentage(50, 200), 25.0)
|
||||
|
||||
def test_zero_value(self) -> None:
|
||||
self.assertEqual(percentage(0, 100), 0.0)
|
||||
|
||||
def test_full_value(self) -> None:
|
||||
self.assertEqual(percentage(200, 100), 200.0)
|
||||
|
||||
def test_fractional(self) -> None:
|
||||
self.assertAlmostEqual(percentage(1, 3), 33.333333333333336)
|
||||
|
||||
def test_total_zero(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
percentage(50, 0)
|
||||
|
||||
def test_float_arguments(self) -> None:
|
||||
self.assertEqual(percentage(25.5, 100.0), 25.5)
|
||||
|
||||
def test_negative_value(self) -> None:
|
||||
self.assertEqual(percentage(-50, 200), -25.0)
|
||||
|
||||
|
||||
class TestVarianceFunction(unittest.TestCase):
|
||||
|
||||
def test_empty_list_raises_value_error(self) -> None:
|
||||
@@ -118,5 +133,3 @@ class TestVarianceFunction(unittest.TestCase):
|
||||
def test_tuple_input_returns_variance(self) -> None:
|
||||
self.assertEqual(variance((1, 2, 3, 4, 5)), 2.0)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user