"""POST /api/recipes/calculate — HTTP-контракт и нормализация ключей.""" from __future__ import annotations import os import tempfile import unittest from app import create_app, db from app.models import Component from config import TestingConfig class RecipesCalculateApiConfig(TestingConfig): _TMP_DIR = tempfile.mkdtemp(prefix="wesp-calc-api-") SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'calc_test.db')}" SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_test.db')}"} AUTH_LOGIN = "calc-api-admin" AUTH_PASSWORD = "calc-api-secret" SCENARIO_CALCULATE_FROM_DRY_MATTER = "CALCULATE_FROM_DRY_MATTER" class RecipesCalculateApiTests(unittest.TestCase): def setUp(self) -> None: self.app = create_app(RecipesCalculateApiConfig) self.client = self.app.test_client() self.ctx = self.app.app_context() self.ctx.push() db.create_all() self.client.post( "/api/auth/login", json={"login": "calc-api-admin", "password": "calc-api-secret"}, ) self.comp_id = "calc-comp-1" db.session.add( Component( id=self.comp_id, name="Зерно", type="grain", dry_matter=80.0, protein=0.0, energy=0.0, price=0.0, ) ) db.session.commit() def tearDown(self) -> None: db.session.remove() db.drop_all() self.ctx.pop() def test_calculate_requires_auth(self) -> None: anon = self.app.test_client() resp = anon.post("/api/recipes/calculate", json={"headsCount": 1}) self.assertEqual(resp.status_code, 401) def test_calculate_empty_body_400(self) -> None: resp = self.client.post("/api/recipes/calculate") self.assertIn(resp.status_code, (400, 415)) def test_calculate_camelCase_weight_mode(self) -> None: resp = self.client.post( "/api/recipes/calculate", json={ "headsCount": 10, "tripPercent": 100, "ingredients": [ {"weightPerHead": 2.0, "dryMatter": 50.0, "componentId": self.comp_id}, ], "unloadingGroups": [{"distributionType": "percent", "value": 100}], }, ) self.assertEqual(resp.status_code, 200, resp.get_data(as_text=True)) data = resp.get_json() self.assertEqual(data["totals"]["totalTripWeight"], 20.0) self.assertEqual(data["ingredients"][0]["weightPerHead"], "2.00") def test_calculate_snake_case_and_component_dm_lookup(self) -> None: resp = self.client.post( "/api/recipes/calculate", json={ "heads_count": 5, "trip_percent": 100, "ingredients": [ {"weight_per_head": 1.0, "dry_matter": 0, "component_id": self.comp_id}, ], }, ) self.assertEqual(resp.status_code, 200) ing = resp.get_json()["ingredients"][0] self.assertEqual(ing["dryMatterPerHead"], 0.8) def test_calculate_from_dry_matter_flag(self) -> None: resp = self.client.post( "/api/recipes/calculate", json={ "headsCount": 10, "tripPercent": 100, "calculateFromDryMatter": True, "ingredients": [ {"dryMatterPerHead": 1.0, "dryMatter": 50.0}, ], }, ) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.get_json()["ingredients"][0]["weightPerHead"], "2.00") def test_calculate_heads_unloading_groups(self) -> None: resp = self.client.post( "/api/recipes/calculate", json={ "headsCount": 100, "tripPercent": 100, "ingredients": [{"weightPerHead": 10.0, "dryMatter": 50.0}], "unloading_groups": [{"distribution_type": "heads", "value": 25}], }, ) self.assertEqual(resp.status_code, 200) groups = resp.get_json()["unloadingGroups"] self.assertEqual(groups[0]["calculatedWeight"], 250) if __name__ == "__main__": unittest.main()