use proptest::prelude::*;
use pgroles_operator::crd::GeneratePasswordSpec;
use pgroles_operator::k8s_names::{
LabelValue, MAX_LABEL_VALUE_LENGTH, MAX_RESOURCE_NAME_LENGTH, is_valid_label_value,
is_valid_resource_name, sanitize_dns_label_segment, truncate_name_prefix,
};
use pgroles_operator::password::generated_secret_name;
fn is_label_value_per_apiserver(value: &str) -> bool {
if value.is_empty() {
return true;
}
if value.len() > MAX_LABEL_VALUE_LENGTH {
return false;
}
let first = value.chars().next().expect("non-empty");
let last = value.chars().next_back().expect("non-empty");
first.is_ascii_alphanumeric()
&& last.is_ascii_alphanumeric()
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
}
fn is_resource_name_per_apiserver(value: &str) -> bool {
if value.is_empty() || value.len() > MAX_RESOURCE_NAME_LENGTH {
return false;
}
value.split('.').all(|label| {
!label.is_empty()
&& label
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& label.chars().next().is_some_and(|c| c != '-')
&& label.chars().next_back().is_some_and(|c| c != '-')
})
}
fn hostile_input() -> impl Strategy<Value = String> {
proptest::collection::vec(
prop_oneof![
Just('a'),
Just('9'),
Just('Z'),
Just('_'),
Just('.'),
Just('-'),
Just('/'),
Just('='),
Just('\0'),
Just(' '),
Just('é'),
Just('日'),
any::<char>(),
],
0..200usize,
)
.prop_map(|chars| chars.into_iter().collect())
}
proptest! {
#[test]
fn sanitized_label_values_are_always_valid(input in hostile_input()) {
let value = LabelValue::sanitize(&input);
let s = value.as_str();
prop_assert!(
is_label_value_per_apiserver(s),
"sanitize({input:?}) produced invalid label value {s:?}"
);
prop_assert_eq!(is_valid_label_value(s), is_label_value_per_apiserver(s));
prop_assert!(s.len() <= MAX_LABEL_VALUE_LENGTH);
}
#[test]
fn sanitizing_a_label_value_is_idempotent(input in hostile_input()) {
let once = LabelValue::sanitize(&input);
let twice = LabelValue::sanitize(once.as_str());
prop_assert_eq!(once, twice);
}
#[test]
fn valid_label_values_are_unchanged(
value in "[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?"
) {
prop_assume!(is_label_value_per_apiserver(&value));
let sanitized = LabelValue::sanitize(&value);
prop_assert_eq!(sanitized.as_str(), value.as_str());
prop_assert!(LabelValue::try_new(&value).is_ok());
}
#[test]
fn truncated_name_prefixes_are_suffixable(
input in hostile_input(),
max_bytes in 0..300usize,
) {
let prefix = truncate_name_prefix(&input, max_bytes);
prop_assert!(prefix.len() <= max_bytes);
prop_assert!(!prefix.ends_with('.'), "trailing dot in {prefix:?}");
prop_assert!(!prefix.ends_with('-'), "trailing dash in {prefix:?}");
prop_assert!(input.starts_with(prefix));
}
#[test]
fn suffixed_truncated_names_are_valid_resource_names(
name in "[a-z0-9]([a-z0-9.-]{0,60}[a-z0-9])?",
) {
prop_assume!(is_resource_name_per_apiserver(&name));
for max_prefix in 1..=name.len() {
let prefix = truncate_name_prefix(&name, max_prefix);
if prefix.is_empty() {
continue;
}
let composed = format!("{prefix}-plan-20260731-212739-abcdef012345");
prop_assert!(
is_resource_name_per_apiserver(&composed),
"composing {:?} at budget {} gave invalid name {:?}",
name,
max_prefix,
composed
);
prop_assert_eq!(
is_valid_resource_name(&composed),
is_resource_name_per_apiserver(&composed)
);
}
}
#[test]
fn sanitized_segments_are_valid_dns_labels(input in hostile_input()) {
let segment = sanitize_dns_label_segment(&input, "fallback");
prop_assert!(!segment.is_empty());
prop_assert!(
is_resource_name_per_apiserver(&segment),
"segment {:?} from {:?} is not a valid DNS label",
segment,
input
);
prop_assert!(!segment.contains('.'));
}
#[test]
fn generated_secret_names_are_valid_resource_names(
policy in hostile_input(),
role in hostile_input(),
) {
let spec = GeneratePasswordSpec {
length: None,
secret_name: None,
secret_key: None,
};
let name = generated_secret_name(&policy, &role, &spec);
prop_assert!(!name.is_empty());
prop_assert!(
is_resource_name_per_apiserver(&name),
"generated_secret_name({:?}, {:?}) produced {:?}",
policy,
role,
name
);
prop_assert_eq!(
is_valid_resource_name(&name),
is_resource_name_per_apiserver(&name)
);
prop_assert!(name.len() <= MAX_RESOURCE_NAME_LENGTH);
}
#[test]
fn resource_name_validator_matches_apiserver_rule(input in hostile_input()) {
prop_assert_eq!(
is_valid_resource_name(&input),
is_resource_name_per_apiserver(&input),
"disagreement on {:?}",
input
);
}
}