Testing Guide
This guide covers running tests, writing new tests, and testing strategies for FIDO Bridge.
Overview
FIDO Bridge has three test layers:
- Unit tests: Test individual functions and modules
- Integration tests: Test interactions between components
- End-to-end tests: Test complete workflows (manual for now)
Running Tests
Rust Tests
Run All Tests
# From project root
cargo test
# With output visible
cargo test -- --nocapture
# Run in release mode (faster, but slower to compile)
cargo test --release
Run Tests for Specific Crate
# Client tests only
cargo test -p client
# Server tests only
cargo test -p server
# Transport tests only
cargo test -p transport
Run Specific Test
# Run single test by name
cargo test test_spake2_pairing
# Run all tests containing "pairing" in name
cargo test pairing
# Run tests in specific file
cargo test --test integration_tests
Run Tests With Debug Logging
RUST_LOG=debug cargo test -- --nocapture
Flutter/Dart Tests
Run All Tests
cd app
flutter test
Run Specific Test File
flutter test test/services/nfc_service_test.dart
Run Tests With Coverage
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html # View coverage report
Kotlin Tests (Android)
cd app/android
./gradlew test # Run unit tests
./gradlew connectedAndroidTest # Run instrumented tests (requires device)
Test Organization
Rust Test Structure
crates/
├── client/
│ ├── src/
│ │ └── *.rs (unit tests inline with #[cfg(test)])
│ └── tests/
│ ├── integration_tests.rs
│ ├── uhid_fido_tests.rs
│ ├── message_handler_tests.rs
│ └── webauthn_handler_tests.rs
├── server/
│ └── tests/
│ └── server_tests.rs
└── transport/
└── src/
└── *.rs (unit tests inline)
Flutter Test Structure
app/
├── lib/
│ └── *.dart (implementation)
└── test/
├── services/
│ ├── nfc_service_test.dart
│ └── background_service_test.dart
├── providers/
│ └── pairing_provider_test.dart
└── widgets/
└── transaction_card_test.dart
Writing Tests
Rust Unit Tests
Location: Inline in same file as code being tested
Example:
// In crates/transport/src/secure_channel.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ephemeral_key_generation() {
let keypair = EphemeralKeyPair::generate();
assert_eq!(keypair.public_key.as_bytes().len(), 32);
}
#[test]
fn test_encryption_decryption_roundtrip() {
let mut sender = SecureChannel::new();
let mut receiver = SecureChannel::new();
// Key exchange
let sender_pub = sender.init_initiator();
let receiver_pub = receiver.init_responder(&sender_pub).unwrap();
sender.complete_exchange(&receiver_pub).unwrap();
// Encrypt and decrypt
let plaintext = b"Hello, FIDO Bridge!";
let ciphertext = sender.encrypt_message(plaintext).unwrap();
let decrypted = receiver.decrypt_message(&ciphertext).unwrap();
assert_eq!(plaintext, &decrypted[..]);
}
#[test]
#[should_panic(expected = "Shared secret not established")]
fn test_encrypt_without_key_exchange_fails() {
let channel = SecureChannel::new();
channel.encrypt_message(b"test").unwrap(); // Should panic
}
}
Rust Integration Tests
Location: crates/<crate>/tests/*.rs
Example:
// In crates/client/tests/integration_tests.rs
use client::message_handler::MessageHandler;
use transport::webauthn_messages::*;
#[tokio::test]
async fn test_getinfo_transaction_flow() {
// Setup
let mut handler = MessageHandler::new();
// Create GetInfo request
let request = GetInfoRequest {
request_id: "test-123".to_string(),
timeout_ms: 30000,
created_at: current_timestamp(),
};
// Process request
let transaction = handler.create_transaction(request).await.unwrap();
// Assert
assert_eq!(transaction.metadata.transaction_type, TransactionType::Enumeration);
assert!(!transaction.is_expired());
}
#[tokio::test]
async fn test_transaction_timeout() {
let transaction = Transaction::new(
TransactionType::Authentication,
1000, // 1 second timeout
"Test".to_string(),
);
// Wait for timeout
tokio::time::sleep(tokio::time::Duration::from_millis(1100)).await;
assert!(transaction.is_expired());
}
Async Tests
#[tokio::test] // Use tokio test runner for async
async fn test_async_function() {
let result = some_async_function().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_timeout() {
let result = tokio::time::timeout(
tokio::time::Duration::from_secs(1),
slow_async_function()
).await;
assert!(result.is_err()); // Should timeout
}
Flutter Widget Tests
Location: app/test/widgets/*.dart
Example:
// In app/test/widgets/transaction_card_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:fido_bridge/src/widgets/transaction_card.dart';
void main() {
testWidgets('TransactionCard displays transaction details', (WidgetTester tester) async {
// Build widget
await tester.pumpWidget(
MaterialApp(
home: TransactionCard(
transaction: Transaction(
id: 'txn-123',
type: TransactionType.authentication,
status: TransactionStatus.pending,
),
),
),
);
// Verify
expect(find.text('Authentication'), findsOneWidget);
expect(find.text('Pending'), findsOneWidget);
});
testWidgets('TransactionCard tap triggers callback', (WidgetTester tester) async {
bool tapped = false;
await tester.pumpWidget(
MaterialApp(
home: TransactionCard(
transaction: testTransaction,
onTap: () => tapped = true,
),
),
);
await tester.tap(find.byType(TransactionCard));
expect(tapped, isTrue);
});
}
Flutter Service Tests (with Mocks)
Example:
// In app/test/services/nfc_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:fido_bridge/src/services/nfc_service.dart';
class MockNfcHandler extends Mock implements NfcHandler {}
void main() {
late NfcService nfcService;
late MockNfcHandler mockHandler;
setUp(() {
mockHandler = MockNfcHandler();
nfcService = NfcService(nfcHandler: mockHandler);
});
test('sendApduCommand returns response', () async {
// Arrange
final command = [0x00, 0xA4, 0x04, 0x00];
final expectedResponse = [0x90, 0x00];
when(mockHandler.transceive(command)).thenAnswer((_) async => expectedResponse);
// Act
final response = await nfcService.sendApduCommand(command);
// Assert
expect(response, equals(expectedResponse));
verify(mockHandler.transceive(command)).called(1);
});
}
Test Coverage
Rust Coverage
Using cargo-tarpaulin:
-
Install:
cargo install cargo-tarpaulin -
Run:
cargo tarpaulin --out Html --output-dir coverage -
View: Open
coverage/index.html
Flutter Coverage
cd app
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Coverage Goals
- Unit tests: 80%+ coverage
- Integration tests: Critical paths covered
- Crypto code: 100% coverage (security-critical)
Mocking and Test Doubles
Rust Mocking (with mockall)
Add dependency:
[dev-dependencies]
mockall = "0.11"
Example:
use mockall::predicate::*;
use mockall::mock;
// Define mock
mock! {
pub ServerClient {}
impl ServerClient {
pub async fn send_message(&self, msg: Vec<u8>) -> Result<(), Error>;
pub async fn poll_messages(&self) -> Result<Vec<Message>, Error>;
}
}
#[tokio::test]
async fn test_with_mock_server() {
let mut mock_server = MockServerClient::new();
// Setup expectations
mock_server
.expect_send_message()
.with(eq(vec![1, 2, 3]))
.times(1)
.returning(|_| Ok(()));
// Use mock in test
let result = mock_server.send_message(vec![1, 2, 3]).await;
assert!(result.is_ok());
}
Flutter Mocking (with mockito)
Add dependency:
dev_dependencies:
mockito: ^5.4.0
build_runner: ^2.4.0
Generate mocks:
flutter pub run build_runner build
Testing Strategies
Testing CTAP2 Protocol
Strategy: Use real CTAP2 test vectors from FIDO Alliance
#[test]
fn test_getinfo_response_parsing() {
// Real GetInfo response from YubiKey 5
let response_bytes = hex::decode(
"00a60158....90000"
).unwrap();
let info = parse_getinfo_response(&response_bytes).unwrap();
assert!(info.versions.contains(&"FIDO_2_0".to_string()));
assert!(info.options.get("rk").unwrap());
}
Testing Encryption
Strategy: Test roundtrip encryption/decryption, test with known vectors
#[test]
fn test_aes_gcm_known_vector() {
// NIST test vector
let key = hex::decode("...").unwrap();
let nonce = hex::decode("...").unwrap();
let plaintext = hex::decode("...").unwrap();
let expected_ciphertext = hex::decode("...").unwrap();
let ciphertext = encrypt_aes_gcm(&key, &nonce, &plaintext).unwrap();
assert_eq!(ciphertext, expected_ciphertext);
}
Testing NFC Communication
Strategy: Use mocked NFC handler, test APDU parsing
// In NfcHandlerTest.kt
@Test
fun testApduCommandParsing() {
val apdu = ApduCommand.parse(byteArrayOf(0x00, 0xA4, 0x04, 0x00))
assertEquals(0x00, apdu.cla)
assertEquals(0xA4, apdu.ins)
}
Testing Transaction Lifecycle
Strategy: Test state transitions, test timeout enforcement
#[tokio::test]
async fn test_transaction_state_machine() {
let mut txn = Transaction::new(
TransactionType::Authentication,
240000,
"Test".to_string(),
);
// Initial state
assert!(matches!(txn.state, TransactionState::AwaitingGetInfo { .. }));
// Transition to MakeCredential
txn.transition_to_make_credential("req-1".to_string(), "example.com".to_string());
assert!(matches!(txn.state, TransactionState::AwaitingMakeCredential { .. }));
// Complete
txn.complete("MakeCredentialResponse".to_string());
assert!(matches!(txn.state, TransactionState::Completed { .. }));
assert!(txn.is_terminal());
}
Integration Testing
Testing Client-Server Communication
Setup: Start embedded server, create client, test message flow
#[tokio::test]
async fn test_client_server_roundtrip() {
// Start embedded server
let server = Server::new("127.0.0.1:3001").start().await.unwrap();
// Create client
let client = Client::new("http://127.0.0.1:3001").await;
// Send message
let msg = WebAuthnMessage::GetInfoRequest(GetInfoRequest { /* ... */ });
client.send_message(&msg).await.unwrap();
// Poll for response
let messages = client.poll_messages().await.unwrap();
assert_eq!(messages.len(), 0); // No response yet (no Android device)
// Cleanup
server.stop().await;
}
Testing Pairing Flow
Setup: Simulate initiator and responder
#[tokio::test]
async fn test_full_pairing_flow() {
let pin = "123456".to_string();
// Initiator
let mut initiator = PairingSession::new_initiator("linux-1".to_string(), pin.clone()).unwrap();
let init_msg = initiator.create_initiator_message().unwrap();
// Responder
let mut responder = PairingSession::new_responder("android-1".to_string(), pin.clone()).unwrap();
let resp_msg = responder.process_initiator_message(init_msg, pin).unwrap();
// Complete
let shared_secret_init = initiator.complete_as_initiator(resp_msg).unwrap();
let shared_secret_resp = responder.get_shared_secret().unwrap();
assert_eq!(shared_secret_init, shared_secret_resp);
}
End-to-End Testing
Manual E2E Test Procedure
-
Build all components:
cargo build --release -p client -p server
cd app && flutter build apk --debug -
Start server:
./target/release/server -
Start client:
sudo ./target/release/client -
Install and open Android app
-
Test pairing:
- Press
Pon Linux client - Scan QR code with Android
- Verify pairing succeeds
- Press
-
Test WebAuthn authentication:
- Visit https://webauthn.io in browser
- Click "Register"
- Tap YubiKey to Android phone when prompted
- Verify registration succeeds
-
Test assertion:
- Click "Authenticate" on webauthn.io
- Tap YubiKey to phone
- Verify authentication succeeds
Automated E2E Tests (Future Work)
Proposed approach:
- Use Selenium/WebDriver for browser automation
- Mock NFC responses for reproducible tests
- Run in CI with headless Chrome
Performance Testing
Benchmarking Crypto Operations
#[bench]
fn bench_aes_gcm_encryption(b: &mut Bencher) {
let key = generate_random_key();
let plaintext = vec![0u8; 1024];
b.iter(|| {
encrypt_aes_gcm(&key, &plaintext)
});
}
Run: cargo bench
Load Testing Server
Using Apache Bench:
# Test server throughput
ab -n 1000 -c 10 http://localhost:3000/api/messages/poll/test-token
# Results show:
# - Requests per second
# - Average latency
# - 95th percentile latency
Continuous Integration
GitHub Actions Workflows
Rust CI (.github/workflows/rust.yml):
name: Rust CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- run: cargo test --all-features
- run: cargo clippy -- -D warnings
Flutter CI (.github/workflows/flutter.yml):
name: Flutter CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.0'
- run: cd app && flutter test
Test Best Practices
General Principles
- Test Behavior, Not Implementation: Test what code does, not how
- One Assertion Per Test: Keep tests focused and easy to debug
- Descriptive Names: Test names should describe what's being tested
- AAA Pattern: Arrange, Act, Assert structure
- Isolated Tests: Tests should not depend on each other
Rust-Specific
- Use
#[should_panic]for expected panics - Test both success and error paths
- Use
Resultin tests for cleaner error handling - Mock external dependencies (file system, network)
Flutter-Specific
- Use
pumpWidgetandpumpAndSettlefor async UI updates - Test widget trees, not pixels
- Use
findmatchers for widget testing - Mock platform channels for native code
Debugging Failed Tests
Rust
# Run single test with output
cargo test test_name -- --nocapture
# Run with backtrace
RUST_BACKTRACE=1 cargo test test_name
# Run with debug logging
RUST_LOG=debug cargo test test_name -- --nocapture
Flutter
# Run with verbose output
flutter test --verbose
# Run specific test
flutter test test/services/nfc_service_test.dart
# Debug in IDE
# Set breakpoint and run test in debug mode
Next Steps
- Contributing - Contribution guidelines
- Building - Build from source
- Architecture - Understand system design