use super::dnssec::*;
const STATUS_SIGNED_CSK: &str = "dnssec-policy: test-policy
current time: Sun Sep 27 22:07:11 2026
key: 33306 (ECDSAP256SHA256), CSK
published: yes - since Sun Sep 27 22:07:06 2026
key signing: yes - since Sun Sep 27 22:07:06 2026
zone signing: yes - since Sun Sep 27 22:07:06 2026
No rollover scheduled
- goal: omnipresent
- dnskey: rumoured
- ds: hidden
- zone rrsig: rumoured
- key rrsig: rumoured
";
const STATUS_INSECURE: &str = "dnssec-policy: insecure
current time: Sun Sep 27 22:33:23 2026
key: 33306 (ECDSAP256SHA256), CSK
published: no
key signing: no
zone signing: no
Key has been removed from the zone
- goal: hidden
- dnskey: unretentive
- ds: hidden
- zone rrsig: unretentive
- key rrsig: unretentive
";
const STATUS_UNSIGNED: &str = "Zone does not have dnssec-policy";
const STATUS_KSK_ZSK_ROLLOVER: &str = "dnssec-policy: split
current time: Sun Sep 27 22:07:11 2026
key: 11111 (ECDSAP256SHA256), KSK
published: yes - since Sun Sep 27 22:07:06 2026
key signing: yes - since Sun Sep 27 22:07:06 2026
No rollover scheduled
- goal: omnipresent
- dnskey: omnipresent
- ds: omnipresent
- key rrsig: omnipresent
key: 22222 (ECDSAP256SHA256), ZSK
published: yes - since Sun Sep 27 22:07:06 2026
zone signing: yes - since Sun Sep 27 22:07:06 2026
Next rollover scheduled on Thu Dec 24 15:35:20 2026
- goal: omnipresent
- dnskey: omnipresent
- zone rrsig: omnipresent
";
#[test]
fn test_parse_status_signed_csk() {
let status = parse_dnssec_status(STATUS_SIGNED_CSK);
assert_eq!(status.policy.as_deref(), Some("test-policy"));
assert!(status.signed);
assert_eq!(status.keys.len(), 1);
let key = &status.keys[0];
assert_eq!(key.tag, 33306);
assert_eq!(key.algorithm, "ECDSAP256SHA256");
assert_eq!(key.role, "CSK");
assert!(key.published);
assert!(key.key_signing);
assert!(key.zone_signing);
assert_eq!(key.published_since.as_deref(), Some("2026-09-27T22:07:06"));
assert_eq!(
key.key_signing_since.as_deref(),
Some("2026-09-27T22:07:06")
);
assert_eq!(
key.zone_signing_since.as_deref(),
Some("2026-09-27T22:07:06")
);
assert_eq!(key.next_rollover, None);
assert!(!key.removed);
assert_eq!(key.goal.as_deref(), Some("omnipresent"));
assert_eq!(key.dnskey_state.as_deref(), Some("rumoured"));
assert_eq!(key.ds_state.as_deref(), Some("hidden"));
assert_eq!(key.zone_rrsig_state.as_deref(), Some("rumoured"));
assert_eq!(key.key_rrsig_state.as_deref(), Some("rumoured"));
}
#[test]
fn test_parse_status_insecure_policy_not_signed() {
let status = parse_dnssec_status(STATUS_INSECURE);
assert_eq!(status.policy.as_deref(), Some("insecure"));
assert!(!status.signed);
let key = &status.keys[0];
assert!(!key.published);
assert!(!key.key_signing);
assert!(!key.zone_signing);
assert_eq!(key.published_since, None);
assert!(key.removed);
assert_eq!(key.goal.as_deref(), Some("hidden"));
assert_eq!(key.dnskey_state.as_deref(), Some("unretentive"));
}
#[test]
fn test_parse_status_unsigned_zone() {
let status = parse_dnssec_status(STATUS_UNSIGNED);
assert_eq!(status.policy, None);
assert!(!status.signed);
assert!(status.keys.is_empty());
}
#[test]
fn test_parse_status_ksk_zsk_with_rollover() {
let status = parse_dnssec_status(STATUS_KSK_ZSK_ROLLOVER);
assert_eq!(status.policy.as_deref(), Some("split"));
assert!(status.signed);
assert_eq!(status.keys.len(), 2);
let ksk = &status.keys[0];
assert_eq!(ksk.tag, 11111);
assert_eq!(ksk.role, "KSK");
assert_eq!(ksk.next_rollover, None);
assert_eq!(ksk.ds_state.as_deref(), Some("omnipresent"));
assert!(!ksk.zone_signing);
assert_eq!(ksk.zone_rrsig_state, None);
let zsk = &status.keys[1];
assert_eq!(zsk.tag, 22222);
assert_eq!(zsk.role, "ZSK");
assert_eq!(zsk.next_rollover.as_deref(), Some("2026-12-24T15:35:20"));
assert!(!zsk.key_signing);
}
#[test]
fn test_parse_status_empty_input() {
let status = parse_dnssec_status("");
assert_eq!(status.policy, None);
assert!(!status.signed);
assert!(status.keys.is_empty());
}
#[test]
fn test_parse_status_unparseable_timestamp_kept_raw() {
let input = "dnssec-policy: p\n\nkey: 1 (ECDSAP256SHA256), CSK\n published: yes - since not a date\n";
let status = parse_dnssec_status(input);
let key = &status.keys[0];
assert!(key.published);
assert_eq!(key.published_since.as_deref(), Some("not a date"));
}
const RFC4509_DNSKEY_B64: &str = "AQOeiiR0GOMYkDshWoSKz9XzfwJr1AYtsmx3TGkJaNXVbfi/2pHm822aJ5iI9BMzNXxeYCmZDRD99WYwYqUSdjMmmAphXdvxegXd/M5+X7OrzKBaMbCVdFLUUh6DhweJBjEVv5f2wwjM9XzcnOf+EPbtG9DMBmADjFDc2w/rljwvFw==";
fn rfc4509_key() -> DnskeyRecord {
let mut rr = String::new();
rr.push_str("dskey.example.com. 86400 IN DNSKEY 256 3 5 ");
rr.push_str(RFC4509_DNSKEY_B64);
parse_dnskey_rr(&rr).expect("RFC 4509 example DNSKEY must parse")
}
#[test]
fn test_parse_dnskey_rr_fields() {
let key = rfc4509_key();
assert_eq!(key.owner, "dskey.example.com.");
assert_eq!(key.flags, 256);
assert_eq!(key.protocol, 3);
assert_eq!(key.algorithm, 5);
assert!(!key.public_key.is_empty());
assert!(!key.is_ksk());
}
#[test]
fn test_key_tag_matches_rfc4509_example() {
assert_eq!(rfc4509_key().key_tag(), 60485);
}
#[test]
fn test_ds_record_matches_rfc4509_example() {
let ds = rfc4509_key().ds_record().expect("DS computation");
assert_eq!(ds.key_tag, 60485);
assert_eq!(ds.algorithm, 5);
assert_eq!(ds.digest_type, DS_DIGEST_TYPE_SHA256);
assert_eq!(
ds.digest,
"D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"
);
assert_eq!(
ds.rdata(),
"60485 5 2 D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"
);
}
#[test]
fn test_ds_owner_name_is_canonicalized_lowercase() {
let rr_upper = format!("DSKEY.EXAMPLE.COM. 86400 IN DNSKEY 256 3 5 {RFC4509_DNSKEY_B64}");
let key = parse_dnskey_rr(&rr_upper).expect("uppercase owner must parse");
assert_eq!(
key.ds_record().expect("DS computation").digest,
"D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"
);
}
#[test]
fn test_parse_dnskey_rr_with_split_base64() {
let mut rr = String::from("dskey.example.com. 86400 IN DNSKEY 256 3 5 ");
let (a, b) = RFC4509_DNSKEY_B64.split_at(40);
rr.push_str(a);
rr.push(' ');
rr.push_str(b);
let key = parse_dnskey_rr(&rr).expect("split base64 must parse");
assert_eq!(key.key_tag(), 60485);
}
#[test]
fn test_parse_dnskey_rr_ksk_flag() {
let rr = format!("example.com. 300 IN DNSKEY 257 3 13 {RFC4509_DNSKEY_B64}");
let key = parse_dnskey_rr(&rr).expect("KSK flags must parse");
assert_eq!(key.flags, 257);
assert!(key.is_ksk());
}
#[test]
fn test_parse_dnskey_rr_rejects_garbage() {
assert!(parse_dnskey_rr("").is_err());
assert!(parse_dnskey_rr("example.com. 300 IN A 192.0.2.1").is_err());
assert!(parse_dnskey_rr("example.com. 300 IN DNSKEY 256 3").is_err());
assert!(parse_dnskey_rr("example.com. 300 IN DNSKEY 256 3 5 !!!notbase64!!!").is_err());
}
#[test]
fn test_parse_key_file_skips_comments() {
let contents = format!(
"; This is a key-signing key, keyid 60485, for dskey.example.com.\n\
; Created: 20260927220706 (Sun Sep 27 22:07:06 2026)\n\
; Publish: 20260927220706 (Sun Sep 27 22:07:06 2026)\n\
; Activate: 20260927220706 (Sun Sep 27 22:07:06 2026)\n\
dskey.example.com. 86400 IN DNSKEY 256 3 5 {RFC4509_DNSKEY_B64}\n"
);
let key = parse_key_file(&contents).expect("key file must parse");
assert_eq!(key.key_tag(), 60485);
}
#[test]
fn test_parse_key_file_without_dnskey_line_fails() {
assert!(parse_key_file("; only a comment\n").is_err());
}
#[test]
fn test_owner_name_too_long_rejected() {
let label = "a".repeat(64);
let rr = format!("{label}.example.com. 300 IN DNSKEY 256 3 5 {RFC4509_DNSKEY_B64}");
let key = parse_dnskey_rr(&rr).expect("parse keeps the textual owner");
assert!(key.ds_record().is_err());
}
#[test]
fn test_ksk_tags_selects_ksk_and_csk_roles() {
let status = parse_dnssec_status(STATUS_KSK_ZSK_ROLLOVER);
let tags = status.ksk_tags();
assert!(tags.contains(&11111), "KSK must be selected");
assert!(!tags.contains(&22222), "ZSK must not be selected");
let csk = parse_dnssec_status(STATUS_SIGNED_CSK);
assert!(csk.ksk_tags().contains(&33306), "CSK must be selected");
}
#[test]
fn test_ksk_tags_empty_for_unsigned_zone() {
let status = parse_dnssec_status(STATUS_UNSIGNED);
assert!(status.ksk_tags().is_empty());
}