Skip to content

Commit 94882ef

Browse files
test: property-based tests for core microcrates and adapters
Adds proptest suites covering determinism, format validity, and panic safety for 10 additional crates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bdfabd0 commit 94882ef

File tree

14 files changed

+798
-0
lines changed

14 files changed

+798
-0
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uselesskey-aws-lc-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ uselesskey-core = { path = "../uselesskey-core", version = "0.3.0" }
4141
hex = "0.4"
4242
serde.workspace = true
4343
insta.workspace = true
44+
proptest.workspace = true
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use proptest::prelude::*;
2+
use uselesskey_core::{Factory, Seed};
3+
4+
proptest! {
5+
#![proptest_config(ProptestConfig { cases: 8, ..ProptestConfig::default() })]
6+
7+
/// Deterministic factory produces the same aws-lc-rs RSA public key for the same seed.
8+
#[cfg(all(feature = "native", any(not(windows), has_nasm), feature = "rsa"))]
9+
#[test]
10+
fn rsa_aws_lc_rs_deterministic(seed in any::<[u8; 32]>()) {
11+
use aws_lc_rs::signature::KeyPair;
12+
use uselesskey_aws_lc_rs::AwsLcRsRsaKeyPairExt;
13+
use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
14+
15+
let fx = Factory::deterministic(Seed::new(seed));
16+
let kp1 = fx.rsa("prop-test", RsaSpec::rs256()).rsa_key_pair_aws_lc_rs();
17+
let kp2 = fx.rsa("prop-test", RsaSpec::rs256()).rsa_key_pair_aws_lc_rs();
18+
19+
prop_assert_eq!(kp1.public_key().as_ref(), kp2.public_key().as_ref());
20+
prop_assert!(kp1.public_modulus_len() > 0);
21+
}
22+
23+
/// Deterministic factory produces the same aws-lc-rs ECDSA public key for the same seed.
24+
#[cfg(all(feature = "native", any(not(windows), has_nasm), feature = "ecdsa"))]
25+
#[test]
26+
fn ecdsa_aws_lc_rs_deterministic(seed in any::<[u8; 32]>()) {
27+
use aws_lc_rs::signature::KeyPair;
28+
use uselesskey_aws_lc_rs::AwsLcRsEcdsaKeyPairExt;
29+
use uselesskey_ecdsa::{EcdsaFactoryExt, EcdsaSpec};
30+
31+
let fx = Factory::deterministic(Seed::new(seed));
32+
let kp1 = fx.ecdsa("prop-test", EcdsaSpec::es256()).ecdsa_key_pair_aws_lc_rs();
33+
let kp2 = fx.ecdsa("prop-test", EcdsaSpec::es256()).ecdsa_key_pair_aws_lc_rs();
34+
35+
prop_assert_eq!(kp1.public_key().as_ref(), kp2.public_key().as_ref());
36+
}
37+
38+
/// Deterministic factory produces the same aws-lc-rs Ed25519 public key for the same seed.
39+
#[cfg(all(feature = "native", any(not(windows), has_nasm), feature = "ed25519"))]
40+
#[test]
41+
fn ed25519_aws_lc_rs_deterministic(seed in any::<[u8; 32]>()) {
42+
use aws_lc_rs::signature::KeyPair;
43+
use uselesskey_aws_lc_rs::AwsLcRsEd25519KeyPairExt;
44+
use uselesskey_ed25519::{Ed25519FactoryExt, Ed25519Spec};
45+
46+
let fx = Factory::deterministic(Seed::new(seed));
47+
let kp1 = fx.ed25519("prop-test", Ed25519Spec::new()).ed25519_key_pair_aws_lc_rs();
48+
let kp2 = fx.ed25519("prop-test", Ed25519Spec::new()).ed25519_key_pair_aws_lc_rs();
49+
50+
prop_assert_eq!(kp1.public_key().as_ref(), kp2.public_key().as_ref());
51+
}
52+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::sync::Arc;
2+
3+
use proptest::prelude::*;
4+
use uselesskey_core_cache::ArtifactCache;
5+
use uselesskey_core_id::{ArtifactId, DerivationVersion};
6+
7+
fn make_id(label: &str, variant: &str) -> ArtifactId {
8+
ArtifactId::new(
9+
"domain:prop",
10+
label,
11+
b"spec",
12+
variant,
13+
DerivationVersion::V1,
14+
)
15+
}
16+
17+
proptest! {
18+
#![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
19+
20+
/// Inserting and retrieving a typed value round-trips correctly.
21+
#[test]
22+
fn insert_and_get_round_trip(label in "[a-z]{1,16}", value in any::<u64>()) {
23+
let cache = ArtifactCache::new();
24+
let id = make_id(&label, "default");
25+
26+
let inserted = cache.insert_if_absent_typed(id.clone(), Arc::new(value));
27+
let fetched = cache.get_typed::<u64>(&id).expect("value should be present");
28+
29+
prop_assert_eq!(*inserted, value);
30+
prop_assert_eq!(*fetched, value);
31+
}
32+
33+
/// insert_if_absent_typed always returns the first inserted value.
34+
#[test]
35+
fn first_value_wins(
36+
label in "[a-z]{1,16}",
37+
first in any::<u64>(),
38+
second in any::<u64>(),
39+
) {
40+
let cache = ArtifactCache::new();
41+
let id = make_id(&label, "default");
42+
43+
let winner = cache.insert_if_absent_typed(id.clone(), Arc::new(first));
44+
let again = cache.insert_if_absent_typed(id, Arc::new(second));
45+
46+
prop_assert_eq!(*winner, first);
47+
prop_assert_eq!(*again, first);
48+
}
49+
50+
/// Distinct labels produce distinct cache entries.
51+
#[test]
52+
fn distinct_labels_no_collision(
53+
label_a in "[a-z]{1,8}",
54+
label_b in "[a-z]{1,8}",
55+
) {
56+
prop_assume!(label_a != label_b);
57+
let cache = ArtifactCache::new();
58+
59+
let id_a = make_id(&label_a, "default");
60+
let id_b = make_id(&label_b, "default");
61+
62+
cache.insert_if_absent_typed(id_a.clone(), Arc::new(1u32));
63+
cache.insert_if_absent_typed(id_b.clone(), Arc::new(2u32));
64+
65+
prop_assert_eq!(*cache.get_typed::<u32>(&id_a).unwrap(), 1u32);
66+
prop_assert_eq!(*cache.get_typed::<u32>(&id_b).unwrap(), 2u32);
67+
prop_assert_eq!(cache.len(), 2);
68+
}
69+
70+
/// Cache len tracks insertions correctly.
71+
#[test]
72+
fn len_tracks_insertions(count in 1usize..=20) {
73+
let cache = ArtifactCache::new();
74+
for i in 0..count {
75+
let id = make_id(&format!("label-{i}"), "default");
76+
cache.insert_if_absent_typed(id, Arc::new(i as u64));
77+
}
78+
prop_assert_eq!(cache.len(), count);
79+
prop_assert!(!cache.is_empty());
80+
}
81+
82+
/// clear() empties the cache.
83+
#[test]
84+
fn clear_empties(count in 1usize..=10) {
85+
let cache = ArtifactCache::new();
86+
for i in 0..count {
87+
let id = make_id(&format!("label-{i}"), "default");
88+
cache.insert_if_absent_typed(id, Arc::new(i as u64));
89+
}
90+
cache.clear();
91+
prop_assert!(cache.is_empty());
92+
prop_assert_eq!(cache.len(), 0);
93+
}
94+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use proptest::prelude::*;
2+
use uselesskey_core_hash::{Hasher, hash32, write_len_prefixed};
3+
4+
proptest! {
5+
#![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
6+
7+
/// hash32 is deterministic for any input.
8+
#[test]
9+
fn hash32_deterministic(data in any::<Vec<u8>>()) {
10+
prop_assert_eq!(hash32(&data), hash32(&data));
11+
}
12+
13+
/// Different inputs produce different hashes (collision resistance).
14+
#[test]
15+
fn hash32_different_inputs_differ(a in any::<Vec<u8>>(), b in any::<Vec<u8>>()) {
16+
prop_assume!(a != b);
17+
prop_assert_ne!(hash32(&a), hash32(&b));
18+
}
19+
20+
/// write_len_prefixed preserves tuple boundaries: [a][b] != [a+b].
21+
#[test]
22+
fn len_prefix_separates_fields(
23+
a in prop::collection::vec(any::<u8>(), 1..32),
24+
b in prop::collection::vec(any::<u8>(), 1..32),
25+
) {
26+
let mut combined = a.clone();
27+
combined.extend_from_slice(&b);
28+
29+
let mut h_two = Hasher::new();
30+
write_len_prefixed(&mut h_two, &a);
31+
write_len_prefixed(&mut h_two, &b);
32+
33+
let mut h_one = Hasher::new();
34+
write_len_prefixed(&mut h_one, &combined);
35+
36+
prop_assert_ne!(h_two.finalize(), h_one.finalize());
37+
}
38+
39+
/// write_len_prefixed is deterministic.
40+
#[test]
41+
fn write_len_prefixed_deterministic(data in any::<Vec<u8>>()) {
42+
let mut h1 = Hasher::new();
43+
write_len_prefixed(&mut h1, &data);
44+
45+
let mut h2 = Hasher::new();
46+
write_len_prefixed(&mut h2, &data);
47+
48+
prop_assert_eq!(h1.finalize(), h2.finalize());
49+
}
50+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use proptest::prelude::*;
2+
use uselesskey_core_id::{ArtifactId, DerivationVersion, Seed, derive_seed};
3+
4+
proptest! {
5+
#![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
6+
7+
/// Same master seed and ArtifactId always produce the same derived seed.
8+
#[test]
9+
fn derive_seed_is_deterministic(
10+
master in any::<[u8; 32]>(),
11+
label in ".*",
12+
spec in any::<[u8; 8]>(),
13+
variant in ".*",
14+
) {
15+
let seed = Seed::new(master);
16+
let id = ArtifactId::new("domain:prop", &label, &spec, &variant, DerivationVersion::V1);
17+
18+
let a = derive_seed(&seed, &id);
19+
let b = derive_seed(&seed, &id);
20+
prop_assert_eq!(a.bytes(), b.bytes());
21+
}
22+
23+
/// Changing the label changes the derived seed.
24+
#[test]
25+
fn different_labels_produce_different_seeds(
26+
master in any::<[u8; 32]>(),
27+
label_a in "[a-z]{1,8}",
28+
label_b in "[a-z]{1,8}",
29+
) {
30+
prop_assume!(label_a != label_b);
31+
let seed = Seed::new(master);
32+
let id_a = ArtifactId::new("domain:prop", &label_a, b"spec", "v", DerivationVersion::V1);
33+
let id_b = ArtifactId::new("domain:prop", &label_b, b"spec", "v", DerivationVersion::V1);
34+
35+
let sa = derive_seed(&seed, &id_a);
36+
let sb = derive_seed(&seed, &id_b);
37+
prop_assert_ne!(sa.bytes(), sb.bytes());
38+
}
39+
40+
/// Changing the variant changes the derived seed.
41+
#[test]
42+
fn different_variants_produce_different_seeds(
43+
master in any::<[u8; 32]>(),
44+
variant_a in "[a-z]{1,8}",
45+
variant_b in "[a-z]{1,8}",
46+
) {
47+
prop_assume!(variant_a != variant_b);
48+
let seed = Seed::new(master);
49+
let id_a = ArtifactId::new("domain:prop", "label", b"spec", &variant_a, DerivationVersion::V1);
50+
let id_b = ArtifactId::new("domain:prop", "label", b"spec", &variant_b, DerivationVersion::V1);
51+
52+
let sa = derive_seed(&seed, &id_a);
53+
let sb = derive_seed(&seed, &id_b);
54+
prop_assert_ne!(sa.bytes(), sb.bytes());
55+
}
56+
57+
/// spec_fingerprint is always the BLAKE3 hash of the spec bytes.
58+
#[test]
59+
fn spec_fingerprint_matches_hash32(spec in any::<Vec<u8>>()) {
60+
let id = ArtifactId::new("d", "l", &spec, "v", DerivationVersion::V1);
61+
let expected = *uselesskey_core_id::hash32(&spec).as_bytes();
62+
prop_assert_eq!(id.spec_fingerprint, expected);
63+
}
64+
65+
/// ArtifactId construction never panics on arbitrary input.
66+
#[test]
67+
fn artifact_id_new_never_panics(
68+
label in ".*",
69+
spec in any::<Vec<u8>>(),
70+
variant in ".*",
71+
version in any::<u16>(),
72+
) {
73+
let _ = ArtifactId::new("domain:prop", label, &spec, variant, DerivationVersion(version));
74+
}
75+
}

0 commit comments

Comments
 (0)