use openjd_model::template::parse::{document_string_to_object, DocumentType};
use openjd_model::CallerLimits;
use openjd_model::{decode_environment_template, decode_job_template};
fn yaml_val(s: &str) -> serde_json::Value {
serde_saphyr::from_str(s).unwrap()
}
fn check_parse_err(doc: &str, doc_type: DocumentType, expected: &[&str]) {
let err = document_string_to_object(doc, doc_type, &CallerLimits::default())
.expect_err(&format!("Expected error for: {doc}"));
let msg = err.to_string();
for line in expected {
assert!(
msg.contains(line),
"Missing in error output: {line:?}\nGot:\n{msg}"
);
}
}
fn check_job_err(s: &str, expected: &[&str]) {
let v = yaml_val(s);
let err = decode_job_template(v, None, &CallerLimits::default())
.expect_err(&format!("Expected error for: {s}"));
let msg = err.to_string();
for line in expected {
assert!(
msg.contains(line),
"Missing in error output: {line:?}\nGot:\n{msg}"
);
}
}
fn check_job_err_with_ext(s: &str, supported: &[&str], expected: &[&str]) {
let v = yaml_val(s);
let err = decode_job_template(v, Some(supported), &CallerLimits::default())
.expect_err(&format!("Expected error for: {s}"));
let msg = err.to_string();
for line in expected {
assert!(
msg.contains(line),
"Missing in error output: {line:?}\nGot:\n{msg}"
);
}
}
fn check_env_err(s: &str, expected: &[&str]) {
let v = yaml_val(s);
let err = decode_environment_template(v, None).expect_err(&format!("Expected error for: {s}"));
let msg = err.to_string();
for line in expected {
assert!(
msg.contains(line),
"Missing in error output: {line:?}\nGot:\n{msg}"
);
}
}
#[test]
fn doc_string_to_object_json_success() {
let result = document_string_to_object(
r#"{"key": "value"}"#,
DocumentType::Json,
&CallerLimits::default(),
)
.unwrap();
assert_eq!(result["key"].as_str().unwrap(), "value");
}
#[test]
fn doc_string_to_object_yaml_success() {
let result =
document_string_to_object("key: value\n", DocumentType::Yaml, &CallerLimits::default())
.unwrap();
assert_eq!(result["key"].as_str().unwrap(), "value");
}
#[test]
fn not_a_dict_json() {
check_parse_err(
"[1, 2, 3]",
DocumentType::Json,
&["not a valid Json document consisting of key-value pairs"],
);
}
#[test]
fn not_a_dict_yaml() {
check_parse_err(
"- 1\n- 2\n- 3\n",
DocumentType::Yaml,
&["not a valid Yaml document consisting of key-value pairs"],
);
}
#[test]
fn bad_parse_json() {
check_parse_err(
"{",
DocumentType::Json,
&["not a valid JSON document consisting of key-value pairs"],
);
}
#[test]
fn bad_parse_yaml() {
check_parse_err(
"-",
DocumentType::Yaml,
&["not a valid Yaml document consisting of key-value pairs"],
);
}
#[test]
fn job_missing_specification_version() {
check_job_err(
r#"{"notspecversion": "badvalue"}"#,
&["Template is missing Open Job Description schema version key: specificationVersion"],
);
}
#[test]
fn job_unknown_version() {
check_job_err(
r#"{"specificationVersion": "badvalue"}"#,
&[
"Unknown template version: badvalue",
"Values allowed for 'specificationVersion' in Job Templates are: jobtemplate-2023-09",
],
);
}
#[test]
fn job_not_a_job_template_version() {
check_job_err(
r#"{"specificationVersion": "environment-2023-09"}"#,
&[
"Specification version 'environment-2023-09' is not a Job Template version",
"Values allowed for 'specificationVersion' in Job Templates are: jobtemplate-2023-09",
],
);
}
#[test]
fn job_decode_success() {
let v = yaml_val(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "name",
"steps": [{"name": "step", "script": {"actions": {"onRun": {"command": "do thing"}}}}]
}"#,
);
decode_job_template(v, None, &CallerLimits::default()).unwrap();
}
#[test]
fn env_missing_specification_version() {
check_env_err(
r#"{"notspecversion": "badvalue"}"#,
&["Template is missing Open Job Description schema version key: specificationVersion"],
);
}
#[test]
fn env_unknown_version() {
check_env_err(
r#"{"specificationVersion": "badvalue"}"#,
&["Unknown template version: badvalue"],
);
}
#[test]
fn env_not_an_environment_template_version() {
check_env_err(
r#"{"specificationVersion": "jobtemplate-2023-09"}"#,
&["Specification version 'jobtemplate-2023-09' is not an Environment Template version"],
);
}
#[test]
fn env_decode_success() {
let v = yaml_val(
r#"{
"specificationVersion": "environment-2023-09",
"environment": {
"name": "FooEnv",
"description": "A description",
"script": {"actions": {"onEnter": {"command": "echo", "args": ["Hello", "World"]}}}
}
}"#,
);
decode_environment_template(v, None).unwrap();
}
#[test]
fn job_extensions_empty_list() {
let v = yaml_val(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": [],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
);
let result = decode_job_template(v, None, &CallerLimits::default());
assert!(result.is_err(), "empty extensions list should be rejected");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("non-empty"),
"error should mention non-empty, got: {msg}"
);
}
#[test]
fn job_extensions_unsupported_no_supported_list() {
check_job_err(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": ["FEATURE_BUNDLE_1"],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
&[
"1 validation error for JobTemplate\n",
"extensions:\n\tUnsupported extension names: FEATURE_BUNDLE_1",
],
);
}
#[test]
fn job_extensions_unsupported_wrong_supported_list() {
check_job_err_with_ext(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": ["FEATURE_BUNDLE_1"],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
&["EXPR"],
&[
"1 validation error for JobTemplate\n",
"extensions:\n\tUnsupported extension names: FEATURE_BUNDLE_1",
],
);
}
#[test]
fn job_extensions_unsupported_empty_supported_list() {
check_job_err_with_ext(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": ["FEATURE_BUNDLE_1"],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
&[],
&[
"1 validation error for JobTemplate\n",
"extensions:\n\tUnsupported extension names: FEATURE_BUNDLE_1",
],
);
}
#[test]
fn job_extensions_invalid_name_format() {
let v = yaml_val(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": ["bad_name"],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
);
assert!(decode_job_template(v, None, &CallerLimits::default()).is_err());
}
#[test]
fn job_extensions_supported_succeeds() {
let v = yaml_val(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": ["FEATURE_BUNDLE_1"],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
);
decode_job_template(v, Some(&["FEATURE_BUNDLE_1"]), &CallerLimits::default()).unwrap();
}
#[test]
fn job_no_extensions_with_unsupported_in_supported_list() {
let v = yaml_val(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "foo"}}}}]
}"#,
);
decode_job_template(v, Some(&["UNSUPPORTED_NAME"]), &CallerLimits::default()).unwrap();
}
#[test]
fn empty_extensions_job_template() {
let v = yaml_val(
r#"{
"specificationVersion": "jobtemplate-2023-09",
"name": "Test",
"extensions": [],
"steps": [{"name": "S", "script": {"actions": {"onRun": {"command": "run"}}}}]
}"#,
);
assert!(
decode_job_template(v, None, &CallerLimits::default()).is_err(),
"Job template with empty extensions should be rejected"
);
let v2 = yaml_val(
r#"{
"specificationVersion": "environment-2023-09",
"extensions": [],
"environment": {
"name": "E",
"script": {"actions": {"onEnter": {"command": "echo"}}}
}
}"#,
);
assert!(
decode_environment_template(v2, None).is_err(),
"Env template with empty extensions should be rejected"
);
}