use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum InvalidDataType {
MissingField,
WrongType,
Empty,
Null,
OutOfRange,
Malformed,
}
impl std::fmt::Display for InvalidDataType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingField => write!(f, "missing-field"),
Self::WrongType => write!(f, "wrong-type"),
Self::Empty => write!(f, "empty"),
Self::Null => write!(f, "null"),
Self::OutOfRange => write!(f, "out-of-range"),
Self::Malformed => write!(f, "malformed"),
}
}
}
impl std::str::FromStr for InvalidDataType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().replace('_', "-").as_str() {
"missing-field" | "missingfield" => Ok(Self::MissingField),
"wrong-type" | "wrongtype" => Ok(Self::WrongType),
"empty" => Ok(Self::Empty),
"null" => Ok(Self::Null),
"out-of-range" | "outofrange" => Ok(Self::OutOfRange),
"malformed" => Ok(Self::Malformed),
_ => Err(format!("Invalid error type: '{}'", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvalidDataConfig {
pub error_rate: f64,
pub error_types: HashSet<InvalidDataType>,
pub target_fields: Vec<String>,
}
impl Default for InvalidDataConfig {
fn default() -> Self {
let mut error_types = HashSet::new();
error_types.insert(InvalidDataType::MissingField);
error_types.insert(InvalidDataType::WrongType);
error_types.insert(InvalidDataType::Empty);
Self {
error_rate: 0.2, error_types,
target_fields: Vec::new(),
}
}
}
impl InvalidDataConfig {
pub fn new(error_rate: f64) -> Self {
Self {
error_rate: error_rate.clamp(0.0, 1.0),
..Default::default()
}
}
pub fn with_error_types(mut self, types: HashSet<InvalidDataType>) -> Self {
self.error_types = types;
self
}
pub fn with_target_fields(mut self, fields: Vec<String>) -> Self {
self.target_fields = fields;
self
}
pub fn parse_error_types(s: &str) -> Result<HashSet<InvalidDataType>, String> {
if s.is_empty() {
return Ok(HashSet::new());
}
s.split(',').map(|t| t.trim().parse::<InvalidDataType>()).collect()
}
}
pub struct InvalidDataGenerator;
impl InvalidDataGenerator {
pub fn generate_should_invalidate(error_rate: f64) -> String {
format!(
"// Determine if this request should use invalid data\n\
const shouldInvalidate = Math.random() < {};\n",
error_rate
)
}
pub fn generate_type_selection(types: &HashSet<InvalidDataType>) -> String {
let type_array: Vec<String> = types.iter().map(|t| format!("'{}'", t)).collect();
format!(
"// Select random invalid data type\n\
const invalidTypes = [{}];\n\
const invalidType = invalidTypes[Math.floor(Math.random() * invalidTypes.length)];\n",
type_array.join(", ")
)
}
pub fn generate_invalidation_logic() -> String {
r#"// Apply invalidation based on selected type
function invalidateField(value, fieldName, invalidType) {
switch (invalidType) {
case 'missing-field':
return undefined; // Will be filtered out
case 'wrong-type':
if (typeof value === 'number') return 'not_a_number';
if (typeof value === 'string') return 12345;
if (typeof value === 'boolean') return 'not_a_boolean';
if (Array.isArray(value)) return 'not_an_array';
return null;
case 'empty':
if (typeof value === 'string') return '';
if (Array.isArray(value)) return [];
if (typeof value === 'object') return {};
return null;
case 'null':
return null;
case 'out-of-range':
if (typeof value === 'number') return value > 0 ? -9999999 : 9999999;
if (typeof value === 'string') return 'x'.repeat(10000);
return value;
case 'malformed':
if (typeof value === 'string') {
// Check common formats and malform them
if (value.includes('@')) return 'not-an-email';
if (value.startsWith('http')) return 'not://a.valid.url';
return value + '%%%invalid%%%';
}
return value;
default:
return value;
}
}
function invalidatePayload(payload, targetFields, invalidType) {
const result = { ...payload };
// Determine which fields to invalidate
let fieldsToInvalidate;
if (targetFields && targetFields.length > 0) {
fieldsToInvalidate = targetFields;
} else {
// Pick a random field
const allFields = Object.keys(result);
fieldsToInvalidate = [allFields[Math.floor(Math.random() * allFields.length)]];
}
for (const field of fieldsToInvalidate) {
if (result.hasOwnProperty(field)) {
const newValue = invalidateField(result[field], field, invalidType);
if (newValue === undefined) {
delete result[field];
} else {
result[field] = newValue;
}
}
}
return result;
}
"#
.to_string()
}
pub fn generate_complete_invalidation(
config: &InvalidDataConfig,
target_fields_js: &str,
) -> String {
let mut code = String::new();
code.push_str(&Self::generate_should_invalidate(config.error_rate));
code.push('\n');
code.push_str(&Self::generate_type_selection(&config.error_types));
code.push('\n');
code.push_str(&format!("const targetFields = {};\n\n", target_fields_js));
code.push_str("// Apply invalidation if needed\n");
code.push_str("const finalPayload = shouldInvalidate\n");
code.push_str(" ? invalidatePayload(payload, targetFields, invalidType)\n");
code.push_str(" : payload;\n");
code
}
pub fn generate_error_checks() -> String {
r#"// Check response based on whether we sent invalid data
if (shouldInvalidate) {
check(res, {
'invalid request: expects error response': (r) => r.status >= 400,
'invalid request: has error message': (r) => {
try {
const body = r.json();
return body.error || body.message || body.errors;
} catch (e) {
return r.body && r.body.length > 0;
}
},
});
} else {
check(res, {
'valid request: status is OK': (r) => r.status >= 200 && r.status < 300,
});
}
"#
.to_string()
}
pub fn generate_helper_functions() -> String {
Self::generate_invalidation_logic()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_invalid_data_type_display() {
assert_eq!(InvalidDataType::MissingField.to_string(), "missing-field");
assert_eq!(InvalidDataType::WrongType.to_string(), "wrong-type");
assert_eq!(InvalidDataType::Empty.to_string(), "empty");
assert_eq!(InvalidDataType::Null.to_string(), "null");
assert_eq!(InvalidDataType::OutOfRange.to_string(), "out-of-range");
assert_eq!(InvalidDataType::Malformed.to_string(), "malformed");
}
#[test]
fn test_invalid_data_type_from_str() {
assert_eq!(
InvalidDataType::from_str("missing-field").unwrap(),
InvalidDataType::MissingField
);
assert_eq!(InvalidDataType::from_str("wrong-type").unwrap(), InvalidDataType::WrongType);
assert_eq!(InvalidDataType::from_str("empty").unwrap(), InvalidDataType::Empty);
assert_eq!(InvalidDataType::from_str("null").unwrap(), InvalidDataType::Null);
assert_eq!(InvalidDataType::from_str("out-of-range").unwrap(), InvalidDataType::OutOfRange);
}
#[test]
fn test_invalid_data_type_from_str_variants() {
assert_eq!(
InvalidDataType::from_str("missing_field").unwrap(),
InvalidDataType::MissingField
);
assert_eq!(InvalidDataType::from_str("wrongtype").unwrap(), InvalidDataType::WrongType);
}
#[test]
fn test_invalid_data_type_from_str_invalid() {
assert!(InvalidDataType::from_str("invalid").is_err());
}
#[test]
fn test_invalid_data_config_default() {
let config = InvalidDataConfig::default();
assert!((config.error_rate - 0.2).abs() < f64::EPSILON);
assert!(config.error_types.contains(&InvalidDataType::MissingField));
assert!(config.error_types.contains(&InvalidDataType::WrongType));
assert!(config.error_types.contains(&InvalidDataType::Empty));
assert!(config.target_fields.is_empty());
}
#[test]
fn test_invalid_data_config_new() {
let config = InvalidDataConfig::new(0.5);
assert!((config.error_rate - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_invalid_data_config_clamp() {
let config1 = InvalidDataConfig::new(1.5);
assert!((config1.error_rate - 1.0).abs() < f64::EPSILON);
let config2 = InvalidDataConfig::new(-0.5);
assert!((config2.error_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_invalid_data_config_builders() {
let mut types = HashSet::new();
types.insert(InvalidDataType::Null);
let config = InvalidDataConfig::new(0.3)
.with_error_types(types)
.with_target_fields(vec!["email".to_string()]);
assert!((config.error_rate - 0.3).abs() < f64::EPSILON);
assert!(config.error_types.contains(&InvalidDataType::Null));
assert_eq!(config.error_types.len(), 1);
assert_eq!(config.target_fields, vec!["email"]);
}
#[test]
fn test_parse_error_types() {
let types = InvalidDataConfig::parse_error_types("missing-field,wrong-type,null").unwrap();
assert_eq!(types.len(), 3);
assert!(types.contains(&InvalidDataType::MissingField));
assert!(types.contains(&InvalidDataType::WrongType));
assert!(types.contains(&InvalidDataType::Null));
}
#[test]
fn test_parse_error_types_empty() {
let types = InvalidDataConfig::parse_error_types("").unwrap();
assert!(types.is_empty());
}
#[test]
fn test_generate_should_invalidate() {
let code = InvalidDataGenerator::generate_should_invalidate(0.2);
assert!(code.contains("Math.random() < 0.2"));
assert!(code.contains("shouldInvalidate"));
}
#[test]
fn test_generate_type_selection() {
let mut types = HashSet::new();
types.insert(InvalidDataType::MissingField);
types.insert(InvalidDataType::Null);
let code = InvalidDataGenerator::generate_type_selection(&types);
assert!(code.contains("invalidTypes"));
assert!(code.contains("Math.random()"));
}
#[test]
fn test_generate_invalidation_logic() {
let code = InvalidDataGenerator::generate_invalidation_logic();
assert!(code.contains("function invalidateField"));
assert!(code.contains("function invalidatePayload"));
assert!(code.contains("missing-field"));
assert!(code.contains("wrong-type"));
assert!(code.contains("out-of-range"));
}
#[test]
fn test_generate_complete_invalidation() {
let config = InvalidDataConfig::default();
let code = InvalidDataGenerator::generate_complete_invalidation(&config, "[]");
assert!(code.contains("shouldInvalidate"));
assert!(code.contains("invalidType"));
assert!(code.contains("targetFields"));
assert!(code.contains("finalPayload"));
}
#[test]
fn test_generate_error_checks() {
let code = InvalidDataGenerator::generate_error_checks();
assert!(code.contains("shouldInvalidate"));
assert!(code.contains("expects error response"));
assert!(code.contains("status is OK"));
}
}