mithril_common/test_utils/
apispec.rs

1//! Tools to helps validate conformity to an OpenAPI specification
2
3use 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
15/// APISpec helps validate conformity to an OpenAPI specification
16pub 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    /// Verify conformity helper of API Specs
25    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    /// APISpec factory from spec
58    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    /// Sets the path to specify/check.
70    pub fn path(&'a mut self, path: &'a str) -> &'a mut APISpec<'a> {
71        self.path = Some(path);
72        self
73    }
74
75    /// Sets the method to specify/check.
76    pub fn method(&'a mut self, method: &'a str) -> &'a mut APISpec<'a> {
77        self.method = Some(method);
78        self
79    }
80
81    /// Sets the content type to specify/check, note that it defaults to "application/json".
82    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    /// Validates if a request is valid
88    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(&parameter.0) {
122                Ok(self)
123            } else {
124                Err(format!("Unexpected query parameter '{}'", parameter.0))
125            }
126        } else {
127            Ok(self)
128        }
129    }
130
131    /// Validates if the status is the expected one
132    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    /// Validates if a response is valid
149    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    /// Validates conformity of a value against a schema
213    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    /// Get default spec file
247    pub fn get_default_spec_file() -> String {
248        "../openapi.yaml".to_string()
249    }
250
251    /// Get spec file for era
252    pub fn get_era_spec_file(era: SupportedEra) -> String {
253        format!("../openapi-{}", era)
254    }
255
256    /// Get all spec files
257    pub fn get_all_spec_files() -> Vec<String> {
258        APISpec::get_all_spec_files_from("..")
259    }
260
261    /// Get all spec files in the directory
262    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    /// Verify that examples are conform to the type definition.
274    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        // The type definition is at the same level as the example (components) unless there is a schema property (paths).
317        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
342// TODO: For now, it verifies only one parameter,
343// should verify with multiple query parameters using an openapi.yaml file for test.
344fn 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    /// To check that the example is verified,
429    /// we create an openapi.yaml with an invalid example.
430    /// If the example is verified, we should have an error message.
431    /// A simple invalid example is one with a wrong type (string instead of integer)
432    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        // INTERNAL_SERVER_ERROR(500) is not one of the defined status code
506        // for this route, so it's the default response spec that is used.
507        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}