1use glob::glob;
4use jsonschema::Validator;
5use reqwest::Url;
6use serde::Serialize;
7use serde_json::{json, Value, Value::Null};
8
9use warp::http::Response;
10use warp::http::StatusCode;
11use warp::hyper::body::Bytes;
12
13use crate::era::SupportedEra;
14
15pub struct APISpec<'a> {
17 openapi: Value,
18 path: Option<&'a str>,
19 method: Option<&'a str>,
20 content_type: Option<&'a str>,
21}
22
23impl<'a> APISpec<'a> {
24 pub fn verify_conformity(
26 spec_files: Vec<String>,
27 method: &str,
28 path: &str,
29 content_type: &str,
30 request_body: &impl Serialize,
31 response: &Response<Bytes>,
32 status_code: &StatusCode,
33 ) -> Result<(), String> {
34 if spec_files.is_empty() {
35 return Err(
36 "OpenAPI need a spec file to validate conformity. None were given.".to_string(),
37 );
38 }
39
40 for spec_file in spec_files {
41 if let Err(e) = APISpec::from_file(&spec_file)
42 .method(method)
43 .path(path)
44 .content_type(content_type)
45 .validate_request(request_body)
46 .and_then(|api| api.validate_response(response))
47 .and_then(|api| api.validate_status(response, status_code))
48 {
49 return Err(format!(
50 "OpenAPI invalid response in {spec_file} on route {path}, reason: {e}\nresponse: {response:#?}"
51 ));
52 }
53 }
54 Ok(())
55 }
56
57 pub fn from_file(path: &str) -> APISpec<'a> {
59 let yaml_spec = std::fs::read_to_string(path).unwrap();
60 let openapi: serde_json::Value = serde_yaml::from_str(&yaml_spec).unwrap();
61 APISpec {
62 openapi,
63 path: None,
64 method: None,
65 content_type: Some("application/json"),
66 }
67 }
68
69 pub fn path(&'a mut self, path: &'a str) -> &'a mut APISpec<'a> {
71 self.path = Some(path);
72 self
73 }
74
75 pub fn method(&'a mut self, method: &'a str) -> &'a mut APISpec<'a> {
77 self.method = Some(method);
78 self
79 }
80
81 pub fn content_type(&'a mut self, content_type: &'a str) -> &'a mut APISpec<'a> {
83 self.content_type = Some(content_type);
84 self
85 }
86
87 fn validate_request(
89 &'a self,
90 request_body: &impl Serialize,
91 ) -> Result<&'a APISpec<'a>, String> {
92 let path = self.path.unwrap();
93 let method = self.method.unwrap().to_lowercase();
94 let content_type = self.content_type.unwrap();
95
96 let openapi_path_entry = path.split('?').next().unwrap();
97 let operation_object = &self.openapi["paths"][openapi_path_entry][method];
98
99 self.validate_query_parameters(path, operation_object)?;
100
101 let request_schema = &operation_object["requestBody"]["content"][content_type]["schema"];
102 let value = &json!(&request_body);
103 self.validate_conformity(value, request_schema)
104 }
105
106 fn validate_query_parameters(
107 &'a self,
108 path: &str,
109 operation_object: &Value,
110 ) -> Result<&'a APISpec<'a>, String> {
111 let fake_base_url = "http://0.0.0.1";
112 let url = Url::parse(&format!("{fake_base_url}{path}")).unwrap();
113
114 check_query_parameter_limitations(&url, operation_object);
115
116 let mut query_pairs = url.query_pairs();
117 if let Some(parameter) = query_pairs.next() {
118 let spec_parameter = &operation_object["parameters"][0];
119 let spec_parameter_name = &spec_parameter["name"].as_str().unwrap();
120 let spec_parameter_in = &spec_parameter["in"].as_str().unwrap();
121 if spec_parameter_in.eq(&"query") && spec_parameter_name.eq(¶meter.0) {
122 Ok(self)
123 } else {
124 Err(format!("Unexpected query parameter '{}'", parameter.0))
125 }
126 } else {
127 Ok(self)
128 }
129 }
130
131 fn validate_status(
133 &'a self,
134 response: &Response<Bytes>,
135 expected_status_code: &StatusCode,
136 ) -> Result<&'a APISpec<'a>, String> {
137 if expected_status_code.as_u16() != response.status().as_u16() {
138 return Err(format!(
139 "expected status code {} but was {}",
140 expected_status_code.as_u16(),
141 response.status().as_u16(),
142 ));
143 }
144
145 Ok(self)
146 }
147
148 fn validate_response(&'a self, response: &Response<Bytes>) -> Result<&'a APISpec<'a>, String> {
150 let body = response.body();
151 let status = response.status();
152
153 let path = self.path.unwrap();
154 let path = path.split('?').next().unwrap();
155 let method = self.method.unwrap().to_lowercase();
156 let content_type = self.content_type.unwrap();
157 let mut openapi = self.openapi.clone();
158
159 let response_spec = {
160 match &mut openapi["paths"][path][&method]["responses"] {
161 Null => None,
162 responses_spec => {
163 let status_code = status.as_str();
164 if responses_spec
165 .as_object()
166 .unwrap()
167 .contains_key(status_code)
168 {
169 Some(&responses_spec[status_code])
170 } else {
171 Some(&responses_spec["default"])
172 }
173 }
174 }
175 };
176 match response_spec {
177 Some(response_spec) => {
178 let response_schema = match &response_spec["content"] {
179 Null => &Null,
180 content => {
181 if content[content_type] == Null {
182 return Err(format!(
183 "Expected content type '{}' but spec is '{}'",
184 content_type, response_spec["content"],
185 ));
186 }
187 &content[content_type]["schema"]
188 }
189 };
190 if body.is_empty() {
191 match response_schema.as_object() {
192 Some(_) => Err("Non empty body expected".to_string()),
193 None => Ok(self),
194 }
195 } else {
196 match response_schema.as_object() {
197 Some(_) => match &serde_json::from_slice(body) {
198 Ok(value) => self.validate_conformity(value, response_schema),
199 Err(_) => Err(format!("Expected a valid json but got: {body:?}")),
200 },
201 None => Err(format!("Expected empty body but got: {body:?}")),
202 }
203 }
204 }
205 None => Err(format!(
206 "Unmatched path and method: {path} {}",
207 method.to_uppercase()
208 )),
209 }
210 }
211
212 fn validate_conformity(
214 &'a self,
215 value: &Value,
216 schema: &Value,
217 ) -> Result<&'a APISpec<'a>, String> {
218 match schema {
219 Null => match value {
220 Null => Ok(self),
221 _ => Err(format!("Expected nothing but got: {value:?}")),
222 },
223 _ => {
224 let mut schema = schema.as_object().unwrap().clone();
225 let components = self.openapi["components"].clone();
226 schema.insert(String::from("components"), components);
227
228 let result_validator = Validator::new(&json!(schema));
229 if let Err(e) = result_validator {
230 return Err(format!("Error creating validator: {e}"));
231 }
232 let validator = result_validator.unwrap();
233 let errors = validator
234 .iter_errors(value)
235 .map(|e| e.to_string())
236 .collect::<Vec<String>>();
237 if errors.is_empty() {
238 Ok(self)
239 } else {
240 Err(errors.join(", "))
241 }
242 }
243 }
244 }
245
246 pub fn get_default_spec_file() -> String {
248 "../openapi.yaml".to_string()
249 }
250
251 pub fn get_era_spec_file(era: SupportedEra) -> String {
253 format!("../openapi-{era}")
254 }
255
256 pub fn get_all_spec_files() -> Vec<String> {
258 APISpec::get_all_spec_files_from("..")
259 }
260
261 pub fn get_all_spec_files_from(root_path: &str) -> Vec<String> {
263 let mut open_api_spec_files = Vec::new();
264 for entry in glob(&format!("{root_path}/openapi*.yaml")).unwrap() {
265 let entry_path = entry.unwrap().to_str().unwrap().to_string();
266 open_api_spec_files.push(entry_path.clone());
267 open_api_spec_files.push(entry_path);
268 }
269
270 open_api_spec_files
271 }
272
273 pub fn verify_examples(&self) -> Vec<String> {
275 self.verify_examples_value("", &self.openapi)
276 }
277
278 fn verify_examples_value(&self, path_to_value: &str, root_value: &Value) -> Vec<String> {
279 let mut errors: Vec<String> = vec![];
280
281 errors.append(&mut self.verify_example_conformity(path_to_value, root_value));
282
283 if let Some(object) = root_value.as_object() {
284 for (value_key, value) in object {
285 errors.append(
286 &mut self.verify_examples_value(&format!("{path_to_value} {value_key}"), value),
287 );
288 }
289 }
290
291 if let Some(array) = root_value.as_array() {
292 for value in array {
293 errors
294 .append(&mut self.verify_examples_value(&format!("{path_to_value}[?]"), value));
295 }
296 }
297
298 errors
299 }
300
301 fn verify_example_conformity(&self, name: &str, component: &Value) -> Vec<String> {
302 fn register_example_errors(
303 apispec: &APISpec,
304 errors: &mut Vec<String>,
305 component_definition: &Value,
306 example: &Value,
307 ) {
308 let result = apispec.validate_conformity(example, component_definition);
309 if let Err(e) = result {
310 errors.push(format!(" {e}\n Example: {example}\n"));
311 }
312 }
313
314 let mut errors = vec![];
315 let component_definition = component.get("schema").unwrap_or(component);
316 if let Some(example) = component.get("example") {
318 register_example_errors(self, &mut errors, component_definition, example);
319 }
320
321 if let Some(examples) = component.get("examples") {
322 if let Some(examples) = examples.as_array() {
323 for example in examples {
324 register_example_errors(self, &mut errors, component_definition, example);
325 }
326 } else {
327 errors.push(format!(
328 " Examples should be an array\n Examples: {examples}\n"
329 ));
330 }
331 }
332
333 if !errors.is_empty() {
334 vec![format!("- {}: Error\n{}", name, errors.join("\n"))]
335 } else {
336 vec![]
337 }
338 }
339}
340
341fn check_query_parameter_limitations(url: &Url, operation_object: &Value) {
344 if url.query_pairs().count() >= 2 {
345 panic!("This method does not work with multiple parameters");
346 }
347
348 if let Some(parameters) = operation_object["parameters"].as_array() {
349 let len = parameters
350 .iter()
351 .filter(|p| p["in"].eq("query"))
352 .collect::<Vec<_>>()
353 .len();
354 if len >= 2 {
355 panic!("This method does not work with multiple parameters");
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use std::fs;
363 use std::path::{Path, PathBuf};
364 use warp::http::Method;
365 use warp::http::StatusCode;
366
367 use super::*;
368 use crate::entities;
369 use crate::messages::{AggregatorFeaturesMessage, SignerMessagePart};
370 use crate::test_utils::{fake_data, TempDir};
371
372 fn build_empty_response(status_code: u16) -> Response<Bytes> {
373 Response::builder()
374 .status(status_code)
375 .body(Bytes::new())
376 .unwrap()
377 }
378
379 fn build_json_response<T: Serialize>(status_code: u16, value: T) -> Response<Bytes> {
380 Response::builder()
381 .status(status_code)
382 .body(Bytes::from(json!(value).to_string().into_bytes()))
383 .unwrap()
384 }
385
386 fn build_response(status_code: u16, content: &'static [u8]) -> Response<Bytes> {
387 Response::builder()
388 .status(status_code)
389 .body(Bytes::from_static(content))
390 .unwrap()
391 }
392
393 fn get_temp_dir(dir_name: &str) -> PathBuf {
394 TempDir::create("apispec", dir_name)
395 }
396
397 fn get_temp_openapi_filename(name: &str, id: u32) -> PathBuf {
398 get_temp_dir(&format!("{name}-{id}")).join("openapi.yaml")
399 }
400
401 fn write_minimal_open_api_file(
402 version: &str,
403 path: &Path,
404 openapi_paths: &str,
405 openapi_components: &str,
406 ) {
407 fs::write(
408 path,
409 format!(
410 r#"openapi: "3.0.0"
411info:
412 version: {version}
413 title: Minimal Open Api File
414
415paths:
416{openapi_paths}
417
418components:
419 schemas:
420{openapi_components}
421"#
422 ),
423 )
424 .unwrap()
425 }
426
427 fn check_example_errors_is_detected(
432 id: u32,
433 paths: &str,
434 components: &str,
435 expected_error_messages: &[&str],
436 ) {
437 let file = get_temp_openapi_filename("example", id);
438
439 write_minimal_open_api_file("1.0.0", &file, paths, components);
440
441 let api_spec = APISpec::from_file(file.to_str().unwrap());
442 let errors: Vec<String> = api_spec.verify_examples();
443
444 assert_eq!(1, errors.len());
445 let error_message = errors.first().unwrap();
446 for expected_message in expected_error_messages {
447 assert!(
448 error_message.contains(expected_message),
449 "Error message: {errors:?}\nshould contains: {expected_message}\n"
450 );
451 }
452 }
453
454 #[test]
455 fn test_validate_a_response_without_body() {
456 let file = get_temp_openapi_filename("validate_a_response_without_body", line!());
457 let paths = r#"
458 /empty-route:
459 get:
460 responses:
461 "204":
462 description: not available
463 "#;
464 write_minimal_open_api_file("1.0.0", &file, paths, "");
465
466 APISpec::from_file(file.to_str().unwrap())
467 .method(Method::GET.as_str())
468 .path("/empty-route")
469 .validate_request(&Null)
470 .unwrap()
471 .validate_response(&build_empty_response(204))
472 .unwrap();
473 }
474
475 #[test]
476 fn test_validate_ok_when_request_without_body_and_expects_response() {
477 APISpec::from_file(&APISpec::get_default_spec_file())
478 .method(Method::GET.as_str())
479 .path("/")
480 .validate_request(&Null)
481 .unwrap()
482 .validate_response(&build_json_response(
483 200,
484 AggregatorFeaturesMessage::dummy(),
485 ))
486 .unwrap();
487 }
488
489 #[test]
490 fn test_validate_ok_when_request_with_body_and_expects_no_response() {
491 assert!(APISpec::from_file(&APISpec::get_default_spec_file())
492 .method(Method::POST.as_str())
493 .path("/register-signer")
494 .validate_request(&SignerMessagePart::dummy())
495 .unwrap()
496 .validate_response(&Response::<Bytes>::new(Bytes::new()))
497 .is_err());
498 }
499
500 #[test]
501 fn test_validate_ok_when_response_match_default_status_code() {
502 let response = build_json_response(
505 StatusCode::INTERNAL_SERVER_ERROR.into(),
506 entities::ServerError::new("an error occurred".to_string()),
507 );
508
509 APISpec::from_file(&APISpec::get_default_spec_file())
510 .method(Method::POST.as_str())
511 .path("/register-signer")
512 .validate_response(&response)
513 .unwrap();
514 }
515
516 #[test]
517 fn test_should_fail_when_the_status_code_is_not_the_expected_one() {
518 let response = build_json_response(
519 StatusCode::INTERNAL_SERVER_ERROR.into(),
520 entities::ServerError::new("an error occurred".to_string()),
521 );
522
523 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
524 let result = api_spec
525 .method(Method::GET.as_str())
526 .path("/")
527 .validate_request(&Null)
528 .unwrap()
529 .validate_status(&response, &StatusCode::OK);
530
531 assert!(result.is_err());
532 assert_eq!(
533 result.err().unwrap().to_string(),
534 format!(
535 "expected status code {} but was {}",
536 StatusCode::OK.as_u16(),
537 StatusCode::INTERNAL_SERVER_ERROR.as_u16()
538 )
539 );
540 }
541
542 #[test]
543 fn test_should_be_ok_when_the_status_code_is_the_right_one() {
544 let response = build_json_response(
545 StatusCode::INTERNAL_SERVER_ERROR.into(),
546 entities::ServerError::new("an error occurred".to_string()),
547 );
548
549 APISpec::from_file(&APISpec::get_default_spec_file())
550 .method(Method::GET.as_str())
551 .path("/")
552 .validate_request(&Null)
553 .unwrap()
554 .validate_status(&response, &StatusCode::INTERNAL_SERVER_ERROR)
555 .unwrap();
556 }
557
558 #[test]
559 fn test_validate_returns_error_when_route_does_not_exist() {
560 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
561 let result = api_spec
562 .method(Method::GET.as_str())
563 .path("/route-not-existing-in-openapi-spec")
564 .validate_response(&build_response(200, b"abcdefgh"));
565
566 assert!(result.is_err());
567 assert_eq!(
568 result.err(),
569 Some("Unmatched path and method: /route-not-existing-in-openapi-spec GET".to_string())
570 );
571 }
572
573 #[test]
574 fn test_validate_returns_error_when_route_exists_but_method_does_not() {
575 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
576 let result = api_spec
577 .method(Method::OPTIONS.as_str())
578 .path("/certificates")
579 .validate_response(&build_response(200, b"abcdefgh"));
580
581 assert!(result.is_err());
582 assert_eq!(
583 result.err(),
584 Some("Unmatched path and method: /certificates OPTIONS".to_string())
585 );
586 }
587 #[test]
588 fn test_validate_returns_error_when_route_exists_but_expects_non_empty_response() {
589 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
590 let result = api_spec
591 .method(Method::GET.as_str())
592 .path("/certificates")
593 .validate_response(&build_empty_response(200));
594
595 assert!(result.is_err());
596 assert_eq!(result.err(), Some("Non empty body expected".to_string()));
597 }
598
599 #[test]
600 fn test_validate_returns_error_when_route_exists_but_expects_empty_response() {
601 {
602 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
603 let result = api_spec
604 .method(Method::POST.as_str())
605 .path("/register-signer")
606 .validate_response(&build_response(201, b"abcdefgh"));
607
608 assert!(result.is_err());
609 assert_eq!(
610 result.err(),
611 Some("Expected empty body but got: b\"abcdefgh\"".to_string())
612 );
613 }
614 {
615 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
616 let result = api_spec
617 .method(Method::POST.as_str())
618 .path("/register-signer")
619 .validate_response(&build_json_response(201, "something"));
620
621 assert!(result.is_err());
622 assert_eq!(
623 result.err(),
624 Some("Expected empty body but got: b\"\\\"something\\\"\"".to_string())
625 );
626 }
627 }
628
629 #[test]
630 fn test_validate_returns_error_when_json_is_not_valid() {
631 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
632 let result = api_spec
633 .method(Method::GET.as_str())
634 .path("/certificates")
635 .validate_request(&Null)
636 .unwrap()
637 .validate_response(&build_response(200, b"not a json"));
638 assert_eq!(
639 result.err(),
640 Some("Expected a valid json but got: b\"not a json\"".to_string())
641 );
642 }
643
644 #[test]
645 fn test_validate_returns_errors_when_route_exists_but_does_not_expect_request_body() {
646 assert!(APISpec::from_file(&APISpec::get_default_spec_file())
647 .method(Method::GET.as_str())
648 .path("/certificates")
649 .validate_request(&fake_data::beacon())
650 .is_err());
651 }
652 #[test]
653 fn test_validate_returns_error_when_route_exists_but_expects_non_empty_request_body() {
654 assert!(APISpec::from_file(&APISpec::get_default_spec_file())
655 .method(Method::POST.as_str())
656 .path("/register-signer")
657 .validate_request(&Null)
658 .is_err());
659 }
660
661 #[test]
662 fn test_validate_returns_error_when_content_type_does_not_exist() {
663 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
664 let result = api_spec
665 .method(Method::GET.as_str())
666 .path("/certificates")
667 .content_type("whatever")
668 .validate_request(&Null)
669 .unwrap()
670 .validate_response(&build_empty_response(200));
671
672 assert!(result.is_err());
673 assert_eq!(
674 result.err().unwrap().to_string(),
675 "Expected content type 'whatever' but spec is '{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/CertificateListMessage\"}}}'",
676 );
677 }
678
679 #[test]
680 fn test_validate_a_response_with_query_parameters() {
681 APISpec::from_file(&APISpec::get_default_spec_file())
682 .method(Method::GET.as_str())
683 .path("/proof/cardano-transaction?transaction_hashes={hash}")
684 .validate_request(&Null)
685 .unwrap()
686 .validate_response(&build_empty_response(404))
687 .map(|_apispec| ())
688 .unwrap();
689 }
690
691 #[test]
692 fn test_validate_a_request_with_wrong_query_parameter_name() {
693 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
694 let result = api_spec
695 .method(Method::GET.as_str())
696 .path("/proof/cardano-transaction?whatever=123")
697 .validate_request(&Null);
698
699 assert!(result.is_err());
700 assert_eq!(
701 result.err().unwrap().to_string(),
702 "Unexpected query parameter 'whatever'",
703 );
704 }
705
706 #[test]
707 fn test_validate_a_request_should_failed_when_query_parameter_is_in_path() {
708 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
709 let result = api_spec
710 .method(Method::GET.as_str())
711 .path("/artifact/cardano-transaction/{hash}?hash=456")
712 .validate_request(&Null);
713
714 assert!(result.is_err());
715 assert_eq!(
716 result.err().unwrap().to_string(),
717 "Unexpected query parameter 'hash'",
718 );
719 }
720
721 #[test]
722 fn test_validate_query_parameters_with_correct_parameter_name() {
723 let api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
724 api_spec
725 .validate_query_parameters(
726 "/proof/cardano-transaction?transaction_hashes=a123,b456",
727 &api_spec.openapi["paths"]["/proof/cardano-transaction"]["get"],
728 )
729 .map(|_apispec| ())
730 .unwrap()
731 }
732
733 #[test]
734 fn test_validate_query_parameters_with_wrong_query_parameter_name() {
735 let api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
736 let result = api_spec.validate_query_parameters(
737 "/proof/cardano-transaction?whatever=123",
738 &api_spec.openapi["paths"]["/proof/cardano-transaction"]["get"],
739 );
740
741 assert!(result.is_err());
742 assert_eq!(
743 result.err().unwrap().to_string(),
744 "Unexpected query parameter 'whatever'",
745 );
746 }
747
748 #[test]
749 fn test_verify_conformity_with_expected_status() {
750 APISpec::verify_conformity(
751 APISpec::get_all_spec_files(),
752 Method::GET.as_str(),
753 "/",
754 "application/json",
755 &Null,
756 &build_json_response(200, AggregatorFeaturesMessage::dummy()),
757 &StatusCode::OK,
758 )
759 .unwrap()
760 }
761
762 #[test]
763 fn test_verify_conformity_with_non_expected_status_returns_error() {
764 let response = build_json_response(200, AggregatorFeaturesMessage::dummy());
765
766 let spec_file = APISpec::get_default_spec_file();
767 let result = APISpec::verify_conformity(
768 vec![spec_file.clone()],
769 Method::GET.as_str(),
770 "/",
771 "application/json",
772 &Null,
773 &response,
774 &StatusCode::BAD_REQUEST,
775 );
776
777 let error_reason = format!(
778 "expected status code {} but was {}",
779 StatusCode::BAD_REQUEST.as_u16(),
780 StatusCode::OK.as_u16()
781 );
782 let error_message = format!(
783 "OpenAPI invalid response in {spec_file} on route /, reason: {error_reason}\nresponse: {response:#?}"
784 );
785 assert!(result.is_err());
786 assert_eq!(result.err().unwrap().to_string(), error_message);
787 }
788
789 #[test]
790 fn test_verify_conformity_when_no_spec_file_returns_error() {
791 let result = APISpec::verify_conformity(
792 vec![],
793 Method::GET.as_str(),
794 "/",
795 "application/json",
796 &Null,
797 &build_json_response(200, AggregatorFeaturesMessage::dummy()),
798 &StatusCode::OK,
799 );
800
801 assert!(result.is_err());
802 assert_eq!(
803 result.err().unwrap().to_string(),
804 "OpenAPI need a spec file to validate conformity. None were given."
805 );
806 }
807
808 #[test]
809 fn test_get_all_spec_files_not_empty() {
810 let spec_files = APISpec::get_all_spec_files();
811 assert!(!spec_files.is_empty());
812 assert!(spec_files.contains(&APISpec::get_default_spec_file()))
813 }
814
815 fn check_example_detect_no_error(id: u32, paths: &str, components: &str) {
816 let file = get_temp_openapi_filename("example", id);
817
818 write_minimal_open_api_file("1.0.0", &file, paths, components);
819
820 let api_spec = APISpec::from_file(file.to_str().unwrap());
821 let errors: Vec<String> = api_spec.verify_examples();
822
823 let error_messages = errors.join("\n");
824 assert_eq!(0, errors.len(), "Error messages: {error_messages}");
825 }
826
827 #[test]
828 fn test_example_success_with_a_valid_example() {
829 let components = r#"
830 MyComponent:
831 type: object
832 properties:
833 id:
834 type: integer
835 example:
836 {
837 "id": 123,
838 }
839 "#;
840 check_example_detect_no_error(line!(), "", components);
841 }
842
843 #[test]
844 fn test_examples_success_with_a_valid_examples() {
845 let components = r#"
846 MyComponent:
847 type: object
848 properties:
849 id:
850 type: integer
851 examples:
852 - {
853 "id": 123
854 }
855 - {
856 "id": 456
857 }
858 "#;
859 check_example_detect_no_error(line!(), "", components);
860 }
861
862 #[test]
863 fn test_examples_is_verified_on_object() {
864 let components = r#"
865 MyComponent:
866 type: object
867 properties:
868 id:
869 type: integer
870 examples:
871 - {
872 "id": 123
873 }
874 - {
875 "id": "abc"
876 }
877 - {
878 "id": "def"
879 }
880 "#;
881 check_example_errors_is_detected(
882 line!(),
883 "",
884 components,
885 &[
886 "\"abc\" is not of type \"integer\"",
887 "\"def\" is not of type \"integer\"",
888 ],
889 );
890 }
891
892 #[test]
893 fn test_examples_should_be_an_array() {
894 let components = r#"
895 MyComponent:
896 type: object
897 properties:
898 id:
899 type: integer
900 examples:
901 {
902 "id": 123
903 }
904 "#;
905 check_example_errors_is_detected(
906 line!(),
907 "",
908 components,
909 &["Examples should be an array", "Examples: {\"id\":123}"],
910 );
911 }
912
913 #[test]
914 fn test_example_is_verified_on_object() {
915 let components = r#"
916 MyComponent:
917 type: object
918 properties:
919 id:
920 type: integer
921 example:
922 {
923 "id": "abc",
924 }
925 "#;
926 check_example_errors_is_detected(
927 line!(),
928 "",
929 components,
930 &["\"abc\" is not of type \"integer\""],
931 );
932 }
933
934 #[test]
935 fn test_example_is_verified_on_array() {
936 let components = r#"
937 MyComponent:
938 type: array
939 items:
940 type: integer
941 example:
942 [
943 "abc"
944 ]
945 "#;
946 check_example_errors_is_detected(
947 line!(),
948 "",
949 components,
950 &["\"abc\" is not of type \"integer\""],
951 );
952 }
953
954 #[test]
955 fn test_example_is_verified_on_array_item() {
956 let components = r#"
957 MyComponent:
958 type: array
959 items:
960 type: integer
961 example:
962 "abc"
963 "#;
964 check_example_errors_is_detected(
965 line!(),
966 "",
967 components,
968 &["\"abc\" is not of type \"integer\""],
969 );
970 }
971
972 #[test]
973 fn test_example_is_verified_on_parameter() {
974 let paths = r#"
975 /my_route:
976 get:
977 parameters:
978 - name: id
979 in: path
980 schema:
981 type: integer
982 example: "abc"
983 "#;
984 check_example_errors_is_detected(
985 line!(),
986 paths,
987 "",
988 &["\"abc\" is not of type \"integer\""],
989 );
990 }
991
992 #[test]
993 fn test_example_is_verified_on_array_parameter() {
994 let paths = r#"
995 /my_route:
996 get:
997 parameters:
998 - name: id
999 in: path
1000 schema:
1001 type: array
1002 items:
1003 type: integer
1004 example:
1005 [
1006 "abc"
1007 ]
1008 "#;
1009 check_example_errors_is_detected(
1010 line!(),
1011 paths,
1012 "",
1013 &["\"abc\" is not of type \"integer\""],
1014 );
1015 }
1016
1017 #[test]
1018 fn test_example_is_verified_on_array_parameter_schema() {
1019 let paths = r#"
1020 /my_route:
1021 get:
1022 parameters:
1023 - name: id
1024 in: path
1025 schema:
1026 type: array
1027 items:
1028 type: integer
1029 example:
1030 [
1031 "abc"
1032 ]
1033 "#;
1034 check_example_errors_is_detected(
1035 line!(),
1036 paths,
1037 "",
1038 &["\"abc\" is not of type \"integer\""],
1039 );
1040 }
1041
1042 #[test]
1043 fn test_example_is_verified_on_array_parameter_item() {
1044 let paths = r#"
1045 /my_route:
1046 get:
1047 parameters:
1048 - name: id
1049 in: path
1050 schema:
1051 type: array
1052 items:
1053 type: integer
1054 example:
1055 "abc"
1056 "#;
1057 check_example_errors_is_detected(
1058 line!(),
1059 paths,
1060 "",
1061 &["\"abc\" is not of type \"integer\""],
1062 );
1063 }
1064
1065 #[test]
1066 fn test_example_is_verified_on_referenced_component() {
1067 let paths = r#"
1068 /my_route:
1069 get:
1070 parameters:
1071 - name: id
1072 in: path
1073 schema:
1074 $ref: '#/components/schemas/MyComponent'
1075 example: "abc"
1076 "#;
1077 let components = r#"
1078 MyComponent:
1079 type: integer
1080 "#;
1081
1082 check_example_errors_is_detected(
1083 line!(),
1084 paths,
1085 components,
1086 &["\"abc\" is not of type \"integer\""],
1087 );
1088 }
1089}