332 lines
9.9 KiB
Rust
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);
|
|
}
|