import os import tempfile import time import unittest from app import create_app, db from config import TestingConfig class ScalesRoutesConfig(TestingConfig): _TMP_DIR = tempfile.mkdtemp(prefix="wesp-scales-routes-tests-") SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'recipes_test.db')}" SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_test.db')}"} AUTH_LOGIN = "scales-admin" AUTH_PASSWORD = "scales-secret" SIMULATION_MODE = True READ_INTERVAL = 0.01 KIOSK_ENFORCE_PAIRED_ONLY = True class ScalesRoutesTests(unittest.TestCase): def setUp(self) -> None: self.app = create_app(ScalesRoutesConfig) self.client = self.app.test_client() self.ctx = self.app.app_context() self.ctx.push() db.create_all() self.client.get("/scales") self._pair_terminal() def _pair_terminal(self) -> None: pair = self.client.post("/api/kiosk/pair-token") self.assertEqual(pair.status_code, 200) token = pair.get_json().get("token") self.assertTrue(token) self.client.get(f"/api/kiosk/pair/confirm?token={token}") confirm = self.client.post( "/api/kiosk/pair/confirm", data={"token": token}, content_type="application/x-www-form-urlencoded", ) self.assertEqual(confirm.status_code, 302) def tearDown(self) -> None: # stop global reader thread between tests from app.routes import scales as scales_module reader = getattr(scales_module, "_scales_reader", None) if reader is not None: reader.stop() scales_module._scales_reader = None db.session.remove() db.drop_all() self.ctx.pop() def test_current_weight_and_tare(self) -> None: weight_resp = self.client.get("/current_weight") self.assertEqual(weight_resp.status_code, 200) wj = weight_resp.get_json() self.assertIn("weight", wj) self.assertTrue(wj.get("simulation_mode")) self.assertTrue(wj.get("hx711_ok")) for _ in range(5): _ = self.client.get("/current_weight") time.sleep(0.1) tare_resp = self.client.post("/tare", json={}) self.assertEqual(tare_resp.status_code, 200) body = tare_resp.get_json() self.assertEqual(body.get("status"), "success") self.assertIn("new_zero", body) def test_calibrate_validation_and_success(self) -> None: missing = self.client.post("/calibrate", json={}) self.assertEqual(missing.status_code, 400) self.assertTrue(missing.get_json().get("error")) invalid = self.client.post("/calibrate", json={"calibration_factor": "oops"}) self.assertEqual(invalid.status_code, 400) self.assertTrue(invalid.get_json().get("error")) ok = self.client.post("/calibrate", json={"calibration_factor": 1.1}) self.assertEqual(ok.status_code, 200) self.assertEqual(ok.get_json().get("calibration_factor"), 1.1) def test_calibration_wizard_and_raw_data(self) -> None: step1 = self.client.post( "/calibrate", json={"target_weight": 50}, headers={}, ) self.assertIn(step1.status_code, (200, 400)) if step1.status_code == 400: # no samples yet, warm up and retry once _ = self.client.get("/current_weight") step1 = self.client.post( "/calibrate", json={"target_weight": 50}, headers={}, ) if step1.status_code == 400: time.sleep(0.1) step1 = self.client.post( "/calibrate", json={"target_weight": 50}, headers={}, ) self.assertEqual(step1.status_code, 200) self.assertEqual(step1.get_json().get("status"), "confirm") from app.routes import scales as scales_module reader = scales_module._get_reader() reader.adjust_simulation_weight(50) for _ in range(5): _ = self.client.get("/current_weight") step2 = self.client.post("/calibrate/continue") if step2.status_code == 400: time.sleep(0.15) for _ in range(5): _ = self.client.get("/current_weight") step2 = self.client.post("/calibrate/continue") self.assertEqual(step2.status_code, 200) self.assertEqual(step2.get_json().get("status"), "success") self.assertIn("calibration_factor", step2.get_json()) raw_resp = self.client.get("/current_raw_data") self.assertEqual(raw_resp.status_code, 200) raw = raw_resp.get_json() self.assertIn("last_50_raw", raw) self.assertIn("readings_history", raw) self.assertIn("weight_history", raw) def test_requires_token_or_session_when_public_enabled(self) -> None: # Не localhost в Host и REMOTE_ADDR — иначе require_paired_terminal всегда пропускает. ext = {"REMOTE_ADDR": "198.51.100.10", "HTTP_HOST": "198.51.100.42"} anon = self.app.test_client() bad = anon.get("/current_weight", environ_overrides=ext) self.assertEqual(bad.status_code, 401) anon.get("/scales", environ_overrides=ext) # pair-token с удалённого хоста — только для администратора (см. can_issue_kiosk_access_link). login = anon.post( "/api/auth/login", json={"login": ScalesRoutesConfig.AUTH_LOGIN, "password": ScalesRoutesConfig.AUTH_PASSWORD}, environ_overrides=ext, ) self.assertEqual(login.status_code, 200) pair_resp = anon.post("/api/kiosk/pair-token", environ_overrides=ext) self.assertEqual(pair_resp.status_code, 200) pair = pair_resp.get_json() self.assertIn("token", pair) anon.get( f"/api/kiosk/pair/confirm?token={pair['token']}", environ_overrides=ext, ) anon.post( "/api/kiosk/pair/confirm", data={"token": pair["token"]}, content_type="application/x-www-form-urlencoded", environ_overrides=ext, ) good = anon.get("/current_weight", environ_overrides=ext) self.assertEqual(good.status_code, 200) if __name__ == "__main__": unittest.main()