montana/Android/Внешний-аудит/10-Покрытие-тестами.md
2026-05-18 22:11:45 +03:00

9.7 KiB
Raw Blame History

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

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

@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)

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

# 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:

# .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 verifyapksigner 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 atomicwrk -t 4 -c 100 -d 30s 'POST /api/vpn/heartbeat' → verify no lost writes
  7. Source code readingMontanaVpnService.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.