#![cfg(feature = "std")]
use proptest::prelude::*;
use uselesskey_core::negative::{CorruptPem, corrupt_pem, flip_byte, truncate_der};
fn pem_strategy() -> impl Strategy<Value = String> {
"[A-Za-z0-9+/]{64,256}"
.prop_map(|body| format!("-----BEGIN TEST KEY-----\n{}\n-----END TEST KEY-----", body))
}
proptest! {
#[test]
fn corrupt_pem_bad_header_contains_marker(pem in pem_strategy()) {
let corrupted = corrupt_pem(&pem, CorruptPem::BadHeader);
prop_assert!(
corrupted.contains("BEGIN CORRUPTED KEY"),
"BadHeader should insert 'BEGIN CORRUPTED KEY', got: {}",
corrupted
);
}
#[test]
fn corrupt_pem_bad_footer_contains_marker(pem in pem_strategy()) {
let corrupted = corrupt_pem(&pem, CorruptPem::BadFooter);
prop_assert!(
corrupted.contains("END CORRUPTED KEY"),
"BadFooter should insert 'END CORRUPTED KEY', got: {}",
corrupted
);
}
#[test]
fn corrupt_pem_bad_base64_contains_marker(pem in pem_strategy()) {
let corrupted = corrupt_pem(&pem, CorruptPem::BadBase64);
prop_assert!(
corrupted.contains("THIS_IS_NOT_BASE64"),
"BadBase64 should insert 'THIS_IS_NOT_BASE64', got: {}",
corrupted
);
}
#[test]
fn corrupt_pem_truncate_produces_correct_length(
pem in pem_strategy(),
fraction in 1usize..10
) {
let target_bytes = pem.len() / fraction.max(1);
prop_assume!(target_bytes < pem.len());
prop_assume!(target_bytes > 0);
let corrupted = corrupt_pem(&pem, CorruptPem::Truncate { bytes: target_bytes });
prop_assert_eq!(
corrupted.chars().count(),
target_bytes,
"Truncate should produce exactly {} chars, got {}",
target_bytes,
corrupted.chars().count()
);
}
#[test]
fn corrupt_pem_extra_blank_line_adds_blank(pem in pem_strategy()) {
let original_lines: Vec<&str> = pem.lines().collect();
let corrupted = corrupt_pem(&pem, CorruptPem::ExtraBlankLine);
let corrupted_lines: Vec<&str> = corrupted.lines().collect();
prop_assert_eq!(
corrupted_lines.len(),
original_lines.len() + 1,
"ExtraBlankLine should add one line"
);
prop_assert!(
corrupted_lines.iter().any(|l| l.is_empty()),
"ExtraBlankLine should produce at least one empty line"
);
}
#[test]
fn flip_byte_changes_exactly_one_byte(
der in prop::collection::vec(any::<u8>(), 1..256),
offset_factor in 0usize..100
) {
let offset = offset_factor % der.len();
let flipped = flip_byte(&der, offset);
prop_assert_eq!(flipped.len(), der.len());
let diff_count = der.iter()
.zip(flipped.iter())
.filter(|(a, b)| a != b)
.count();
prop_assert_eq!(
diff_count, 1,
"flip_byte should change exactly one byte, changed {}",
diff_count
);
prop_assert_ne!(
flipped[offset], der[offset],
"flip_byte should change the byte at offset {}", offset
);
}
#[test]
fn flip_byte_out_of_range_returns_original(
der in prop::collection::vec(any::<u8>(), 1..256),
extra in 0usize..100
) {
let offset = der.len() + extra;
let result = flip_byte(&der, offset);
prop_assert_eq!(result, der, "flip_byte with out-of-range offset should return original");
}
#[test]
fn flip_byte_is_self_inverse(
der in prop::collection::vec(any::<u8>(), 1..256),
offset_factor in 0usize..100
) {
let offset = offset_factor % der.len();
let flipped_once = flip_byte(&der, offset);
let flipped_twice = flip_byte(&flipped_once, offset);
prop_assert_eq!(flipped_twice, der, "flip_byte should be its own inverse");
}
#[test]
fn truncate_der_zero_returns_empty(
der in prop::collection::vec(any::<u8>(), 1..256)
) {
let result = truncate_der(&der, 0);
prop_assert!(result.is_empty(), "truncate_der(_, 0) should return empty vec");
}
#[test]
fn truncate_der_preserves_prefix(
der in prop::collection::vec(any::<u8>(), 2..256),
len in 1usize..100
) {
let actual_len = len.min(der.len());
let result = truncate_der(&der, actual_len);
for (i, &byte) in result.iter().enumerate() {
prop_assert_eq!(
byte, der[i],
"truncate_der should preserve byte at index {}", i
);
}
}
}