mockforge_bench/
invalid_data.rs

1//! Invalid data generation for error testing
2//!
3//! This module provides functionality to generate invalid request data
4//! for testing error handling. Supports mixing valid and invalid requests
5//! based on a configurable error rate.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10/// Types of invalid data that can be generated
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum InvalidDataType {
14    /// Omit a required field
15    MissingField,
16    /// Provide wrong data type (string where number expected, etc.)
17    WrongType,
18    /// Provide empty string where value required
19    Empty,
20    /// Provide null where not nullable
21    Null,
22    /// Provide value outside min/max constraints
23    OutOfRange,
24    /// Provide malformed data (invalid email, URL, etc.)
25    Malformed,
26}
27
28impl std::fmt::Display for InvalidDataType {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::MissingField => write!(f, "missing-field"),
32            Self::WrongType => write!(f, "wrong-type"),
33            Self::Empty => write!(f, "empty"),
34            Self::Null => write!(f, "null"),
35            Self::OutOfRange => write!(f, "out-of-range"),
36            Self::Malformed => write!(f, "malformed"),
37        }
38    }
39}
40
41impl std::str::FromStr for InvalidDataType {
42    type Err = String;
43
44    fn from_str(s: &str) -> Result<Self, Self::Err> {
45        match s.to_lowercase().replace('_', "-").as_str() {
46            "missing-field" | "missingfield" => Ok(Self::MissingField),
47            "wrong-type" | "wrongtype" => Ok(Self::WrongType),
48            "empty" => Ok(Self::Empty),
49            "null" => Ok(Self::Null),
50            "out-of-range" | "outofrange" => Ok(Self::OutOfRange),
51            "malformed" => Ok(Self::Malformed),
52            _ => Err(format!("Invalid error type: '{}'", s)),
53        }
54    }
55}
56
57/// Configuration for invalid data generation
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct InvalidDataConfig {
60    /// Percentage of requests that should use invalid data (0.0 to 1.0)
61    pub error_rate: f64,
62    /// Types of invalid data to generate
63    pub error_types: HashSet<InvalidDataType>,
64    /// Specific fields to target for invalidation (if empty, any field)
65    pub target_fields: Vec<String>,
66}
67
68impl Default for InvalidDataConfig {
69    fn default() -> Self {
70        let mut error_types = HashSet::new();
71        error_types.insert(InvalidDataType::MissingField);
72        error_types.insert(InvalidDataType::WrongType);
73        error_types.insert(InvalidDataType::Empty);
74
75        Self {
76            error_rate: 0.2, // 20% invalid by default
77            error_types,
78            target_fields: Vec::new(),
79        }
80    }
81}
82
83impl InvalidDataConfig {
84    /// Create a new config with a specific error rate
85    pub fn new(error_rate: f64) -> Self {
86        Self {
87            error_rate: error_rate.clamp(0.0, 1.0),
88            ..Default::default()
89        }
90    }
91
92    /// Set the error types to generate
93    pub fn with_error_types(mut self, types: HashSet<InvalidDataType>) -> Self {
94        self.error_types = types;
95        self
96    }
97
98    /// Add specific target fields
99    pub fn with_target_fields(mut self, fields: Vec<String>) -> Self {
100        self.target_fields = fields;
101        self
102    }
103
104    /// Parse error types from a comma-separated string
105    pub fn parse_error_types(s: &str) -> Result<HashSet<InvalidDataType>, String> {
106        if s.is_empty() {
107            return Ok(HashSet::new());
108        }
109
110        s.split(',')
111            .map(|t| t.trim().parse::<InvalidDataType>())
112            .collect()
113    }
114}
115
116/// Generates k6 JavaScript code for invalid data testing
117pub struct InvalidDataGenerator;
118
119impl InvalidDataGenerator {
120    /// Generate k6 code for determining if current request should be invalid
121    pub fn generate_should_invalidate(error_rate: f64) -> String {
122        format!(
123            "// Determine if this request should use invalid data\n\
124             const shouldInvalidate = Math.random() < {};\n",
125            error_rate
126        )
127    }
128
129    /// Generate k6 code for selecting a random invalid data type
130    pub fn generate_type_selection(types: &HashSet<InvalidDataType>) -> String {
131        let type_array: Vec<String> = types.iter().map(|t| format!("'{}'", t)).collect();
132
133        format!(
134            "// Select random invalid data type\n\
135             const invalidTypes = [{}];\n\
136             const invalidType = invalidTypes[Math.floor(Math.random() * invalidTypes.length)];\n",
137            type_array.join(", ")
138        )
139    }
140
141    /// Generate k6 code for creating invalid data based on type
142    pub fn generate_invalidation_logic() -> String {
143        r#"// Apply invalidation based on selected type
144function invalidateField(value, fieldName, invalidType) {
145  switch (invalidType) {
146    case 'missing-field':
147      return undefined; // Will be filtered out
148    case 'wrong-type':
149      if (typeof value === 'number') return 'not_a_number';
150      if (typeof value === 'string') return 12345;
151      if (typeof value === 'boolean') return 'not_a_boolean';
152      if (Array.isArray(value)) return 'not_an_array';
153      return null;
154    case 'empty':
155      if (typeof value === 'string') return '';
156      if (Array.isArray(value)) return [];
157      if (typeof value === 'object') return {};
158      return null;
159    case 'null':
160      return null;
161    case 'out-of-range':
162      if (typeof value === 'number') return value > 0 ? -9999999 : 9999999;
163      if (typeof value === 'string') return 'x'.repeat(10000);
164      return value;
165    case 'malformed':
166      if (typeof value === 'string') {
167        // Check common formats and malform them
168        if (value.includes('@')) return 'not-an-email';
169        if (value.startsWith('http')) return 'not://a.valid.url';
170        return value + '%%%invalid%%%';
171      }
172      return value;
173    default:
174      return value;
175  }
176}
177
178function invalidatePayload(payload, targetFields, invalidType) {
179  const result = { ...payload };
180
181  // Determine which fields to invalidate
182  let fieldsToInvalidate;
183  if (targetFields && targetFields.length > 0) {
184    fieldsToInvalidate = targetFields;
185  } else {
186    // Pick a random field
187    const allFields = Object.keys(result);
188    fieldsToInvalidate = [allFields[Math.floor(Math.random() * allFields.length)]];
189  }
190
191  for (const field of fieldsToInvalidate) {
192    if (result.hasOwnProperty(field)) {
193      const newValue = invalidateField(result[field], field, invalidType);
194      if (newValue === undefined) {
195        delete result[field];
196      } else {
197        result[field] = newValue;
198      }
199    }
200  }
201
202  return result;
203}
204"#
205        .to_string()
206    }
207
208    /// Generate k6 code for a complete invalid data test scenario
209    pub fn generate_complete_invalidation(config: &InvalidDataConfig, target_fields_js: &str) -> String {
210        let mut code = String::new();
211
212        code.push_str(&Self::generate_should_invalidate(config.error_rate));
213        code.push('\n');
214        code.push_str(&Self::generate_type_selection(&config.error_types));
215        code.push('\n');
216        code.push_str(&format!(
217            "const targetFields = {};\n\n",
218            target_fields_js
219        ));
220        code.push_str("// Apply invalidation if needed\n");
221        code.push_str("const finalPayload = shouldInvalidate\n");
222        code.push_str("  ? invalidatePayload(payload, targetFields, invalidType)\n");
223        code.push_str("  : payload;\n");
224
225        code
226    }
227
228    /// Generate k6 code for checking expected error responses
229    pub fn generate_error_checks() -> String {
230        r#"// Check response based on whether we sent invalid data
231if (shouldInvalidate) {
232  check(res, {
233    'invalid request: expects error response': (r) => r.status >= 400,
234    'invalid request: has error message': (r) => {
235      try {
236        const body = r.json();
237        return body.error || body.message || body.errors;
238      } catch (e) {
239        return r.body && r.body.length > 0;
240      }
241    },
242  });
243} else {
244  check(res, {
245    'valid request: status is OK': (r) => r.status >= 200 && r.status < 300,
246  });
247}
248"#
249        .to_string()
250    }
251
252    /// Generate complete test helper functions
253    pub fn generate_helper_functions() -> String {
254        Self::generate_invalidation_logic()
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use std::str::FromStr;
262
263    #[test]
264    fn test_invalid_data_type_display() {
265        assert_eq!(InvalidDataType::MissingField.to_string(), "missing-field");
266        assert_eq!(InvalidDataType::WrongType.to_string(), "wrong-type");
267        assert_eq!(InvalidDataType::Empty.to_string(), "empty");
268        assert_eq!(InvalidDataType::Null.to_string(), "null");
269        assert_eq!(InvalidDataType::OutOfRange.to_string(), "out-of-range");
270        assert_eq!(InvalidDataType::Malformed.to_string(), "malformed");
271    }
272
273    #[test]
274    fn test_invalid_data_type_from_str() {
275        assert_eq!(
276            InvalidDataType::from_str("missing-field").unwrap(),
277            InvalidDataType::MissingField
278        );
279        assert_eq!(
280            InvalidDataType::from_str("wrong-type").unwrap(),
281            InvalidDataType::WrongType
282        );
283        assert_eq!(
284            InvalidDataType::from_str("empty").unwrap(),
285            InvalidDataType::Empty
286        );
287        assert_eq!(
288            InvalidDataType::from_str("null").unwrap(),
289            InvalidDataType::Null
290        );
291        assert_eq!(
292            InvalidDataType::from_str("out-of-range").unwrap(),
293            InvalidDataType::OutOfRange
294        );
295    }
296
297    #[test]
298    fn test_invalid_data_type_from_str_variants() {
299        // With underscores
300        assert_eq!(
301            InvalidDataType::from_str("missing_field").unwrap(),
302            InvalidDataType::MissingField
303        );
304
305        // Without separator
306        assert_eq!(
307            InvalidDataType::from_str("wrongtype").unwrap(),
308            InvalidDataType::WrongType
309        );
310    }
311
312    #[test]
313    fn test_invalid_data_type_from_str_invalid() {
314        assert!(InvalidDataType::from_str("invalid").is_err());
315    }
316
317    #[test]
318    fn test_invalid_data_config_default() {
319        let config = InvalidDataConfig::default();
320        assert!((config.error_rate - 0.2).abs() < f64::EPSILON);
321        assert!(config.error_types.contains(&InvalidDataType::MissingField));
322        assert!(config.error_types.contains(&InvalidDataType::WrongType));
323        assert!(config.error_types.contains(&InvalidDataType::Empty));
324        assert!(config.target_fields.is_empty());
325    }
326
327    #[test]
328    fn test_invalid_data_config_new() {
329        let config = InvalidDataConfig::new(0.5);
330        assert!((config.error_rate - 0.5).abs() < f64::EPSILON);
331    }
332
333    #[test]
334    fn test_invalid_data_config_clamp() {
335        let config1 = InvalidDataConfig::new(1.5);
336        assert!((config1.error_rate - 1.0).abs() < f64::EPSILON);
337
338        let config2 = InvalidDataConfig::new(-0.5);
339        assert!((config2.error_rate - 0.0).abs() < f64::EPSILON);
340    }
341
342    #[test]
343    fn test_invalid_data_config_builders() {
344        let mut types = HashSet::new();
345        types.insert(InvalidDataType::Null);
346
347        let config = InvalidDataConfig::new(0.3)
348            .with_error_types(types)
349            .with_target_fields(vec!["email".to_string()]);
350
351        assert!((config.error_rate - 0.3).abs() < f64::EPSILON);
352        assert!(config.error_types.contains(&InvalidDataType::Null));
353        assert_eq!(config.error_types.len(), 1);
354        assert_eq!(config.target_fields, vec!["email"]);
355    }
356
357    #[test]
358    fn test_parse_error_types() {
359        let types = InvalidDataConfig::parse_error_types("missing-field,wrong-type,null").unwrap();
360        assert_eq!(types.len(), 3);
361        assert!(types.contains(&InvalidDataType::MissingField));
362        assert!(types.contains(&InvalidDataType::WrongType));
363        assert!(types.contains(&InvalidDataType::Null));
364    }
365
366    #[test]
367    fn test_parse_error_types_empty() {
368        let types = InvalidDataConfig::parse_error_types("").unwrap();
369        assert!(types.is_empty());
370    }
371
372    #[test]
373    fn test_generate_should_invalidate() {
374        let code = InvalidDataGenerator::generate_should_invalidate(0.2);
375        assert!(code.contains("Math.random() < 0.2"));
376        assert!(code.contains("shouldInvalidate"));
377    }
378
379    #[test]
380    fn test_generate_type_selection() {
381        let mut types = HashSet::new();
382        types.insert(InvalidDataType::MissingField);
383        types.insert(InvalidDataType::Null);
384
385        let code = InvalidDataGenerator::generate_type_selection(&types);
386        assert!(code.contains("invalidTypes"));
387        assert!(code.contains("Math.random()"));
388    }
389
390    #[test]
391    fn test_generate_invalidation_logic() {
392        let code = InvalidDataGenerator::generate_invalidation_logic();
393        assert!(code.contains("function invalidateField"));
394        assert!(code.contains("function invalidatePayload"));
395        assert!(code.contains("missing-field"));
396        assert!(code.contains("wrong-type"));
397        assert!(code.contains("out-of-range"));
398    }
399
400    #[test]
401    fn test_generate_complete_invalidation() {
402        let config = InvalidDataConfig::default();
403        let code = InvalidDataGenerator::generate_complete_invalidation(&config, "[]");
404
405        assert!(code.contains("shouldInvalidate"));
406        assert!(code.contains("invalidType"));
407        assert!(code.contains("targetFields"));
408        assert!(code.contains("finalPayload"));
409    }
410
411    #[test]
412    fn test_generate_error_checks() {
413        let code = InvalidDataGenerator::generate_error_checks();
414        assert!(code.contains("shouldInvalidate"));
415        assert!(code.contains("expects error response"));
416        assert!(code.contains("status is OK"));
417    }
418}