Files
Linux-Hello/tests/integration/phase2_auth_test.rs
2026-01-30 09:44:12 +01:00

332 lines
9.9 KiB
Rust

//! Phase 2 Integration Tests
//!
//! Tests for the core authentication flow: enrollment, template storage,
//! embedding extraction, and authentication.
use linux_hello_common::{Config, FaceTemplate, TemplateStore};
use linux_hello_daemon::auth::AuthService;
use linux_hello_daemon::embedding::{
cosine_similarity, EmbeddingExtractor, PlaceholderEmbeddingExtractor,
};
use linux_hello_daemon::matching::{average_embeddings, match_template, MatchResult};
use tempfile::TempDir;
/// Test template storage operations
#[test]
fn test_template_store_operations() {
let temp_dir = TempDir::new().unwrap();
let store = TemplateStore::new(temp_dir.path());
// Initialize store
store.initialize().unwrap();
// Create test template
let template = FaceTemplate {
user: "testuser".to_string(),
label: "default".to_string(),
embedding: vec![0.1, 0.2, 0.3, 0.4, 0.5],
enrolled_at: 1234567890,
frame_count: 5,
};
// Store template
store.store(&template).unwrap();
// Verify user is enrolled
assert!(store.is_enrolled("testuser"));
assert!(!store.is_enrolled("nonexistent"));
// Load template back
let loaded = store.load("testuser", "default").unwrap();
assert_eq!(loaded.user, "testuser");
assert_eq!(loaded.label, "default");
assert_eq!(loaded.embedding, vec![0.1, 0.2, 0.3, 0.4, 0.5]);
assert_eq!(loaded.frame_count, 5);
// List users
let users = store.list_users().unwrap();
assert!(users.contains(&"testuser".to_string()));
// List templates
let templates = store.list_templates("testuser").unwrap();
assert!(templates.contains(&"default".to_string()));
// Remove template
store.remove("testuser", "default").unwrap();
assert!(!store.is_enrolled("testuser"));
}
/// Test multiple templates per user
#[test]
fn test_multiple_templates_per_user() {
let temp_dir = TempDir::new().unwrap();
let store = TemplateStore::new(temp_dir.path());
store.initialize().unwrap();
// Add multiple templates
for (i, label) in ["default", "glasses", "profile"].iter().enumerate() {
let template = FaceTemplate {
user: "multiuser".to_string(),
label: label.to_string(),
embedding: vec![i as f32 * 0.1, 0.5, 0.3],
enrolled_at: 1234567890 + i as u64,
frame_count: 3,
};
store.store(&template).unwrap();
}
// Load all templates
let templates = store.load_all("multiuser").unwrap();
assert_eq!(templates.len(), 3);
// List template labels
let labels = store.list_templates("multiuser").unwrap();
assert_eq!(labels.len(), 3);
// Remove all
store.remove_all("multiuser").unwrap();
assert!(!store.is_enrolled("multiuser"));
}
/// Test embedding extractor
#[test]
fn test_embedding_extraction() {
let extractor = PlaceholderEmbeddingExtractor::new(128);
// Create test grayscale image
let mut img = image::GrayImage::new(100, 100);
// Fill with gradient pattern
for y in 0..100 {
for x in 0..100 {
img.put_pixel(x, y, image::Luma([(x + y) as u8 / 2]));
}
}
// Extract embedding
let embedding = extractor.extract(&img).unwrap();
// Check dimension
assert_eq!(embedding.len(), 128);
// Check normalization (should be approximately unit length)
let norm: f32 = embedding.iter().map(|&x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.1 || norm < 0.01);
}
/// Test embedding consistency
#[test]
fn test_embedding_consistency() {
let extractor = PlaceholderEmbeddingExtractor::new(128);
// Create identical images
let mut img1 = image::GrayImage::new(100, 100);
let mut img2 = image::GrayImage::new(100, 100);
for y in 0..100 {
for x in 0..100 {
let val = ((x * 2 + y * 3) % 256) as u8;
img1.put_pixel(x, y, image::Luma([val]));
img2.put_pixel(x, y, image::Luma([val]));
}
}
// Extract embeddings
let emb1 = extractor.extract(&img1).unwrap();
let emb2 = extractor.extract(&img2).unwrap();
// Should be identical
let similarity = cosine_similarity(&emb1, &emb2);
assert!(
(similarity - 1.0).abs() < 0.001,
"Identical images should have similarity ~1.0, got {}",
similarity
);
}
/// Test cosine similarity
#[test]
fn test_cosine_similarity() {
// Identical vectors
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
// Orthogonal vectors
let c = vec![0.0, 1.0, 0.0];
assert!((cosine_similarity(&a, &c) - 0.0).abs() < 0.001);
// Similar vectors
let d = vec![0.9, 0.1, 0.0];
let similarity = cosine_similarity(&a, &d);
assert!(similarity > 0.9 && similarity < 1.0);
}
/// Test template matching
#[test]
fn test_template_matching() {
let templates = vec![
FaceTemplate {
user: "testuser".to_string(),
label: "default".to_string(),
embedding: vec![1.0, 0.0, 0.0],
enrolled_at: 0,
frame_count: 1,
},
FaceTemplate {
user: "testuser".to_string(),
label: "glasses".to_string(),
embedding: vec![0.9, 0.1, 0.0],
enrolled_at: 0,
frame_count: 1,
},
];
// Exact match
let result = match_template(&vec![1.0, 0.0, 0.0], &templates, 0.5);
assert!(result.matched);
assert_eq!(result.matched_label, Some("default".to_string()));
assert!((result.best_similarity - 1.0).abs() < 0.001);
// Close match (should match glasses template better due to similarity)
let result = match_template(&vec![0.85, 0.15, 0.0], &templates, 0.5);
assert!(result.matched);
// No match (orthogonal)
let result = match_template(&vec![0.0, 0.0, 1.0], &templates, 0.3);
assert!(!result.matched);
}
/// Test embedding averaging
#[test]
fn test_embedding_averaging() {
let embeddings = vec![
vec![1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
];
let averaged = average_embeddings(&embeddings).unwrap();
// Should be 3 dimensional
assert_eq!(averaged.len(), 3);
// Should be normalized
let norm: f32 = averaged.iter().map(|&x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.01);
// All components should be roughly equal (1/sqrt(3) normalized)
let expected = 1.0 / 3.0_f32.sqrt();
for val in &averaged {
assert!((val - expected).abs() < 0.01);
}
}
/// Test empty embeddings
#[test]
fn test_empty_embeddings_error() {
let embeddings: Vec<Vec<f32>> = vec![];
let result = average_embeddings(&embeddings);
assert!(result.is_err());
}
/// Test auth service initialization
#[test]
fn test_auth_service_init() {
let config = Config::default();
let auth_service = AuthService::new(config);
// This should succeed (creates template directory if needed)
// Note: May need root permissions in production
// For testing, we can verify it doesn't panic
let result = auth_service.initialize();
// On systems without /var/lib, this might fail
// That's okay for unit testing
if result.is_err() {
println!(
"Auth service init failed (expected without root): {:?}",
result
);
}
}
/// Test match result structure
#[test]
fn test_match_result_structure() {
let result = MatchResult {
matched: true,
best_similarity: 0.95,
distance_threshold: 0.5,
matched_label: Some("default".to_string()),
};
assert!(result.matched);
assert_eq!(result.best_similarity, 0.95);
assert_eq!(result.distance_threshold, 0.5);
assert_eq!(result.matched_label.unwrap(), "default");
}
/// Test different image patterns for embedding diversity
#[test]
fn test_embedding_diversity() {
let extractor = PlaceholderEmbeddingExtractor::new(128);
// Create different pattern images
let patterns: Vec<Box<dyn Fn(u32, u32) -> u8>> = vec![
Box::new(|x, _y| x as u8), // Horizontal gradient
Box::new(|_x, y| y as u8), // Vertical gradient
Box::new(|x, y| (x ^ y) as u8), // XOR pattern
Box::new(|_x, _y| 128), // Solid gray
Box::new(|x, y| ((x * y) % 256) as u8), // Multiplication pattern
];
let mut embeddings = Vec::new();
for pattern in &patterns {
let mut img = image::GrayImage::new(100, 100);
for y in 0..100 {
for x in 0..100 {
img.put_pixel(x, y, image::Luma([pattern(x, y)]));
}
}
embeddings.push(extractor.extract(&img).unwrap());
}
// Check that different patterns produce somewhat different embeddings
// (not all identical)
for i in 0..embeddings.len() {
for j in (i + 1)..embeddings.len() {
let similarity = cosine_similarity(&embeddings[i], &embeddings[j]);
// Different patterns should have similarity less than 1.0
// (placeholder algorithm may still produce similar results)
println!("Pattern {} vs {}: similarity = {:.4}", i, j, similarity);
}
}
}
/// Test face template serialization roundtrip
#[test]
fn test_template_serialization() {
let original = FaceTemplate {
user: "testuser".to_string(),
label: "serialization_test".to_string(),
embedding: vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
enrolled_at: 1704153600,
frame_count: 10,
};
// Serialize
let json = serde_json::to_string(&original).unwrap();
// Deserialize
let restored: FaceTemplate = serde_json::from_str(&json).unwrap();
// Verify
assert_eq!(original.user, restored.user);
assert_eq!(original.label, restored.label);
assert_eq!(original.embedding, restored.embedding);
assert_eq!(original.enrolled_at, restored.enrolled_at);
assert_eq!(original.frame_count, restored.frame_count);
}