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!("{}/openapi*.yaml", root_path)).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!(" {}\n Example: {}\n", e, example));
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: {}\n",
329 examples
330 ));
331 }
332 }
333
334 if !errors.is_empty() {
335 vec![format!("- {}: Error\n{}", name, errors.join("\n"))]
336 } else {
337 vec![]
338 }
339 }
340}
341
342fn check_query_parameter_limitations(url: &Url, operation_object: &Value) {
345 if url.query_pairs().count() >= 2 {
346 panic!("This method does not work with multiple parameters");
347 }
348
349 if let Some(parameters) = operation_object["parameters"].as_array() {
350 let len = parameters
351 .iter()
352 .filter(|p| p["in"].eq("query"))
353 .collect::<Vec<_>>()
354 .len();
355 if len >= 2 {
356 panic!("This method does not work with multiple parameters");
357 }
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use std::fs;
364 use std::path::{Path, PathBuf};
365 use warp::http::Method;
366 use warp::http::StatusCode;
367
368 use super::*;
369 use crate::entities;
370 use crate::messages::{AggregatorFeaturesMessage, SignerMessagePart};
371 use crate::test_utils::{fake_data, TempDir};
372
373 fn build_empty_response(status_code: u16) -> Response<Bytes> {
374 Response::builder()
375 .status(status_code)
376 .body(Bytes::new())
377 .unwrap()
378 }
379
380 fn build_json_response<T: Serialize>(status_code: u16, value: T) -> Response<Bytes> {
381 Response::builder()
382 .status(status_code)
383 .body(Bytes::from(json!(value).to_string().into_bytes()))
384 .unwrap()
385 }
386
387 fn build_response(status_code: u16, content: &'static [u8]) -> Response<Bytes> {
388 Response::builder()
389 .status(status_code)
390 .body(Bytes::from_static(content))
391 .unwrap()
392 }
393
394 fn get_temp_dir(dir_name: &str) -> PathBuf {
395 TempDir::create("apispec", dir_name)
396 }
397
398 fn get_temp_openapi_filename(name: &str, id: u32) -> PathBuf {
399 get_temp_dir(&format!("{name}-{id}")).join("openapi.yaml")
400 }
401
402 fn write_minimal_open_api_file(
403 version: &str,
404 path: &Path,
405 openapi_paths: &str,
406 openapi_components: &str,
407 ) {
408 fs::write(
409 path,
410 format!(
411 r#"openapi: "3.0.0"
412info:
413 version: {version}
414 title: Minimal Open Api File
415
416paths:
417{openapi_paths}
418
419components:
420 schemas:
421{openapi_components}
422"#
423 ),
424 )
425 .unwrap()
426 }
427
428 fn check_example_errors_is_detected(
433 id: u32,
434 paths: &str,
435 components: &str,
436 expected_error_messages: &[&str],
437 ) {
438 let file = get_temp_openapi_filename("example", id);
439
440 write_minimal_open_api_file("1.0.0", &file, paths, components);
441
442 let api_spec = APISpec::from_file(file.to_str().unwrap());
443 let errors: Vec<String> = api_spec.verify_examples();
444
445 assert_eq!(1, errors.len());
446 let error_message = errors.first().unwrap();
447 for expected_message in expected_error_messages {
448 assert!(
449 error_message.contains(expected_message),
450 "Error message: {:?}\nshould contains: {}\n",
451 errors,
452 expected_message
453 );
454 }
455 }
456
457 #[test]
458 fn test_validate_a_response_without_body() {
459 let file = get_temp_openapi_filename("validate_a_response_without_body", line!());
460 let paths = r#"
461 /empty-route:
462 get:
463 responses:
464 "204":
465 description: not available
466 "#;
467 write_minimal_open_api_file("1.0.0", &file, paths, "");
468
469 APISpec::from_file(file.to_str().unwrap())
470 .method(Method::GET.as_str())
471 .path("/empty-route")
472 .validate_request(&Null)
473 .unwrap()
474 .validate_response(&build_empty_response(204))
475 .unwrap();
476 }
477
478 #[test]
479 fn test_validate_ok_when_request_without_body_and_expects_response() {
480 APISpec::from_file(&APISpec::get_default_spec_file())
481 .method(Method::GET.as_str())
482 .path("/")
483 .validate_request(&Null)
484 .unwrap()
485 .validate_response(&build_json_response(
486 200,
487 AggregatorFeaturesMessage::dummy(),
488 ))
489 .unwrap();
490 }
491
492 #[test]
493 fn test_validate_ok_when_request_with_body_and_expects_no_response() {
494 assert!(APISpec::from_file(&APISpec::get_default_spec_file())
495 .method(Method::POST.as_str())
496 .path("/register-signer")
497 .validate_request(&SignerMessagePart::dummy())
498 .unwrap()
499 .validate_response(&Response::<Bytes>::new(Bytes::new()))
500 .is_err());
501 }
502
503 #[test]
504 fn test_validate_ok_when_response_match_default_status_code() {
505 let response = build_json_response(
508 StatusCode::INTERNAL_SERVER_ERROR.into(),
509 entities::ServerError::new("an error occurred".to_string()),
510 );
511
512 APISpec::from_file(&APISpec::get_default_spec_file())
513 .method(Method::POST.as_str())
514 .path("/register-signer")
515 .validate_response(&response)
516 .unwrap();
517 }
518
519 #[test]
520 fn test_should_fail_when_the_status_code_is_not_the_expected_one() {
521 let response = build_json_response(
522 StatusCode::INTERNAL_SERVER_ERROR.into(),
523 entities::ServerError::new("an error occurred".to_string()),
524 );
525
526 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
527 let result = api_spec
528 .method(Method::GET.as_str())
529 .path("/")
530 .validate_request(&Null)
531 .unwrap()
532 .validate_status(&response, &StatusCode::OK);
533
534 assert!(result.is_err());
535 assert_eq!(
536 result.err().unwrap().to_string(),
537 format!(
538 "expected status code {} but was {}",
539 StatusCode::OK.as_u16(),
540 StatusCode::INTERNAL_SERVER_ERROR.as_u16()
541 )
542 );
543 }
544
545 #[test]
546 fn test_should_be_ok_when_the_status_code_is_the_right_one() {
547 let response = build_json_response(
548 StatusCode::INTERNAL_SERVER_ERROR.into(),
549 entities::ServerError::new("an error occurred".to_string()),
550 );
551
552 APISpec::from_file(&APISpec::get_default_spec_file())
553 .method(Method::GET.as_str())
554 .path("/")
555 .validate_request(&Null)
556 .unwrap()
557 .validate_status(&response, &StatusCode::INTERNAL_SERVER_ERROR)
558 .unwrap();
559 }
560
561 #[test]
562 fn test_validate_returns_error_when_route_does_not_exist() {
563 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
564 let result = api_spec
565 .method(Method::GET.as_str())
566 .path("/route-not-existing-in-openapi-spec")
567 .validate_response(&build_response(200, b"abcdefgh"));
568
569 assert!(result.is_err());
570 assert_eq!(
571 result.err(),
572 Some("Unmatched path and method: /route-not-existing-in-openapi-spec GET".to_string())
573 );
574 }
575
576 #[test]
577 fn test_validate_returns_error_when_route_exists_but_method_does_not() {
578 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
579 let result = api_spec
580 .method(Method::OPTIONS.as_str())
581 .path("/certificates")
582 .validate_response(&build_response(200, b"abcdefgh"));
583
584 assert!(result.is_err());
585 assert_eq!(
586 result.err(),
587 Some("Unmatched path and method: /certificates OPTIONS".to_string())
588 );
589 }
590 #[test]
591 fn test_validate_returns_error_when_route_exists_but_expects_non_empty_response() {
592 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
593 let result = api_spec
594 .method(Method::GET.as_str())
595 .path("/certificates")
596 .validate_response(&build_empty_response(200));
597
598 assert!(result.is_err());
599 assert_eq!(result.err(), Some("Non empty body expected".to_string()));
600 }
601
602 #[test]
603 fn test_validate_returns_error_when_route_exists_but_expects_empty_response() {
604 {
605 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
606 let result = api_spec
607 .method(Method::POST.as_str())
608 .path("/register-signer")
609 .validate_response(&build_response(201, b"abcdefgh"));
610
611 assert!(result.is_err());
612 assert_eq!(
613 result.err(),
614 Some("Expected empty body but got: b\"abcdefgh\"".to_string())
615 );
616 }
617 {
618 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
619 let result = api_spec
620 .method(Method::POST.as_str())
621 .path("/register-signer")
622 .validate_response(&build_json_response(201, "something"));
623
624 assert!(result.is_err());
625 assert_eq!(
626 result.err(),
627 Some("Expected empty body but got: b\"\\\"something\\\"\"".to_string())
628 );
629 }
630 }
631
632 #[test]
633 fn test_validate_returns_error_when_json_is_not_valid() {
634 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
635 let result = api_spec
636 .method(Method::GET.as_str())
637 .path("/certificates")
638 .validate_request(&Null)
639 .unwrap()
640 .validate_response(&build_response(200, b"not a json"));
641 assert_eq!(
642 result.err(),
643 Some("Expected a valid json but got: b\"not a json\"".to_string())
644 );
645 }
646
647 #[test]
648 fn test_validate_returns_errors_when_route_exists_but_does_not_expect_request_body() {
649 assert!(APISpec::from_file(&APISpec::get_default_spec_file())
650 .method(Method::GET.as_str())
651 .path("/certificates")
652 .validate_request(&fake_data::beacon())
653 .is_err());
654 }
655 #[test]
656 fn test_validate_returns_error_when_route_exists_but_expects_non_empty_request_body() {
657 assert!(APISpec::from_file(&APISpec::get_default_spec_file())
658 .method(Method::POST.as_str())
659 .path("/register-signer")
660 .validate_request(&Null)
661 .is_err());
662 }
663
664 #[test]
665 fn test_validate_returns_error_when_content_type_does_not_exist() {
666 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
667 let result = api_spec
668 .method(Method::GET.as_str())
669 .path("/certificates")
670 .content_type("whatever")
671 .validate_request(&Null)
672 .unwrap()
673 .validate_response(&build_empty_response(200));
674
675 assert!(result.is_err());
676 assert_eq!(
677 result.err().unwrap().to_string(),
678 "Expected content type 'whatever' but spec is '{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/CertificateListMessage\"}}}'",
679 );
680 }
681
682 #[test]
683 fn test_validate_a_response_with_query_parameters() {
684 APISpec::from_file(&APISpec::get_default_spec_file())
685 .method(Method::GET.as_str())
686 .path("/proof/cardano-transaction?transaction_hashes={hash}")
687 .validate_request(&Null)
688 .unwrap()
689 .validate_response(&build_empty_response(404))
690 .map(|_apispec| ())
691 .unwrap();
692 }
693
694 #[test]
695 fn test_validate_a_request_with_wrong_query_parameter_name() {
696 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
697 let result = api_spec
698 .method(Method::GET.as_str())
699 .path("/proof/cardano-transaction?whatever=123")
700 .validate_request(&Null);
701
702 assert!(result.is_err());
703 assert_eq!(
704 result.err().unwrap().to_string(),
705 "Unexpected query parameter 'whatever'",
706 );
707 }
708
709 #[test]
710 fn test_validate_a_request_should_failed_when_query_parameter_is_in_path() {
711 let mut api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
712 let result = api_spec
713 .method(Method::GET.as_str())
714 .path("/artifact/cardano-transaction/{hash}?hash=456")
715 .validate_request(&Null);
716
717 assert!(result.is_err());
718 assert_eq!(
719 result.err().unwrap().to_string(),
720 "Unexpected query parameter 'hash'",
721 );
722 }
723
724 #[test]
725 fn test_validate_query_parameters_with_correct_parameter_name() {
726 let api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
727 api_spec
728 .validate_query_parameters(
729 "/proof/cardano-transaction?transaction_hashes=a123,b456",
730 &api_spec.openapi["paths"]["/proof/cardano-transaction"]["get"],
731 )
732 .map(|_apispec| ())
733 .unwrap()
734 }
735
736 #[test]
737 fn test_validate_query_parameters_with_wrong_query_parameter_name() {
738 let api_spec = APISpec::from_file(&APISpec::get_default_spec_file());
739 let result = api_spec.validate_query_parameters(
740 "/proof/cardano-transaction?whatever=123",
741 &api_spec.openapi["paths"]["/proof/cardano-transaction"]["get"],
742 );
743
744 assert!(result.is_err());
745 assert_eq!(
746 result.err().unwrap().to_string(),
747 "Unexpected query parameter 'whatever'",
748 );
749 }
750
751 #[test]
752 fn test_verify_conformity_with_expected_status() {
753 APISpec::verify_conformity(
754 APISpec::get_all_spec_files(),
755 Method::GET.as_str(),
756 "/",
757 "application/json",
758 &Null,
759 &build_json_response(200, AggregatorFeaturesMessage::dummy()),
760 &StatusCode::OK,
761 )
762 .unwrap()
763 }
764
765 #[test]
766 fn test_verify_conformity_with_non_expected_status_returns_error() {
767 let response = build_json_response(200, AggregatorFeaturesMessage::dummy());
768
769 let spec_file = APISpec::get_default_spec_file();
770 let result = APISpec::verify_conformity(
771 vec![spec_file.clone()],
772 Method::GET.as_str(),
773 "/",
774 "application/json",
775 &Null,
776 &response,
777 &StatusCode::BAD_REQUEST,
778 );
779
780 let error_reason = format!(
781 "expected status code {} but was {}",
782 StatusCode::BAD_REQUEST.as_u16(),
783 StatusCode::OK.as_u16()
784 );
785 let error_message = format!(
786 "OpenAPI invalid response in {spec_file} on route /, reason: {error_reason}\nresponse: {response:#?}"
787 );
788 assert!(result.is_err());
789 assert_eq!(result.err().unwrap().to_string(), error_message);
790 }
791
792 #[test]
793 fn test_verify_conformity_when_no_spec_file_returns_error() {
794 let result = APISpec::verify_conformity(
795 vec![],
796 Method::GET.as_str(),
797 "/",
798 "application/json",
799 &Null,
800 &build_json_response(200, AggregatorFeaturesMessage::dummy()),
801 &StatusCode::OK,
802 );
803
804 assert!(result.is_err());
805 assert_eq!(
806 result.err().unwrap().to_string(),
807 "OpenAPI need a spec file to validate conformity. None were given."
808 );
809 }
810
811 #[test]
812 fn test_get_all_spec_files_not_empty() {
813 let spec_files = APISpec::get_all_spec_files();
814 assert!(!spec_files.is_empty());
815 assert!(spec_files.contains(&APISpec::get_default_spec_file()))
816 }
817
818 fn check_example_detect_no_error(id: u32, paths: &str, components: &str) {
819 let file = get_temp_openapi_filename("example", id);
820
821 write_minimal_open_api_file("1.0.0", &file, paths, components);
822
823 let api_spec = APISpec::from_file(file.to_str().unwrap());
824 let errors: Vec<String> = api_spec.verify_examples();
825
826 let error_messages = errors.join("\n");
827 assert_eq!(0, errors.len(), "Error messages: {}", error_messages);
828 }
829
830 #[test]
831 fn test_example_success_with_a_valid_example() {
832 let components = r#"
833 MyComponent:
834 type: object
835 properties:
836 id:
837 type: integer
838 example:
839 {
840 "id": 123,
841 }
842 "#;
843 check_example_detect_no_error(line!(), "", components);
844 }
845
846 #[test]
847 fn test_examples_success_with_a_valid_examples() {
848 let components = r#"
849 MyComponent:
850 type: object
851 properties:
852 id:
853 type: integer
854 examples:
855 - {
856 "id": 123
857 }
858 - {
859 "id": 456
860 }
861 "#;
862 check_example_detect_no_error(line!(), "", components);
863 }
864
865 #[test]
866 fn test_examples_is_verified_on_object() {
867 let components = r#"
868 MyComponent:
869 type: object
870 properties:
871 id:
872 type: integer
873 examples:
874 - {
875 "id": 123
876 }
877 - {
878 "id": "abc"
879 }
880 - {
881 "id": "def"
882 }
883 "#;
884 check_example_errors_is_detected(
885 line!(),
886 "",
887 components,
888 &[
889 "\"abc\" is not of type \"integer\"",
890 "\"def\" is not of type \"integer\"",
891 ],
892 );
893 }
894
895 #[test]
896 fn test_examples_should_be_an_array() {
897 let components = r#"
898 MyComponent:
899 type: object
900 properties:
901 id:
902 type: integer
903 examples:
904 {
905 "id": 123
906 }
907 "#;
908 check_example_errors_is_detected(
909 line!(),
910 "",
911 components,
912 &["Examples should be an array", "Examples: {\"id\":123}"],
913 );
914 }
915
916 #[test]
917 fn test_example_is_verified_on_object() {
918 let components = r#"
919 MyComponent:
920 type: object
921 properties:
922 id:
923 type: integer
924 example:
925 {
926 "id": "abc",
927 }
928 "#;
929 check_example_errors_is_detected(
930 line!(),
931 "",
932 components,
933 &["\"abc\" is not of type \"integer\""],
934 );
935 }
936
937 #[test]
938 fn test_example_is_verified_on_array() {
939 let components = r#"
940 MyComponent:
941 type: array
942 items:
943 type: integer
944 example:
945 [
946 "abc"
947 ]
948 "#;
949 check_example_errors_is_detected(
950 line!(),
951 "",
952 components,
953 &["\"abc\" is not of type \"integer\""],
954 );
955 }
956
957 #[test]
958 fn test_example_is_verified_on_array_item() {
959 let components = r#"
960 MyComponent:
961 type: array
962 items:
963 type: integer
964 example:
965 "abc"
966 "#;
967 check_example_errors_is_detected(
968 line!(),
969 "",
970 components,
971 &["\"abc\" is not of type \"integer\""],
972 );
973 }
974
975 #[test]
976 fn test_example_is_verified_on_parameter() {
977 let paths = r#"
978 /my_route:
979 get:
980 parameters:
981 - name: id
982 in: path
983 schema:
984 type: integer
985 example: "abc"
986 "#;
987 check_example_errors_is_detected(
988 line!(),
989 paths,
990 "",
991 &["\"abc\" is not of type \"integer\""],
992 );
993 }
994
995 #[test]
996 fn test_example_is_verified_on_array_parameter() {
997 let paths = r#"
998 /my_route:
999 get:
1000 parameters:
1001 - name: id
1002 in: path
1003 schema:
1004 type: array
1005 items:
1006 type: integer
1007 example:
1008 [
1009 "abc"
1010 ]
1011 "#;
1012 check_example_errors_is_detected(
1013 line!(),
1014 paths,
1015 "",
1016 &["\"abc\" is not of type \"integer\""],
1017 );
1018 }
1019
1020 #[test]
1021 fn test_example_is_verified_on_array_parameter_schema() {
1022 let paths = r#"
1023 /my_route:
1024 get:
1025 parameters:
1026 - name: id
1027 in: path
1028 schema:
1029 type: array
1030 items:
1031 type: integer
1032 example:
1033 [
1034 "abc"
1035 ]
1036 "#;
1037 check_example_errors_is_detected(
1038 line!(),
1039 paths,
1040 "",
1041 &["\"abc\" is not of type \"integer\""],
1042 );
1043 }
1044
1045 #[test]
1046 fn test_example_is_verified_on_array_parameter_item() {
1047 let paths = r#"
1048 /my_route:
1049 get:
1050 parameters:
1051 - name: id
1052 in: path
1053 schema:
1054 type: array
1055 items:
1056 type: integer
1057 example:
1058 "abc"
1059 "#;
1060 check_example_errors_is_detected(
1061 line!(),
1062 paths,
1063 "",
1064 &["\"abc\" is not of type \"integer\""],
1065 );
1066 }
1067
1068 #[test]
1069 fn test_example_is_verified_on_referenced_component() {
1070 let paths = r#"
1071 /my_route:
1072 get:
1073 parameters:
1074 - name: id
1075 in: path
1076 schema:
1077 $ref: '#/components/schemas/MyComponent'
1078 example: "abc"
1079 "#;
1080 let components = r#"
1081 MyComponent:
1082 type: integer
1083 "#;
1084
1085 check_example_errors_is_detected(
1086 line!(),
1087 paths,
1088 components,
1089 &["\"abc\" is not of type \"integer\""],
1090 );
1091 }
1092}