#![expect(
clippy::expect_used,
reason = "a test that cannot build its fixture should fail loudly and name it"
)]
use typed_openapi::Document;
const TOY: &str = include_str!("fixtures/toy.yaml");
const CORRECTIONS: &str = include_str!("fixtures/corrections.yaml");
const CLI: &str = include_str!("fixtures/cli.yaml");
const OVERLAYS: &[&str] = &[CORRECTIONS, CLI];
fn mutated(from: &str, to: &str) -> String {
assert!(
TOY.contains(from),
"the fixture no longer contains `{from}`"
);
TOY.replacen(from, to, 1)
}
fn bless(document: &str) -> Result<serde_json::Value, typed_openapi::overlay::OverlayError> {
let mut doc = typed_openapi::overlay::parse(document)?;
for layer in OVERLAYS {
doc = typed_openapi::overlay::apply(doc, layer)?;
}
Ok(doc)
}
#[test]
fn the_unmutated_document_blesses() {
assert!(bless(TOY).is_ok());
}
#[test]
fn renaming_the_corrected_field_fails_the_bless() {
let error = bless(&mutated(" total:\n", " amount:\n"))
.expect_err("the Overlay corrects a field that is no longer there");
assert!(error.to_string().contains("does not apply"), "{error}");
}
#[test]
fn retyping_the_corrected_field_fails_the_bless() {
let error = bless(&mutated(
" type: string\n format: money\n",
" type: number\n",
))
.expect_err("the Overlay corrects a shape that is no longer there");
assert!(error.to_string().contains("does not apply"), "{error}");
}
#[test]
fn retyping_the_corrected_currency_fails_the_bless() {
let error = bless(&mutated(
" currency:\n type: string\n description: ISO 4217 code\n",
" currency:\n $ref: '#/components/schemas/CurrencyCode'\n",
))
.expect_err("the Overlay states a rule for a shape that is no longer there");
assert!(error.to_string().contains("does not apply"), "{error}");
}
#[test]
fn moving_a_gated_operation_to_another_method_fails_the_bless() {
let error = bless(&mutated(
" /vouchers/{id}/enshrine:\n post:\n",
" /vouchers/{id}/enshrine:\n put:\n",
))
.expect_err("the Overlay names a gate on an operation that is no longer a POST");
assert!(error.to_string().contains("does not apply"), "{error}");
}
#[test]
fn removing_a_gated_operation_fails_the_bless() {
let error = bless(&without(
" /vouchers/{id}/enshrine:",
" /vouchers/{id}/render:",
))
.expect_err("the Overlay names a gate on an operation that is gone");
assert!(error.to_string().contains("does not apply"), "{error}");
}
#[test]
fn removing_an_operation_the_overlay_does_not_mention_passes_the_bless() {
let doc = Document::load(&without(" /contacts:", " /documents:"), OVERLAYS)
.expect("the document still loads");
assert!(doc.get("createContact").is_none());
assert!(doc.get("createVoucher").is_some());
}
fn without(path: &str, next: &str) -> String {
let before = TOY
.split(path)
.next()
.unwrap_or_else(|| panic!("the fixture declares `{path}`"));
let after = TOY
.split(next)
.nth(1)
.unwrap_or_else(|| panic!("the fixture declares `{next}`"));
format!("{before}{next}{after}")
}
#[test]
fn adding_or_removing_an_unrelated_field_passes_the_bless() {
let added = mutated(
" currency:\n",
" note:\n type: string\n currency:\n",
);
let doc = Document::load(&added, OVERLAYS).expect("the document still loads");
let typed_openapi::model::Body::JsonFields(fields) =
doc.get("createVoucher").expect("createVoucher").body()
else {
panic!("createVoucher takes a flat JSON body");
};
assert!(fields.iter().any(|f| f.name() == "note"));
let removed = mutated(
" id:\n type: integer\n format: int64\n \
description: Server-assigned id\n",
"",
);
assert!(Document::load(&removed, OVERLAYS).is_ok());
}