Skip to main content

Automation Plan — SAPOT Mobile App

Generated: 2026-06-20


Framework Selection

LayerFrameworkRationale
Unit tests (TypeScript)Jest + jest-expoAlready configured; global mocks in jest-setup.js; path alias @/; supports real tweetnacl without mocking
Component tests (React)RNTL (React Native Testing Library)Already in use; render, fireEvent, renderHook patterns established
E2E mobile testsMaestroDeclarative YAML flows; excellent React Native support; MCP integration available; runs on Android emulator + device
Backend API testsPytest + FastAPI TestClientNative to FastAPI; in-process (no network); fixture-based DB seeding
Backend WebSocket testsPytest + pytest-asyncio + httpx WS clientAsync WS support; pairs with TestClient

Phase 1 — Fix Critical Security Bugs (Week 1)

These are active production bugs. Fix before writing any tests.

TaskAction
Remove testing endpoints from productionDelete testing.py from router registration in server/app/main.py
Add auth to GPS monitor WSAdd JWT validation to /gps/ws/monitor/rescuers/{id} in server/app/api/gps.py
Rate-limit /auth/existsAdd @limiter.limit("30/minute") in server/app/api/auth.py
Fix captive portal NameErrorRename dbsession in disconnect_guest_session in server/app/api/captive_portal.py
Confirm with Pytestserver/tests/test_security.py covering REG-070–REG-073

Phase 2 — Backend API Tests (Weeks 2–3)

The backend is 100% untested. Start here: stateless, easy to isolate, highest security risk.

Target: 80% endpoint coverage with Pytest.

Conftest setup

# server/tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

@pytest.fixture
def client():
with TestClient(app) as c:
yield c

@pytest.fixture
def auth_headers(client):
client.post("/auth/", json={
"username": "testuser", "firstName": "T", "lastName": "U",
"password": "password123", "terms_accepted": True
})
r = client.post("/auth/token", data={"username": "testuser", "password": "password123"})
return {"Authorization": f"Bearer {r.json()['access_token']}"}

@pytest.fixture
def admin_headers(client):
# Seed admin role directly via DB (never via /testing/ endpoint)
...

Test file execution order

  1. tests/test_security.py — critical bugs (API-220–226)
  2. tests/test_auth.py — registration, login, refresh, logout (API-010–036)
  3. tests/test_sync.py — WatermelonDB pull/push protocol (API-140–149)
  4. tests/test_keys.py — ECDH keys + wrapped keys (API-100–117)
  5. tests/test_ws_signaling.py — WebSocket signaling (API-160–175)
  6. tests/test_gps.py — GPS streaming + REST (API-150–158)
  7. tests/test_admin.py — admin CRUD (API-180–194)
  8. tests/test_forgot_password.py — all reset flows (API-050–075)
  9. tests/test_gsm.py — GSM/SMS using mock endpoints (API-200–210)
  10. tests/test_captive_portal.py — captive portal (API-250–255)

Phase 3 — Unit Tests for Untested Services (Weeks 3–5)

Mock pattern to follow (from existing tests)

// From features/shared/connection/services/__tests__/connection-service.test.ts
import { createConnectionServiceDependencyMocks } from '@/test/mocks/service.mock-builders'
import { ConnectionService } from '../connection-service'

describe('ConnectionService', () => {
let mocks: ReturnType<typeof createConnectionServiceDependencyMocks>
let service: ConnectionService

beforeEach(() => {
mocks = createConnectionServiceDependencyMocks()
service = new ConnectionService(mocks)
})

it('...', () => { ... })
})

Encryption services (CRITICAL — write first)

ServiceTarget Test File
LocalEncryptionServicefeatures/shared/crypto/__tests__/local-encryption-service.test.ts
KeyDerivationfeatures/shared/services/__tests__/key-derivation.test.ts
TcpEncryptionServicefeatures/shared/services/__tests__/tcp-encryption.test.ts
WsEncryptionServicefeatures/shared/services/__tests__/ws-encryption.test.ts
PeerKeyServicefeatures/shared/crypto/__tests__/peer-key-service.test.ts
KeyRecoveryServicefeatures/shared/services/__tests__/key-recovery-service.test.ts

Other high-risk services

ServiceTarget Test File
WsSignalingAdapterfeatures/shared/adapters/__tests__/ws-signaling-adapter.test.ts
AppModeStorefeatures/shared/stores/__tests__/app-mode-store.test.ts
SyncServicefeatures/sync/services/__tests__/sync-service.test.ts
GpsLocationServicefeatures/gps/services/__tests__/gps-location-service.test.ts
GuestMigrationServicefeatures/auth/services/__tests__/guest-migration-service.test.ts
SignalingServicefeatures/shared/connection/services/__tests__/signaling-service.test.ts
Android foreground-service lifecyclefeatures/shared/hooks/__tests__/use-foreground-service.test.ts
use-lockout-timerfeatures/auth/hooks/__tests__/use-lockout-timer.test.ts

Pure functions — highest ROI, lowest effort (< 30 min each)

FunctionTarget Test File
haversine(lat1, lng1, lat2, lng2)features/gps/utils/__tests__/haversine.test.ts
formatRelativeTime(ts)features/gps/utils/__tests__/format-relative-time.test.ts
formatAnnouncementDate(ts)features/announcements/utils/__tests__/format-announcement-date.test.ts
directConversationId(a, b)features/chat/utils/__tests__/direct-conversation-id.test.ts
smsConversationId(phone)features/chat/utils/__tests__/sms-conversation-id.test.ts
extractResetToken(url)features/auth/utils/__tests__/extract-reset-token.test.ts
generateGuestUsername()features/auth/utils/__tests__/guest-username-generator.test.ts
formatDate(ts)features/shared/utils/__tests__/format-date.test.ts

Phase 4 — Maestro E2E Flows (Weeks 5–7)

Project structure

mobile-app/sapot-mobile-app/.maestro/
auth/
server-login.yaml
guest-login.yaml
register.yaml
logout.yaml
guest-migration.yaml
chat/
send-message.yaml
call/
accept-call.yaml
reject-call.yaml
end-call.yaml
notifications/
background-call.yaml
cold-start-call.yaml
regression/
smoke-test.yaml

Canonical flow example

# .maestro/auth/server-login.yaml
appId: com.sapot.mobile.dev
---
- launchApp
- tapOn: "Server"
- tapOn: "Proceed"
- assertVisible: "Login"
- inputText:
id: "username-input"
text: "${USERNAME}"
- inputText:
id: "password-input"
text: "${PASSWORD}"
- tapOn: "Login"
- assertVisible: "Chats"

E2E flow priority order

PriorityFlowREG IDs
P0Server loginREG-001
P0Guest login (tab visibility)REG-002, REG-012, REG-013, REG-014
P0LogoutREG-004
P0Accept incoming callREG-030
P0Reject incoming callREG-031
P0End callREG-035
P0Background call notificationREG-052
P0Cold-start from notificationREG-053
P0Guest migrationREG-082
P1Send chat messageREG-020 (integration)
P1QR scan → Chat RoomREG-101
P1Map tab for rescuerREG-108
P1Safe-area on all screensREG-111
P2Server Host Override absent in prodREG-207

Phase 5 — RNTL Component Tests (Weeks 7–8)

Target screens with business-rule rendering. Follow this pattern:

import { render } from '@testing-library/react-native'
import { createUserContainerWrapper } from '@/test/mocks/auth-container-context.mock'

it('hides Public Chat tab for guests', () => {
const { queryByText } = render(
<TabLayout />,
{ wrapper: createUserContainerWrapper({ isGuest: true }) }
)
expect(queryByText('Public Chat')).toBeNull()
})

Target components

ComponentKey Scenarios
app/(drawer)/(tabs)/_layout.tsxTab visibility per user type
app/auth/login/server-login.tsxLockoutBanner, BannedBanner, AttemptsWarning
app/auth/login/lan-login.tsxField validation, no server call
app/(drawer)/(tabs)/call/incoming.tsxCaller info, accept/reject buttons
features/chat/components/chat-room.tsxConnection state indicators
features/shared/components/server-status-banner.tsxShown/hidden per transport mode
features/shared/components/offline-expired-banner.tsxShown when session offline+expired

CI Integration

# .github/workflows/test.yml
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}'
- run: npm run typecheck
- run: npm run lint

api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r server/requirements.txt
- run: pytest server/tests/ -v --tb=short

e2e-smoke:
runs-on: ubuntu-latest
steps:
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
script: maestro test .maestro/regression/smoke-test.yaml

Coverage Targets

LayerCurrentAfter Phase 3After Phase 5
Business logic (Jest)~18%65%80%
UI components (RNTL)~5%30%60%
Backend API (Pytest)0%75%85%
E2E flows (Maestro)010 flows15 flows

Mock Strategy for New Tests

ScenarioPatternReference
Service with DIcreateXxxDependencyMocks() from test/mocks/service.mock-builders.tscall-service.test.ts
WatermelonDB repocreateCollectionMock() + createWatermelonDbMock()message-repository.test.ts
Hook with contextcreateUserContainerWrapper()use-auth-container.test.tsx
NaCl cryptoUse REAL nacl.randomBytes() / nacl.box.keyPair() — do NOT mockconversation-key-store.test.ts
Axios API callscreateMockAxiosInstance()client.test.ts
expo-secure-storeAdd to jest-setup.js: jest.mock('expo-secure-store', () => ({getItemAsync: jest.fn(), setItemAsync: jest.fn(), deleteItemAsync: jest.fn()}))New
expo-locationAdd to jest-setup.js: jest.mock('expo-location', () => ({requestForegroundPermissionsAsync: jest.fn(), watchPositionAsync: jest.fn()}))New
Time-dependent tests (cooldowns)Use jest.useFakeTimers() + jest.setSystemTime()New

What NOT to Automate

ItemReason
Theme visual appearanceManual QA; color accuracy not verifiable headless
OTA update deliveryEAS platform; outside app code
Bluetooth audio routingDevice-specific; no reliable emulator support
Real SMS deliveryUse /gsm/mock/* endpoints in tests
Map tile renderingMapLibre visual output; manual QA only
30-day recovery key cooldownWall-clock time; mock with jest.setSystemTime()
90-day security question cooldownSame — mock time
Android notification exact appearanceOS-rendered; verify shown, not appearance

Estimated Effort

PhaseDurationOutput
1 — Security fixes1 week4 bug fixes + 5 Pytest tests
2 — Backend API2 weeks~100 Pytest tests
3 — Unit tests3 weeks~80 Jest tests
4 — E2E Maestro2 weeks15 Maestro flows
5 — RNTL components1 week~30 RNTL tests
Total9 weeks~230 tests; 80%+ coverage