use serde_json::{json, Value};
use ucp_schema::{resolve, Direction, ResolveError, ResolveOptions};
mod visibility_parsing {
use super::*;
#[test]
fn shorthand_string() {
let schema = json!({
"type": "object",
"properties": {
"status": { "type": "string", "ucp_request": "omit" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("status").is_none());
}
#[test]
fn object_form() {
let schema = json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_request": {
"create": "omit",
"update": "required"
}
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_none());
let options = ResolveOptions::new(Direction::Request, "update");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_some());
assert!(result["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
}
#[test]
fn missing_annotation_defaults_to_include() {
let schema = json!({
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("name").is_some());
assert!(result["required"]
.as_array()
.unwrap()
.contains(&json!("name")));
}
#[test]
fn missing_operation_defaults_to_include() {
let schema = json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_request": { "create": "omit" }
}
}
});
let options = ResolveOptions::new(Direction::Request, "read");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_some());
}
#[test]
fn both_request_and_response_annotations() {
let schema = json!({
"type": "object",
"properties": {
"context": {
"type": "object",
"ucp_request": "optional",
"ucp_response": "omit"
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("context").is_some());
let options = ResolveOptions::new(Direction::Response, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("context").is_none());
}
}
mod error_handling {
use super::*;
#[test]
fn invalid_annotation_type_errors() {
let schema = json!({
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": 123 }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options);
assert!(matches!(
result,
Err(ResolveError::InvalidAnnotationType { .. })
));
}
#[test]
fn unknown_visibility_errors() {
let schema = json!({
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": "readonly" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options);
assert!(matches!(
result,
Err(ResolveError::UnknownVisibility { value, .. }) if value == "readonly"
));
}
#[test]
fn unknown_visibility_in_dict_errors() {
let schema = json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_request": { "create": "maybe" }
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options);
assert!(matches!(
result,
Err(ResolveError::UnknownVisibility { value, .. }) if value == "maybe"
));
}
}
mod operation_normalization {
use super::*;
#[test]
fn operations_are_case_insensitive() {
let schema = json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_request": { "create": "omit" }
}
}
});
let options = ResolveOptions::new(Direction::Request, "Create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_none());
let options = ResolveOptions::new(Direction::Request, "CREATE");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_none());
}
}
mod transformation {
use super::*;
#[test]
fn omit_removes_field_from_properties() {
let schema = json!({
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_none());
assert!(result["properties"].get("name").is_some());
}
#[test]
fn omit_removes_field_from_required() {
let schema = json!({
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
let required = result["required"].as_array().unwrap();
assert!(!required.contains(&json!("id")));
assert!(required.contains(&json!("name")));
}
#[test]
fn required_adds_to_required_array() {
let schema = json!({
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": "required" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
let required = result["required"].as_array().unwrap();
assert!(required.contains(&json!("id")));
}
#[test]
fn optional_removes_from_required_array() {
let schema = json!({
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string", "ucp_request": "optional" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
let required = result["required"].as_array().unwrap();
assert!(!required.contains(&json!("id")));
}
#[test]
fn include_preserves_original_state() {
let schema = json!({
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_some());
assert!(result["properties"].get("name").is_some());
let required = result["required"].as_array().unwrap();
assert!(required.contains(&json!("id")));
assert!(!required.contains(&json!("name")));
}
#[test]
fn all_fields_omitted_yields_empty_schema() {
let schema = json!({
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string", "ucp_request": "omit" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["properties"], json!({}));
assert_eq!(result["required"], json!([]));
}
#[test]
fn annotations_stripped_from_output() {
let schema = json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_request": "required",
"ucp_response": "omit"
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"]["id"].get("ucp_request").is_none());
assert!(result["properties"]["id"].get("ucp_response").is_none());
}
}
mod required_array {
use super::*;
#[test]
fn omitted_field_removed_from_required() {
let schema = json!({
"type": "object",
"required": ["id", "name", "email"],
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"name": { "type": "string" },
"email": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
let required = result["required"].as_array().unwrap();
assert!(!required.contains(&json!("id")));
assert!(required.contains(&json!("name")));
assert!(required.contains(&json!("email")));
}
#[test]
fn required_field_added_to_required() {
let schema = json!({
"type": "object",
"required": ["name"],
"properties": {
"id": { "type": "string", "ucp_request": "required" },
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
let required = result["required"].as_array().unwrap();
assert!(required.contains(&json!("id")));
assert!(required.contains(&json!("name")));
}
#[test]
fn unrelated_required_fields_preserved() {
let schema = json!({
"type": "object",
"required": ["name", "email"],
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"name": { "type": "string" },
"email": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
let required = result["required"].as_array().unwrap();
assert!(required.contains(&json!("name")));
assert!(required.contains(&json!("email")));
}
}
mod recursion {
use super::*;
#[test]
fn nested_properties() {
let schema = json!({
"type": "object",
"properties": {
"buyer": {
"type": "object",
"properties": {
"email": {
"type": "string",
"ucp_request": { "create": "required" }
},
"phone": {
"type": "string",
"ucp_request": { "create": "omit" }
}
}
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"]["buyer"]["properties"]
.get("email")
.is_some());
let buyer_required = result["properties"]["buyer"]["required"]
.as_array()
.unwrap();
assert!(buyer_required.contains(&json!("email")));
assert!(result["properties"]["buyer"]["properties"]
.get("phone")
.is_none());
}
#[test]
fn array_items() {
let schema = json!({
"type": "object",
"properties": {
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string", "ucp_request": "required" },
"price": { "type": "number", "ucp_request": "omit" }
}
}
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"]["line_items"]["items"]["properties"]
.get("sku")
.is_some());
assert!(result["properties"]["line_items"]["items"]["properties"]
.get("price")
.is_none());
}
#[test]
fn defs() {
let schema = json!({
"type": "object",
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"internal_id": { "type": "string", "ucp_request": "omit" }
}
}
},
"properties": {
"shipping": { "$ref": "#/$defs/address" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["$defs"]["address"]["properties"]
.get("street")
.is_some());
assert!(result["$defs"]["address"]["properties"]
.get("internal_id")
.is_none());
}
#[test]
fn deep_nesting_five_levels() {
let schema = json!({
"type": "object",
"properties": {
"level1": {
"type": "object",
"properties": {
"level2": {
"type": "object",
"properties": {
"level3": {
"type": "object",
"properties": {
"level4": {
"type": "object",
"properties": {
"level5": {
"type": "string",
"ucp_request": "omit"
}
}
}
}
}
}
}
}
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(
result["properties"]["level1"]["properties"]["level2"]["properties"]["level3"]
["properties"]["level4"]["properties"]
.get("level5")
.is_none()
);
}
}
mod composition {
use super::*;
#[test]
fn allof_transforms_each_branch() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": "omit" }
}
},
{
"type": "object",
"properties": {
"name": { "type": "string", "ucp_request": "required" }
}
}
]
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_none());
assert!(result["allOf"][1]["properties"].get("name").is_some());
assert!(result["allOf"][1]["required"]
.as_array()
.unwrap()
.contains(&json!("name")));
}
#[test]
fn allof_tighten_omit_to_required() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": { "create": "omit" } }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": { "create": "required" } }
}
}
]
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_none());
assert!(result["allOf"][1]["properties"].get("id").is_some());
assert!(result["allOf"][1]["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
}
#[test]
fn allof_loosen_required_to_omit_base_wins() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": { "create": "required" } }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_request": { "create": "omit" } }
}
}
]
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_some());
assert!(result["allOf"][0]["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
assert!(result["allOf"][1]["properties"].get("id").is_none());
}
#[test]
fn anyof_transforms_each_branch() {
let schema = json!({
"anyOf": [
{
"type": "object",
"properties": {
"card": { "type": "object", "ucp_request": "required" }
}
},
{
"type": "object",
"properties": {
"token": { "type": "string", "ucp_request": "required" }
}
}
]
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["anyOf"][0]["properties"].get("card").is_some());
assert!(result["anyOf"][1]["properties"].get("token").is_some());
}
#[test]
fn oneof_transforms_each_branch() {
let schema = json!({
"oneOf": [
{
"type": "object",
"properties": {
"type": { "const": "credit_card" },
"number": { "type": "string", "ucp_request": "required" }
}
},
{
"type": "object",
"properties": {
"type": { "const": "bank_account" },
"routing": { "type": "string", "ucp_request": "required" }
}
}
]
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["oneOf"][0]["properties"].get("number").is_some());
assert!(result["oneOf"][1]["properties"].get("routing").is_some());
}
}
mod allof_propagation {
use super::*;
#[test]
fn extension_annotation_propagates_to_base() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let options = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &options).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_none());
assert!(result["allOf"][1]["properties"].get("id").is_none());
}
#[test]
fn per_operation_propagation() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"expensive": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"expensive": {
"type": "string",
"ucp_response": { "search": "omit", "get_product": "required" }
}
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("expensive").is_none());
let opts = ResolveOptions::new(Direction::Response, "get_product");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("expensive").is_some());
assert!(result["allOf"][0]["required"]
.as_array()
.unwrap()
.contains(&json!("expensive")));
}
#[test]
fn last_writer_wins_across_branches() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"status": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"status": { "type": "string", "ucp_response": "required" }
}
},
{
"type": "object",
"properties": {
"status": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("status").is_none());
}
#[test]
fn own_annotation_not_overwritten() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "required" }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_some());
assert!(result["allOf"][0]["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
assert!(result["allOf"][1]["properties"].get("id").is_none());
}
#[test]
fn no_annotations_no_propagation() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"name": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"tag": { "type": "string" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("name").is_some());
assert!(result["allOf"][1]["properties"].get("tag").is_some());
}
#[test]
fn monotonicity_required_to_omit_rejected() {
let schema = json!({
"allOf": [
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts);
assert!(matches!(
result,
Err(ResolveError::MonotonicityViolation { .. })
));
}
#[test]
fn monotonicity_required_to_optional_rejected() {
let schema = json!({
"allOf": [
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "optional" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts);
assert!(matches!(
result,
Err(ResolveError::MonotonicityViolation { .. })
));
}
#[test]
fn monotonicity_required_to_required_ok() {
let schema = json!({
"allOf": [
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "required" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_some());
assert!(result["allOf"][0]["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
}
#[test]
fn monotonicity_optional_to_omit_ok() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"tag": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"tag": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("tag").is_none());
}
#[test]
fn monotonicity_per_op_some_ok_some_not() {
let schema = json!({
"allOf": [
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_response": {
"search": "omit",
"get_product": "required"
}
}
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
assert!(matches!(
resolve(&schema, &opts),
Err(ResolveError::MonotonicityViolation { .. })
));
let opts = ResolveOptions::new(Direction::Response, "get_product");
assert!(resolve(&schema, &opts).is_ok());
}
#[test]
fn type_conflict_detected() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"count": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"count": { "type": "number" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
assert!(matches!(
resolve(&schema, &opts),
Err(ResolveError::TypeConflict { .. })
));
}
#[test]
fn same_type_no_conflict() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
assert!(resolve(&schema, &opts).is_ok());
}
#[test]
fn disjoint_properties_no_conflict() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"count": { "type": "number" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
assert!(resolve(&schema, &opts).is_ok());
}
#[test]
fn branch_without_properties_is_skipped() {
let schema = json!({
"allOf": [
{ "$ref": "#/$defs/base" },
{
"type": "object",
"properties": {
"extra": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0].get("$ref").is_some());
assert!(result["allOf"][1]["properties"].get("extra").is_none());
}
#[test]
fn single_branch_allof() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_none());
}
#[test]
fn non_object_property_in_branch_skipped() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": "not_an_object"
}
},
{
"type": "object",
"properties": {
"id": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert_eq!(
result["allOf"][0]["properties"]["id"],
json!("not_an_object")
);
}
#[test]
fn ghost_field_annotation_ignored() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"phantom": { "type": "string", "ucp_response": "omit" }
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("id").is_some());
assert!(result["allOf"][0]["properties"].get("phantom").is_none());
assert!(result["allOf"][1]["properties"].get("phantom").is_none());
}
#[test]
fn per_op_split_across_branches() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"data": {
"type": "string",
"ucp_response": { "search": "omit" }
}
}
},
{
"type": "object",
"properties": {
"data": {
"type": "string",
"ucp_response": { "lookup": "omit" }
}
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("data").is_none());
let opts = ResolveOptions::new(Direction::Response, "lookup");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"].get("data").is_some());
}
#[test]
fn nested_objects_not_propagated() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"inner": {
"type": "object",
"properties": {
"deep": { "type": "string" }
}
}
}
},
{
"type": "object",
"properties": {
"inner": {
"type": "object",
"properties": {
"deep": { "type": "string", "ucp_response": "omit" }
}
}
}
}
]
});
let opts = ResolveOptions::new(Direction::Response, "search");
let result = resolve(&schema, &opts).unwrap();
assert!(result["allOf"][0]["properties"]["inner"]["properties"]
.get("deep")
.is_some());
assert!(result["allOf"][1]["properties"]["inner"]["properties"]
.get("deep")
.is_none());
}
}
mod additional_properties {
use super::*;
#[test]
fn false_preserved_after_filtering() {
let schema = json!({
"type": "object",
"additionalProperties": false,
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
assert!(result["properties"].get("id").is_none());
}
#[test]
fn true_becomes_false_in_strict_mode() {
let schema = json!({
"type": "object",
"additionalProperties": true,
"properties": {
"id": { "type": "string", "ucp_request": "omit" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
}
#[test]
fn true_unchanged_in_non_strict_mode() {
let schema = json!({
"type": "object",
"additionalProperties": true,
"properties": {
"id": { "type": "string", "ucp_request": "omit" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(false);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(true));
}
#[test]
fn schema_form_transformed() {
let schema = json!({
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"internal": { "type": "string", "ucp_request": "omit" }
}
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["additionalProperties"]["properties"]
.get("internal")
.is_none());
}
}
mod integration {
use super::*;
use std::fs;
use std::path::Path;
fn load_fixture(name: &str) -> Value {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name);
let content = fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Failed to read fixture: {}", path.display()));
serde_json::from_str(&content).expect("Failed to parse fixture JSON")
}
#[test]
fn checkout_create_request() {
let schema = load_fixture("checkout.json");
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_none());
assert!(result["properties"].get("line_items").is_some());
assert!(result["required"]
.as_array()
.unwrap()
.contains(&json!("line_items")));
assert!(result["properties"].get("buyer").is_some());
assert!(!result["required"]
.as_array()
.unwrap()
.contains(&json!("buyer")));
assert!(result["properties"].get("status").is_none());
assert!(result["properties"].get("totals").is_none());
}
#[test]
fn checkout_update_request() {
let schema = load_fixture("checkout.json");
let options = ResolveOptions::new(Direction::Request, "update");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_some());
assert!(result["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
assert!(result["properties"].get("line_items").is_some());
assert!(!result["required"]
.as_array()
.unwrap()
.contains(&json!("line_items")));
}
#[test]
fn checkout_read_request() {
let schema = load_fixture("checkout.json");
let options = ResolveOptions::new(Direction::Request, "read");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_some());
assert!(result["required"]
.as_array()
.unwrap()
.contains(&json!("id")));
assert!(result["properties"].get("line_items").is_none());
assert!(result["properties"].get("buyer").is_none());
}
#[test]
fn checkout_response() {
let schema = load_fixture("checkout.json");
let options = ResolveOptions::new(Direction::Response, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result["properties"].get("id").is_some());
assert!(result["properties"].get("status").is_some());
assert!(result["properties"].get("totals").is_some());
}
#[test]
fn invalid_annotation_type_from_file() {
let schema = load_fixture("invalid/bad_annotation_type.json");
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options);
assert!(matches!(
result,
Err(ResolveError::InvalidAnnotationType { .. })
));
}
#[test]
fn unknown_visibility_from_file() {
let schema = load_fixture("invalid/unknown_visibility.json");
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options);
assert!(matches!(
result,
Err(ResolveError::UnknownVisibility { .. })
));
}
}
mod strict_mode {
use super::*;
#[test]
fn default_is_not_strict() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create");
let result = resolve(&schema, &options).unwrap();
assert!(result.get("additionalProperties").is_none());
}
#[test]
fn injects_additional_properties_false() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
}
#[test]
fn preserves_explicit_false() {
let schema = json!({
"type": "object",
"additionalProperties": false,
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
}
#[test]
fn preserves_custom_schema() {
let schema = json!({
"type": "object",
"additionalProperties": { "type": "string" },
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!({ "type": "string" }));
}
#[test]
fn applies_to_nested_objects() {
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"city": { "type": "string" }
}
}
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
assert_eq!(
result["properties"]["address"]["additionalProperties"],
json!(false)
);
}
#[test]
fn applies_to_array_items() {
let schema = json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" }
}
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert!(result.get("additionalProperties").is_none());
assert_eq!(result["items"]["additionalProperties"], json!(false));
}
#[test]
fn applies_to_defs() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"$defs": {
"Address": {
"type": "object",
"properties": {
"city": { "type": "string" }
}
}
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(
result["$defs"]["Address"]["additionalProperties"],
json!(false)
);
}
#[test]
fn uses_unevaluated_for_allof() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
]
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["unevaluatedProperties"], json!(false));
assert!(result["allOf"][0].get("additionalProperties").is_none());
assert!(result["allOf"][1].get("additionalProperties").is_none());
}
#[test]
fn non_strict_mode_skips_injection() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(false);
let result = resolve(&schema, &options).unwrap();
assert!(result.get("additionalProperties").is_none());
}
#[test]
fn non_strict_mode_preserves_true() {
let schema = json!({
"type": "object",
"additionalProperties": true,
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(false);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(true));
}
#[test]
fn detects_object_by_properties_key() {
let schema = json!({
"properties": {
"name": { "type": "string" }
}
});
let options = ResolveOptions::new(Direction::Request, "create").strict(true);
let result = resolve(&schema, &options).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
}
}