#![expect(
clippy::uninlined_format_args,
clippy::unwrap_used,
reason = "Pre-existing profile unit test debt moved from hl7v2-prof; cleanup is separate from this behavior-preserving module collapse."
)]
#[cfg(test)]
mod unit_tests {
use super::super::{
Profile, compare_timestamps_for_before, load_profile, parse_hl7_ts_with_precision, validate,
};
use crate::parse;
fn adt_a01_msg() -> String {
let mut s = String::new();
s.push_str("MSH|^~\\&|SND|SF|RCV|RF|20250101000000||ADT^A01|MSG1|P|2.5.1\r");
s.push_str("PID|1||123456^^^HOSP^MR||Doe^John||19800101|M||||||||||||||||\r");
s
}
#[test]
fn test_load_simple_profile() {
let y = r#"
message_structure: "simple"
version: "2.5.1"
segments:
- id: "PID"
constraints:
- path: "PID.3"
required: true
- path: "PID.8"
required: true
"#;
let p: Profile = load_profile(y).unwrap();
let msg = parse(adt_a01_msg().as_bytes()).unwrap();
let probs = validate(&msg, &p);
assert!(probs.is_empty(), "unexpected problems: {probs:?}");
}
#[test]
fn test_cross_field_equals() {
let y = r#"
message_structure: "xfield"
version: "2.5.1"
segments:
- id: "PID"
cross_field_rules:
- id: "test-rule"
description: "Sex must be M"
conditions:
- field: "PID.8"
operator: "eq"
value: "M"
actions: []
"#;
let p: Profile = load_profile(y).unwrap();
let msg = parse(adt_a01_msg().as_bytes()).unwrap();
let probs = validate(&msg, &p);
assert!(probs.is_empty(), "unexpected problems: {probs:?}");
}
#[test]
fn test_temporal_before_with_partial_precision() {
let mut msg = String::new();
msg.push_str("MSH|^~\\&|SND|SF|RCV|RF|20250101000000||ADT^A01|MSG1|P|2.5.1\r");
msg.push_str("PID|1||123456^^^HOSP^MR||Doe^John||19800101|M||||||||||||||||\r");
msg.push_str("PV1|1|O|CLINIC|||||||20241201\r"); msg.push_str("ORC|RE|||20241201103000\r");
let y = r#"
message_structure: "temporal"
version: "2.5.1"
segments:
- id: "PID"
- id: "PV1"
- id: "ORC"
cross_field_rules:
- id: "date-before-datetime"
description: "PV1 date should be before ORC datetime"
conditions:
- field: "PV1.10"
operator: "before"
value: "ORC.4"
actions: []
"#;
let p: Profile = load_profile(y).unwrap();
let message = parse(msg.as_bytes()).unwrap();
let probs = validate(&message, &p);
assert!(probs.is_empty(), "unexpected problems: {probs:?}");
}
#[test]
fn test_temporal_before_with_same_date_partial_precision() {
let mut msg = String::new();
msg.push_str("MSH|^~\\&|SND|SF|RCV|RF|20250101000000||ADT^A01|MSG1|P|2.5.1\r");
msg.push_str("PID|1||123456^^^HOSP^MR||Doe^John||19800101|M||||||||||||||||\r");
msg.push_str("PV1|1|O|CLINIC|||||||20241201\r"); msg.push_str("ORC|RE|||20241201\r");
let y = r#"
message_structure: "temporal"
version: "2.5.1"
segments:
- id: "PID"
- id: "PV1"
- id: "ORC"
cross_field_rules:
- id: "date-before-date"
description: "PV1 date should be before ORC date"
validation_mode: "assert"
conditions:
- field: "PV1.10"
operator: "before"
value: "ORC.4"
actions: []
"#;
let p: Profile = load_profile(y).unwrap();
let message = parse(msg.as_bytes()).unwrap();
let probs = validate(&message, &p);
assert!(!probs.is_empty(), "expected problems but got none");
}
#[test]
fn debug_compare_same_dates() {
let date_str = "20241201";
let ts1 = parse_hl7_ts_with_precision(date_str).unwrap();
let ts2 = parse_hl7_ts_with_precision(date_str).unwrap();
println!("ts1: {:?}, ts2: {:?}", ts1, ts2);
let result = compare_timestamps_for_before(&ts1, &ts2);
println!("compare_timestamps_for_before result: {}", result);
assert!(!result, "Expected false for equal dates, but got true");
}
#[test]
fn test_table_precedence() {
let y = r#"
message_structure: "table_precedence"
version: "2.5.1"
segments:
- id: "PID"
valuesets:
- path: "PID.8"
name: "HL70001"
hl7_tables:
- id: "HL70001"
name: "Administrative Sex"
version: "2.5.1"
codes:
- value: "M"
description: "Male"
status: "A"
- value: "F"
description: "Female"
status: "A"
table_precedence:
- "HL70001"
"#;
let p: Profile = load_profile(y).unwrap();
let msg = parse(adt_a01_msg().as_bytes()).unwrap();
let probs = validate(&msg, &p);
assert!(probs.is_empty(), "unexpected problems: {probs:?}");
}
#[test]
fn test_expression_guardrails() {
let y = r#"
message_structure: "expression_guardrails"
version: "2.5.1"
segments:
- id: "PID"
expression_guardrails:
max_complexity: 10
allowed_functions:
- "length"
- "matches_regex"
prohibited_fields: []
max_nesting_depth: 3
allow_field_comparisons: true
custom_rules:
- id: "simple_rule"
description: "PID.5.1 should be at least 2 characters"
script: "field(PID.5.1).length() > 1"
"#;
let p: Profile = load_profile(y).unwrap();
let msg = parse(adt_a01_msg().as_bytes()).unwrap();
let probs = validate(&msg, &p);
assert!(probs.is_empty(), "unexpected problems: {probs:?}");
}
}
#[cfg(test)]
mod profile_load_error_tests {
use super::super::{ProfileLoadError, load_profile_checked};
#[test]
fn test_load_profile_checked_valid() {
let y = r#"
message_structure: "ADT_A01"
version: "2.5.1"
segments:
- id: "MSH"
"#;
let result = load_profile_checked(y);
assert!(result.is_ok());
let profile = result.unwrap();
assert_eq!(profile.message_structure, "ADT_A01");
assert_eq!(profile.version, "2.5.1");
}
#[test]
fn test_load_profile_checked_invalid_yaml() {
let y = "this is not: valid:: yaml:::";
let result = load_profile_checked(y);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ProfileLoadError::YamlParse(_)));
}
#[test]
fn test_profile_load_error_display() {
let err = ProfileLoadError::YamlParse("unexpected token".to_string());
assert_eq!(format!("{}", err), "YAML parse error: unexpected token");
let err = ProfileLoadError::MissingField {
field: "message_structure".to_string(),
};
assert_eq!(
format!("{}", err),
"Missing required field: message_structure"
);
let err = ProfileLoadError::InvalidValue {
field: "version".to_string(),
details: "must be a valid HL7 version".to_string(),
};
assert_eq!(
format!("{}", err),
"Invalid value for field 'version': must be a valid HL7 version"
);
let err = ProfileLoadError::Io("file not found".to_string());
assert_eq!(format!("{}", err), "IO error: file not found");
let err = ProfileLoadError::InheritanceCycle("A -> B -> A".to_string());
assert_eq!(
format!("{}", err),
"Profile inheritance cycle detected: A -> B -> A"
);
let err = ProfileLoadError::ParentNotFound("base_profile".to_string());
assert_eq!(format!("{}", err), "Parent profile not found: base_profile");
}
#[test]
fn test_profile_load_error_from_yaml_error() {
let yaml_err = serde_yaml::from_str::<serde_yaml::Value>("invalid: ::: yaml").unwrap_err();
let load_err: ProfileLoadError = yaml_err.into();
assert!(matches!(load_err, ProfileLoadError::YamlParse(_)));
}
#[test]
fn test_profile_load_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let load_err: ProfileLoadError = io_err.into();
assert!(matches!(load_err, ProfileLoadError::Io(_)));
}
}
#[cfg(test)]
mod profile_lint_tests {
use super::super::{ProfileLintSeverity, lint_profile_yaml};
#[test]
fn test_lint_profile_yaml_accepts_minimal_profile() {
let y = r#"
message_structure: "ADT_A01"
version: "2.5.1"
segments:
- id: "MSH"
constraints:
- path: "MSH.9"
required: true
"#;
let report = lint_profile_yaml(y);
assert!(report.valid, "unexpected lint report: {report:?}");
assert_eq!(report.issue_count, 0);
}
#[test]
fn test_lint_profile_yaml_reports_structural_errors() {
let y = r#"
message_structure: ""
version: "2.5.1"
segments:
- id: ""
constraints:
- path: "PID.x"
pattern: "["
cross_field_rules:
- id: "rule-1"
validation_mode: "sometimes"
description: "invalid mode"
conditions:
- field: "PID.3"
operator: "matches_regex"
actions:
- field: "PID.5"
action: "invent"
valueset: "missing"
table_precedence:
- "HL79999"
"#;
let report = lint_profile_yaml(y);
let codes: Vec<&str> = report
.issues
.iter()
.map(|issue| issue.code.as_str())
.collect();
assert!(!report.valid);
assert!(codes.contains(&"empty_message_structure"));
assert!(codes.contains(&"empty_segment_id"));
assert!(codes.contains(&"invalid_hl7_path"));
assert!(codes.contains(&"invalid_constraint_pattern"));
assert!(codes.contains(&"unknown_cross_field_validation_mode"));
assert!(codes.contains(&"missing_rule_condition_regex"));
assert!(codes.contains(&"unknown_rule_action"));
assert!(codes.contains(&"unknown_action_valueset"));
assert!(codes.contains(&"unknown_table_precedence_entry"));
assert!(
report
.issues
.iter()
.any(|issue| issue.severity == ProfileLintSeverity::Error)
);
}
#[test]
fn test_lint_profile_yaml_warns_for_ignored_top_level_keys() {
let y = r#"
message_structure: "ADT_A01"
version: "2.5.1"
segments:
- id: "MSH"
rules: []
description: "profile metadata"
"#;
let report = lint_profile_yaml(y);
assert!(report.valid, "warnings should not fail profile lint");
assert_eq!(report.warning_count, 1);
assert!(
report
.issues
.iter()
.all(|issue| issue.severity == ProfileLintSeverity::Warning)
);
assert!(
report
.issues
.iter()
.any(|issue| issue.path.as_deref() == Some("rules"))
);
}
}