use super::{
MAX_SAFE_INTEGER, boolean, kind_name, member_keys, member_path, safe_integer, strings, text,
wrong_kind,
};
use monochromatic_jsonc_edit::{JsoncValue, parse_jsonc};
fn value(source: &str) -> JsoncValue {
let document: JsoncValue =
parse_jsonc(format!("[{source}]").as_str()).expect("fixture is valid JSONC");
let elements: &[JsoncValue] = document.elements().expect("array wrapper");
assert_eq!(elements.len(), 1, "one value per fixture");
return elements[0].clone();
}
#[test]
fn member_paths_join_with_one_dot() {
assert_eq!(member_path("", "policies"), "policies");
assert_eq!(
member_path("policies", "final-newline"),
"policies.final-newline"
);
assert_eq!(member_path("a.b", ""), "a.b.");
}
#[test]
fn kind_names_cover_every_json_type() {
for (source, expected) in [
("null", "null"),
("true", "a boolean"),
("1", "a number"),
("\"x\"", "a string"),
("[]", "an array"),
("{}", "an object"),
] {
assert_eq!(kind_name(&value(source)), expected);
}
}
#[test]
fn wrong_kind_messages_name_key_found_kind_and_expectation() {
assert_eq!(
wrong_kind(&value("1"), "hooks", "an object").message,
"Configuration key hooks must be an object, found a number."
);
assert_eq!(
wrong_kind(&value("null"), "hooks", "an object").message,
"Configuration key hooks must not be null; remove it to use the default or give it an object."
);
assert_eq!(
wrong_kind(&value("[]"), "", "an object").message,
"The configuration document must be an object, found an array."
);
assert_eq!(
wrong_kind(&value("null"), "", "an object").message,
"The configuration document must not be null; remove it to use the default or give it an object."
);
}
#[test]
fn member_keys_decode_escapes_in_source_order() {
let document = value(r#"{ "b": 1, "a": 2, "": 3 }"#);
let entries = document.entries().expect("object fixture");
assert_eq!(
member_keys(entries, "").expect("distinct keys"),
vec![String::from("b"), String::from("a"), String::new()]
);
}
#[test]
fn member_keys_reject_duplicates_after_decoding() {
for (source, path, expected) in [
(
r#"{ "hooks": {}, "hooks": {} }"#,
"",
"Configuration key hooks is defined more than once; keep exactly one definition.",
),
(
r#"{ "a": 1, "b": 2, "a": 3 }"#,
"policies",
"Configuration key policies.a is defined more than once; keep exactly one definition.",
),
] {
let document = value(source);
let entries = document.entries().expect("object fixture");
assert_eq!(
member_keys(entries, path).expect_err("duplicate").message,
expected
);
}
}
#[test]
fn member_keys_reject_unpaired_surrogates() {
let document = value(r#"{ "\ud800": 1 }"#);
let entries = document.entries().expect("object fixture");
let top = member_keys(entries, "")
.expect_err("lone surrogate")
.message;
assert!(
top.starts_with("A key under the configuration document is not valid Unicode text"),
"{top}"
);
let nested = member_keys(entries, "policies")
.expect_err("lone surrogate")
.message;
assert!(
nested.starts_with("A key under policies is not valid Unicode text"),
"{nested}"
);
}
#[test]
fn boolean_accepts_only_json_booleans() {
assert_eq!(boolean(&value("true"), "k"), Ok(true));
assert_eq!(boolean(&value("false"), "k"), Ok(false));
for source in ["\"true\"", "1", "0", "[]", "{}", "null"] {
let message = boolean(&value(source), "hooks.concurrentCommits")
.expect_err(source)
.message;
assert!(
message.starts_with("Configuration key hooks.concurrentCommits must"),
"{message}"
);
assert!(message.contains("true or false"), "{message}");
}
}
#[test]
fn text_accepts_only_valid_unicode_strings() {
assert_eq!(text(&value(r#""ab\n""#), "k"), Ok(String::from("ab\n")));
assert_eq!(text(&value("\"\""), "k"), Ok(String::new()));
assert_eq!(
text(&value("3"), "policies.x").expect_err("number").message,
"Configuration key policies.x must be a string, found a number."
);
let surrogate = text(&value(r#""\udfff""#), "policies.x")
.expect_err("lone surrogate")
.message;
assert!(
surrogate.starts_with("Configuration key policies.x is not valid Unicode text"),
"{surrogate}"
);
}
#[test]
fn strings_require_an_array_of_strings() {
assert_eq!(
strings(&value(r#"["b", "a", "b"]"#), "k"),
Ok(vec![
String::from("b"),
String::from("a"),
String::from("b")
])
);
assert_eq!(strings(&value("[]"), "k"), Ok(Vec::<String>::new()));
assert_eq!(
strings(&value("\"a\""), "p.exclude")
.expect_err("string")
.message,
"Configuration key p.exclude must be an array of strings, found a string."
);
assert_eq!(
strings(&value(r#"["a", 2]"#), "p.exclude")
.expect_err("number element")
.message,
"Configuration key p.exclude[1] must be a string, found a number."
);
assert_eq!(
strings(&value(r#"[null]"#), "p.exclude")
.expect_err("null element")
.message,
"Configuration key p.exclude[0] must not be null; remove it to use the default or give it a string."
);
}
#[test]
fn safe_integer_accepts_exact_whole_numbers_within_bounds() {
for (source, minimum, expected) in [
("0", 0, 0),
("-0", 0, 0),
("0e9", 0, 0),
("0.0", 0, 0),
("1", 1, 1),
("1000", 0, 1000),
("1e3", 0, 1000),
("2.0", 0, 2),
("1.5e1", 0, 15),
("9007199254740991", 0, MAX_SAFE_INTEGER),
("9.007199254740991e15", 1, MAX_SAFE_INTEGER),
("5", 5, 5),
] {
assert_eq!(
safe_integer(&value(source), "k", minimum),
Ok(expected),
"{source}"
);
}
}
#[test]
fn safe_integer_rejects_inexact_or_out_of_range_values() {
for (source, minimum) in [
("-1", 0),
("1.5", 0),
("1e-1", 0),
("0.1", 0),
("9007199254740992", 0),
("1e16", 0),
("1e400", 0),
("18446744073709551616", 0),
("99999999999999999999999999", 0),
("0", 1),
("4", 5),
("\"3\"", 0),
("true", 0),
("[1]", 0),
("null", 0),
] {
let message = safe_integer(&value(source), "landing.reserveAfterLostRaces", minimum)
.expect_err(source)
.message;
assert!(
message.starts_with("Configuration key landing.reserveAfterLostRaces must"),
"{source}: {message}"
);
assert!(
message.contains(format!("a whole number from {minimum} to 9007199254740991").as_str()),
"{source}: {message}"
);
}
assert_eq!(
safe_integer(&value("1.5"), "k", 0)
.expect_err("fraction")
.message,
"Configuration key k must be a whole number from 0 to 9007199254740991, found 1.5."
);
}