use kynos_openapi::{
Document, Severity, SpecError, SpecVersion, Violation,
emit::downgrade::three_two_only_constructs, validate::Validator,
};
use proptest::prelude::*;
#[path = "support/mod.rs"]
mod support;
use support::*;
fn to_json(document: &Document) -> String {
document
.to_json()
.expect("every generated value is representable in JSON")
}
fn parse(json: &str) -> Document {
serde_json::from_str(json).expect("what the model emits, the model reads")
}
fn rendered(violations: &[Violation]) -> Vec<String> {
let mut rendered: Vec<String> = violations.iter().map(ToString::to_string).collect();
rendered.sort();
rendered
}
fn rendered_errors(violations: &[Violation]) -> Vec<String> {
let mut rendered: Vec<String> = violations
.iter()
.filter(|violation| violation.severity == Severity::Error)
.map(ToString::to_string)
.collect();
rendered.sort();
rendered
}
fn unchecked_pointers(document: &Document) -> Vec<String> {
fn is_media_type_schema(tokens: &[String]) -> bool {
match tokens {
[.., parent, _, last] if parent == "content" => {
last == "schema" || last == "itemSchema"
}
[components, media_types, _, last]
if components == "components" && media_types == "mediaTypes" =>
{
last == "schema" || last == "itemSchema"
}
_ => false,
}
}
fn walk(value: &serde_json::Value, tokens: &mut Vec<String>, found: &mut Vec<String>) {
let pointer = |tokens: &[String]| {
tokens.iter().fold("#".to_owned(), |pointer, token| {
format!("{pointer}/{token}")
})
};
match value {
serde_json::Value::Object(map) => {
if map.contains_key("x-kynos-unchecked") {
found.push(pointer(tokens));
}
for (key, child) in map {
tokens.push(key.replace('~', "~0").replace('/', "~1"));
walk(child, tokens, found);
tokens.pop();
}
}
serde_json::Value::Array(items) => {
for (index, child) in items.iter().enumerate() {
tokens.push(index.to_string());
walk(child, tokens, found);
tokens.pop();
}
}
serde_json::Value::Bool(true) if is_media_type_schema(tokens) => {
found.push(pointer(tokens));
}
_ => {}
}
}
let json = serde_json::to_value(document).expect("every generated value is representable");
let mut found = Vec::new();
walk(&json, &mut Vec::new(), &mut found);
found.sort();
found
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn a_document_survives_a_json_round_trip(document in arb_document()) {
prop_assert_eq!(parse(&to_json(&document)), document);
}
#[test]
fn serialization_is_deterministic(document in arb_document()) {
let json = to_json(&document);
prop_assert_eq!(to_json(&document.clone()), json.clone());
prop_assert_eq!(to_json(&parse(&json)), json);
}
#[cfg(feature = "yaml")]
#[test]
fn yaml_emission_is_what_serde_yaml_ng_writes_for_the_model(document in arb_document()) {
prop_assert_eq!(
document.to_yaml().expect("every generated value is representable in YAML"),
serde_yaml_ng::to_string(&document).expect("the model serializes to YAML")
);
}
#[test]
fn validation_is_total(document in arb_document()) {
for &version in VERSIONS {
let validator = Validator::new(version);
let violations = validator.validate(&document);
prop_assert_eq!(rendered(&validator.validate(&document)), rendered(&violations));
let errors = document.validate(version).err().unwrap_or_default();
prop_assert_eq!(rendered(&errors), rendered_errors(&violations));
for violation in &violations {
prop_assert!(violation.location.starts_with('#'));
}
}
}
#[test]
fn every_unchecked_schema_is_reported_once_where_it_sits(document in arb_document()) {
let expected = unchecked_pointers(&document);
for &version in VERSIONS {
let mut reported: Vec<String> = Validator::new(version)
.validate(&document)
.into_iter()
.filter(|violation| violation.error == SpecError::UncheckedSchema)
.map(|violation| violation.location)
.collect();
reported.sort();
prop_assert_eq!(&reported, &expected);
}
}
#[test]
fn emitting_refuses_a_lossy_downgrade(document in arb_document()) {
let blockers = three_two_only_constructs(&document);
match document.emit(SpecVersion::V3_1) {
Ok(emitted) => {
prop_assert!(blockers.is_empty());
prop_assert_eq!(&emitted.openapi, "3.1.2");
prop_assert_eq!(emitted.emit(SpecVersion::V3_1).ok(), Some(emitted.clone()));
prop_assert_eq!(three_two_only_constructs(&emitted), blockers);
}
Err(error) => {
prop_assert!(!blockers.is_empty());
prop_assert_eq!(error, SpecError::RequiresV3_2 { blockers });
}
}
}
#[cfg(feature = "openapi32")]
#[test]
fn emitting_as_the_newer_version_always_succeeds(document in arb_document()) {
let emitted = document.emit(SpecVersion::V3_2).expect("3.2 expresses everything");
prop_assert_eq!(&emitted.openapi, "3.2.0");
prop_assert_eq!(emitted.emit(SpecVersion::V3_2).ok(), Some(emitted.clone()));
prop_assert_eq!(emitted.spec_version(), Some(SpecVersion::V3_2));
}
}