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!("{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    /// 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!("    {e}\n    Example: {example}\n"));
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: {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
341// TODO: For now, it verifies only one parameter,
342// should verify with multiple query parameters using an openapi.yaml file for test.
343fn 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    /// To check that the example is verified,
428    /// we create an openapi.yaml with an invalid example.
429    /// If the example is verified, we should have an error message.
430    /// A simple invalid example is one with a wrong type (string instead of integer)
431    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        // INTERNAL_SERVER_ERROR(500) is not one of the defined status code
503        // for this route, so it's the default response spec that is used.
504        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}