210 lines
9.7 KiB
Markdown
210 lines
9.7 KiB
Markdown
|
|
# 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.
|