1use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum InvalidDataType {
14 MissingField,
16 WrongType,
18 Empty,
20 Null,
22 OutOfRange,
24 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#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct InvalidDataConfig {
60 pub error_rate: f64,
62 pub error_types: HashSet<InvalidDataType>,
64 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, error_types,
78 target_fields: Vec::new(),
79 }
80 }
81}
82
83impl InvalidDataConfig {
84 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 pub fn with_error_types(mut self, types: HashSet<InvalidDataType>) -> Self {
94 self.error_types = types;
95 self
96 }
97
98 pub fn with_target_fields(mut self, fields: Vec<String>) -> Self {
100 self.target_fields = fields;
101 self
102 }
103
104 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
116pub struct InvalidDataGenerator;
118
119impl InvalidDataGenerator {
120 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 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 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 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 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 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 assert_eq!(
301 InvalidDataType::from_str("missing_field").unwrap(),
302 InvalidDataType::MissingField
303 );
304
305 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}