Skip to main content

Testing Guide

This guide covers running tests, writing new tests, and testing strategies for FIDO Bridge.

Overview

FIDO Bridge has three test layers:

  1. Unit tests: Test individual functions and modules
  2. Integration tests: Test interactions between components
  3. 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:

  1. Install:

    cargo install cargo-tarpaulin
  2. Run:

    cargo tarpaulin --out Html --output-dir coverage
  3. 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

  1. Build all components:

    cargo build --release -p client -p server
    cd app && flutter build apk --debug
  2. Start server:

    ./target/release/server
  3. Start client:

    sudo ./target/release/client
  4. Install and open Android app

  5. Test pairing:

    • Press P on Linux client
    • Scan QR code with Android
    • Verify pairing succeeds
  6. Test WebAuthn authentication:

    • Visit https://webauthn.io in browser
    • Click "Register"
    • Tap YubiKey to Android phone when prompted
    • Verify registration succeeds
  7. 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

  1. Test Behavior, Not Implementation: Test what code does, not how
  2. One Assertion Per Test: Keep tests focused and easy to debug
  3. Descriptive Names: Test names should describe what's being tested
  4. AAA Pattern: Arrange, Act, Assert structure
  5. Isolated Tests: Tests should not depend on each other

Rust-Specific

  1. Use #[should_panic] for expected panics
  2. Test both success and error paths
  3. Use Result in tests for cleaner error handling
  4. Mock external dependencies (file system, network)

Flutter-Specific

  1. Use pumpWidget and pumpAndSettle for async UI updates
  2. Test widget trees, not pixels
  3. Use find matchers for widget testing
  4. 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