Files
fgl-aircon/tests/ayla/test_json.cpp
Petr Polezhaev e91d4605b1 core(M1): ayla-криптография, конверт, JSON (jsmn), HTTP-клиент
- crypto: KDF Ayla (двойной HMAC, suffix 0x30/31/32; app/dev направления),
  AES-256-CBC с непрерывной цепочкой (iv обновляется mbedtls на месте),
  Java-паддинг >=1 NUL; mode-latch против misuse (encrypt|decrypt);
  zeroize ключей при повторном init; векторы из APK (4 сессии × 4 сообщения,
  включая legacy-приём без NUL) — scripts/gen_kdf_vectors.py.
- envelope: pack/unpack {"enc","sign"}; расшифровка (движение цепочки)
  ДО проверки подписи; сравнение подписи в константном времени;
  extract_seq_no — depth-1 сканер без лимита токенов (OOB после escape
  исправлен, регресс-тесты по ASan-репро ревьюера).
- json: Writer (фикс. буфер, стек глубин, escape, ok()=false при
  переполнении) + Doc на jsmn (64 токена, unescape, overflow-guard).
- httpc: блокирующий POST/PUT для local_reg (статус 200-599, дренаж,
  shutdown перед close).
- third_party/jsmn (MIT, JSMN_STATIC).
- CMake: mbedtls системный (/usr/include/mbedtls3) или FetchContent;
  IDF: PRIV_REQUIRES mbedtls.
- CI: 3 конфигурации — gcc-Release, gcc-Debug+ASan/UBSan, clang-Release;
  6/6 тестов стабильно; ESP-IDF esp32 build complete.
Ревью под-агентом: 3 круга (OOB-блокер + тестовые флаки закрыты), APPROVED.
2026-09-22 15:46:18 +03:00

157 lines
4.9 KiB
C++
Raw Permalink 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.
// Тесты JSON: writer (фиксированный буфер, вложенность, переполнение)
// и парсер (jsmn) — форматы протокола.
#include "doctest/doctest.h"
#include <cstring>
#include <string>
#include "ayla/json.hpp"
using fgl::ayla::json::Doc;
using fgl::ayla::json::Writer;
TEST_CASE("json writer: формы протокола") {
// datapoint-команда (properties)
char buf[512];
Writer w(buf, sizeof(buf));
w.begin_object(); // {
w.key("properties");
w.begin_array();
w.begin_object();
w.key("property");
w.begin_object();
w.key("base_type"); w.string("integer");
w.key("name"); w.string("fan_speed");
w.key("value"); w.integer(3);
w.key("id"); w.string("ab12cd34");
w.end_object();
w.end_object();
w.end_array();
w.end_object();
REQUIRE(w.ok());
CHECK(std::string(buf) ==
"{\"properties\":[{\"property\":{\"base_type\":\"integer\","
"\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}");
// GET-команда (cmds)
Writer w2(buf, sizeof(buf));
w2.begin_object();
w2.key("cmds");
w2.begin_array();
w2.begin_object();
w2.key("cmd");
w2.begin_object();
w2.key("cmd_id"); w2.integer(5);
w2.key("method"); w2.string("GET");
w2.key("resource"); w2.string("property.json?name=op");
w2.key("data"); w2.string("");
w2.key("uri"); w2.string("/local_lan/property/datapoint.json");
w2.end_object();
w2.end_object();
w2.end_array();
w2.end_object();
REQUIRE(w2.ok());
CHECK(std::string(buf) ==
"{\"cmds\":[{\"cmd\":{\"cmd_id\":5,\"method\":\"GET\","
"\"resource\":\"property.json?name=op\",\"data\":\"\","
"\"uri\":\"/local_lan/property/datapoint.json\"}}]}");
// key_exchange ответ + массив целых + bool/null
Writer w3(buf, sizeof(buf));
w3.begin_object();
w3.key("random_2"); w3.string("abcd1234");
w3.key("time_2"); w3.integer(-123);
w3.key("flags");
w3.begin_array();
w3.integer(1); w3.integer(2); w3.integer(3);
w3.end_array();
w3.key("ok"); w3.boolean(true);
w3.key("nothing"); w3.literal("null");
w3.end_object();
REQUIRE(w3.ok());
CHECK(std::string(buf) ==
"{\"random_2\":\"abcd1234\",\"time_2\":-123,\"flags\":[1,2,3],"
"\"ok\":true,\"nothing\":null}");
}
TEST_CASE("json writer: экранирование и переполнение") {
char buf[64];
Writer w(buf, sizeof(buf));
w.begin_object();
w.key("a\"b\\c"); // ключ с кавычкой и обратным слешем
w.string("x\ny\"z");
w.end_object();
REQUIRE(w.ok());
CHECK(std::string(buf) == "{\"a\\\"b\\\\c\":\"x\\ny\\\"z\"}");
// Переполнение буфера — ok() = false, не выходим за границы.
Writer w2(buf, sizeof(buf));
w2.begin_object();
w2.key("data");
w2.string("0123456789012345678901234567890123456789012345678901234567890123456789");
w.end_object();
CHECK_FALSE(w2.ok());
CHECK(strlen(buf) < sizeof(buf)); // NUL-терминировано всегда
}
TEST_CASE("json parser: форматы протокола") {
// datapoint push
char buf[128];
strcpy(buf, "{\"name\":\"fan_speed\",\"value\":3,\"metadata\":{}}");
Doc doc;
REQUIRE(doc.parse(buf));
char name[32];
REQUIRE(doc.get_string("name", name, sizeof(name)));
CHECK(std::string(name) == "fan_speed");
int64_t value = -1;
REQUIRE(doc.get_int("value", &value));
CHECK(value == 3);
bool b = true;
CHECK_FALSE(doc.get_bool("value", &b)); // не bool
CHECK_FALSE(doc.get_string("absent", name, sizeof(name)));
// key_exchange
strcpy(buf, "{\"key_exchange\":{\"ver\":1,\"proto\":1,\"key_id\":62999,"
"\"random_1\":\"ab+/12\",\"time_1\":-987654321,\"sec\":\"\"}}");
REQUIRE(doc.parse(buf));
int64_t ver = -1, key_id = -1, time_1 = -1;
REQUIRE(doc.get_int("ver", &ver));
REQUIRE(doc.get_int("key_id", &key_id));
REQUIRE(doc.get_int("time_1", &time_1));
char rnd[32];
REQUIRE(doc.get_string("random_1", rnd, sizeof(rnd)));
CHECK(ver == 1);
CHECK(key_id == 62999);
CHECK(time_1 == -987654321);
CHECK(std::string(rnd) == "ab+/12");
// envelope payload
strcpy(buf, "{\"seq_no\":42,\"data\":{\"name\":\"x\",\"value\":true}}");
REQUIRE(doc.parse(buf));
int64_t seq = -1;
REQUIRE(doc.get_int("seq_no", &seq));
CHECK(seq == 42);
bool v = false;
REQUIRE(doc.get_bool("value", &v));
CHECK(v);
// Мусор
strcpy(buf, "not json");
CHECK_FALSE(doc.parse(buf));
strcpy(buf, "[1,2]");
CHECK_FALSE(doc.parse(buf)); // верхний уровень — объект
}
TEST_CASE("json parser: экранированные строки") {
char buf[96];
strcpy(buf, "{\"name\":\"a\\\"b\\\\c\\nd\"}");
Doc doc;
REQUIRE(doc.parse(buf));
char out[32];
REQUIRE(doc.get_string("name", out, sizeof(out)));
CHECK(std::string(out) == "a\"b\\c\nd");
// Не помещается в out
char small[4];
CHECK_FALSE(doc.get_string("name", small, sizeof(small)));
}