montana/Android/Внешний-аудит/10-Покрытие-тестами.md

210 lines
9.7 KiB
Markdown
Raw Normal View History

2026-05-18 22:11:45 +03:00
# 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.