9.7 KiB
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. Аудитор-проверяемые сценарии (для внешнего аудитора)
При получении этого пакета, рекомендуется аудитору самостоятельно выполнить:
- APK signature verify —
apksigner verify --print-certs montana-v6.5.0.apk→ fingerprint match - APK unpack + diff — собрать APK с нуля по
08-Воспроизводимая-сборка.md→ сравнить с published - BIP39 cross-tool verify — взять mnemonic из приложения, проверить в
python-bip39,bitcoinjs/bip39что entropy и seed совпадают - VPN active probing — попробовать active probe на
91.132.142.42:443→ verify SNI Echo returnsgoogletagmanager.comcontent - Sticky pin verify — 20 запросов с разными UA через VPN → distribution exits должен показать stickiness
- balances.json atomic —
wrk -t 4 -c 100 -d 30s 'POST /api/vpn/heartbeat'→ verify no lost writes - Source code reading —
MontanaVpnService.kt450 строк целиком,app.htmlJS секции (отбросив base64 images),app.py280 строк
§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.