use tencrypt_core::{
cert_state_from_str, cert_state_names, dry_run_issue, new_request, next_backoff_delay,
next_backoff_delay_jittered, reconcile_step, resolve_backoff, BackoffConfig, BackoffState,
CertState, DomainPolicy, ReconcileDecision,
};
#[test]
fn reconcile_every_state_is_idempotent() {
for name in cert_state_names() {
let state = cert_state_from_str(name)
.unwrap_or_else(|| panic!("cert_state_from_str failed for {:?}", name));
let first = reconcile_step(state);
let second = reconcile_step(state);
assert_eq!(
first, second,
"reconcile_step({:?}) is not idempotent",
name
);
}
}
#[test]
fn reconcile_all_states_covered() {
let valid_decisions = [
ReconcileDecision::Proceed,
ReconcileDecision::Wait,
ReconcileDecision::Done,
ReconcileDecision::RetryOrAbandon,
];
for name in cert_state_names() {
let state = cert_state_from_str(name).unwrap();
let decision = reconcile_step(state);
assert!(
valid_decisions.contains(&decision),
"unexpected decision {:?} for state {:?}",
decision,
name
);
}
}
#[test]
fn reconcile_terminal_states_do_not_proceed() {
assert_ne!(
reconcile_step(CertState::Issued),
ReconcileDecision::Proceed
);
assert_ne!(
reconcile_step(CertState::Revoked),
ReconcileDecision::Proceed
);
}
#[test]
fn reconcile_failed_never_returns_done() {
assert_ne!(reconcile_step(CertState::Failed), ReconcileDecision::Done);
}
#[test]
fn full_dry_run_workflow_reaches_issued() {
let req = new_request(
"app.example.com".to_string(),
"integration-test".to_string(),
);
let report = dry_run_issue(&req).expect("dry_run_issue must succeed for valid hostname");
assert_eq!(report.hostname, "app.example.com");
assert_eq!(report.status, "success");
assert!(
!report.transitions.is_empty(),
"expected at least one transition"
);
let last = report.transitions.last().unwrap();
assert_eq!(
last.to,
CertState::Issued,
"final transition must reach Issued"
);
}
#[test]
fn dry_run_expected_transition_sequence() {
let req = new_request(
"seq.example.com".to_string(),
"integration-test".to_string(),
);
let report = dry_run_issue(&req).unwrap();
assert_eq!(report.transitions[0].from, CertState::Requested);
for window in report.transitions.windows(2) {
assert_eq!(
window[0].to, window[1].from,
"transition chain is broken between {:?} and {:?}",
window[0].to, window[1].from
);
}
}
#[test]
fn dry_run_rejects_invalid_hostname() {
let req = new_request("localhost".to_string(), "integration-test".to_string());
assert!(
dry_run_issue(&req).is_err(),
"dry_run_issue must reject hostname without a dot"
);
}
#[test]
fn resolve_backoff_is_deterministic_across_calls() {
let default = BackoffConfig::default();
let policies = vec![
DomainPolicy {
hostname_pattern: "a.example.com".to_string(),
backoff: Some(BackoffConfig {
max_retries: 2,
base_delay_ms: 200,
max_delay_ms: 4_000,
}),
},
DomainPolicy {
hostname_pattern: "b.example.com".to_string(),
backoff: None,
},
];
for hostname in ["a.example.com", "b.example.com", "unknown.example.com"] {
let first = resolve_backoff(hostname, &policies, &default);
let second = resolve_backoff(hostname, &policies, &default);
assert_eq!(
first.max_retries, second.max_retries,
"resolve_backoff is not deterministic for {:?}",
hostname
);
}
}
#[test]
fn jittered_backoff_never_exceeds_max_delay() {
let cfg = BackoffConfig {
max_retries: 10,
base_delay_ms: 100,
max_delay_ms: 5_000,
};
for attempt in 0..cfg.max_retries {
for jitter_ms in [0u64, 100, 1_000, 10_000, u64::MAX / 2] {
let state = BackoffState { attempt };
if let Some(d) = next_backoff_delay_jittered(&cfg, &state, jitter_ms) {
assert!(
d.as_millis() <= cfg.max_delay_ms as u128,
"jittered delay {} ms exceeded max_delay_ms {} for attempt={} jitter={}",
d.as_millis(),
cfg.max_delay_ms,
attempt,
jitter_ms
);
}
}
}
}
#[test]
fn plain_backoff_never_exceeds_max_delay() {
let cfg = BackoffConfig {
max_retries: 10,
base_delay_ms: 1_000,
max_delay_ms: 8_000,
};
for attempt in 0..cfg.max_retries {
let state = BackoffState { attempt };
if let Some(d) = next_backoff_delay(&cfg, &state) {
assert!(
d.as_millis() <= cfg.max_delay_ms as u128,
"delay {} ms exceeded max_delay_ms {}",
d.as_millis(),
cfg.max_delay_ms
);
}
}
}
#[test]
fn backoff_exhausted_at_exactly_max_retries() {
let cfg = BackoffConfig {
max_retries: 3,
base_delay_ms: 1_000,
max_delay_ms: 60_000,
};
assert!(next_backoff_delay(&cfg, &BackoffState { attempt: 2 }).is_some());
assert!(next_backoff_delay(&cfg, &BackoffState { attempt: 3 }).is_none());
}
#[test]
fn cert_state_names_round_trip() {
for name in cert_state_names() {
let state = cert_state_from_str(name);
assert!(
state.is_some(),
"cert_state_from_str returned None for {:?}",
name
);
}
}