# 10. Покрытие тестами — Montana Android v6.5.0 ## §1. Текущий статус **Automated tests:** **ноль**. В проекте нет ни одного `@Test` ни в Kotlin, ни в JS, ни в Python backend. Это **финальный finding** аудиторского пакета: всё проверялось manually через empirical execution (запуск на Pixel 9 Pro XL). ## §2. Что проверено manually | Сценарий | Метод проверки | Результат | |----------|----------------|-----------| | VPN start → tun0 default route | `adb shell ip route show table all` | ✓ default через tun0 | | Heartbeat через VPN cascade | `adb logcat -s MontanaVPN`, наблюдение `hb: 200 via=true node=newyork` | ✓ | | Стичинг по source IP | 10x `curl https://api.ipify.org` с одного IP — все идут на один exit | ✓ Frankfurt 10/10 | | Балансы атомарно учитываются | Concurrent heartbeats от двух Pixels (gedankenexp) | Не проверено | | BIP39 mnemonic generation | Generation в app, проверка через `python3 -c "import bip39; ..."` | Не выполнено, отложено | | BIP39 recovery deterministic | Recovery того же mnemonic на двух Pixel-устройствах | Не выполнено | | Coin spin только при VPN on | Visual inspection screenshots | ✓ | | Status text «Идёт чеканка» после первого heartbeat | Visual inspection | ✓ | | haproxy backend failover | Simulated DOWN backend | Не выполнено | | BIP39 wordlist integrity check fail-closed | Подмена `bip39-en.txt` в APK + проверка `bip39()` returns "" | Не выполнено | | Long-running VPN session stability | 24 часа без перезагрузки | Не выполнено | | Memory leak detection | `adb shell dumpsys meminfo` over time | Spot-checked OK (~30 MB native heap) | ## §3. Что **обязательно** покрыть тестами до mainnet ### Unit tests (JVM, без устройства) **Файл:** `app/src/test/java/quest/montana/app/BIP39DerivationTest.kt` ```kotlin class BIP39DerivationTest { @Test fun `mnemonic нулевой entropy дает known vector`() { // Test Vector 1 from BIP39 spec — все-нули entropy // assert mnemonic = "abandon abandon ... art" // assert seed (PBKDF2) byte-exact match с эталонным } @Test fun `mnemonic все единицы entropy дает known vector`() { ... } @Test fun `recovery валидирует checksum`() { // невалидный 24-word с поломанной checksum → throw } @Test fun `recovery отвергает неизвестные слова`() { // "foo bar baz ..." → throw } @Test fun `deriveAddr детерминирован`() { // тот же mnemonic 100 раз → тот же адрес } @Test fun `deriveAddr изменение domain_separator меняет result`() { // регрессионный тест на immutable префикс "montana-v1:" } } ``` ### Instrumented tests (на устройстве, реальный WebView) **Файл:** `app/src/androidTest/java/quest/montana/app/RecoveryE2ETest.kt` ```kotlin @RunWith(AndroidJUnit4::class) class RecoveryE2ETest { @Test fun `создание кошелька — адрес 40-hex`() { // start activity, нажать "Войти в Монтану" → "Создать ключ" // wait localStorage("m.addr") set // assert .length == 40, matches /^[0-9a-f]+$/ } @Test fun `recovery известного mnemonic дает фиксированный адрес`() { // localStorage.set("m.seed", "ranch basket ...") // reload page // assert localStorage("m.addr") == expected hex } @Test fun `cross-device recovery — два устройства тот же адрес`() { // Для 5-10 устройств с разными WebView версиями (manual setup) // Same input mnemonic → same output address // Если расхождение — bug WebCrypto implementation } } ``` ### Backend integration tests (Moscow) **Файл:** `/opt/montana-vpn-balance/tests/test_api.py` (pytest) ```python def test_heartbeat_from_non_montana_ip_rejects(): r = client.post("/api/vpn/heartbeat", json={"address": "0"*40}) assert r.json["reason"] == "not_via_montana_vpn" def test_heartbeat_throttling(): # send 2 heartbeats < MIN_INTERVAL apart, second must throttle ... def test_concurrent_heartbeats_no_lost_writes(): # 100 concurrent POST /api/vpn/heartbeat # all balance updates persisted (count == 100) ... def test_purge_removes_inactive_zero_balance(): # seed test data, run purge, verify only target records gone ... ``` ### haproxy failover tests **Файл:** `scripts/test-haproxy-failover.sh` ```bash # 1. Запустить с тремя UP backend # 2. systemctl stop xray-pinned@fra # 3. Через 10 секунд: verify backend "fra" DOWN в haproxy stats # 4. Verify sticked клиенты с fra переехали на другой backend # 5. systemctl start xray-pinned@fra # 6. Verify backend "fra" UP через ~10 секунд ``` ### Long-running stability tests **Сценарий:** 24-hour endurance run на Pixel: - VPN включён - Heartbeats каждые 5 секунд (≈17k heartbeats / сутки) - Memory / battery / CPU мониторинг - Verify: нет утечек, нет ANR, баланс корректно растёт **Tool:** `adb shell dumpsys meminfo` + `adb shell dumpsys batterystats` + logcat anomaly detection. ## §4. Coverage budget для Phase 2 | Категория | Tests планируется | Coverage % goal | |-----------|---------------------|------------------| | BIP39 derivation | 6 unit tests | 95% (`mnemonicToEntropy`, `entropyToMnemonic`, `deriveAddr`, `mnemonicToSeed`) | | Recovery E2E | 3 instrumented tests | 80% (включая ошибки) | | Backend Flask | 12 integration tests | 80% (heartbeat, purge, balance, throttling, race) | | haproxy failover | 2 scenarios scripts | manual | | Cross-device determinism | 1 manual matrix | по 5 устройствам | | Long-running stability | 1 endurance scenario | 24-hour, 7-day | **Cost estimate:** 1 неделя. ## §5. Test fixtures location (если создадим) ``` Android/MontanaApp/app/src/test/resources/ ├── bip39-test-vectors.json — стандартные BIP39 test vectors └── montana-derivation-vectors.json — наши специфичные (mnemonic → adr) Android/MontanaApp/app/src/androidTest/resources/ └── (то же, для instrumented) /opt/montana-vpn-balance/tests/fixtures/ ├── balances-snapshot.json — initial state for tests └── heartbeat-requests.json — replay scenarios ``` ## §6. CI integration (currently absent) **Текущий status:** билды и проверки выполняются автором manually. Нет GitHub Actions / GitLab CI / Bitrise. **Closure path:** ```yaml # .github/workflows/android-tests.yml on: [push, pull_request] jobs: test: runs-on: macos-14 steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '17' } - run: cd Montana/Android/MontanaApp && ./gradlew test - run: cd Montana/Android/MontanaApp && ./gradlew connectedAndroidTest # требует emulator - uses: actions/upload-artifact@v4 with: { name: test-results, path: '**/build/reports/tests/**' } ``` **Cost estimate:** 2 дня (включая Android emulator runner setup). ## §7. Аудитор-проверяемые сценарии (для внешнего аудитора) При получении этого пакета, рекомендуется аудитору **самостоятельно** выполнить: 1. **APK signature verify** — `apksigner verify --print-certs montana-v6.5.0.apk` → fingerprint match 2. **APK unpack + diff** — собрать APK с нуля по `08-Воспроизводимая-сборка.md` → сравнить с published 3. **BIP39 cross-tool verify** — взять mnemonic из приложения, проверить в `python-bip39`, `bitcoinjs/bip39` что entropy и seed совпадают 4. **VPN active probing** — попробовать active probe на `91.132.142.42:443` → verify SNI Echo returns `googletagmanager.com` content 5. **Sticky pin verify** — 20 запросов с разными UA через VPN → distribution exits должен показать stickiness 6. **balances.json atomic** — `wrk -t 4 -c 100 -d 30s 'POST /api/vpn/heartbeat'` → verify no lost writes 7. **Source code reading** — `MontanaVpnService.kt` 450 строк целиком, `app.html` JS секции (отбросив base64 images), `app.py` 280 строк ## §8. Summary | Зона | Статус | |------|--------| | Automated unit tests | **отсутствуют** (главный open finding) | | Automated instrumented tests | **отсутствуют** | | Manual empirical verification | basic выполнен (см. §2) | | Long-running stability | **не проверено** | | Cross-device recovery determinism | **не проверено** | | Backend integration tests | **отсутствуют** | | CI/CD | **отсутствует** | **Это самый большой open finding пакета.** Phase 2 закрытие — обязательное условие для production-mainnet.