284 lines
8.3 KiB
Rust
284 lines
8.3 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use schemars::JsonSchema;
|
|
use schemars::SchemaGenerator;
|
|
|
|
use crate::schema::SettingSchemaEntry;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fixture settings used for per-type schema validation tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn entries() -> Vec<&'static SettingSchemaEntry> {
|
|
inventory::iter::<SettingSchemaEntry>.into_iter().collect()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Invariant tests (run over all registered entries)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn at_least_one_entry_exists() {
|
|
assert!(
|
|
!entries().is_empty(),
|
|
"Expected at least one SettingSchemaEntry to be registered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_empty_storage_keys() {
|
|
for entry in entries() {
|
|
assert!(
|
|
!entry.storage_key.is_empty(),
|
|
"Found entry with empty storage_key"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn all_defaults_produce_valid_json() {
|
|
for entry in entries() {
|
|
let json = (entry.default_value_fn)();
|
|
assert!(
|
|
serde_json::from_str::<serde_json::Value>(&json).is_ok(),
|
|
"default_value_fn for '{}' produced invalid JSON: {json}",
|
|
entry.storage_key
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn all_schemas_are_serializable() {
|
|
let mut schema_gen = SchemaGenerator::default();
|
|
for entry in entries() {
|
|
let schema = (entry.schema_fn)(&mut schema_gen);
|
|
assert!(
|
|
serde_json::to_string(&schema).is_ok(),
|
|
"schema_fn for '{}' produced a non-serializable schema",
|
|
entry.storage_key
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn no_duplicate_storage_keys() {
|
|
// NOTE: when run in the settings crate alone, test modules may
|
|
// register fixture settings with overlapping keys (e.g. both
|
|
// mod_tests and macros_tests define "SimpleSetting"). This test
|
|
// is most useful when run from the app crate where real settings
|
|
// are registered. We still check for duplicates but collect them
|
|
// all before reporting, to give a clear picture.
|
|
let mut seen = HashSet::new();
|
|
let mut duplicates = Vec::new();
|
|
for entry in entries() {
|
|
if !seen.insert(entry.storage_key) {
|
|
duplicates.push(entry.storage_key);
|
|
}
|
|
}
|
|
// In the app crate context (no test-only fixtures), there should
|
|
// be zero duplicates.
|
|
#[cfg(not(test))]
|
|
assert!(
|
|
duplicates.is_empty(),
|
|
"Duplicate storage_keys: {duplicates:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn all_supported_platforms_fn_succeed() {
|
|
for entry in entries() {
|
|
// Just call it to ensure it doesn't panic.
|
|
let _ = (entry.supported_platforms_fn)();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hierarchy_values_are_well_formed() {
|
|
for entry in entries() {
|
|
if let Some(h) = entry.hierarchy {
|
|
assert!(
|
|
!h.starts_with('.') && !h.ends_with('.'),
|
|
"Hierarchy for '{}' has leading/trailing dots: '{h}'",
|
|
entry.storage_key
|
|
);
|
|
assert!(
|
|
!h.contains(".."),
|
|
"Hierarchy for '{}' contains consecutive dots: '{h}'",
|
|
entry.storage_key
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// $ref resolution test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn all_refs_resolve_to_definitions() {
|
|
let mut schema_gen = SchemaGenerator::default();
|
|
|
|
// Process all entries through the shared generator
|
|
for entry in entries() {
|
|
let _schema = (entry.schema_fn)(&mut schema_gen);
|
|
}
|
|
|
|
// Collect all $ref pointers from the schemas
|
|
fn collect_refs(value: &serde_json::Value, refs: &mut HashSet<String>) {
|
|
match value {
|
|
serde_json::Value::Object(map) => {
|
|
if let Some(serde_json::Value::String(r)) = map.get("$ref") {
|
|
refs.insert(r.clone());
|
|
}
|
|
for v in map.values() {
|
|
collect_refs(v, refs);
|
|
}
|
|
}
|
|
serde_json::Value::Array(arr) => {
|
|
for v in arr {
|
|
collect_refs(v, refs);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Re-process to collect refs from the output
|
|
let mut schema_gen2 = SchemaGenerator::default();
|
|
let mut all_refs = HashSet::new();
|
|
for entry in entries() {
|
|
let schema = (entry.schema_fn)(&mut schema_gen2);
|
|
let value = serde_json::to_value(&schema).unwrap();
|
|
collect_refs(&value, &mut all_refs);
|
|
}
|
|
|
|
let defs = schema_gen2.definitions();
|
|
for r in &all_refs {
|
|
// schemars 1.x uses "#/$defs/TypeName"
|
|
if let Some(type_name) = r.strip_prefix("#/$defs/") {
|
|
assert!(
|
|
defs.contains_key(type_name),
|
|
"$ref '{r}' does not resolve to any definition. Available: {:?}",
|
|
defs.keys().collect::<Vec<_>>()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Per-type schema validation (using fixture settings)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Helper to generate a schema for a type and return it as a JSON value.
|
|
fn schema_value_for<T: JsonSchema>() -> serde_json::Value {
|
|
let mut schema_gen = SchemaGenerator::default();
|
|
let schema = T::json_schema(&mut schema_gen);
|
|
schema.to_value()
|
|
}
|
|
|
|
#[test]
|
|
fn bool_schema() {
|
|
let v = schema_value_for::<bool>();
|
|
assert_eq!(v.get("type").and_then(|t| t.as_str()), Some("boolean"));
|
|
}
|
|
|
|
#[test]
|
|
fn string_schema() {
|
|
let v = schema_value_for::<String>();
|
|
assert_eq!(v.get("type").and_then(|t| t.as_str()), Some("string"));
|
|
}
|
|
|
|
#[test]
|
|
fn simple_enum_schema() {
|
|
#[derive(JsonSchema)]
|
|
#[allow(dead_code)]
|
|
enum SimpleEnum {
|
|
A,
|
|
B,
|
|
C,
|
|
}
|
|
|
|
let v = schema_value_for::<SimpleEnum>();
|
|
let variants = v.get("enum").and_then(|e| e.as_array());
|
|
assert!(variants.is_some(), "Expected enum array in schema");
|
|
let names: Vec<&str> = variants
|
|
.unwrap()
|
|
.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.collect();
|
|
assert_eq!(names, vec!["A", "B", "C"]);
|
|
}
|
|
|
|
#[test]
|
|
fn optional_field_schema() {
|
|
let v = schema_value_for::<Option<String>>();
|
|
// schemars 0.8 may represent Option<T> as:
|
|
// - {"anyOf": [...]}
|
|
// - {"type": ["string", "null"]}
|
|
let has_any_of = v.get("anyOf").is_some();
|
|
let has_one_of = v.get("oneOf").is_some();
|
|
let has_nullable_type = v
|
|
.get("type")
|
|
.and_then(|t| t.as_array())
|
|
.is_some_and(|arr| arr.iter().any(|t| t.as_str() == Some("null")));
|
|
assert!(
|
|
has_any_of || has_one_of || has_nullable_type,
|
|
"Expected anyOf, oneOf, or nullable type array for Option<String>, got: {v}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn struct_schema() {
|
|
#[derive(JsonSchema)]
|
|
#[allow(dead_code)]
|
|
struct MyStruct {
|
|
name: String,
|
|
count: u32,
|
|
}
|
|
|
|
let v = schema_value_for::<MyStruct>();
|
|
assert_eq!(v.get("type").and_then(|t| t.as_str()), Some("object"));
|
|
let props = v.get("properties").and_then(|p| p.as_object());
|
|
assert!(props.is_some(), "Expected properties in struct schema");
|
|
let props = props.unwrap();
|
|
assert!(props.contains_key("name"));
|
|
assert!(props.contains_key("count"));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_struct_ref_resolves() {
|
|
#[derive(JsonSchema)]
|
|
#[allow(dead_code)]
|
|
struct Inner {
|
|
value: i32,
|
|
}
|
|
|
|
#[derive(JsonSchema)]
|
|
#[allow(dead_code)]
|
|
struct Outer {
|
|
inner: Inner,
|
|
}
|
|
|
|
let mut schema_gen = SchemaGenerator::default();
|
|
let schema = Outer::json_schema(&mut schema_gen);
|
|
let v = serde_json::to_value(schema).unwrap();
|
|
|
|
// The inner field should be a $ref
|
|
let inner_prop = v
|
|
.pointer("/properties/inner")
|
|
.expect("Expected inner property");
|
|
|
|
let has_ref = inner_prop.get("$ref").is_some() || inner_prop.get("allOf").is_some();
|
|
assert!(
|
|
has_ref,
|
|
"Expected $ref or allOf for nested struct, got: {inner_prop}"
|
|
);
|
|
|
|
// The definition should exist
|
|
let defs = schema_gen.definitions();
|
|
assert!(
|
|
defs.contains_key("Inner"),
|
|
"Expected 'Inner' in definitions"
|
|
);
|
|
}
|