1use crate::core::ModelAdmin;
14use crate::types::AdminError;
15use std::collections::HashMap;
16
17const MAX_FIELDS: usize = 100;
19
20const MAX_STRING_LENGTH: usize = 1_000_000; const MAX_PAYLOAD_SIZE: usize = 10_000_000; pub fn validate_mutation_data(
54 data: &HashMap<String, serde_json::Value>,
55 model_admin: &dyn ModelAdmin,
56 is_update: bool,
57) -> Result<(), AdminError> {
58 validate_field_count(data)?;
60
61 validate_payload_size(data)?;
63
64 let allowed_fields = get_allowed_fields(model_admin);
66 let readonly_fields: Vec<&str> = model_admin.readonly_fields();
67 let pk_field = model_admin.pk_field();
68
69 for (field_name, value) in data {
71 validate_field_allowed(field_name, &allowed_fields)?;
73
74 if readonly_fields.contains(&field_name.as_str()) {
76 return Err(AdminError::ValidationError(format!(
77 "Field '{}' is read-only and cannot be modified",
78 field_name
79 )));
80 }
81
82 if is_update && field_name == pk_field {
86 return Err(AdminError::ValidationError(format!(
87 "Primary key field '{}' cannot be modified",
88 field_name
89 )));
90 }
91
92 validate_value_size(field_name, value)?;
94 }
95
96 Ok(())
97}
98
99fn get_allowed_fields(model_admin: &dyn ModelAdmin) -> Vec<&str> {
103 model_admin
104 .fields()
105 .unwrap_or_else(|| model_admin.list_display())
106}
107
108fn validate_field_count(data: &HashMap<String, serde_json::Value>) -> Result<(), AdminError> {
110 if data.len() > MAX_FIELDS {
111 return Err(AdminError::ValidationError(format!(
112 "Too many fields in request: {} (max {})",
113 data.len(),
114 MAX_FIELDS
115 )));
116 }
117 Ok(())
118}
119
120fn validate_payload_size(data: &HashMap<String, serde_json::Value>) -> Result<(), AdminError> {
122 let total_size: usize = data
123 .iter()
124 .map(|(k, v)| k.len() + v.to_string().len())
125 .sum();
126
127 if total_size > MAX_PAYLOAD_SIZE {
128 return Err(AdminError::ValidationError(format!(
129 "Payload too large: {} bytes (max {} bytes)",
130 total_size, MAX_PAYLOAD_SIZE
131 )));
132 }
133 Ok(())
134}
135
136fn validate_field_allowed(field_name: &str, allowed_fields: &[&str]) -> Result<(), AdminError> {
138 if !allowed_fields.contains(&field_name) {
139 return Err(AdminError::ValidationError(format!(
140 "Field '{}' is not allowed. Allowed fields: {:?}",
141 field_name, allowed_fields
142 )));
143 }
144 Ok(())
145}
146
147fn validate_value_size(field_name: &str, value: &serde_json::Value) -> Result<(), AdminError> {
149 match value {
150 serde_json::Value::String(s) => {
151 if s.len() > MAX_STRING_LENGTH {
152 return Err(AdminError::ValidationError(format!(
153 "Field '{}' value too long: {} bytes (max {} bytes)",
154 field_name,
155 s.len(),
156 MAX_STRING_LENGTH
157 )));
158 }
159 }
160 serde_json::Value::Array(arr) if arr.len() > MAX_FIELDS => {
161 return Err(AdminError::ValidationError(format!(
162 "Field '{}' array too large: {} elements (max {})",
163 field_name,
164 arr.len(),
165 MAX_FIELDS
166 )));
167 }
168 serde_json::Value::Object(obj) if obj.len() > MAX_FIELDS => {
169 return Err(AdminError::ValidationError(format!(
170 "Field '{}' object too large: {} keys (max {})",
171 field_name,
172 obj.len(),
173 MAX_FIELDS
174 )));
175 }
176 _ => {}
177 }
178 Ok(())
179}
180
181#[cfg(all(test, server))]
182mod tests {
183 use super::*;
184 use crate::core::ModelAdminConfig;
185 use rstest::rstest;
186
187 fn create_test_admin() -> ModelAdminConfig {
188 ModelAdminConfig::builder()
189 .model_name("TestModel")
190 .list_display(vec!["id", "name", "email", "created_at"])
191 .fields(vec!["id", "name", "email", "created_at"])
192 .readonly_fields(vec!["created_at"])
193 .build()
194 .unwrap()
195 }
196
197 #[rstest]
198 fn test_validate_empty_data() {
199 let admin = create_test_admin();
200 let data = HashMap::new();
201 assert!(validate_mutation_data(&data, &admin, false).is_ok());
202 }
203
204 #[rstest]
205 fn test_validate_allowed_field() {
206 let admin = create_test_admin();
207 let mut data = HashMap::new();
208 data.insert("name".to_string(), serde_json::json!("Alice"));
209
210 assert!(validate_mutation_data(&data, &admin, false).is_ok());
211 }
212
213 #[rstest]
214 fn test_validate_disallowed_field() {
215 let admin = create_test_admin();
216 let mut data = HashMap::new();
217 data.insert("hacked".to_string(), serde_json::json!("value"));
218
219 let result = validate_mutation_data(&data, &admin, false);
220 assert!(result.is_err());
221 assert!(matches!(
222 result.unwrap_err(),
223 AdminError::ValidationError(_)
224 ));
225 }
226
227 #[rstest]
228 fn test_validate_readonly_field() {
229 let admin = create_test_admin();
230 let mut data = HashMap::new();
231 data.insert("created_at".to_string(), serde_json::json!("2024-01-01"));
232
233 let result = validate_mutation_data(&data, &admin, false);
234 assert!(result.is_err());
235 let err = result.unwrap_err();
236 assert!(matches!(err, AdminError::ValidationError(_)));
237 assert!(err.to_string().contains("read-only"));
238 }
239
240 #[rstest]
241 fn test_validate_pk_field_on_update() {
242 let admin = create_test_admin();
243 let mut data = HashMap::new();
244 data.insert("id".to_string(), serde_json::json!(999));
245
246 let result = validate_mutation_data(&data, &admin, true);
247 assert!(result.is_err());
248 let err = result.unwrap_err();
249 assert!(matches!(err, AdminError::ValidationError(_)));
250 assert!(err.to_string().contains("Primary key"));
251 }
252
253 #[rstest]
254 fn test_validate_pk_field_on_create() {
255 let admin = create_test_admin();
257 let mut data = HashMap::new();
258 data.insert("id".to_string(), serde_json::json!(999));
259
260 let result = validate_mutation_data(&data, &admin, false);
261 assert!(result.is_ok());
262 }
263
264 #[rstest]
265 fn test_validate_too_many_fields() {
266 let admin = create_test_admin();
267 let mut data = HashMap::new();
268
269 for i in 0..=MAX_FIELDS {
271 data.insert(format!("name_{}", i), serde_json::json!("value"));
272 }
273
274 let result = validate_mutation_data(&data, &admin, false);
275 assert!(result.is_err());
276 let err = result.unwrap_err();
277 assert!(matches!(err, AdminError::ValidationError(_)));
278 assert!(err.to_string().contains("Too many fields"));
279 }
280
281 #[rstest]
282 fn test_validate_string_too_long() {
283 let admin = create_test_admin();
284 let mut data = HashMap::new();
285 data.insert(
286 "name".to_string(),
287 serde_json::json!("x".repeat(MAX_STRING_LENGTH + 1)),
288 );
289
290 let result = validate_mutation_data(&data, &admin, false);
291 assert!(result.is_err());
292 let err = result.unwrap_err();
293 assert!(matches!(err, AdminError::ValidationError(_)));
294 assert!(err.to_string().contains("too long"));
295 }
296
297 #[rstest]
298 fn test_validate_array_too_large() {
299 let admin = create_test_admin();
300 let mut data = HashMap::new();
301 let large_array: Vec<_> = (0..=MAX_FIELDS).map(|i| serde_json::json!(i)).collect();
302 data.insert("name".to_string(), serde_json::json!(large_array));
303
304 let result = validate_mutation_data(&data, &admin, false);
305 assert!(result.is_err());
306 let err = result.unwrap_err();
307 assert!(matches!(err, AdminError::ValidationError(_)));
308 assert!(err.to_string().contains("array too large"));
309 }
310
311 #[rstest]
312 fn test_validate_uses_list_display_as_fallback() {
313 let admin = ModelAdminConfig::builder()
315 .model_name("TestModel")
316 .list_display(vec!["id", "title"])
317 .build()
318 .unwrap();
319
320 let mut data = HashMap::new();
321 data.insert("title".to_string(), serde_json::json!("Test"));
322
323 assert!(validate_mutation_data(&data, &admin, false).is_ok());
324 }
325
326 #[rstest]
329 #[case::below_limit(99, true)]
330 #[case::at_limit(100, true)]
331 #[case::above_limit(101, false)]
332 fn test_mutation_field_count_boundary(#[case] field_count: usize, #[case] should_pass: bool) {
333 let field_names: Vec<String> = (0..field_count).map(|i| format!("f_{}", i)).collect();
336 let field_refs: Vec<&str> = field_names.iter().map(|s| s.as_str()).collect();
337 let admin = ModelAdminConfig::builder()
338 .model_name("TestModel")
339 .list_display(field_refs.clone())
340 .fields(field_refs)
341 .build()
342 .unwrap();
343
344 let mut data = HashMap::new();
345 for i in 0..field_count {
346 data.insert(format!("f_{}", i), serde_json::json!("v"));
347 }
348
349 let result = validate_mutation_data(&data, &admin, false);
351
352 assert_eq!(
354 result.is_ok(),
355 should_pass,
356 "field_count={}, expected pass={}, got {:?}",
357 field_count,
358 should_pass,
359 result
360 );
361 }
362
363 #[rstest]
366 #[case::within_limit(999_999, true)]
367 #[case::at_limit(1_000_000, true)]
368 #[case::above_limit(1_000_001, false)]
369 fn test_mutation_string_length_boundary(#[case] length: usize, #[case] should_pass: bool) {
370 let admin = create_test_admin();
372 let mut data = HashMap::new();
373 data.insert("name".to_string(), serde_json::json!("x".repeat(length)));
374
375 let result = validate_mutation_data(&data, &admin, false);
377
378 assert_eq!(
380 result.is_ok(),
381 should_pass,
382 "length={}, expected pass={}, got {:?}",
383 length,
384 should_pass,
385 result
386 );
387 }
388
389 #[rstest]
392 #[case::field_in_allowlist_not_readonly_create(true, false, false, true, true)]
393 #[case::field_in_allowlist_not_readonly_update(true, false, false, false, true)]
394 #[case::field_not_in_allowlist(false, false, false, true, false)]
395 #[case::field_is_readonly_on_create(true, true, false, true, false)]
396 #[case::field_is_readonly_on_update(true, true, false, false, false)]
397 #[case::pk_field_on_create(true, false, true, true, true)]
398 #[case::pk_field_on_update(true, false, true, false, false)]
399 fn test_mutation_validation_decision_table(
400 #[case] in_allowlist: bool,
401 #[case] is_readonly: bool,
402 #[case] is_pk: bool,
403 #[case] is_create: bool,
404 #[case] should_pass: bool,
405 ) {
406 let field_name = if is_pk { "id" } else { "name" };
408 let is_update = !is_create;
409
410 let mut fields_list = vec!["id"];
411 if in_allowlist && !is_pk {
412 fields_list.push("name");
413 }
414
415 let readonly = if is_readonly && !is_pk {
416 vec!["name"]
417 } else {
418 vec![]
419 };
420
421 let admin = ModelAdminConfig::builder()
422 .model_name("TestModel")
423 .list_display(fields_list.clone())
424 .fields(fields_list)
425 .readonly_fields(readonly)
426 .build()
427 .unwrap();
428
429 let mut data = HashMap::new();
430 data.insert(field_name.to_string(), serde_json::json!("test_value"));
431
432 let result = validate_mutation_data(&data, &admin, is_update);
434
435 assert_eq!(
437 result.is_ok(),
438 should_pass,
439 "in_allowlist={}, is_readonly={}, is_pk={}, is_create={}, expected pass={}, got {:?}",
440 in_allowlist,
441 is_readonly,
442 is_pk,
443 is_create,
444 should_pass,
445 result
446 );
447 }
448}