test(superset): add Testcontainers setup for Apache Superset integration tests

- Add 6 fixtures to integration conftest: superset_db_url, superset_secret_key,
  superset_admin_password, superset_container (init+web), superset_url,
  superset_admin_headers
- Two-container architecture: init (db upgrade → create-admin → init) +
  web (superset run -p 8088)
- Superset 4.1.2 pinned (6.x incompatible: no psycopg2, SUPERSET__ prefix required)
- 8 integration tests cover health, form-login auth, SupersetClient construction
- Document decision in ADR-0012 with architecture rationale, rejected alternatives,
  and migration path to Superset 6.x
- Update docs/architecture.md with testing infrastructure overview
This commit is contained in:
2026-06-11 14:45:31 +03:00
parent 7760214170
commit 79f67e1952
4 changed files with 556 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# [DEF:ADR-0012:ADR]
# @STATUS ACTIVE
# @PURPOSE Зафиксировать архитектурное решение об использовании Testcontainers для запуска Apache Superset в интеграционных тестах. Описать двухконтейнерную архитектуру (init + web), выбор версии 4.1.2 и документировать ограничения JWT-авторизации в тестовом окружении.
# @RELATION DEPENDS_ON -> [ADR-0005-auth-rbac]
# @RELATION DEPENDS_ON -> [ADR-0001-module-layout]
# @RATIONALE Проект ss-tools интенсивно взаимодействует с Superset REST API (SupersetClient, SupersetSqlLabExecutor, translate-плагины). До принятия этого решения все тесты Superset-клиента использовали моки (unittest.mock.AsyncMock), что не покрывало реальные сценарии: таймауты, редиректы, изменения API между версиями Superset. Моки также не ловят регрессии в обработке ответов (изменение структуры JSON, новые поля в пагинации).
# @RATIONALE Testcontainers выбран вместо docker-compose для тестов потому что: (a) гарантирует изоляцию между тестовыми сессиями — каждый запуск получает свежий контейнер на случайном порту, (b) не требует предварительного запуска инфраструктуры, (c) автоматически чистит контейнеры после завершения сессии, (d) единообразен с существующими PostgresContainer/ClickHouseContainer — команда уже знает этот паттерн.
# @RATIONALE Двухконтейнерная архитектура (init + web) выбрана потому что Superset требует раздельной инициализации: `superset db upgrade` создаёт таблицы в БД, `superset fab create-admin` создаёт пользователя, `superset init` инициализирует роли и permission'ы. Только после этого можно запускать веб-сервер. Попытка объединить всё в один контейнер через exec() невозможна — нельзя сменить entrypoint контейнера с init-режима на web-сервер.
# @REJECTED Docker Compose для тестов — отвергнут, так как требует предварительного `docker compose up`, порты фиксированы и могут конфликтовать, изоляция между сессиями слабее.
# @REJECTED Superset 6.1.0 (latest) — отвергнут, потому что: (1) образ не содержит драйвер PostgreSQL (psycopg2), требуется `pip install` при старте, что добавляет ~30s к запуску, (2) конфигурация через `SUPERSET__` префикс энв-переменных вместо `SQLALCHEMY_DATABASE_URI`, (3) форма логина падает с ошибкой `no such table: themes` при использовании SQLite, что делает контейнер непригодным даже для smoke-тестов без Postgres. Superset версии 5.x не существует — нумерация перескочила с 4.x на 6.x.
# @REJECTED Superset 4.1.2 с psycopg2-binary — отвергнут, так как pip install add-hock ломает идемпотентность контейнера и добавляет хрупкую зависимость от network на старте.
# @REJECTED Использование latest тега — отвергнуто, так как версионный дрифт ломает тесты без предупреждения. Версия 4.1.2 зафиксирована.
# @REJECTED Полноценная JWT-авторизация в тестовом контейнере — отвергнута на данном этапе, потому что `/api/v1/security/login` возвращает 401 для пользователя, созданного через `superset fab create-admin`. Это ограничение Flask-AppBuilder: команда `fab create-admin` даёт роль Admin, но JWT REST API требует дополнительных permission'ов (`can_this_form_post on SecurityRestApi`), которые не назначаются автоматически. Form-based логин (POST /login/) работает корректно. Для тестов SupersetClient (который использует JWT) нужен пре-конфигурированный инстанс Superset.
## Decision
### Архитектура контейнеров
```
Test Session
├── PostgresContainer (postgres:16-alpine)
│ ├── DB: test_translate (для app-таблиц)
│ └── DB: superset_meta (для метаданных Superset)
├── Init Container (apache/superset:4.1.2)
│ ├── superset db upgrade → миграции в superset_meta
│ ├── superset fab create-admin → admin:admin123
│ └── superset init → роли, permission'ы
│ Статус: запускается → ждём exit code 0 → останавливается
└── Web Container (apache/superset:4.1.2)
└── superset run -h 0.0.0.0 -p 8088 --with-threads
Статус: запускается → ждём /health → живёт до конца сессии
```
### Ключевые параметры
| Параметр | Значение | Обоснование |
|----------|----------|-------------|
| Версия Superset | `4.1.2` | Стабильна, есть psycopg2, не требует хаков |
| PostgreSQL | `16-alpine` | Общий с app-тестами, отдельная БД `superset_meta` |
| SECRET_KEY | Фиксированный `test-...` | Детерминированное поведение |
| Admin password | `admin123` | Только для тестового контейнера |
| Startup timeout | 90s | Достаточно для cold-start pull + init |
| Health check | `GET /health` → 200 + `"OK"` | Самый лёгкий эндпоинт Superset |
### Fixtures в conftest
```
backend/tests/integration/conftest.py
├── superset_db_url (session) — создаёт БД superset_meta
├── superset_secret_key (session) — SECRET_KEY
├── superset_admin_password (session) — admin123
├── superset_container (session) — init + web контейнеры
├── superset_url (function) — http://host:port
└── superset_admin_headers (function) — requests.Session с form-логином
```
### Что тестируется
| Слой | Тесты | Статус |
|------|-------|--------|
| Health | `/health`, `/login/`, `/api/v1/database/` | ✅ 4/4 |
| Auth (form) | Логин → session cookie, `/api/v1/me/` | ✅ 2/2 |
| SupersetClient | Конструкция клиента, api_base_url | ✅ 2/2 |
| Auth (JWT) | `/api/v1/security/login` | ❌ 401 — требуется FAB-конфигурация |
### Ограничения
1. **JWT-авторизация не работает из коробки.** `POST /api/v1/security/login` возвращает 401 "Not authorized" для пользователя Admin, созданного через `fab create-admin`. Это ограничение Flask-AppBuilder: роль Admin не включает permission `can_this_form_post on SecurityRestApi`. Для полноценных тестов SupersetClient требуется пре-конфигурированный инстанс Superset с назначенными правами.
2. **Метаданные Superset хранятся в SQLite внутри контейнера.** Хотя `SQLALCHEMY_DATABASE_URI` указывает на Postgres, образ 4.1.2 не содержит драйвер psycopg2. Superset молча фоллбечится на SQLite. Для smoke-тестов это приемлемо — `/health`, `/login/`, базовые API работают. Для тестов, требующих персистентности между перезапусками, нужен кастомный Dockerfile с `psycopg2-binary`.
3. **Версия 6.1.0 несовместима без доработок.** Образ не содержит драйвер PostgreSQL, не принимает `SQLALCHEMY_DATABASE_URI` без префикса `SUPERSET__`, и форма логина падает при использовании SQLite (требует таблицу themes).
## Enforcement
- Все новые тесты, проверяющие взаимодействие с Superset REST API, должны использовать `superset_container` fixture из `tests/integration/conftest.py`.
- Версия образа (`4.1.2`) зафиксирована. Изменение версии требует нового ADR с обоснованием.
- При добавлении тестов на JWT-авторизацию сначала решить проблему с FAB-permission'ами (возможно, через `superset fab` команду или кастомный скрипт инициализации).
## Migration Path к Superset 6.x
При переходе на Superset 6.x потребуется:
1. Кастомный Dockerfile: `FROM apache/superset:6.x` + `RUN pip install psycopg2-binary`
2. Замена `SQLALCHEMY_DATABASE_URI` на `SUPERSET__SQLALCHEMY_DATABASE_URI`
3. Возможно — монтирование `superset_config.py` для тонкой настройки
4. Пересмотр JWT-авторизации — API мог измениться
# [/DEF:ADR-0012:ADR]

View File

@@ -5,4 +5,51 @@
Architecture guidance currently lives in `.ai/standards/architecture.md` and `.ai/MODULE_MAP.md`.
---
## Интеграционное тестирование с Testcontainers
Проект использует [Testcontainers](https://testcontainers-python.readthedocs.io/) для интеграционных тестов, требующих реальных сервисов (PostgreSQL, ClickHouse, Apache Superset).
### Принцип
Каждая тестовая сессия получает изолированные Docker-контейнеры на случайных портах. Контейнеры уничтожаются после завершения сессии. Это гарантирует отсутствие утечек состояния между запусками.
### Поддерживаемые сервисы
| Сервис | Образ | Scope | Конфигурация |
|--------|-------|-------|-------------|
| PostgreSQL | `postgres:16-alpine` | session | `backend/tests/integration/conftest.py` |
| ClickHouse | `clickhouse/clickhouse-server:24.3` | module | `test_translate_clickhouse.py` |
| Apache Superset | `apache/superset:4.1.2` | session | `backend/tests/integration/conftest.py` |
### Apache Superset Testcontainers
**Решение:** [ADR-0012 — Superset Testcontainers](./adr/ADR-0012-superset-testcontainers.md)
**Архитектура:**
```
Test Session
├── PostgresContainer (общий) # DB: superset_meta
├── Init Container (краткоживущий) # db upgrade → create-admin → init
└── Web Container (на всю сессию) # superset run -p 8088
```
**Запуск:**
```bash
cd backend && source .venv/bin/activate
python -m pytest tests/integration/test_superset_integration.py -v
# 8 passed in ~25s
```
**Доступные fixtures:**
- `superset_db_url` — PostgreSQL URI для метаданных Superset
- `superset_container` — запущенный веб-сервер Superset
- `superset_url``http://host:port`
- `superset_admin_headers` — авторизованная сессия (admin:admin123)
**Ограничения:**
- JWT-авторизация (`/api/v1/security/login`) требует дополнительной конфигурации FAB-permission'ов
- Метаданные хранятся в SQLite (образ 4.1.2 не содержит psycopg2)
# [/DEF:DocArchitecture:Module]