- doc/testing-v0.2.6.md: full E2E testing report (8 groups, 33 tests) - Group 1: E2E pipeline (MQTT→SQS→Postgres→API) - Group 2: Device API error codes (7 tests) - Group 3: MQTT Auth edge cases (11 tests) - Group 4: Device lifecycle PATCH/DELETE - Group 5: ACL topic isolation - Group 6: Non-standard MQTT payloads - Group 7: Telemetry API limits/filters - Group 8: Load tests (900 msgs, race condition) - deployments: bump mqtt-bridge + sqs-consumer to v0.2.6
536 lines
28 KiB
Markdown
536 lines
28 KiB
Markdown
# 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 <pass>` | 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:<correct-pass>
|
||
# {"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 <pass>` | 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 |
|