768 lines
25 KiB
Rust
768 lines
25 KiB
Rust
mod normalization_details {
|
|
use crank_import::rest::{
|
|
ImportFindingSeverity, ImportParseError, NormalizationConfig, normalize_verified_document,
|
|
preview_document, preview_document_legacy_v1,
|
|
};
|
|
|
|
fn normalize_document(
|
|
document: &str,
|
|
config: &NormalizationConfig,
|
|
) -> Result<crank_import::rest::NormalizedIr, ImportParseError> {
|
|
normalize_verified_document(
|
|
document,
|
|
crank_import::rest::SourceDigest::parse("b".repeat(64)).unwrap(),
|
|
config,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn operation_identity_does_not_depend_on_operation_id() {
|
|
let document = r#"
|
|
openapi: 3.0.3
|
|
info: { title: Identity }
|
|
paths:
|
|
/same:
|
|
get:
|
|
operationId: duplicate
|
|
responses: { '200': { description: ok } }
|
|
post:
|
|
operationId: duplicate
|
|
responses: { '201': { description: ok } }
|
|
"#;
|
|
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
assert_ne!(ir.operations[0].stable_id, ir.operations[1].stable_id);
|
|
assert_eq!(ir.operations[0].key, "GET /same");
|
|
assert_eq!(ir.operations[1].key, "POST /same");
|
|
}
|
|
|
|
#[test]
|
|
fn blank_and_duplicate_operation_ids_have_exact_findings_and_path_servers_win() {
|
|
let document = r#"
|
|
openapi: 3.0.3
|
|
info: { title: IDs }
|
|
servers: [{ url: https://document.test }]
|
|
paths:
|
|
/one:
|
|
servers: [{ url: https://path.test }]
|
|
get:
|
|
operationId: duplicate
|
|
responses: { '200': { description: ok } }
|
|
/two:
|
|
get:
|
|
operationId: duplicate
|
|
responses: { '200': { description: ok } }
|
|
/blank:
|
|
get:
|
|
operationId: ' '
|
|
responses: { '200': { description: ok } }
|
|
"#;
|
|
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
let duplicate = ir
|
|
.operations
|
|
.iter()
|
|
.filter(|operation| {
|
|
operation
|
|
.findings
|
|
.iter()
|
|
.any(|finding| finding.code == "duplicate_operation_id")
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(duplicate.len(), 2);
|
|
assert!(duplicate.iter().all(|operation| {
|
|
operation
|
|
.findings
|
|
.iter()
|
|
.any(|finding| finding.location.pointer.ends_with("/operationId"))
|
|
}));
|
|
let blank = ir
|
|
.operations
|
|
.iter()
|
|
.find(|operation| operation.path == "/blank")
|
|
.unwrap();
|
|
assert!(
|
|
blank
|
|
.findings
|
|
.iter()
|
|
.any(|finding| finding.code == "missing_operation_id")
|
|
);
|
|
let preview = preview_document(document).unwrap();
|
|
let blank_preview = preview
|
|
.groups
|
|
.iter()
|
|
.flat_map(|group| &group.operations)
|
|
.find(|operation| operation.path == "/blank")
|
|
.unwrap();
|
|
assert_eq!(
|
|
blank_preview
|
|
.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "missing_operation_id")
|
|
.count(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
preview
|
|
.groups
|
|
.iter()
|
|
.flat_map(|group| &group.operations)
|
|
.find(|operation| operation.path == "/one")
|
|
.unwrap()
|
|
.server_urls,
|
|
vec!["https://path.test"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parameters_keep_exact_path_and_operation_source_pointers() {
|
|
let openapi = r#"
|
|
openapi: 3.0.3
|
|
info: { title: Parameter locations }
|
|
paths:
|
|
/items/{id}:
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
get:
|
|
operationId: getItem
|
|
parameters:
|
|
- { name: filter, in: query, schema: { type: string } }
|
|
responses: { '200': { description: ok } }
|
|
"#;
|
|
let ir = normalize_document(openapi, &NormalizationConfig::default()).unwrap();
|
|
let parameters = &ir.operations[0].parameters;
|
|
assert_eq!(
|
|
parameters[0].source_location.pointer,
|
|
"/paths/~1items~1{id}/parameters/0"
|
|
);
|
|
assert_eq!(
|
|
parameters[1].source_location.pointer,
|
|
"/paths/~1items~1{id}/get/parameters/0"
|
|
);
|
|
assert_ne!(parameters[0].construct_id, parameters[1].construct_id);
|
|
|
|
let swagger = r#"
|
|
swagger: '2.0'
|
|
info: { title: Swagger parameter locations }
|
|
paths:
|
|
/items/{id}:
|
|
parameters:
|
|
- { name: id, in: path, required: true, type: string }
|
|
post:
|
|
operationId: updateItem
|
|
parameters:
|
|
- { name: body, in: body, schema: { type: object } }
|
|
responses: { '200': { description: ok } }
|
|
"#;
|
|
let ir = normalize_document(swagger, &NormalizationConfig::default()).unwrap();
|
|
let parameter = &ir.operations[0].parameters[0];
|
|
assert_eq!(
|
|
parameter.source_location.pointer,
|
|
"/paths/~1items~1{id}/parameters/0"
|
|
);
|
|
assert!(ir.operations[0].request_body_schema.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_parameter_omissions_are_errors_at_the_dropped_item_pointer() {
|
|
let document = r#"
|
|
openapi: 3.1.0
|
|
info: { title: Parameter findings }
|
|
servers: [{ url: https://example.test }]
|
|
paths:
|
|
/items/{id}:
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: path_cookie, in: cookie }
|
|
- not-an-object
|
|
- { $ref: '#/components/parameters/Id' }
|
|
get:
|
|
operationId: getItem
|
|
parameters:
|
|
- { in: query }
|
|
- { name: missing_location }
|
|
- { name: form_value, in: formData }
|
|
- { name: operation_cookie, in: cookie }
|
|
- { name: query, in: query, schema: { type: string } }
|
|
responses: { '200': { description: ok } }
|
|
components:
|
|
parameters:
|
|
Id: { name: id, in: path, required: true, schema: { type: string } }
|
|
"#;
|
|
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
assert_eq!(
|
|
ir.operations[0]
|
|
.parameters
|
|
.iter()
|
|
.map(|parameter| parameter.name.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["id", "query"]
|
|
);
|
|
let errors = ir
|
|
.findings
|
|
.iter()
|
|
.filter(|finding| {
|
|
[
|
|
"invalid_parameter",
|
|
"missing_parameter_name",
|
|
"missing_parameter_location",
|
|
"unsupported_parameter_location",
|
|
"unsupported_cookie_parameter",
|
|
"unresolved_parameter_reference",
|
|
]
|
|
.contains(&finding.code.as_str())
|
|
})
|
|
.map(|finding| (finding.code.as_str(), finding.location.pointer.as_str()))
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
assert_eq!(
|
|
errors,
|
|
std::collections::BTreeSet::from([
|
|
(
|
|
"unsupported_cookie_parameter",
|
|
"/paths/~1items~1{id}/parameters/1",
|
|
),
|
|
("invalid_parameter", "/paths/~1items~1{id}/parameters/2",),
|
|
(
|
|
"unresolved_parameter_reference",
|
|
"/paths/~1items~1{id}/parameters/3",
|
|
),
|
|
(
|
|
"missing_parameter_name",
|
|
"/paths/~1items~1{id}/get/parameters/0",
|
|
),
|
|
(
|
|
"missing_parameter_location",
|
|
"/paths/~1items~1{id}/get/parameters/1",
|
|
),
|
|
(
|
|
"unsupported_parameter_location",
|
|
"/paths/~1items~1{id}/get/parameters/2",
|
|
),
|
|
(
|
|
"unsupported_cookie_parameter",
|
|
"/paths/~1items~1{id}/get/parameters/3",
|
|
),
|
|
])
|
|
);
|
|
assert!(
|
|
ir.findings
|
|
.iter()
|
|
.filter(|finding| {
|
|
[
|
|
"invalid_parameter",
|
|
"missing_parameter_name",
|
|
"missing_parameter_location",
|
|
"unsupported_parameter_location",
|
|
"unsupported_cookie_parameter",
|
|
"unresolved_parameter_reference",
|
|
]
|
|
.contains(&finding.code.as_str())
|
|
})
|
|
.all(|finding| finding.severity == ImportFindingSeverity::Error)
|
|
);
|
|
|
|
let legacy = preview_document_legacy_v1(document).unwrap();
|
|
assert!(!legacy.findings.iter().any(|finding| {
|
|
[
|
|
"invalid_parameter",
|
|
"missing_parameter_name",
|
|
"missing_parameter_location",
|
|
"unsupported_parameter_location",
|
|
"unresolved_parameter_reference",
|
|
]
|
|
.contains(&finding.code.as_str())
|
|
}));
|
|
assert!(
|
|
!legacy.groups[0].operations[0]
|
|
.findings
|
|
.iter()
|
|
.any(|finding| finding.code == "unsupported_cookie_parameter")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn swagger_parameter_omissions_are_errors_at_the_dropped_item_pointer() {
|
|
let document = r#"
|
|
swagger: '2.0'
|
|
info: { title: Swagger parameter findings }
|
|
host: example.test
|
|
paths:
|
|
/items/{id}:
|
|
parameters:
|
|
- { name: id, in: path, required: true, type: string }
|
|
- { name: path_cookie, in: cookie }
|
|
- not-an-object
|
|
- { $ref: '#/parameters/Id' }
|
|
get:
|
|
operationId: getItem
|
|
parameters:
|
|
- { in: query }
|
|
- { name: missing_location }
|
|
- { name: form_value, in: formData }
|
|
- { name: operation_cookie, in: cookie }
|
|
- { name: query, in: query, type: string }
|
|
responses: { '200': { description: ok } }
|
|
parameters:
|
|
Id: { name: id, in: path, required: true, type: string }
|
|
"#;
|
|
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
assert_eq!(
|
|
ir.operations[0]
|
|
.parameters
|
|
.iter()
|
|
.map(|parameter| parameter.name.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["id", "query"]
|
|
);
|
|
let errors = ir
|
|
.findings
|
|
.iter()
|
|
.filter(|finding| {
|
|
[
|
|
"invalid_parameter",
|
|
"missing_parameter_name",
|
|
"missing_parameter_location",
|
|
"unsupported_parameter_location",
|
|
"unsupported_cookie_parameter",
|
|
"unresolved_parameter_reference",
|
|
]
|
|
.contains(&finding.code.as_str())
|
|
})
|
|
.map(|finding| (finding.code.as_str(), finding.location.pointer.as_str()))
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
assert_eq!(
|
|
errors,
|
|
std::collections::BTreeSet::from([
|
|
(
|
|
"unsupported_cookie_parameter",
|
|
"/paths/~1items~1{id}/parameters/1",
|
|
),
|
|
("invalid_parameter", "/paths/~1items~1{id}/parameters/2",),
|
|
(
|
|
"unresolved_parameter_reference",
|
|
"/paths/~1items~1{id}/parameters/3",
|
|
),
|
|
(
|
|
"missing_parameter_name",
|
|
"/paths/~1items~1{id}/get/parameters/0",
|
|
),
|
|
(
|
|
"missing_parameter_location",
|
|
"/paths/~1items~1{id}/get/parameters/1",
|
|
),
|
|
(
|
|
"unsupported_parameter_location",
|
|
"/paths/~1items~1{id}/get/parameters/2",
|
|
),
|
|
(
|
|
"unsupported_cookie_parameter",
|
|
"/paths/~1items~1{id}/get/parameters/3",
|
|
),
|
|
])
|
|
);
|
|
assert!(
|
|
ir.findings
|
|
.iter()
|
|
.filter(|finding| {
|
|
[
|
|
"invalid_parameter",
|
|
"missing_parameter_name",
|
|
"missing_parameter_location",
|
|
"unsupported_parameter_location",
|
|
"unsupported_cookie_parameter",
|
|
"unresolved_parameter_reference",
|
|
]
|
|
.contains(&finding.code.as_str())
|
|
})
|
|
.all(|finding| finding.severity == ImportFindingSeverity::Error)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_invalid_servers_and_tags_keep_valid_siblings_with_exact_findings() {
|
|
let document = r#"
|
|
openapi: 3.1.0
|
|
info: { title: Server and tag findings }
|
|
servers:
|
|
- { url: https://document.test/ }
|
|
- not-an-object
|
|
- {}
|
|
- { url: 42 }
|
|
paths:
|
|
/items:
|
|
servers:
|
|
- { url: https://path.test/ }
|
|
- false
|
|
- {}
|
|
get:
|
|
operationId: getItems
|
|
servers:
|
|
- { url: https://operation.test/ }
|
|
- { url: false }
|
|
tags: [items, 42, {}, null]
|
|
responses: { '200': { description: ok } }
|
|
"#;
|
|
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
assert_eq!(ir.source.servers, vec!["https://document.test"]);
|
|
assert_eq!(ir.operations[0].servers, vec!["https://operation.test"]);
|
|
assert_eq!(ir.operations[0].tags, vec!["items"]);
|
|
|
|
let server_errors = ir
|
|
.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "invalid_server")
|
|
.map(|finding| finding.location.pointer.as_str())
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
assert_eq!(
|
|
server_errors,
|
|
std::collections::BTreeSet::from([
|
|
"/servers/1",
|
|
"/servers/2",
|
|
"/servers/3",
|
|
"/paths/~1items/servers/1",
|
|
"/paths/~1items/servers/2",
|
|
"/paths/~1items/get/servers/1",
|
|
])
|
|
);
|
|
assert!(
|
|
ir.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "invalid_server")
|
|
.all(|finding| finding.severity == ImportFindingSeverity::Error)
|
|
);
|
|
let tag_warnings = ir
|
|
.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "invalid_tag")
|
|
.map(|finding| finding.location.pointer.as_str())
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
assert_eq!(
|
|
tag_warnings,
|
|
std::collections::BTreeSet::from([
|
|
"/paths/~1items/get/tags/1",
|
|
"/paths/~1items/get/tags/2",
|
|
"/paths/~1items/get/tags/3",
|
|
])
|
|
);
|
|
assert!(
|
|
ir.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "invalid_tag")
|
|
.all(|finding| finding.severity == ImportFindingSeverity::Warning)
|
|
);
|
|
|
|
let legacy = preview_document_legacy_v1(document).unwrap();
|
|
assert_eq!(legacy.source.servers, vec!["https://document.test"]);
|
|
assert_eq!(
|
|
legacy.groups[0].operations[0].server_urls,
|
|
vec!["https://operation.test"]
|
|
);
|
|
assert_eq!(legacy.groups[0].operations[0].category, "items");
|
|
assert!(
|
|
!legacy.findings.iter().any(|finding| {
|
|
["invalid_server", "invalid_tag"].contains(&finding.code.as_str())
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn swagger_invalid_tags_keep_valid_siblings_with_exact_warning_pointers() {
|
|
let document = r#"
|
|
swagger: '2.0'
|
|
info: { title: Swagger tag findings }
|
|
host: example.test
|
|
paths:
|
|
/items:
|
|
get:
|
|
operationId: getItems
|
|
tags: [items, 42, {}, null]
|
|
responses: { '200': { description: ok } }
|
|
"#;
|
|
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
assert_eq!(ir.operations[0].tags, vec!["items"]);
|
|
let warnings = ir
|
|
.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "invalid_tag")
|
|
.map(|finding| finding.location.pointer.as_str())
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
assert_eq!(
|
|
warnings,
|
|
std::collections::BTreeSet::from([
|
|
"/paths/~1items/get/tags/1",
|
|
"/paths/~1items/get/tags/2",
|
|
"/paths/~1items/get/tags/3",
|
|
])
|
|
);
|
|
assert!(
|
|
ir.findings
|
|
.iter()
|
|
.filter(|finding| finding.code == "invalid_tag")
|
|
.all(|finding| finding.severity == ImportFindingSeverity::Warning)
|
|
);
|
|
|
|
let legacy = preview_document_legacy_v1(document).unwrap();
|
|
assert_eq!(legacy.groups[0].operations[0].category, "items");
|
|
assert!(
|
|
!legacy
|
|
.findings
|
|
.iter()
|
|
.any(|finding| finding.code == "invalid_tag")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn v2_response_selection_uses_all_success_codes_then_openapi_wildcard_and_default() {
|
|
let openapi = r#"
|
|
openapi: 3.1.0
|
|
info: { title: Response priority }
|
|
paths:
|
|
/numeric:
|
|
get:
|
|
operationId: numeric
|
|
responses:
|
|
'200': { description: no schema }
|
|
'203': { description: accepted, content: { application/json: { schema: { type: string } } } }
|
|
'206': { description: partial, content: { application/json: { schema: { type: integer } } } }
|
|
/wildcard:
|
|
get:
|
|
operationId: wildcard
|
|
responses:
|
|
'200': { description: unsupported, content: { text/plain: { schema: { type: string } } } }
|
|
'2xX': { description: wildcard, content: { application/json: { schema: { type: boolean } } } }
|
|
default: { description: fallback, content: { application/json: { schema: { type: string } } } }
|
|
/default:
|
|
get:
|
|
operationId: fallback
|
|
responses:
|
|
'203': { description: no schema, content: { application/json: {} } }
|
|
default: { description: fallback, content: { application/json: { schema: { type: number } } } }
|
|
"#;
|
|
let ir = normalize_document(openapi, &NormalizationConfig::default()).unwrap();
|
|
let response_pointer = |path: &str| {
|
|
ir.operations
|
|
.iter()
|
|
.find(|operation| operation.path == path)
|
|
.unwrap()
|
|
.response_schema
|
|
.as_ref()
|
|
.unwrap()
|
|
.location
|
|
.pointer
|
|
.as_str()
|
|
};
|
|
assert_eq!(
|
|
response_pointer("/numeric"),
|
|
"/paths/~1numeric/get/responses/203/content/application~1json/schema"
|
|
);
|
|
assert_eq!(
|
|
response_pointer("/wildcard"),
|
|
"/paths/~1wildcard/get/responses/2xX/content/application~1json/schema"
|
|
);
|
|
assert_eq!(
|
|
response_pointer("/default"),
|
|
"/paths/~1default/get/responses/default/content/application~1json/schema"
|
|
);
|
|
|
|
let swagger = r#"
|
|
swagger: '2.0'
|
|
info: { title: Swagger response priority }
|
|
paths:
|
|
/numeric:
|
|
get:
|
|
operationId: numeric
|
|
responses:
|
|
'200': { description: no schema }
|
|
'206': { description: partial, schema: { type: integer } }
|
|
default: { description: fallback, schema: { type: string } }
|
|
/no-wildcard:
|
|
get:
|
|
operationId: noWildcard
|
|
responses:
|
|
'2XX': { description: ignored wildcard, schema: { type: string } }
|
|
default: { description: fallback, schema: { type: boolean } }
|
|
"#;
|
|
let ir = normalize_document(swagger, &NormalizationConfig::default()).unwrap();
|
|
let response_pointer = |path: &str| {
|
|
ir.operations
|
|
.iter()
|
|
.find(|operation| operation.path == path)
|
|
.unwrap()
|
|
.response_schema
|
|
.as_ref()
|
|
.unwrap()
|
|
.location
|
|
.pointer
|
|
.as_str()
|
|
};
|
|
assert_eq!(
|
|
response_pointer("/numeric"),
|
|
"/paths/~1numeric/get/responses/206/schema"
|
|
);
|
|
assert_eq!(
|
|
response_pointer("/no-wildcard"),
|
|
"/paths/~1no-wildcard/get/responses/default/schema"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn schemas_keep_selected_source_pointers_and_pointer_derived_ids() {
|
|
let openapi = r#"
|
|
openapi: 3.1.0
|
|
info: { title: Schema locations }
|
|
paths:
|
|
/items:
|
|
post:
|
|
operationId: updateItems
|
|
requestBody:
|
|
content:
|
|
application/vnd.example+json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
a/b: { type: string }
|
|
a~1b: { type: string }
|
|
responses:
|
|
'201':
|
|
description: created
|
|
content:
|
|
application/vnd.example+json:
|
|
schema: { type: integer }
|
|
"#;
|
|
let ir = normalize_document(openapi, &NormalizationConfig::default()).unwrap();
|
|
let operation = &ir.operations[0];
|
|
assert_eq!(
|
|
operation
|
|
.request_body_schema
|
|
.as_ref()
|
|
.unwrap()
|
|
.location
|
|
.pointer,
|
|
"/paths/~1items/post/requestBody/content/application~1vnd.example+json/schema"
|
|
);
|
|
assert_eq!(
|
|
operation.response_schema.as_ref().unwrap().location.pointer,
|
|
"/paths/~1items/post/responses/201/content/application~1vnd.example+json/schema"
|
|
);
|
|
let crank_import::rest::NormalizedSchemaKind::Object { properties, .. } =
|
|
&operation.request_body_schema.as_ref().unwrap().kind
|
|
else {
|
|
panic!("request schema must be object");
|
|
};
|
|
assert_ne!(
|
|
properties["a/b"].construct_id,
|
|
properties["a~1b"].construct_id
|
|
);
|
|
|
|
let swagger = r#"
|
|
swagger: '2.0'
|
|
info: { title: Swagger schema locations }
|
|
paths:
|
|
/items:
|
|
post:
|
|
operationId: updateItems
|
|
parameters:
|
|
- in: body
|
|
name: body
|
|
schema: { type: object }
|
|
responses:
|
|
'201': { description: created, schema: { type: integer } }
|
|
"#;
|
|
let ir = normalize_document(swagger, &NormalizationConfig::default()).unwrap();
|
|
assert_eq!(
|
|
ir.operations[0]
|
|
.request_body_schema
|
|
.as_ref()
|
|
.unwrap()
|
|
.location
|
|
.pointer,
|
|
"/paths/~1items/post/parameters/0/schema"
|
|
);
|
|
assert_eq!(
|
|
ir.operations[0]
|
|
.response_schema
|
|
.as_ref()
|
|
.unwrap()
|
|
.location
|
|
.pointer,
|
|
"/paths/~1items/post/responses/201/schema"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_every_unresolved_reference_from_the_full_source_tree() {
|
|
let document = r#"
|
|
openapi: 3.1.0
|
|
info: { title: References }
|
|
paths:
|
|
/ok:
|
|
parameters:
|
|
- { $ref: '#/components/parameters/Id' }
|
|
get:
|
|
operationId: getOk
|
|
requestBody: { $ref: '#/components/requestBodies/Body' }
|
|
responses:
|
|
'200': { $ref: '#/components/responses/Ok' }
|
|
/reused:
|
|
$ref: '#/components/pathItems/Reusable'
|
|
components:
|
|
schemas:
|
|
Loop: { $ref: '#/components/schemas/Loop' }
|
|
Remote: { $ref: 'https://example.test/schema.json#/Remote' }
|
|
parameters:
|
|
Id: { $ref: '#/components/parameters/Id' }
|
|
requestBodies:
|
|
Body: { $ref: '#/components/requestBodies/Body' }
|
|
responses:
|
|
Ok: { $ref: '#/components/responses/Ok' }
|
|
pathItems:
|
|
Reusable: { $ref: '#/components/pathItems/Reusable' }
|
|
"#;
|
|
let first = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
let second = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
|
assert_eq!(
|
|
serde_json::to_vec(&first).unwrap(),
|
|
serde_json::to_vec(&second).unwrap()
|
|
);
|
|
assert_eq!(first.unresolved_references.len(), 10);
|
|
let references = first
|
|
.unresolved_references
|
|
.iter()
|
|
.map(|reference| (reference.uri.as_str(), reference.location.pointer.as_str()))
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
assert!(references.contains(&(
|
|
"#/components/parameters/Id",
|
|
"/paths/~1ok/parameters/0/$ref"
|
|
)));
|
|
assert!(references.contains(&(
|
|
"#/components/requestBodies/Body",
|
|
"/paths/~1ok/get/requestBody/$ref"
|
|
)));
|
|
assert!(references.contains(&(
|
|
"#/components/responses/Ok",
|
|
"/paths/~1ok/get/responses/200/$ref"
|
|
)));
|
|
assert!(references.contains(&("#/components/pathItems/Reusable", "/paths/~1reused/$ref")));
|
|
assert!(
|
|
references.contains(&("#/components/schemas/Loop", "/components/schemas/Loop/$ref"))
|
|
);
|
|
assert!(references.contains(&(
|
|
"https://example.test/schema.json#/Remote",
|
|
"/components/schemas/Remote/$ref"
|
|
)));
|
|
assert!(first.unresolved_references.iter().all(|reference| {
|
|
reference.construct_id
|
|
== format!(
|
|
"reference:{}",
|
|
reference
|
|
.location
|
|
.pointer
|
|
.replace('~', "~0")
|
|
.replace('/', "~1")
|
|
)
|
|
&& first.coverage.iter().any(|entry| {
|
|
entry.construct_id == reference.construct_id
|
|
&& entry.location == reference.location
|
|
})
|
|
}));
|
|
}
|
|
}
|