1mod ai_assisted;
7mod schema_based;
8
9use crate::OpenApiSpec;
10use async_trait::async_trait;
11use chrono;
12use mockforge_foundation::ai_response::{AiResponseConfig, RequestContext};
13use mockforge_foundation::error::Result;
14use mockforge_foundation::intelligent_behavior::Persona;
15use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
16use rand::Rng;
17use serde_json::Value;
18use std::collections::HashMap;
19use uuid;
20
21#[async_trait]
26pub trait AiGenerator: Send + Sync {
27 async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
36}
37
38fn inject_response_violations_enabled() -> bool {
45 std::env::var("MOCKFORGE_INJECT_RESPONSE_VIOLATIONS")
46 .ok()
47 .map(|s| matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
48 .unwrap_or(false)
49}
50
51pub struct ResponseGenerator;
53
54impl ResponseGenerator {
55 pub fn generate_response(
57 spec: &OpenApiSpec,
58 operation: &Operation,
59 status_code: u16,
60 content_type: Option<&str>,
61 ) -> Result<Value> {
62 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
63 }
64
65 pub fn generate_response_with_expansion(
67 spec: &OpenApiSpec,
68 operation: &Operation,
69 status_code: u16,
70 content_type: Option<&str>,
71 expand_tokens: bool,
72 ) -> Result<Value> {
73 Self::generate_response_with_expansion_and_mode(
74 spec,
75 operation,
76 status_code,
77 content_type,
78 expand_tokens,
79 None,
80 None,
81 )
82 }
83
84 pub fn generate_response_with_expansion_and_mode(
86 spec: &OpenApiSpec,
87 operation: &Operation,
88 status_code: u16,
89 content_type: Option<&str>,
90 expand_tokens: bool,
91 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
92 selector: Option<&crate::response_selection::ResponseSelector>,
93 ) -> Result<Value> {
94 Self::generate_response_with_expansion_and_mode_and_persona(
95 spec,
96 operation,
97 status_code,
98 content_type,
99 expand_tokens,
100 selection_mode,
101 selector,
102 None, )
104 }
105
106 #[allow(clippy::too_many_arguments)]
108 pub fn generate_response_with_expansion_and_mode_and_persona(
109 spec: &OpenApiSpec,
110 operation: &Operation,
111 status_code: u16,
112 content_type: Option<&str>,
113 expand_tokens: bool,
114 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
115 selector: Option<&crate::response_selection::ResponseSelector>,
116 persona: Option<&Persona>,
117 ) -> Result<Value> {
118 Self::generate_response_with_scenario_and_mode_and_persona(
119 spec,
120 operation,
121 status_code,
122 content_type,
123 expand_tokens,
124 None, selection_mode,
126 selector,
127 persona,
128 )
129 }
130
131 pub fn generate_response_with_scenario(
157 spec: &OpenApiSpec,
158 operation: &Operation,
159 status_code: u16,
160 content_type: Option<&str>,
161 expand_tokens: bool,
162 scenario: Option<&str>,
163 ) -> Result<Value> {
164 Self::generate_response_with_scenario_and_mode(
165 spec,
166 operation,
167 status_code,
168 content_type,
169 expand_tokens,
170 scenario,
171 None,
172 None,
173 )
174 }
175
176 #[allow(clippy::too_many_arguments)]
178 pub fn generate_response_with_scenario_and_mode(
179 spec: &OpenApiSpec,
180 operation: &Operation,
181 status_code: u16,
182 content_type: Option<&str>,
183 expand_tokens: bool,
184 scenario: Option<&str>,
185 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
186 selector: Option<&crate::response_selection::ResponseSelector>,
187 ) -> Result<Value> {
188 Self::generate_response_with_scenario_and_mode_and_persona(
189 spec,
190 operation,
191 status_code,
192 content_type,
193 expand_tokens,
194 scenario,
195 selection_mode,
196 selector,
197 None, )
199 }
200
201 #[allow(clippy::too_many_arguments)]
203 pub fn generate_response_with_scenario_and_mode_and_persona(
204 spec: &OpenApiSpec,
205 operation: &Operation,
206 status_code: u16,
207 content_type: Option<&str>,
208 expand_tokens: bool,
209 scenario: Option<&str>,
210 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
211 selector: Option<&crate::response_selection::ResponseSelector>,
212 _persona: Option<&Persona>,
213 ) -> Result<Value> {
214 let response = Self::find_response_for_status(&operation.responses, status_code);
216
217 tracing::debug!(
218 "Finding response for status code {}: {:?}",
219 status_code,
220 if response.is_some() {
221 "found"
222 } else {
223 "not found"
224 }
225 );
226
227 let body = match response {
228 Some(response_ref) => match response_ref {
229 ReferenceOr::Item(response) => {
230 tracing::debug!(
231 "Using direct response item with {} content types",
232 response.content.len()
233 );
234 Self::generate_from_response_with_scenario_and_mode(
235 spec,
236 response,
237 content_type,
238 expand_tokens,
239 scenario,
240 selection_mode,
241 selector,
242 )
243 }
244 ReferenceOr::Reference { reference } => {
245 tracing::debug!("Resolving response reference: {}", reference);
246 if let Some(resolved_response) = spec.get_response(reference) {
247 tracing::debug!(
248 "Resolved response reference with {} content types",
249 resolved_response.content.len()
250 );
251 Self::generate_from_response_with_scenario_and_mode(
252 spec,
253 resolved_response,
254 content_type,
255 expand_tokens,
256 scenario,
257 selection_mode,
258 selector,
259 )
260 } else {
261 tracing::warn!("Response reference '{}' not found in spec", reference);
262 Ok(Value::Object(serde_json::Map::new()))
263 }
264 }
265 },
266 None => {
267 tracing::warn!(
268 "No response found for status code {} in operation. Available status codes: {:?}",
269 status_code,
270 operation.responses.responses.keys().collect::<Vec<_>>()
271 );
272 Ok(Value::Object(serde_json::Map::new()))
273 }
274 };
275
276 body.map(|b| Self::maybe_inject_response_violation(spec, operation, status_code, b))
285 }
286
287 fn maybe_inject_response_violation(
294 spec: &OpenApiSpec,
295 operation: &Operation,
296 status_code: u16,
297 body: Value,
298 ) -> Value {
299 if !inject_response_violations_enabled() {
300 return body;
301 }
302 if !(200..300).contains(&status_code) {
303 return body;
304 }
305 let Some(required_field) =
306 Self::first_required_field_for_status(spec, operation, status_code)
307 else {
308 return body;
309 };
310 match body {
311 Value::Object(mut map) => {
312 if map.remove(&required_field).is_some() {
313 tracing::info!(
314 "MOCKFORGE_INJECT_RESPONSE_VIOLATIONS dropped required field '{required_field}' from {status_code} response"
315 );
316 }
317 Value::Object(map)
318 }
319 other => other,
320 }
321 }
322
323 fn first_required_field_for_status(
329 spec: &OpenApiSpec,
330 operation: &Operation,
331 status_code: u16,
332 ) -> Option<String> {
333 let response_ref = Self::find_response_for_status(&operation.responses, status_code)?;
334 let response: &Response = match response_ref {
335 ReferenceOr::Item(r) => r,
336 ReferenceOr::Reference { reference } => spec.get_response(reference)?,
337 };
338 let media = response.content.get("application/json")?;
339 let schema_ref = media.schema.as_ref()?;
340 let schema = match schema_ref {
341 ReferenceOr::Item(s) => s.clone(),
342 ReferenceOr::Reference { reference } => spec.resolve_schema_ref(reference)?,
343 };
344 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
345 obj.required.first().cloned()
346 } else {
347 None
348 }
349 }
350
351 fn find_response_for_status(
353 responses: &Responses,
354 status_code: u16,
355 ) -> Option<&ReferenceOr<Response>> {
356 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
358 return Some(response);
359 }
360
361 if let Some(default_response) = &responses.default {
363 return Some(default_response);
364 }
365
366 None
367 }
368
369 #[allow(dead_code)]
371 fn generate_from_response(
372 spec: &OpenApiSpec,
373 response: &Response,
374 content_type: Option<&str>,
375 expand_tokens: bool,
376 ) -> Result<Value> {
377 Self::generate_from_response_with_scenario(
378 spec,
379 response,
380 content_type,
381 expand_tokens,
382 None,
383 )
384 }
385
386 #[allow(dead_code)]
388 fn generate_from_response_with_scenario(
389 spec: &OpenApiSpec,
390 response: &Response,
391 content_type: Option<&str>,
392 expand_tokens: bool,
393 scenario: Option<&str>,
394 ) -> Result<Value> {
395 Self::generate_from_response_with_scenario_and_mode(
396 spec,
397 response,
398 content_type,
399 expand_tokens,
400 scenario,
401 None,
402 None,
403 )
404 }
405
406 fn generate_from_response_with_scenario_and_mode(
408 spec: &OpenApiSpec,
409 response: &Response,
410 content_type: Option<&str>,
411 expand_tokens: bool,
412 scenario: Option<&str>,
413 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
414 selector: Option<&crate::response_selection::ResponseSelector>,
415 ) -> Result<Value> {
416 Self::generate_from_response_with_scenario_and_mode_and_persona(
417 spec,
418 response,
419 content_type,
420 expand_tokens,
421 scenario,
422 selection_mode,
423 selector,
424 None, )
426 }
427
428 #[allow(clippy::too_many_arguments)]
430 #[allow(dead_code)]
431 fn generate_from_response_with_scenario_and_mode_and_persona(
432 spec: &OpenApiSpec,
433 response: &Response,
434 content_type: Option<&str>,
435 expand_tokens: bool,
436 scenario: Option<&str>,
437 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
438 selector: Option<&crate::response_selection::ResponseSelector>,
439 persona: Option<&Persona>,
440 ) -> Result<Value> {
441 if let Some(content_type) = content_type {
443 if let Some(media_type) = response.content.get(content_type) {
444 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
445 spec,
446 media_type,
447 expand_tokens,
448 scenario,
449 selection_mode,
450 selector,
451 persona,
452 );
453 }
454 }
455
456 let preferred_types = ["application/json", "application/xml", "text/plain"];
458
459 for content_type in &preferred_types {
460 if let Some(media_type) = response.content.get(*content_type) {
461 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
462 spec,
463 media_type,
464 expand_tokens,
465 scenario,
466 selection_mode,
467 selector,
468 persona,
469 );
470 }
471 }
472
473 if let Some((_, media_type)) = response.content.iter().next() {
475 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
476 spec,
477 media_type,
478 expand_tokens,
479 scenario,
480 selection_mode,
481 selector,
482 persona,
483 );
484 }
485
486 Ok(Value::Object(serde_json::Map::new()))
488 }
489
490 #[allow(dead_code)]
492 fn generate_from_media_type(
493 spec: &OpenApiSpec,
494 media_type: &openapiv3::MediaType,
495 expand_tokens: bool,
496 ) -> Result<Value> {
497 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
498 }
499
500 #[allow(dead_code)]
502 fn generate_from_media_type_with_scenario(
503 spec: &OpenApiSpec,
504 media_type: &openapiv3::MediaType,
505 expand_tokens: bool,
506 scenario: Option<&str>,
507 ) -> Result<Value> {
508 Self::generate_from_media_type_with_scenario_and_mode(
509 spec,
510 media_type,
511 expand_tokens,
512 scenario,
513 None,
514 None,
515 )
516 }
517
518 #[allow(dead_code)]
520 fn generate_from_media_type_with_scenario_and_mode(
521 spec: &OpenApiSpec,
522 media_type: &openapiv3::MediaType,
523 expand_tokens: bool,
524 scenario: Option<&str>,
525 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
526 selector: Option<&crate::response_selection::ResponseSelector>,
527 ) -> Result<Value> {
528 Self::generate_from_media_type_with_scenario_and_mode_and_persona(
529 spec,
530 media_type,
531 expand_tokens,
532 scenario,
533 selection_mode,
534 selector,
535 None, )
537 }
538
539 fn generate_from_media_type_with_scenario_and_mode_and_persona(
541 spec: &OpenApiSpec,
542 media_type: &openapiv3::MediaType,
543 expand_tokens: bool,
544 scenario: Option<&str>,
545 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
546 selector: Option<&crate::response_selection::ResponseSelector>,
547 persona: Option<&Persona>,
548 ) -> Result<Value> {
549 if let Some(example) = &media_type.example {
553 tracing::debug!("Using explicit example from media type: {:?}", example);
554 if expand_tokens {
556 let expanded_example = Self::expand_templates(example);
557 return Ok(expanded_example);
558 } else {
559 return Ok(example.clone());
560 }
561 }
562
563 if !media_type.examples.is_empty() {
567 use crate::response_selection::{ResponseSelectionMode, ResponseSelector};
568
569 tracing::debug!(
570 "Found {} examples in media type, available examples: {:?}",
571 media_type.examples.len(),
572 media_type.examples.keys().collect::<Vec<_>>()
573 );
574
575 if let Some(scenario_name) = scenario {
577 if let Some(example_ref) = media_type.examples.get(scenario_name) {
578 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
579 match Self::extract_example_value_with_persona(
580 spec,
581 example_ref,
582 expand_tokens,
583 persona,
584 media_type.schema.as_ref(),
585 ) {
586 Ok(value) => return Ok(value),
587 Err(e) => {
588 tracing::warn!(
589 "Failed to extract example for scenario '{}': {}, falling back",
590 scenario_name,
591 e
592 );
593 }
594 }
595 } else {
596 tracing::warn!(
597 "Scenario '{}' not found in examples, falling back based on selection mode",
598 scenario_name
599 );
600 }
601 }
602
603 let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
605
606 let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
608
609 if example_names.is_empty() {
610 tracing::warn!("Examples map is empty, falling back to schema generation");
612 } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
613 tracing::debug!("Scenario not found, using selection mode: {:?}", mode);
615 } else {
616 let selected_index = if let Some(sel) = selector {
618 sel.select(&example_names)
619 } else {
620 let temp_selector = ResponseSelector::new(mode);
622 temp_selector.select(&example_names)
623 };
624
625 if let Some(example_name) = example_names.get(selected_index) {
626 if let Some(example_ref) = media_type.examples.get(example_name) {
627 tracing::debug!(
628 "Using example '{}' from examples map (mode: {:?}, index: {})",
629 example_name,
630 mode,
631 selected_index
632 );
633 match Self::extract_example_value_with_persona(
634 spec,
635 example_ref,
636 expand_tokens,
637 persona,
638 media_type.schema.as_ref(),
639 ) {
640 Ok(value) => return Ok(value),
641 Err(e) => {
642 tracing::warn!(
643 "Failed to extract example '{}': {}, trying fallback",
644 example_name,
645 e
646 );
647 }
648 }
649 }
650 }
651 }
652
653 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
656 tracing::debug!(
657 "Using first example '{}' from examples map as fallback",
658 example_name
659 );
660 match Self::extract_example_value_with_persona(
661 spec,
662 example_ref,
663 expand_tokens,
664 persona,
665 media_type.schema.as_ref(),
666 ) {
667 Ok(value) => {
668 tracing::debug!(
669 "Successfully extracted fallback example '{}'",
670 example_name
671 );
672 return Ok(value);
673 }
674 Err(e) => {
675 tracing::error!(
676 "Failed to extract fallback example '{}': {}, falling back to schema generation",
677 example_name,
678 e
679 );
680 }
682 }
683 }
684 } else {
685 tracing::debug!("No examples found in media type, will use schema generation");
686 }
687
688 if let Some(schema_ref) = &media_type.schema {
691 Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
692 } else {
693 Ok(Value::Object(serde_json::Map::new()))
694 }
695 }
696
697 #[allow(dead_code)]
700 fn extract_example_value(
701 spec: &OpenApiSpec,
702 example_ref: &ReferenceOr<openapiv3::Example>,
703 expand_tokens: bool,
704 ) -> Result<Value> {
705 Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
706 }
707
708 fn extract_example_value_with_persona(
710 spec: &OpenApiSpec,
711 example_ref: &ReferenceOr<openapiv3::Example>,
712 expand_tokens: bool,
713 persona: Option<&Persona>,
714 schema_ref: Option<&ReferenceOr<Schema>>,
715 ) -> Result<Value> {
716 let mut value = match example_ref {
717 ReferenceOr::Item(example) => {
718 if let Some(v) = &example.value {
719 tracing::debug!("Using example from examples map: {:?}", v);
720 if expand_tokens {
721 Self::expand_templates(v)
722 } else {
723 v.clone()
724 }
725 } else {
726 return Ok(Value::Object(serde_json::Map::new()));
727 }
728 }
729 ReferenceOr::Reference { reference } => {
730 if let Some(example) = spec.get_example(reference) {
732 if let Some(v) = &example.value {
733 tracing::debug!("Using resolved example reference: {:?}", v);
734 if expand_tokens {
735 Self::expand_templates(v)
736 } else {
737 v.clone()
738 }
739 } else {
740 return Ok(Value::Object(serde_json::Map::new()));
741 }
742 } else {
743 tracing::warn!("Example reference '{}' not found", reference);
744 return Ok(Value::Object(serde_json::Map::new()));
745 }
746 }
747 };
748
749 value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
751
752 Ok(value)
753 }
754
755 fn expand_example_items_if_needed(
758 _spec: &OpenApiSpec,
759 mut example: Value,
760 _persona: Option<&Persona>,
761 _schema_ref: Option<&ReferenceOr<Schema>>,
762 ) -> Value {
763 let has_nested_items = example
766 .get("data")
767 .and_then(|v| v.as_object())
768 .map(|obj| obj.contains_key("items"))
769 .unwrap_or(false);
770
771 let has_flat_items = example.get("items").is_some();
772
773 if !has_nested_items && !has_flat_items {
774 return example; }
776
777 let total = example
779 .get("data")
780 .and_then(|d| d.get("total"))
781 .or_else(|| example.get("total"))
782 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
783
784 let limit = example
785 .get("data")
786 .and_then(|d| d.get("limit"))
787 .or_else(|| example.get("limit"))
788 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
789
790 let items_array = example
792 .get("data")
793 .and_then(|d| d.get("items"))
794 .or_else(|| example.get("items"))
795 .and_then(|v| v.as_array())
796 .cloned();
797
798 if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
799 let current_count = items.len() as u64;
800 let expected_count = std::cmp::min(total_val, limit_val);
801 let max_items = 100; let expected_count = std::cmp::min(expected_count, max_items);
803
804 if current_count < expected_count && !items.is_empty() {
806 tracing::debug!(
807 "Expanding example items array: {} -> {} items (total={}, limit={})",
808 current_count,
809 expected_count,
810 total_val,
811 limit_val
812 );
813
814 let template = items[0].clone();
816 let additional_count = expected_count - current_count;
817
818 for i in 0..additional_count {
820 let mut new_item = template.clone();
821 let item_index = current_count + i + 1;
823 Self::add_item_variation(&mut new_item, item_index);
824 items.push(new_item);
825 }
826
827 if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
829 data_obj.insert("items".to_string(), Value::Array(items));
830 } else if let Some(root_obj) = example.as_object_mut() {
831 root_obj.insert("items".to_string(), Value::Array(items));
832 }
833 }
834 }
835
836 example
837 }
838
839 pub fn generate_from_examples(
841 response: &Response,
842 content_type: Option<&str>,
843 ) -> Result<Option<Value>> {
844 use openapiv3::ReferenceOr;
845
846 if let Some(content_type) = content_type {
848 if let Some(media_type) = response.content.get(content_type) {
849 if let Some(example) = &media_type.example {
851 return Ok(Some(example.clone()));
852 }
853
854 for (_, example_ref) in &media_type.examples {
856 if let ReferenceOr::Item(example) = example_ref {
857 if let Some(value) = &example.value {
858 return Ok(Some(value.clone()));
859 }
860 }
861 }
863 }
864 }
865
866 for (_, media_type) in &response.content {
868 if let Some(example) = &media_type.example {
870 return Ok(Some(example.clone()));
871 }
872
873 for (_, example_ref) in &media_type.examples {
875 if let ReferenceOr::Item(example) = example_ref {
876 if let Some(value) = &example.value {
877 return Ok(Some(value.clone()));
878 }
879 }
880 }
882 }
883
884 Ok(None)
885 }
886
887 fn expand_templates(value: &Value) -> Value {
889 match value {
890 Value::String(s) => {
891 let expanded = s
892 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
893 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
894 Value::String(expanded)
895 }
896 Value::Object(map) => {
897 let mut new_map = serde_json::Map::new();
898 for (key, val) in map {
899 new_map.insert(key.clone(), Self::expand_templates(val));
900 }
901 Value::Object(new_map)
902 }
903 Value::Array(arr) => {
904 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
905 Value::Array(new_arr)
906 }
907 _ => value.clone(),
908 }
909 }
910}
911
912#[derive(Debug, Clone)]
914pub struct MockResponse {
915 pub status_code: u16,
917 pub headers: HashMap<String, String>,
919 pub body: Option<Value>,
921}
922
923impl MockResponse {
924 pub fn new(status_code: u16) -> Self {
926 Self {
927 status_code,
928 headers: HashMap::new(),
929 body: None,
930 }
931 }
932
933 pub fn with_header(mut self, name: String, value: String) -> Self {
935 self.headers.insert(name, value);
936 self
937 }
938
939 pub fn with_body(mut self, body: Value) -> Self {
941 self.body = Some(body);
942 self
943 }
944}
945
946#[derive(Debug, Clone)]
948pub struct OpenApiSecurityRequirement {
949 pub scheme: String,
951 pub scopes: Vec<String>,
953}
954
955impl OpenApiSecurityRequirement {
956 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
958 Self { scheme, scopes }
959 }
960}
961
962#[derive(Debug, Clone)]
964pub struct OpenApiOperation {
965 pub method: String,
967 pub path: String,
969 pub operation: Operation,
971}
972
973impl OpenApiOperation {
974 pub fn new(method: String, path: String, operation: Operation) -> Self {
976 Self {
977 method,
978 path,
979 operation,
980 }
981 }
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987 use openapiv3::ReferenceOr;
988 use serde_json::json;
989
990 struct MockAiGenerator {
992 response: Value,
993 }
994
995 #[async_trait]
996 impl AiGenerator for MockAiGenerator {
997 async fn generate(&self, _prompt: &str, _config: &AiResponseConfig) -> Result<Value> {
998 Ok(self.response.clone())
999 }
1000 }
1001
1002 #[test]
1003 fn generates_example_using_referenced_schemas() {
1004 let yaml = r#"
1005openapi: 3.0.3
1006info:
1007 title: Test API
1008 version: "1.0.0"
1009paths:
1010 /apiaries:
1011 get:
1012 responses:
1013 '200':
1014 description: ok
1015 content:
1016 application/json:
1017 schema:
1018 $ref: '#/components/schemas/Apiary'
1019components:
1020 schemas:
1021 Apiary:
1022 type: object
1023 properties:
1024 id:
1025 type: string
1026 hive:
1027 $ref: '#/components/schemas/Hive'
1028 Hive:
1029 type: object
1030 properties:
1031 name:
1032 type: string
1033 active:
1034 type: boolean
1035 "#;
1036
1037 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1038 let path_item = spec
1039 .spec
1040 .paths
1041 .paths
1042 .get("/apiaries")
1043 .and_then(ReferenceOr::as_item)
1044 .expect("path item");
1045 let operation = path_item.get.as_ref().expect("GET operation");
1046
1047 let response =
1048 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1049 .expect("generate response");
1050
1051 let obj = response.as_object().expect("response object");
1052 assert!(obj.contains_key("id"));
1053 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1054 assert!(hive.contains_key("name"));
1055 assert!(hive.contains_key("active"));
1056 }
1057
1058 #[test]
1063 fn inject_response_violations_off_by_default_leaves_required_fields() {
1064 let yaml = r#"
1065openapi: 3.0.3
1066info:
1067 title: t
1068 version: "1"
1069paths:
1070 /thing:
1071 get:
1072 responses:
1073 '200':
1074 description: ok
1075 content:
1076 application/json:
1077 schema:
1078 $ref: '#/components/schemas/Thing'
1079components:
1080 schemas:
1081 Thing:
1082 type: object
1083 required: [must_have]
1084 properties:
1085 must_have:
1086 type: string
1087 optional:
1088 type: string
1089 "#;
1090 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1091 let path_item = spec
1092 .spec
1093 .paths
1094 .paths
1095 .get("/thing")
1096 .and_then(ReferenceOr::as_item)
1097 .expect("path");
1098 let op = path_item.get.as_ref().expect("op");
1099 let body =
1100 ResponseGenerator::generate_response(&spec, op, 200, Some("application/json")).unwrap();
1101 assert!(body.as_object().unwrap().contains_key("must_have"));
1102 }
1103
1104 #[test]
1111 fn first_required_field_for_status_finds_required_in_referenced_schema() {
1112 let yaml = r#"
1113openapi: 3.0.3
1114info:
1115 title: t
1116 version: "1"
1117paths:
1118 /thing:
1119 get:
1120 responses:
1121 '200':
1122 description: ok
1123 content:
1124 application/json:
1125 schema:
1126 $ref: '#/components/schemas/Thing'
1127components:
1128 schemas:
1129 Thing:
1130 type: object
1131 required: [comment, location]
1132 properties:
1133 comment:
1134 type: string
1135 location:
1136 type: string
1137 "#;
1138 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1139 let path_item = spec
1140 .spec
1141 .paths
1142 .paths
1143 .get("/thing")
1144 .and_then(ReferenceOr::as_item)
1145 .expect("path");
1146 let op = path_item.get.as_ref().expect("op");
1147 let first = ResponseGenerator::first_required_field_for_status(&spec, op, 200);
1148 assert_eq!(first.as_deref(), Some("comment"), "first declared required field is picked");
1149 }
1150
1151 #[test]
1154 fn first_required_field_returns_none_when_no_required_or_non_2xx() {
1155 let yaml = r#"
1156openapi: 3.0.3
1157info:
1158 title: t
1159 version: "1"
1160paths:
1161 /thing:
1162 get:
1163 responses:
1164 '200':
1165 description: ok
1166 content:
1167 application/json:
1168 schema:
1169 type: object
1170 properties:
1171 whatever:
1172 type: string
1173 '500':
1174 description: err
1175 content:
1176 application/json:
1177 schema:
1178 type: object
1179 required: [code]
1180 properties:
1181 code:
1182 type: string
1183 "#;
1184 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1185 let path_item = spec
1186 .spec
1187 .paths
1188 .paths
1189 .get("/thing")
1190 .and_then(ReferenceOr::as_item)
1191 .expect("path");
1192 let op = path_item.get.as_ref().expect("op");
1193 assert!(ResponseGenerator::first_required_field_for_status(&spec, op, 200).is_none());
1195 assert_eq!(
1199 ResponseGenerator::first_required_field_for_status(&spec, op, 500).as_deref(),
1200 Some("code")
1201 );
1202 }
1203
1204 #[tokio::test]
1205 async fn test_generate_ai_response_with_generator() {
1206 let ai_config = AiResponseConfig {
1207 enabled: true,
1208 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1209 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1210 context: None,
1211 temperature: 0.7,
1212 max_tokens: 1000,
1213 schema: None,
1214 cache_enabled: true,
1215 };
1216 let context = RequestContext {
1217 method: "GET".to_string(),
1218 path: "/api/users".to_string(),
1219 path_params: HashMap::new(),
1220 query_params: HashMap::new(),
1221 headers: HashMap::new(),
1222 body: None,
1223 multipart_fields: HashMap::new(),
1224 multipart_files: HashMap::new(),
1225 };
1226 let mock_generator = MockAiGenerator {
1227 response: json!({"message": "Generated response"}),
1228 };
1229
1230 let result =
1231 ResponseGenerator::generate_ai_response(&ai_config, &context, Some(&mock_generator))
1232 .await;
1233
1234 assert!(result.is_ok());
1235 let value = result.unwrap();
1236 assert_eq!(value["message"], "Generated response");
1237 }
1238
1239 #[tokio::test]
1240 async fn test_generate_ai_response_without_generator() {
1241 let ai_config = AiResponseConfig {
1242 enabled: true,
1243 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1244 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1245 context: None,
1246 temperature: 0.7,
1247 max_tokens: 1000,
1248 schema: None,
1249 cache_enabled: true,
1250 };
1251 let context = RequestContext {
1252 method: "POST".to_string(),
1253 path: "/api/users".to_string(),
1254 path_params: HashMap::new(),
1255 query_params: HashMap::new(),
1256 headers: HashMap::new(),
1257 body: None,
1258 multipart_fields: HashMap::new(),
1259 multipart_files: HashMap::new(),
1260 };
1261
1262 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1263
1264 assert!(result.is_err());
1266 let err = result.unwrap_err().to_string();
1267 assert!(
1268 err.contains("no AI generator configured"),
1269 "Expected 'no AI generator configured' error, got: {}",
1270 err
1271 );
1272 }
1273
1274 #[tokio::test]
1275 async fn test_generate_ai_response_no_prompt() {
1276 let ai_config = AiResponseConfig {
1277 enabled: true,
1278 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1279 prompt: None,
1280 context: None,
1281 temperature: 0.7,
1282 max_tokens: 1000,
1283 schema: None,
1284 cache_enabled: true,
1285 };
1286 let context = RequestContext {
1287 method: "GET".to_string(),
1288 path: "/api/test".to_string(),
1289 path_params: HashMap::new(),
1290 query_params: HashMap::new(),
1291 headers: HashMap::new(),
1292 body: None,
1293 multipart_fields: HashMap::new(),
1294 multipart_files: HashMap::new(),
1295 };
1296
1297 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1298
1299 assert!(result.is_err());
1300 }
1301
1302 #[test]
1303 fn test_generate_response_with_expansion() {
1304 let spec = OpenApiSpec::from_string(
1305 r#"openapi: 3.0.0
1306info:
1307 title: Test API
1308 version: 1.0.0
1309paths:
1310 /users:
1311 get:
1312 responses:
1313 '200':
1314 description: OK
1315 content:
1316 application/json:
1317 schema:
1318 type: object
1319 properties:
1320 id:
1321 type: integer
1322 name:
1323 type: string
1324"#,
1325 Some("yaml"),
1326 )
1327 .unwrap();
1328
1329 let operation = spec
1330 .spec
1331 .paths
1332 .paths
1333 .get("/users")
1334 .and_then(|p| p.as_item())
1335 .and_then(|p| p.get.as_ref())
1336 .unwrap();
1337
1338 let response = ResponseGenerator::generate_response_with_expansion(
1339 &spec,
1340 operation,
1341 200,
1342 Some("application/json"),
1343 true,
1344 )
1345 .unwrap();
1346
1347 assert!(response.is_object());
1348 }
1349
1350 #[test]
1351 fn test_generate_response_with_scenario() {
1352 let spec = OpenApiSpec::from_string(
1353 r#"openapi: 3.0.0
1354info:
1355 title: Test API
1356 version: 1.0.0
1357paths:
1358 /users:
1359 get:
1360 responses:
1361 '200':
1362 description: OK
1363 content:
1364 application/json:
1365 examples:
1366 happy:
1367 value:
1368 id: 1
1369 name: "Happy User"
1370 sad:
1371 value:
1372 id: 2
1373 name: "Sad User"
1374"#,
1375 Some("yaml"),
1376 )
1377 .unwrap();
1378
1379 let operation = spec
1380 .spec
1381 .paths
1382 .paths
1383 .get("/users")
1384 .and_then(|p| p.as_item())
1385 .and_then(|p| p.get.as_ref())
1386 .unwrap();
1387
1388 let response = ResponseGenerator::generate_response_with_scenario(
1389 &spec,
1390 operation,
1391 200,
1392 Some("application/json"),
1393 false,
1394 Some("happy"),
1395 )
1396 .unwrap();
1397
1398 assert_eq!(response["id"], 1);
1399 assert_eq!(response["name"], "Happy User");
1400 }
1401
1402 #[test]
1403 fn test_generate_response_with_referenced_response() {
1404 let spec = OpenApiSpec::from_string(
1405 r#"openapi: 3.0.0
1406info:
1407 title: Test API
1408 version: 1.0.0
1409paths:
1410 /users:
1411 get:
1412 responses:
1413 '200':
1414 $ref: '#/components/responses/UserResponse'
1415components:
1416 responses:
1417 UserResponse:
1418 description: User response
1419 content:
1420 application/json:
1421 schema:
1422 type: object
1423 properties:
1424 id:
1425 type: integer
1426 name:
1427 type: string
1428"#,
1429 Some("yaml"),
1430 )
1431 .unwrap();
1432
1433 let operation = spec
1434 .spec
1435 .paths
1436 .paths
1437 .get("/users")
1438 .and_then(|p| p.as_item())
1439 .and_then(|p| p.get.as_ref())
1440 .unwrap();
1441
1442 let response =
1443 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1444 .unwrap();
1445
1446 assert!(response.is_object());
1447 }
1448
1449 #[test]
1450 fn test_generate_response_with_default_status() {
1451 let spec = OpenApiSpec::from_string(
1452 r#"openapi: 3.0.0
1453info:
1454 title: Test API
1455 version: 1.0.0
1456paths:
1457 /users:
1458 get:
1459 responses:
1460 '200':
1461 description: OK
1462 default:
1463 description: Error
1464 content:
1465 application/json:
1466 schema:
1467 type: object
1468 properties:
1469 error:
1470 type: string
1471"#,
1472 Some("yaml"),
1473 )
1474 .unwrap();
1475
1476 let operation = spec
1477 .spec
1478 .paths
1479 .paths
1480 .get("/users")
1481 .and_then(|p| p.as_item())
1482 .and_then(|p| p.get.as_ref())
1483 .unwrap();
1484
1485 let response =
1487 ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
1488 .unwrap();
1489
1490 assert!(response.is_object());
1491 }
1492
1493 #[test]
1494 fn test_generate_response_with_example_in_media_type() {
1495 let spec = OpenApiSpec::from_string(
1496 r#"openapi: 3.0.0
1497info:
1498 title: Test API
1499 version: 1.0.0
1500paths:
1501 /users:
1502 get:
1503 responses:
1504 '200':
1505 description: OK
1506 content:
1507 application/json:
1508 example:
1509 id: 1
1510 name: "Example User"
1511"#,
1512 Some("yaml"),
1513 )
1514 .unwrap();
1515
1516 let operation = spec
1517 .spec
1518 .paths
1519 .paths
1520 .get("/users")
1521 .and_then(|p| p.as_item())
1522 .and_then(|p| p.get.as_ref())
1523 .unwrap();
1524
1525 let response =
1526 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1527 .unwrap();
1528
1529 assert_eq!(response["id"], 1);
1530 assert_eq!(response["name"], "Example User");
1531 }
1532
1533 #[test]
1534 fn test_generate_response_with_schema_example() {
1535 let spec = OpenApiSpec::from_string(
1536 r#"openapi: 3.0.0
1537info:
1538 title: Test API
1539 version: 1.0.0
1540paths:
1541 /users:
1542 get:
1543 responses:
1544 '200':
1545 description: OK
1546 content:
1547 application/json:
1548 schema:
1549 type: object
1550 example:
1551 id: 42
1552 name: "Schema Example"
1553 properties:
1554 id:
1555 type: integer
1556 name:
1557 type: string
1558"#,
1559 Some("yaml"),
1560 )
1561 .unwrap();
1562
1563 let operation = spec
1564 .spec
1565 .paths
1566 .paths
1567 .get("/users")
1568 .and_then(|p| p.as_item())
1569 .and_then(|p| p.get.as_ref())
1570 .unwrap();
1571
1572 let response =
1573 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1574 .unwrap();
1575
1576 assert!(response.is_object());
1578 }
1579
1580 #[test]
1581 fn test_generate_response_with_referenced_schema() {
1582 let spec = OpenApiSpec::from_string(
1583 r#"openapi: 3.0.0
1584info:
1585 title: Test API
1586 version: 1.0.0
1587paths:
1588 /users:
1589 get:
1590 responses:
1591 '200':
1592 description: OK
1593 content:
1594 application/json:
1595 schema:
1596 $ref: '#/components/schemas/User'
1597components:
1598 schemas:
1599 User:
1600 type: object
1601 properties:
1602 id:
1603 type: integer
1604 name:
1605 type: string
1606"#,
1607 Some("yaml"),
1608 )
1609 .unwrap();
1610
1611 let operation = spec
1612 .spec
1613 .paths
1614 .paths
1615 .get("/users")
1616 .and_then(|p| p.as_item())
1617 .and_then(|p| p.get.as_ref())
1618 .unwrap();
1619
1620 let response =
1621 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1622 .unwrap();
1623
1624 assert!(response.is_object());
1625 assert!(response.get("id").is_some());
1626 assert!(response.get("name").is_some());
1627 }
1628
1629 #[test]
1630 fn test_generate_response_with_array_schema() {
1631 let spec = OpenApiSpec::from_string(
1632 r#"openapi: 3.0.0
1633info:
1634 title: Test API
1635 version: 1.0.0
1636paths:
1637 /users:
1638 get:
1639 responses:
1640 '200':
1641 description: OK
1642 content:
1643 application/json:
1644 schema:
1645 type: array
1646 items:
1647 type: object
1648 properties:
1649 id:
1650 type: integer
1651 name:
1652 type: string
1653"#,
1654 Some("yaml"),
1655 )
1656 .unwrap();
1657
1658 let operation = spec
1659 .spec
1660 .paths
1661 .paths
1662 .get("/users")
1663 .and_then(|p| p.as_item())
1664 .and_then(|p| p.get.as_ref())
1665 .unwrap();
1666
1667 let response =
1668 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1669 .unwrap();
1670
1671 assert!(response.is_array());
1672 }
1673
1674 #[test]
1675 fn test_generate_response_with_different_content_types() {
1676 let spec = OpenApiSpec::from_string(
1677 r#"openapi: 3.0.0
1678info:
1679 title: Test API
1680 version: 1.0.0
1681paths:
1682 /users:
1683 get:
1684 responses:
1685 '200':
1686 description: OK
1687 content:
1688 application/json:
1689 schema:
1690 type: object
1691 text/plain:
1692 schema:
1693 type: string
1694"#,
1695 Some("yaml"),
1696 )
1697 .unwrap();
1698
1699 let operation = spec
1700 .spec
1701 .paths
1702 .paths
1703 .get("/users")
1704 .and_then(|p| p.as_item())
1705 .and_then(|p| p.get.as_ref())
1706 .unwrap();
1707
1708 let json_response =
1710 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1711 .unwrap();
1712 assert!(json_response.is_object());
1713
1714 let text_response =
1716 ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
1717 .unwrap();
1718 assert!(text_response.is_string());
1719 }
1720
1721 #[test]
1722 fn test_generate_response_without_content_type() {
1723 let spec = OpenApiSpec::from_string(
1724 r#"openapi: 3.0.0
1725info:
1726 title: Test API
1727 version: 1.0.0
1728paths:
1729 /users:
1730 get:
1731 responses:
1732 '200':
1733 description: OK
1734 content:
1735 application/json:
1736 schema:
1737 type: object
1738 properties:
1739 id:
1740 type: integer
1741"#,
1742 Some("yaml"),
1743 )
1744 .unwrap();
1745
1746 let operation = spec
1747 .spec
1748 .paths
1749 .paths
1750 .get("/users")
1751 .and_then(|p| p.as_item())
1752 .and_then(|p| p.get.as_ref())
1753 .unwrap();
1754
1755 let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
1757
1758 assert!(response.is_object());
1759 }
1760
1761 #[test]
1762 fn test_generate_response_with_no_content() {
1763 let spec = OpenApiSpec::from_string(
1764 r#"openapi: 3.0.0
1765info:
1766 title: Test API
1767 version: 1.0.0
1768paths:
1769 /users:
1770 delete:
1771 responses:
1772 '204':
1773 description: No Content
1774"#,
1775 Some("yaml"),
1776 )
1777 .unwrap();
1778
1779 let operation = spec
1780 .spec
1781 .paths
1782 .paths
1783 .get("/users")
1784 .and_then(|p| p.as_item())
1785 .and_then(|p| p.delete.as_ref())
1786 .unwrap();
1787
1788 let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
1789
1790 assert!(response.is_object());
1792 assert!(response.as_object().unwrap().is_empty());
1793 }
1794
1795 #[test]
1796 fn test_generate_response_with_expansion_disabled() {
1797 let spec = OpenApiSpec::from_string(
1798 r#"openapi: 3.0.0
1799info:
1800 title: Test API
1801 version: 1.0.0
1802paths:
1803 /users:
1804 get:
1805 responses:
1806 '200':
1807 description: OK
1808 content:
1809 application/json:
1810 schema:
1811 type: object
1812 properties:
1813 id:
1814 type: integer
1815 name:
1816 type: string
1817"#,
1818 Some("yaml"),
1819 )
1820 .unwrap();
1821
1822 let operation = spec
1823 .spec
1824 .paths
1825 .paths
1826 .get("/users")
1827 .and_then(|p| p.as_item())
1828 .and_then(|p| p.get.as_ref())
1829 .unwrap();
1830
1831 let response = ResponseGenerator::generate_response_with_expansion(
1832 &spec,
1833 operation,
1834 200,
1835 Some("application/json"),
1836 false, )
1838 .unwrap();
1839
1840 assert!(response.is_object());
1841 }
1842
1843 #[test]
1844 fn test_generate_response_with_array_schema_referenced_items() {
1845 let spec = OpenApiSpec::from_string(
1847 r#"openapi: 3.0.0
1848info:
1849 title: Test API
1850 version: 1.0.0
1851paths:
1852 /items:
1853 get:
1854 responses:
1855 '200':
1856 description: OK
1857 content:
1858 application/json:
1859 schema:
1860 type: array
1861 items:
1862 $ref: '#/components/schemas/Item'
1863components:
1864 schemas:
1865 Item:
1866 type: object
1867 properties:
1868 id:
1869 type: string
1870 name:
1871 type: string
1872"#,
1873 Some("yaml"),
1874 )
1875 .unwrap();
1876
1877 let operation = spec
1878 .spec
1879 .paths
1880 .paths
1881 .get("/items")
1882 .and_then(|p| p.as_item())
1883 .and_then(|p| p.get.as_ref())
1884 .unwrap();
1885
1886 let response =
1887 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1888 .unwrap();
1889
1890 let arr = response.as_array().expect("response should be array");
1892 assert!(!arr.is_empty());
1893 if let Some(item) = arr.first() {
1894 let obj = item.as_object().expect("item should be object");
1895 assert!(obj.contains_key("id") || obj.contains_key("name"));
1896 }
1897 }
1898
1899 #[test]
1900 fn test_generate_response_with_array_schema_missing_reference() {
1901 let spec = OpenApiSpec::from_string(
1903 r#"openapi: 3.0.0
1904info:
1905 title: Test API
1906 version: 1.0.0
1907paths:
1908 /items:
1909 get:
1910 responses:
1911 '200':
1912 description: OK
1913 content:
1914 application/json:
1915 schema:
1916 type: array
1917 items:
1918 $ref: '#/components/schemas/NonExistentItem'
1919components:
1920 schemas: {}
1921"#,
1922 Some("yaml"),
1923 )
1924 .unwrap();
1925
1926 let operation = spec
1927 .spec
1928 .paths
1929 .paths
1930 .get("/items")
1931 .and_then(|p| p.as_item())
1932 .and_then(|p| p.get.as_ref())
1933 .unwrap();
1934
1935 let response =
1936 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1937 .unwrap();
1938
1939 let arr = response.as_array().expect("response should be array");
1941 assert!(!arr.is_empty());
1942 }
1943
1944 #[test]
1945 fn test_generate_response_with_array_example_and_pagination() {
1946 let spec = OpenApiSpec::from_string(
1948 r#"openapi: 3.0.0
1949info:
1950 title: Test API
1951 version: 1.0.0
1952paths:
1953 /products:
1954 get:
1955 responses:
1956 '200':
1957 description: OK
1958 content:
1959 application/json:
1960 schema:
1961 type: array
1962 example: [{"id": 1, "name": "Product 1"}]
1963 items:
1964 type: object
1965 properties:
1966 id:
1967 type: integer
1968 name:
1969 type: string
1970"#,
1971 Some("yaml"),
1972 )
1973 .unwrap();
1974
1975 let operation = spec
1976 .spec
1977 .paths
1978 .paths
1979 .get("/products")
1980 .and_then(|p| p.as_item())
1981 .and_then(|p| p.get.as_ref())
1982 .unwrap();
1983
1984 let response =
1985 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1986 .unwrap();
1987
1988 let arr = response.as_array().expect("response should be array");
1990 assert!(!arr.is_empty());
1991 if let Some(item) = arr.first() {
1992 let obj = item.as_object().expect("item should be object");
1993 assert!(obj.contains_key("id") || obj.contains_key("name"));
1994 }
1995 }
1996
1997 #[test]
1998 fn test_generate_response_with_missing_response_reference() {
1999 let spec = OpenApiSpec::from_string(
2001 r#"openapi: 3.0.0
2002info:
2003 title: Test API
2004 version: 1.0.0
2005paths:
2006 /users:
2007 get:
2008 responses:
2009 '200':
2010 $ref: '#/components/responses/NonExistentResponse'
2011components:
2012 responses: {}
2013"#,
2014 Some("yaml"),
2015 )
2016 .unwrap();
2017
2018 let operation = spec
2019 .spec
2020 .paths
2021 .paths
2022 .get("/users")
2023 .and_then(|p| p.as_item())
2024 .and_then(|p| p.get.as_ref())
2025 .unwrap();
2026
2027 let response =
2028 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2029 .unwrap();
2030
2031 assert!(response.is_object());
2033 assert!(response.as_object().unwrap().is_empty());
2034 }
2035
2036 #[test]
2037 fn test_generate_response_with_no_response_for_status() {
2038 let spec = OpenApiSpec::from_string(
2040 r#"openapi: 3.0.0
2041info:
2042 title: Test API
2043 version: 1.0.0
2044paths:
2045 /users:
2046 get:
2047 responses:
2048 '404':
2049 description: Not found
2050"#,
2051 Some("yaml"),
2052 )
2053 .unwrap();
2054
2055 let operation = spec
2056 .spec
2057 .paths
2058 .paths
2059 .get("/users")
2060 .and_then(|p| p.as_item())
2061 .and_then(|p| p.get.as_ref())
2062 .unwrap();
2063
2064 let response =
2066 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2067 .unwrap();
2068
2069 assert!(response.is_object());
2071 assert!(response.as_object().unwrap().is_empty());
2072 }
2073}