mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-06-01 06:10:31 +00:00
Rename dna-analyzer-example to rvdna across all source files, tests, and benchmarks. Add crates.io metadata (repository, docs, keywords). Publish rvdna v0.1.0 to crates.io and @ruvector/rvdna v0.1.0 to npm with NAPI-RS platform loader, JS fallbacks, and TypeScript definitions. Also publishes workspace deps at v2.0.2: ruvector-math, ruvector-core, ruvector-filter, ruvector-collections, ruvector-graph, ruvector-gnn. Co-Authored-By: claude-flow <ruv@ruv.net>
157 lines
5.9 KiB
Rust
157 lines
5.9 KiB
Rust
//! Security validation tests for DNA analyzer - NO MOCKS, real computation only
|
|
use ::rvdna::error::DnaError;
|
|
use ::rvdna::types::*;
|
|
use ::rvdna::VectorEntry;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
|
|
#[test]
|
|
fn test_buffer_overflow_protection() {
|
|
// 10M+ bases shouldn't cause OOM/crash
|
|
let large_size = 10_000_000;
|
|
let bases: Vec<Nucleotide> = (0..large_size)
|
|
.map(|i| match i % 4 {
|
|
0 => Nucleotide::A, 1 => Nucleotide::C, 2 => Nucleotide::G, _ => Nucleotide::T,
|
|
}).collect();
|
|
let seq = DnaSequence::new(bases);
|
|
assert_eq!(seq.len(), large_size);
|
|
let rc = seq.reverse_complement();
|
|
assert_eq!(rc.len(), large_size);
|
|
assert!(seq.to_kmer_vector(11, 512).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_base_handling() {
|
|
// Non-ACGTN characters rejected gracefully
|
|
for input in ["ACGTX", "ACGT123", "ACGT!@#"] {
|
|
let result = DnaSequence::from_str(input);
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DnaError::InvalidSequence(_)));
|
|
}
|
|
assert!(DnaSequence::from_str("ACGTN").is_ok());
|
|
assert!(DnaSequence::from_str("acgtn").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_unicode_injection() {
|
|
// Unicode/malicious IDs don't break indexing
|
|
let seq = DnaSequence::from_str("ACGTACGT").unwrap();
|
|
let vector = seq.to_kmer_vector(3, 128).unwrap();
|
|
let temp_dir = std::env::temp_dir().join(format!("dna_test_{}", std::process::id()));
|
|
let _ = std::fs::create_dir_all(&temp_dir);
|
|
let index = KmerIndex::new(3, 128, temp_dir.join("unicode").to_str().unwrap()).unwrap();
|
|
|
|
for id in ["seq_cafe_dna", "patient123", "seq_hidden"] {
|
|
let entry = VectorEntry { id: Some(id.to_string()), vector: vector.clone(), metadata: None };
|
|
assert!(index.db().insert(entry).is_ok());
|
|
}
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[test]
|
|
fn test_path_traversal_prevention() {
|
|
// Verify KmerIndex handles unusual paths without panicking
|
|
// The key security property: operations complete or fail gracefully
|
|
let temp_dir = std::env::temp_dir().join(format!("dna_path_{}", std::process::id()));
|
|
let _ = std::fs::create_dir_all(&temp_dir);
|
|
|
|
for path in ["../../../tmp/evil", "../../etc/passwd"] {
|
|
let full_path = temp_dir.join(path);
|
|
// KmerIndex creation with traversal paths should either succeed
|
|
// (contained to actual resolved path) or fail gracefully - never panic
|
|
let result = std::panic::catch_unwind(|| {
|
|
KmerIndex::new(3, 128, full_path.to_str().unwrap())
|
|
});
|
|
assert!(result.is_ok(), "Path traversal should not cause panic");
|
|
}
|
|
|
|
// Clean up any created dirs
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
let _ = std::fs::remove_dir_all(std::env::temp_dir().join("evil"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_integer_overflow_kmer() {
|
|
// k=64 would overflow, k=0 invalid
|
|
let seq = DnaSequence::from_str("ACGTACGTACGTACGT").unwrap();
|
|
assert!(matches!(seq.to_kmer_vector(64, 512).unwrap_err(), DnaError::InvalidKmerSize(64)));
|
|
assert!(seq.to_kmer_vector(0, 512).is_err());
|
|
assert!(seq.to_kmer_vector(11, 512).is_ok());
|
|
assert!(seq.to_kmer_vector(15, 512).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_input_safety() {
|
|
// Empty inputs handled safely
|
|
assert!(matches!(DnaSequence::from_str("").unwrap_err(), DnaError::EmptySequence));
|
|
let empty = DnaSequence::new(vec![]);
|
|
assert!(empty.is_empty() && empty.len() == 0);
|
|
assert!(empty.complement().is_empty());
|
|
assert!(empty.reverse_complement().is_empty());
|
|
assert_eq!(empty.to_string(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_null_byte_handling() {
|
|
// Null bytes rejected
|
|
assert!(DnaSequence::from_str("ACGT\0").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_access_safety() {
|
|
// 10 threads accessing VectorDB concurrently
|
|
let temp_dir = std::env::temp_dir().join(format!("dna_conc_{}", std::process::id()));
|
|
let _ = std::fs::create_dir_all(&temp_dir);
|
|
let index = Arc::new(Mutex::new(KmerIndex::new(3, 128, temp_dir.join("idx").to_str().unwrap()).unwrap()));
|
|
|
|
let handles: Vec<_> = (0..10).map(|i| {
|
|
let idx_clone = Arc::clone(&index);
|
|
thread::spawn(move || {
|
|
let seq = DnaSequence::from_str("ACGTACGTACGT").unwrap();
|
|
let entry = VectorEntry { id: Some(format!("seq_{}", i)), vector: seq.to_kmer_vector(3, 128).unwrap(), metadata: None };
|
|
idx_clone.lock().unwrap().db().insert(entry).unwrap();
|
|
})
|
|
}).collect();
|
|
|
|
for h in handles { assert!(h.join().is_ok()); }
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quality_score_bounds() {
|
|
// Phred >93 rejected, 0-93 accepted
|
|
assert!(matches!(QualityScore::new(100).unwrap_err(), DnaError::InvalidQuality(100)));
|
|
assert!(QualityScore::new(0).is_ok());
|
|
assert!(QualityScore::new(93).is_ok());
|
|
assert!((QualityScore::new(30).unwrap().to_error_probability() - 0.001).abs() < 1e-6);
|
|
assert!((QualityScore::new(0).unwrap().to_error_probability() - 1.0).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_variant_position_overflow() {
|
|
// u64::MAX positions handled
|
|
let pos = GenomicPosition {
|
|
chromosome: 25, position: u64::MAX,
|
|
reference_allele: Nucleotide::A, alternate_allele: Some(Nucleotide::G),
|
|
};
|
|
assert_eq!(pos.position, u64::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_methylation_bounds() {
|
|
// Beta values clamped to [0,1]
|
|
for val in [-0.5f32, 0.0, 0.5, 1.0, 1.5] {
|
|
let clamped = val.clamp(0.0, 1.0);
|
|
assert!(clamped >= 0.0 && clamped <= 1.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_deterministic_output() {
|
|
// Same input -> same output (no randomness)
|
|
let seq = DnaSequence::from_str("ACGTACGTACGTACGT").unwrap();
|
|
assert_eq!(seq.to_kmer_vector(11, 512).unwrap(), seq.to_kmer_vector(11, 512).unwrap());
|
|
assert_eq!(seq.reverse_complement().to_string(), seq.reverse_complement().to_string());
|
|
assert_eq!(seq.complement().to_string(), seq.complement().to_string());
|
|
assert_eq!(seq.to_string(), seq.to_string());
|
|
}
|