diff --git a/doc/testing-v0.2.6.md b/doc/testing-v0.2.6.md new file mode 100644 index 0000000..890dd21 --- /dev/null +++ b/doc/testing-v0.2.6.md @@ -0,0 +1,535 @@ +# 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 |