Files
fgl-aircon/tests/tools/test_fglair_discover.py
T
petr.polezhaev 31cd56f082 core(M4): fglctl, fglair-discover, README библиотеки; M5 — вне скоупа core-релиза
- README.md (корень): сборка POSIX/ESP-IDF-компонентом, тесты, получение
  ключа (fglair-discover), примеры fglctl; planned-компоненты помечены.
- examples/cli/fglctl.cpp: monitor/get/set/status на публичном API
  (batch GET шаблона, конверсии, wait_online, логи в stderr, exit-коды;
  сборка с -Wall -Wextra -Werror).
- tools/fglair-discover: облачный provisioning (eu/us/cn, endpoints и
  app_id/secret из PROTOCOL §7), --format json|esphome-secrets, --out 0600,
  обработка URLError/KeyError; stdlib-only.
- tests/tools/test_fglair_discover.py: мок-облако (http.server), 4 теста,
  ctest tools_fglair_discover.
- docs/reports/M4_SOAK.md: 60-мин soak на AP-WC1E — 0 рассинхронов,
  33/33 свойства (вкл. строковые), 273 опроса модуля, pushes_bad=0;
  24-ч прогон — приёмочный шаг перед релизом.
- План: M4 ✅; M5 (FglHub/mDNS) — ВНЕ СКОУПА core-релиза.
- Гигиена: config_kata.json (корень+docs/legacy) в .gitignore; MQTT-креды
  из docs/legacy/run.sh заменены плейсхолдером (история чистится отдельно).
- Фиксы ревью: mock арм fail-pushes после активации (флейк san-сборки);
  fglctl: валидация strtoll (abc/99 — отказ, exit!=0), чтение config с
  предупреждением об обрезке, Config{} вместо memset; discover: 0600.
CI: 11/11 ×3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
2026-09-29 01:16:58 +03:00

140 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Тест fglair-discover против мок-облака (stdlib http.server).
Мок эмулирует: /users/sign_in.json, /apiv1/devices.json,
/apiv1/dsns/<dsn>/lan.json (PROTOCOL.md §7). Проверяет оба формата вывода,
фильтр --device, отказ с 401.
"""
import json
import subprocess
import sys
import threading
import unittest
import http.server
import urllib.parse
import os
DISCOVER = os.path.join(os.path.dirname(__file__), "..", "..",
"tools", "fglair-discover")
TOKEN = "test-token-123"
DEVICES = [
{"device": {"dsn": "AC000W00TEST0001", "product_name": "Living Room",
"oem_model": "AP-WC1E", "mac": "A0:C9:A0:0D:61:C9",
"lan_ip": "192.0.2.10"}},
{"device": {"dsn": "AC000W00TEST0002", "product_name": "Bedroom",
"oem_model": "AP-WB2E", "mac": "A0:C9:A0:0D:61:CA",
"lan_ip": "192.0.2.11"}},
]
LANIPS = {
"AC000W00TEST0001": {"lanip": {"lanip_key": "KEY1abc==", "lanip_key_id": 111}},
"AC000W00TEST0002": {"lanip": {"lanip_key": "KEY2def==", "lanip_key_id": 222}},
}
class MockCloud(http.server.BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _json(self, code, obj):
body = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
n = int(self.headers.get("Content-Length") or 0)
body = json.loads(self.rfile.read(n) or b"{}")
if self.path == "/users/sign_in.json":
app = body.get("user", {}).get("application", {})
if app.get("app_id") == "FGLair-eu-id" and \
app.get("app_secret") and \
body["user"]["password"] == "pw":
self._json(200, {"access_token": TOKEN})
else:
self._json(401, {"error": "invalid_credentials"})
else:
self._json(404, {})
def do_GET(self):
if not self.headers.get("Authorization") == f"auth_token {TOKEN}":
self._json(401, {"error": "auth_required"})
return
if self.path == "/apiv1/devices.json":
self._json(200, DEVICES)
return
parsed = urllib.parse.urlparse(self.path)
if parsed.path.startswith("/apiv1/dsns/") and \
parsed.path.endswith("/lan.json"):
dsn = parsed.path.split("/")[3]
self._json(200, LANIPS.get(dsn, {"lanip": {}}))
return
self._json(404, {})
def run_discover(extra):
return subprocess.run(
[sys.executable, DISCOVER, "--region", "eu",
"--email", "u@x.y", "--password", "pw"] + extra,
capture_output=True, text=True, timeout=30)
class TestDiscover(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), MockCloud)
cls.port = cls.srv.server_address[1]
threading.Thread(target=cls.srv.serve_forever, daemon=True).start()
@classmethod
def tearDownClass(cls):
cls.srv.shutdown()
def _base(self, extra=None):
return ["--insecure", "--api-base", f"http://127.0.0.1:{self.port}"] \
+ (extra or [])
def test_json_full(self):
r = run_discover(self._base())
self.assertEqual(r.returncode, 0, r.stderr)
cfgs = [json.loads(l) for l in r.stdout.strip().splitlines()]
self.assertEqual(len(cfgs), 2)
c = cfgs[0]
self.assertEqual(c["dsn"], "AC000W00TEST0001")
self.assertEqual(c["lanip_key"], "KEY1abc==")
self.assertEqual(c["lanip_key_id"], 111)
self.assertEqual(c["model"], "AP-WC1E")
self.assertEqual(c["temp_type"], "C")
self.assertEqual(c["mac_address"], "a0c9a00d61c9")
def test_device_filter(self):
r = run_discover(self._base(["--device", "Bedroom"]))
self.assertEqual(r.returncode, 0, r.stderr)
cfgs = [json.loads(l) for l in r.stdout.strip().splitlines()]
self.assertEqual(len(cfgs), 1)
self.assertEqual(cfgs[0]["dsn"], "AC000W00TEST0002")
def test_esphome_secrets(self):
r = run_discover(self._base(["--device", "Living Room",
"--format", "esphome-secrets"]))
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn('livingroom_dsn: "AC000W00TEST0001"', r.stdout)
self.assertIn('livingroom_lanip_key: "KEY1abc=="', r.stdout)
self.assertIn("livingroom_lanip_key_id: 111", r.stdout)
def test_bad_password(self):
r = subprocess.run(
[sys.executable, DISCOVER, "--region", "eu", "--email", "u@x.y",
"--password", "wrong", "--insecure",
"--api-base", f"http://127.0.0.1:{self.port}"],
capture_output=True, text=True, timeout=30)
self.assertNotEqual(r.returncode, 0)
self.assertIn("Ошибка входа", r.stderr)
if __name__ == "__main__":
unittest.main()