> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md # IoT — Отчёт по тестированию v0.2.5 / v0.2.6 > Дата: 2026-04-21 > Кластер: iot-naeel, namespace: sless > Образ: naeel/iot-operator:v0.2.6 > Тестировал: GitHub Copilot (Claude Sonnet 4.6) --- ## Контекст В ходе двух сессий (2026-04-12 и 2026-04-21) после деплоя v0.2.3 были обнаружены и исправлены три бага, выпущены v0.2.4 → v0.2.5 → v0.2.6. Затем проведено расширенное тестирование: 7 категорий, 33 теста. --- ## Баги, найденные в процессе тестирования ### BUG-01 — MQTTAuth ищет IoTDevice по Name=deviceID (критический) **Версия:** v0.2.3 **Компонент:** `internal/api/handler/iot_device_handler.go`, функция `MQTTAuth` **Симптом:** MQTT CONNECT возвращал `Connection Refused: not authorised (5)` для всех устройств. **Причина:** Хэндлер вызывал `h.K8s.Get(client.ObjectKey{Name: deviceID})`, где `deviceID` — это `Spec.DeviceID` (`e2e-test-01`), но имя K8s объекта IoTDevice может быть другим (`e2e-test-device`). Объект не находился → `NotFound` → `deny`. **Диагностика:** ```bash # Auth endpoint напрямую возвращал deny при правильном пароле: curl -s -X POST http://iot-operator.sless.svc:9090/internal/mqtt/auth \ -d username:sless_e2e-test-01 # {"result":"deny"} # Secret существует и пароль совпадает: kubectl get secret iot-e2e-test-01 -n sless -o jsonpath="{.data.mqtt-password}" | base64 -d # fa8180c07ffc1d665d21e95a443235a93926f16e9d98c319539263f05a333ef0 # IoTDevice объект называется e2e-test-device, а не e2e-test-01: kubectl get iotdevice -n sless # NAME DEVICEID # e2e-test-device e2e-test-01 ``` **Фикс:** Заменён `Get` на `List` + фильтр по `Spec.DeviceID == deviceID`. **Версия фикса:** v0.2.4 --- ### BUG-02 — PG15+: CREATE DATABASE OWNER требует GRANT (критический) **Версия:** v0.2.4 **Компонент:** `internal/storage/iotpg/iot_telemetry_store.go`, функция `EnsureTenantDB` **Симптом:** sqs-consumer не мог создать tenant DB для первого сообщения от нового namespace. ``` level=ERROR msg="process telemetry message" err="iotpg: create database tenant_sless: pq: must be able to SET ROLE \"tenant_sless\" (42501)" ``` **Причина:** PostgreSQL 15+ требует `GRANT role TO CURRENT_USER` перед `CREATE DATABASE ... OWNER role`. В PG17 это обязательно. **Фикс:** Добавлен `GRANT {userName} TO CURRENT_USER` перед `CREATE DATABASE`. **Версия фикса:** v0.2.5 --- ### BUG-03 — Query param `?device=` вместо `?device_id=` (minor) **Версия:** v0.2.5 **Компонент:** `internal/api/handler/iot_telemetry_handler.go`, функция `ListIoTTelemetry` **Симптом:** `GET /telemetry?device_id=dev-02` возвращал все записи (фильтр не работал). ```bash curl ".../telemetry?device_id=dev-02" # {"count":6,...} ← должно быть count:1 ``` **Причина:** Хэндлер читал `r.URL.Query().Get("device")`, а не `"device_id"`. Несоответствие с именем поля в ответе (`device_id`) и документацией. **Фикс:** Параметр переименован в `device_id`. **Версия фикса:** v0.2.6 --- ## Состояние кластера на момент тестирования ``` Кластер: iot-naeel (185.247.187.149:6443) Namespace: sless Дата: 2026-04-21 ПОДЫ: emqx-858d99fcc7-mgjjx 1/1 Running 8d iot-operator-6b4fcc47cc-b5rwf 1/1 Running ~30m iot-mqtt-bridge-74d5c4688-5qqqv 1/1 Running ~16s iot-sqs-consumer-d456d4f8c-n8pnk 1/1 Running ~16s ОБРАЗЫ (все компоненты): naeel/iot-operator:v0.2.6 УСТРОЙСТВА: e2e-test-device device_id=e2e-test-01 phase=Active enabled=true e2e-dev-02 device_id=dev-02 phase=Active enabled=true e2e-dev-03 device_id=dev-03 phase=Active enabled=true e2e-dev-04 device_id=dev-04 phase=Active enabled=true ТЕЛЕМЕТРИЯ В POSTGRES (tenant_sless): e2e-test-01: >1000 записей (cap API = 1000) dev-02: 21 запись dev-03: 21 запись dev-04: 21 запись ``` --- ## Результаты тестирования ### Группа 1 — E2E пайплайн (базовый) Тесты из сессии 2026-04-12, подтверждены после деплоя v0.2.5. | # | Тест | Команда / Действие | Ожидание | Результат | |---|------|--------------------|----------|-----------| | 1.1 | Создание устройства | `POST /v1/namespaces/sless/iot/devices` | 201, phase после reconcile = Active | ✅ PASS | | 1.2 | Получение credentials | `GET /devices/e2e-test-device` | mqtt_username, mqtt_password, secret_name | ✅ PASS | | 1.3 | MQTT CONNECT | `mosquitto_pub -u sless_e2e-test-01 -P ` | CONNACK(0) | ✅ PASS | | 1.4 | MQTT → SQS | Проверка логов mqtt-bridge | `forwarded IoT telemetry to SQS` | ✅ PASS | | 1.5 | SQS → Postgres | Проверка логов sqs-consumer | `telemetry saved to Postgres` | ✅ PASS | | 1.6 | Telemetry API | `GET /telemetry?device_id=e2e-test-01` | count≥1, payload совпадает | ✅ PASS | --- ### Группа 2 — Device API: коды ошибок | # | Тест | Запрос | Ожидание | Факт | Результат | |---|------|--------|----------|------|-----------| | 2.1 | POST без `name` | `{}` | 400 `name is required` | 400 ✓ | ✅ PASS | | 2.2 | POST без `device_id` | `{"name":"x"}` | 400 `device_id is required` | 400 ✓ | ✅ PASS | | 2.3 | POST дубликат | существующее имя | 409 `iot device already exists` | 409 ✓ | ✅ PASS | | 2.4 | GET несуществующий | `GET /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS | | 2.5 | DELETE несуществующий | `DELETE /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS | | 2.6 | PATCH без `enabled` | `{}` | 400 `enabled field is required` | 400 ✓ | ✅ PASS | | 2.7 | POST невалидный JSON | `not-json` | 400 `invalid JSON: ...` | 400 ✓ | ✅ PASS | --- ### Группа 3 — MQTT Auth: edge cases Все запросы идут на `POST /internal/mqtt/auth`. Корректный ответ всегда HTTP 200. Результат определяется полем `result` в теле: `"allow"` или `"deny"`. | # | Тест | Входные данные | Ожидание | Результат | |---|------|----------------|----------|-----------| | 3.1 | Неверный пароль | правильный username, неверный pass | deny | ✅ PASS | | 3.2 | Устройство disabled | правильные credentials, `enabled=false` | deny | ✅ PASS | | 3.3 | Пустое тело | `{}` | deny | ✅ PASS | | 3.4 | Username без `_` | `"username":"nounderscore"` | deny | ✅ PASS | | 3.5 | Несуществующий deviceID | `"username":"sless_ghost-999"` | deny | ✅ PASS | | 3.6 | Пустой namespace | `"username":"_dev01"` | deny | ✅ PASS | | 3.7 | Пустой deviceID | `"username":"sless_"` | deny | ✅ PASS | | 3.8 | Bridge: неверный пароль | bridge username, wrong pass | deny | ✅ PASS | | 3.9 | Невалидный JSON body | `not-json` | deny | ✅ PASS | | 3.10 | Bridge: правильные credentials | MQTT_USERNAME + MQTT_PASSWORD | allow | ✅ PASS | | 3.11 | Правильные device credentials | sless_e2e-test-01 + корректный pass | allow + ACL rules | ✅ PASS | **ACL в ответе при allow (пример для устройства):** ```json { "result": "allow", "acl": [ {"permission":"allow","action":"publish","topic":"sless/telemetry/e2e-test-01"}, {"permission":"allow","action":"subscribe","topic":"sless/telemetry/e2e-test-01"}, {"permission":"deny","action":"all","topic":"#"} ] } ``` --- ### Группа 4 — Device lifecycle (PATCH / DELETE) | # | Тест | Действие | Ожидание | Результат | |---|------|----------|----------|-----------| | 4.1 | Отключение устройства | `PATCH enabled=false` | 200, phase → Disabled | ✅ PASS | | 4.2 | Auth отключённого | auth с правильным паролем | deny | ✅ PASS | | 4.3 | Включение обратно | `PATCH enabled=true` | 200, reconcile → phase=Active | ✅ PASS | | 4.4 | DELETE устройства | `DELETE /devices/to-delete` | 204 | ✅ PASS | | 4.5 | Cascade: Secret удалён | `kubectl get secret iot-del-01` | NotFound | ✅ PASS | | 4.6 | Cascade: IoTDevice удалён | `kubectl get iotdevice to-delete` | NotFound | ✅ PASS | --- ### Группа 5 — ACL изоляция топиков | # | Тест | Действие | Ожидание | Результат | |---|------|----------|----------|-----------| | 5.1 | Публикация в свой топик | `sless/telemetry/e2e-test-01` | CONNACK(0), forwarded | ✅ PASS | | 5.2 | Публикация в чужой топик | `sless/telemetry/ANOTHER-DEVICE` | CONNACK(0), но не forwarded | ✅ PASS | > EMQX применяет ACL после аутентификации. При публикации в запрещённый топик > клиент получает CONNACK(0) (аутентификация прошла), но PUBLISH тихо отбрасывается. > Bridge не получает сообщение — подтверждено отсутствием записи в логах. --- ### Группа 6 — MQTT: нестандартные payload | # | Тест | Payload | Поведение | Ожидание | Результат | |---|------|---------|-----------|----------|-----------| | 6.1 | Невалидный JSON | `THIS IS NOT JSON AT ALL !@#` | bridge оборачивает в строку | `"payload":"THIS IS NOT JSON AT ALL !@#"` | ✅ PASS (by design) | | 6.2 | Пустой payload | `""` | bridge оборачивает в строку | `"payload":""` | ✅ PASS (by design) | | 6.3 | Неизвестное устройство | любой payload | CONNACK(5) not authorised | отклонено | ✅ PASS | > Дизайн-решение: bridge намеренно принимает любой payload (не только JSON). > Невалидный payload оборачивается в JSON-строку (`json.Marshal(string(payload))`). > Это позволяет передавать raw данные от устройств старых форматов. --- ### Группа 7 — Telemetry API: граничные значения | # | Тест | Параметры | Ожидание | Факт | Результат | |---|------|-----------|----------|------|-----------| | 7.1 | limit=2 | `?limit=2` | count=2 | count=2 ✓ | ✅ PASS | | 7.2 | limit=0 (default) | `?limit=0` | count=50 | count=50 ✓ | ✅ PASS | | 7.3 | limit отрицательный | `?limit=-1` | count=50 (default) | count=50 ✓ | ✅ PASS | | 7.4 | limit cap | `?limit=9999`, >1000 записей | count=1000 | count=1000 ✓ | ✅ PASS | | 7.5 | Фильтр device_id | `?device_id=dev-02` | только записи dev-02 | count=21, все dev-02 ✓ | ✅ PASS | | 7.6 | Несуществующий device_id | `?device_id=ghost` | count=0, items=[] | count=0 ✓ | ✅ PASS | | 7.7 | Несуществующий namespace | `/namespaces/unknown-ns-xyz/...` | count=0, items=[] (нет tenant DB) | count=0 ✓ | ✅ PASS | --- ### Группа 8 — Нагрузочные тесты | # | Тест | Параметры | Ожидание | Результат | |---|------|-----------|----------|-----------| | 8.1 | 100 сообщений подряд | 1 устройство, 100 publish | все 100 в Postgres | ✅ PASS (count=103) | | 8.2 | 900 сообщений подряд | 1 устройство, 900 publish | все в Postgres | ✅ PASS | | 8.3 | 3 устройства × 20 сообщений | параллельно | изоляция: каждый dev получил ровно 21 | ✅ PASS | | 8.4 | 10 create+delete race | 10 параллельных goroutine | все 204, нет утечек | ✅ PASS | > Замечание: в тесте 8.1 и 8.2 использовался один и тот же device. > Финальный count dev-02/03/04 = 21 (1 из предыдущей сессии + 20 нагрузочных). --- ## Итоги **Всего тестов: 33** **Пройдено: 33 / 33 (100%)** **Найдено багов: 3** (все исправлены) | Баг | Серьёзность | Версия обнаружения | Версия фикса | |-----|-------------|-------------------|--------------| | BUG-01: MQTTAuth Get→List по Spec.DeviceID | Critical | v0.2.3 | v0.2.4 | | BUG-02: PG15+ GRANT перед CREATE DATABASE OWNER | Critical | v0.2.4 | v0.2.5 | | BUG-03: query param `device` → `device_id` | Minor | v0.2.5 | v0.2.6 | **Финальная версия: `naeel/iot-operator:v0.2.6`** --- ## История версий | Версия | Дата | Изменение | |--------|------|-----------| | v0.2.3 | 2026-04-12 | Деплой: bridge auth fix (bridge username без `_`) | | v0.2.4 | 2026-04-12 | Fix BUG-01: MQTTAuth List вместо Get | | v0.2.5 | 2026-04-12 | Fix BUG-02: GRANT перед CREATE DATABASE (PG15+) | | v0.2.6 | 2026-04-21 | Fix BUG-03: query param device→device_id | EOF cat > /home/naeel/terra/IoT/doc/testing-v0.2.6.md << 'EOF' # IoT — Отчёт по тестированию v0.2.5 / v0.2.6 > Дата: 2026-04-21 > Кластер: iot-naeel, namespace: sless > Образ: naeel/iot-operator:v0.2.6 > Тестировал: GitHub Copilot (Claude Sonnet 4.6) --- ## Контекст В ходе двух сессий (2026-04-12 и 2026-04-21) после деплоя v0.2.3 были обнаружены и исправлены три бага, выпущены v0.2.4 → v0.2.5 → v0.2.6. Затем проведено расширенное тестирование: 7 категорий, 33 теста. --- ## Баги, найденные в процессе тестирования ### BUG-01 — MQTTAuth ищет IoTDevice по Name=deviceID (критический) **Версия:** v0.2.3 **Компонент:** `internal/api/handler/iot_device_handler.go`, функция `MQTTAuth` **Симптом:** MQTT CONNECT возвращал `Connection Refused: not authorised (5)` для всех устройств. **Причина:** Хэндлер вызывал `h.K8s.Get(client.ObjectKey{Name: deviceID})`, где `deviceID` — это `Spec.DeviceID` (`e2e-test-01`), но имя K8s объекта IoTDevice может быть другим (`e2e-test-device`). Объект не находился → `NotFound` → `deny`. **Диагностика:** ```bash # Auth endpoint напрямую возвращал deny при правильном пароле: curl -s -X POST http://iot-operator.sless.svc:9090/internal/mqtt/auth \ -d password: # {"result":"deny"} # Secret существует и пароль совпадает: kubectl get secret iot-e2e-test-01 -n sless -o jsonpath="{.data.mqtt-password}" | base64 -d # fa8180c07ffc1d665d21e95a443235a93926f16e9d98c319539263f05a333ef0 # IoTDevice объект называется e2e-test-device, а не e2e-test-01: kubectl get iotdevice -n sless # NAME DEVICEID # e2e-test-device e2e-test-01 ``` **Фикс:** Заменён `Get` на `List` + фильтр по `Spec.DeviceID == deviceID`. **Версия фикса:** v0.2.4 --- ### BUG-02 — PG15+: CREATE DATABASE OWNER требует GRANT (критический) **Версия:** v0.2.4 **Компонент:** `internal/storage/iotpg/iot_telemetry_store.go`, функция `EnsureTenantDB` **Симптом:** sqs-consumer не мог создать tenant DB для первого сообщения от нового namespace. ``` level=ERROR msg="process telemetry message" err="iotpg: create database tenant_sless: pq: must be able to SET ROLE \"tenant_sless\" (42501)" ``` **Причина:** PostgreSQL 15+ требует `GRANT role TO CURRENT_USER` перед `CREATE DATABASE ... OWNER role`. В PG17 это обязательно. **Фикс:** Добавлен `GRANT {userName} TO CURRENT_USER` перед `CREATE DATABASE`. **Версия фикса:** v0.2.5 --- ### BUG-03 — Query param `?device=` вместо `?device_id=` (minor) **Версия:** v0.2.5 **Компонент:** `internal/api/handler/iot_telemetry_handler.go`, функция `ListIoTTelemetry` **Симптом:** `GET /telemetry?device_id=dev-02` возвращал все записи (фильтр не работал). ```bash curl ".../telemetry?device_id=dev-02" # {"count":6,...} ← должно быть count:1 ``` **Причина:** Хэндлер читал `r.URL.Query().Get("device")`, а не `"device_id"`. Несоответствие с именем поля в ответе (`device_id`) и документацией. **Фикс:** Параметр переименован в `device_id`. **Версия фикса:** v0.2.6 --- ## Состояние кластера на момент тестирования ``` Кластер: iot-naeel (185.247.187.149:6443) Namespace: sless Дата: 2026-04-21 ПОДЫ: emqx-858d99fcc7-mgjjx 1/1 Running 8d iot-operator-6b4fcc47cc-b5rwf 1/1 Running ~30m iot-mqtt-bridge-74d5c4688-5qqqv 1/1 Running ~16s iot-sqs-consumer-d456d4f8c-n8pnk 1/1 Running ~16s ОБРАЗЫ (все компоненты): naeel/iot-operator:v0.2.6 УСТРОЙСТВА: e2e-test-device device_id=e2e-test-01 phase=Active enabled=true e2e-dev-02 device_id=dev-02 phase=Active enabled=true e2e-dev-03 device_id=dev-03 phase=Active enabled=true e2e-dev-04 device_id=dev-04 phase=Active enabled=true ТЕЛЕМЕТРИЯ В POSTGRES (tenant_sless): e2e-test-01: >1000 записей (cap API = 1000) dev-02: 21 запись dev-03: 21 запись dev-04: 21 запись ``` --- ## Результаты тестирования ### Группа 1 — E2E пайплайн (базовый) Тесты из сессии 2026-04-12, подтверждены после деплоя v0.2.5. | # | Тест | Команда / Действие | Ожидание | Результат | |---|------|--------------------|----------|-----------| | 1.1 | Создание устройства | `POST /v1/namespaces/sless/iot/devices` | 201, phase после reconcile = Active | ✅ PASS | | 1.2 | Получение credentials | `GET /devices/e2e-test-device` | mqtt_username, mqtt_password, secret_name | ✅ PASS | | 1.3 | MQTT CONNECT | `mosquitto_pub -u sless_e2e-test-01 -P ` | CONNACK(0) | ✅ PASS | | 1.4 | MQTT → SQS | Проверка логов mqtt-bridge | `forwarded IoT telemetry to SQS` | ✅ PASS | | 1.5 | SQS → Postgres | Проверка логов sqs-consumer | `telemetry saved to Postgres` | ✅ PASS | | 1.6 | Telemetry API | `GET /telemetry?device_id=e2e-test-01` | count≥1, payload совпадает | ✅ PASS | --- ### Группа 2 — Device API: коды ошибок | # | Тест | Запрос | Ожидание | Факт | Результат | |---|------|--------|----------|------|-----------| | 2.1 | POST без `name` | `{}` | 400 `name is required` | 400 ✓ | ✅ PASS | | 2.2 | POST без `device_id` | `{"name":"x"}` | 400 `device_id is required` | 400 ✓ | ✅ PASS | | 2.3 | POST дубликат | существующее имя | 409 `iot device already exists` | 409 ✓ | ✅ PASS | | 2.4 | GET несуществующий | `GET /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS | | 2.5 | DELETE несуществующий | `DELETE /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS | | 2.6 | PATCH без `enabled` | `{}` | 400 `enabled field is required` | 400 ✓ | ✅ PASS | | 2.7 | POST невалидный JSON | `not-json` | 400 `invalid JSON: ...` | 400 ✓ | ✅ PASS | --- ### Группа 3 — MQTT Auth: edge cases Все запросы идут на `POST /internal/mqtt/auth`. Корректный ответ всегда HTTP 200. Результат определяется полем `result` в теле: `"allow"` или `"deny"`. | # | Тест | Входные данные | Ожидание | Результат | |---|------|----------------|----------|-----------| | 3.1 | Неверный пароль | правильный username, неверный pass | deny | ✅ PASS | | 3.2 | Устройство disabled | правильные credentials, `enabled=false` | deny | ✅ PASS | | 3.3 | Пустое тело | `{}` | deny | ✅ PASS | | 3.4 | Username без `_` | `"username":"nounderscore"` | deny | ✅ PASS | | 3.5 | Несуществующий deviceID | `"username":"sless_ghost-999"` | deny | ✅ PASS | | 3.6 | Пустой namespace | `"username":"_dev01"` | deny | ✅ PASS | | 3.7 | Пустой deviceID | `"username":"sless_"` | deny | ✅ PASS | | 3.8 | Bridge: неверный пароль | bridge username, wrong pass | deny | ✅ PASS | | 3.9 | Невалидный JSON body | `not-json` | deny | ✅ PASS | | 3.10 | Bridge: правильные credentials | MQTT_USERNAME + MQTT_PASSWORD | allow | ✅ PASS | | 3.11 | Правильные device credentials | sless_e2e-test-01 + корректный pass | allow + ACL rules | ✅ PASS | **ACL в ответе при allow (пример для устройства):** ```json { "result": "allow", "acl": [ {"permission":"allow","action":"publish","topic":"sless/telemetry/e2e-test-01"}, {"permission":"allow","action":"subscribe","topic":"sless/telemetry/e2e-test-01"}, {"permission":"deny","action":"all","topic":"#"} ] } ``` --- ### Группа 4 — Device lifecycle (PATCH / DELETE) | # | Тест | Действие | Ожидание | Результат | |---|------|----------|----------|-----------| | 4.1 | Отключение устройства | `PATCH enabled=false` | 200, phase → Disabled | ✅ PASS | | 4.2 | Auth отключённого | auth с правильным паролем | deny | ✅ PASS | | 4.3 | Включение обратно | `PATCH enabled=true` | 200, reconcile → phase=Active | ✅ PASS | | 4.4 | DELETE устройства | `DELETE /devices/to-delete` | 204 | ✅ PASS | | 4.5 | Cascade: Secret удалён | `kubectl get secret iot-del-01` | NotFound | ✅ PASS | | 4.6 | Cascade: IoTDevice удалён | `kubectl get iotdevice to-delete` | NotFound | ✅ PASS | --- ### Группа 5 — ACL изоляция топиков | # | Тест | Действие | Ожидание | Результат | |---|------|----------|----------|-----------| | 5.1 | Публикация в свой топик | `sless/telemetry/e2e-test-01` | CONNACK(0), forwarded | ✅ PASS | | 5.2 | Публикация в чужой топик | `sless/telemetry/ANOTHER-DEVICE` | CONNACK(0), но не forwarded | ✅ PASS | > EMQX применяет ACL после аутентификации. При публикации в запрещённый топик > клиент получает CONNACK(0) (аутентификация прошла), но PUBLISH тихо отбрасывается. > Bridge не получает сообщение — подтверждено отсутствием записи в логах. --- ### Группа 6 — MQTT: нестандартные payload | # | Тест | Payload | Поведение | Ожидание | Результат | |---|------|---------|-----------|----------|-----------| | 6.1 | Невалидный JSON | `THIS IS NOT JSON AT ALL !@#` | bridge оборачивает в строку | `"payload":"THIS IS NOT JSON AT ALL !@#"` | ✅ PASS (by design) | | 6.2 | Пустой payload | `""` | bridge оборачивает в строку | `"payload":""` | ✅ PASS (by design) | | 6.3 | Неизвестное устройство | любой payload | CONNACK(5) not authorised | отклонено | ✅ PASS | > Дизайн-решение: bridge намеренно принимает любой payload (не только JSON). > Невалидный payload оборачивается в JSON-строку (`json.Marshal(string(payload))`). > Это позволяет передавать raw данные от устройств старых форматов. --- ### Группа 7 — Telemetry API: граничные значения | # | Тест | Параметры | Ожидание | Факт | Результат | |---|------|-----------|----------|------|-----------| | 7.1 | limit=2 | `?limit=2` | count=2 | count=2 ✓ | ✅ PASS | | 7.2 | limit=0 (default) | `?limit=0` | count=50 | count=50 ✓ | ✅ PASS | | 7.3 | limit отрицательный | `?limit=-1` | count=50 (default) | count=50 ✓ | ✅ PASS | | 7.4 | limit cap | `?limit=9999`, >1000 записей | count=1000 | count=1000 ✓ | ✅ PASS | | 7.5 | Фильтр device_id | `?device_id=dev-02` | только записи dev-02 | count=21, все dev-02 ✓ | ✅ PASS | | 7.6 | Несуществующий device_id | `?device_id=ghost` | count=0, items=[] | count=0 ✓ | ✅ PASS | | 7.7 | Несуществующий namespace | `/namespaces/unknown-ns-xyz/...` | count=0, items=[] (нет tenant DB) | count=0 ✓ | ✅ PASS | --- ### Группа 8 — Нагрузочные тесты | # | Тест | Параметры | Ожидание | Результат | |---|------|-----------|----------|-----------| | 8.1 | 100 сообщений подряд | 1 устройство, 100 publish | все 100 в Postgres | ✅ PASS (count=103) | | 8.2 | 900 сообщений подряд | 1 устройство, 900 publish | все в Postgres | ✅ PASS | | 8.3 | 3 устройства × 20 сообщений | параллельно | изоляция: каждый dev получил ровно 21 | ✅ PASS | | 8.4 | 10 create+delete race | 10 параллельных goroutine | все 204, нет утечек | ✅ PASS | > Замечание: в тесте 8.1 и 8.2 использовался один и тот же device. > Финальный count dev-02/03/04 = 21 (1 из предыдущей сессии + 20 нагрузочных). --- ## Итоги **Всего тестов: 33** **Пройдено: 33 / 33 (100%)** **Найдено багов: 3** (все исправлены) | Баг | Серьёзность | Версия обнаружения | Версия фикса | |-----|-------------|-------------------|--------------| | BUG-01: MQTTAuth Get→List по Spec.DeviceID | Critical | v0.2.3 | v0.2.4 | | BUG-02: PG15+ GRANT перед CREATE DATABASE OWNER | Critical | v0.2.4 | v0.2.5 | | BUG-03: query param `device` → `device_id` | Minor | v0.2.5 | v0.2.6 | **Финальная версия: `naeel/iot-operator:v0.2.6`** --- ## История версий | Версия | Дата | Изменение | |--------|------|-----------| | v0.2.3 | 2026-04-12 | Деплой: bridge auth fix (bridge username без `_`) | | v0.2.4 | 2026-04-12 | Fix BUG-01: MQTTAuth List вместо Get | | v0.2.5 | 2026-04-12 | Fix BUG-02: GRANT перед CREATE DATABASE (PG15+) | | v0.2.6 | 2026-04-21 | Fix BUG-03: query param device→device_id |