use cratefield_tables::{
ErrorCode, MAX_DETAIL_ERRORS, MAX_UNKNOWN_KEY_CHARS, Schema, normalize_row, validate_row,
};
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Manifest {
tables: Schema,
}
fn schema(fragment: &str) -> Schema {
toml::from_str::<Manifest>(fragment)
.expect("the fragment parses")
.tables
}
const POST: &str = r#"
[tables.post]
primary_key = "id"
[[tables.post.fields]]
name = "id"
kind = "text"
required = true
[[tables.post.fields]]
name = "title"
kind = "text"
min_len = 1
max_len = 5
required = true
[[tables.post.fields]]
name = "status"
kind = "enum"
values = ["draft", "published"]
"#;
const TYPED: &str = r#"
[tables.thing]
primary_key = "id"
[[tables.thing.fields]]
name = "id"
kind = "text"
required = true
[[tables.thing.fields]]
name = "count"
kind = "integer"
[[tables.thing.fields]]
name = "email"
kind = "text"
format = "email"
[[tables.thing.fields]]
name = "site"
kind = "text"
"#;
#[test]
fn a_rejection_becomes_the_validation_failed_problem_core_already_publishes() {
let schema = schema(POST);
let table = schema.table("post").unwrap();
let errors =
validate_row(table, &json!({"id": "x", "title": "far too long"})).expect_err("too long");
let problem = errors.problem();
assert_eq!(problem.slug, "validation-failed");
assert_eq!(problem.status.as_u16(), 400);
assert_eq!(
problem.detail.as_deref(),
Some("title is 12 characters, longer than the maximum of 5")
);
assert_eq!(
problem.type_uri("https://test.example/problems/"),
"https://test.example/problems/validation-failed"
);
let converted: cratefield_core::Problem = errors.into();
assert_eq!(converted.slug, "validation-failed");
}
#[test]
fn the_detail_joins_every_reason_in_order() {
let schema = schema(POST);
let table = schema.table("post").unwrap();
let errors =
validate_row(table, &json!({"status": "archived", "extra": 1})).expect_err("three reasons");
assert_eq!(
errors.detail(),
"id is required; title is required; status is not one of draft, published; \
extra is not a declared field of `post`"
);
}
#[test]
fn a_row_that_is_not_an_object_is_reported_against_the_row_itself() {
let schema = schema(POST);
let table = schema.table("post").unwrap();
let errors = validate_row(table, &json!([1, 2])).expect_err("not an object");
assert_eq!(errors.errors().len(), 1);
assert_eq!(errors.errors()[0].field, "");
assert_eq!(errors.errors()[0].code, ErrorCode::NotAnObject);
assert_eq!(
errors.detail(),
"the row must be a JSON object, not an array"
);
}
#[test]
fn a_long_list_is_summarised_in_the_detail_but_never_truncated_in_the_list() {
let schema = schema(POST);
let table = schema.table("post").unwrap();
let mut row = serde_json::Map::new();
row.insert("id".to_owned(), json!("x"));
row.insert("title".to_owned(), json!("ok"));
for index in 0..20 {
row.insert(format!("extra{index:02}"), json!(1));
}
let errors =
validate_row(table, &serde_json::Value::Object(row)).expect_err("twenty unknown keys");
assert_eq!(errors.errors().len(), 20, "the list keeps every reason");
assert_eq!(
errors.detail().matches("is not a declared field").count(),
MAX_DETAIL_ERRORS
);
assert!(errors.detail().ends_with("(and 10 more)"), "{errors}");
}
#[test]
fn uniqueness_is_not_a_row_check() {
let schema = schema(
r#"
[tables.post]
[[tables.post.fields]]
name = "id"
kind = "text"
required = true
[[tables.post.fields]]
name = "slug"
kind = "text"
unique = true
"#,
);
let table = schema.table("post").unwrap();
for _ in 0..2 {
validate_row(table, &json!({"id": "x", "slug": "same"})).expect("no uniqueness check here");
}
}
#[test]
fn an_integer_outside_i64_is_refused_rather_than_truncated() {
let schema = schema(TYPED);
let table = schema.table("thing").expect("declared");
for raw in [
"9223372036854775808",
"9223372036854775809",
"-9223372036854775809",
"-9223372036854775808.0",
"1e19",
"-1e19",
] {
let row: serde_json::Value =
serde_json::from_str(&format!(r#"{{"id":"a","count":{raw}}}"#)).expect("json");
let errors = validate_row(table, &row)
.expect_err(&format!("{raw} is not representable and must be refused"));
assert_eq!(errors.errors()[0].code, ErrorCode::WrongType, "{raw}");
}
for raw in [
"9223372036854775807",
"-9223372036854775808",
"42.0",
"1e18",
"0",
] {
let row: serde_json::Value =
serde_json::from_str(&format!(r#"{{"id":"a","count":{raw}}}"#)).expect("json");
assert!(validate_row(table, &row).is_ok(), "{raw} must be accepted");
}
}
#[test]
fn an_unknown_key_is_echoed_back_bounded() {
let schema = schema(POST);
let table = schema.table("post").expect("declared");
let long = "x".repeat(5000);
let row = json!({ "id": "a", "title": "t", &long: 1 });
let errors = validate_row(table, &row).expect_err("an unknown key is an error");
let echoed = &errors.errors()[0].field;
assert_eq!(errors.errors()[0].code, ErrorCode::UnknownField);
assert_eq!(
echoed.chars().count(),
MAX_UNKNOWN_KEY_CHARS + 1,
"the cut key plus its marker"
);
assert!(echoed.starts_with("xxxx") && echoed.ends_with('…'));
assert!(
errors.detail().len() < 500,
"the problem detail is still {} bytes",
errors.detail().len()
);
let wide = "\u{1f680}".repeat(200);
let row = json!({ "id": "a", "title": "t", &wide: 1 });
let errors = validate_row(table, &row).expect_err("an unknown key is an error");
let echoed = &errors.errors()[0].field;
assert_eq!(echoed.chars().count(), MAX_UNKNOWN_KEY_CHARS + 1);
assert!(
echoed
.chars()
.take(MAX_UNKNOWN_KEY_CHARS)
.all(|c| c == '\u{1f680}'),
"a code point was split: {echoed:?}"
);
let exact = "y".repeat(MAX_UNKNOWN_KEY_CHARS);
let row = json!({ "id": "a", "title": "t", &exact: 1 });
let errors = validate_row(table, &row).expect_err("an unknown key is an error");
assert_eq!(errors.errors()[0].field, exact);
}
#[test]
fn normalize_row_makes_unique_mean_what_it_means_everywhere_else() {
let schema = schema(TYPED);
let table = schema.table("thing").expect("declared");
let written = normalize_row(
table,
json!({ "id": "a", "email": " Alice@Example.COM " }),
);
assert_eq!(written["email"], json!("alice@example.com"));
assert_eq!(normalize_row(table, written.clone()), written);
let untouched = normalize_row(table, json!({ "id": " A ", "site": "HTTPS://X.COM" }));
assert_eq!(untouched["id"], json!(" A "));
assert_eq!(untouched["site"], json!("HTTPS://X.COM"));
let wrong = normalize_row(table, json!({ "id": "a", "email": 42 }));
assert_eq!(wrong["email"], json!(42));
assert!(validate_row(table, &wrong).is_err());
let bad = normalize_row(table, json!({ "id": "a", "email": "NOT AN ADDRESS" }));
assert!(validate_row(table, &bad).is_err());
}