foundry_mcp/utils/
validation.rs

1//! Enhanced validation utilities with detailed error reporting and security checks
2
3use crate::models::{Project, Specification, Task, TaskList, TechStack, Vision};
4use chrono::Utc;
5use std::collections::HashMap;
6
7/// Validation error with context and suggestions
8#[derive(Debug, Clone)]
9pub struct ValidationError {
10    pub field: String,
11    pub value: String,
12    pub error_type: ValidationErrorType,
13    pub message: String,
14    pub suggestion: Option<String>,
15}
16
17/// Types of validation errors
18#[derive(Debug, Clone, PartialEq)]
19pub enum ValidationErrorType {
20    Required,
21    TooLong,
22    TooShort,
23    InvalidFormat,
24    InvalidCharacters,
25    AlreadyExists,
26    SecurityRisk,
27    InvalidRange,
28    InvalidTimestamp,
29}
30
31impl ValidationError {
32    pub fn new(field: &str, value: &str, error_type: ValidationErrorType, message: &str) -> Self {
33        Self {
34            field: field.to_string(),
35            value: value.to_string(),
36            error_type,
37            message: message.to_string(),
38            suggestion: None,
39        }
40    }
41
42    pub fn with_suggestion(mut self, suggestion: &str) -> Self {
43        self.suggestion = Some(suggestion.to_string());
44        self
45    }
46
47    pub fn format_error(&self) -> String {
48        let mut error_msg = format!("Field '{}': {}", self.field, self.message);
49        
50        if let Some(suggestion) = &self.suggestion {
51            error_msg.push_str(&format!("\nSuggestion: {}", suggestion));
52        }
53        
54        error_msg
55    }
56}
57
58/// Validation result with detailed errors
59pub type ValidationResult = Result<(), Vec<ValidationError>>;
60
61/// Enhanced validation context
62pub struct ValidationContext {
63    pub existing_projects: Vec<String>,
64    pub existing_specs: HashMap<String, Vec<String>>, // project_name -> spec_names
65    pub strict_mode: bool,
66    pub max_content_length: usize,
67}
68
69impl Default for ValidationContext {
70    fn default() -> Self {
71        Self {
72            existing_projects: Vec::new(),
73            existing_specs: HashMap::new(),
74            strict_mode: false,
75            max_content_length: 50_000,
76        }
77    }
78}
79
80/// Enhanced project validation with detailed error reporting
81pub fn validate_project_enhanced(project: &Project, context: &ValidationContext) -> ValidationResult {
82    let mut errors = Vec::new();
83
84    // Validate project name
85    if project.name.trim().is_empty() {
86        errors.push(
87            ValidationError::new(
88                "name",
89                &project.name,
90                ValidationErrorType::Required,
91                "Project name is required"
92            ).with_suggestion("Provide a descriptive name in lowercase with hyphens or underscores, e.g., 'my-awesome-project'")
93        );
94    } else {
95        // Check length
96        if project.name.len() > 100 {
97            errors.push(
98                ValidationError::new(
99                    "name",
100                    &project.name,
101                    ValidationErrorType::TooLong,
102                    "Project name cannot exceed 100 characters"
103                ).with_suggestion("Use a shorter, more concise name")
104            );
105        }
106
107        // Check format
108        if !project.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
109            errors.push(
110                ValidationError::new(
111                    "name",
112                    &project.name,
113                    ValidationErrorType::InvalidCharacters,
114                    "Project name can only contain letters, numbers, hyphens, and underscores"
115                ).with_suggestion("Replace special characters with hyphens or underscores")
116            );
117        }
118
119        // Check for uppercase letters
120        if project.name.chars().any(|c| c.is_uppercase()) {
121            errors.push(
122                ValidationError::new(
123                    "name",
124                    &project.name,
125                    ValidationErrorType::InvalidFormat,
126                    "Project name should be lowercase"
127                ).with_suggestion("Convert the name to lowercase")
128            );
129        }
130
131        // Check availability
132        if context.existing_projects.contains(&project.name) {
133            errors.push(
134                ValidationError::new(
135                    "name",
136                    &project.name,
137                    ValidationErrorType::AlreadyExists,
138                    "Project name already exists"
139                ).with_suggestion("Choose a different project name or load the existing project")
140            );
141        }
142
143        // Security check
144        if let Err(security_error) = validate_input_security(&project.name) {
145            errors.push(
146                ValidationError::new(
147                    "name",
148                    &project.name,
149                    ValidationErrorType::SecurityRisk,
150                    &security_error
151                ).with_suggestion("Use only safe characters in project names")
152            );
153        }
154    }
155
156    // Validate description
157    if project.description.trim().is_empty() {
158        errors.push(
159            ValidationError::new(
160                "description",
161                &project.description,
162                ValidationErrorType::Required,
163                "Project description is required"
164            ).with_suggestion("Provide a brief description of what this project does")
165        );
166    } else if project.description.len() > 2000 {
167        errors.push(
168            ValidationError::new(
169                "description",
170                &project.description,
171                ValidationErrorType::TooLong,
172                "Project description cannot exceed 2000 characters"
173            ).with_suggestion("Shorten the description or move detailed information to the vision overview")
174        );
175    }
176
177    // Security check for description
178    if let Err(security_error) = validate_input_security(&project.description) {
179        errors.push(
180            ValidationError::new(
181                "description",
182                &project.description,
183                ValidationErrorType::SecurityRisk,
184                &security_error
185            ).with_suggestion("Remove any potentially dangerous content from the description")
186        );
187    }
188
189    // Validate timestamps
190    if project.created_at > project.updated_at {
191        errors.push(
192            ValidationError::new(
193                "timestamps",
194                &format!("created: {}, updated: {}", project.created_at, project.updated_at),
195                ValidationErrorType::InvalidTimestamp,
196                "Created timestamp cannot be after updated timestamp"
197            ).with_suggestion("Ensure the updated timestamp is equal to or after the created timestamp")
198        );
199    }
200
201    // Validate tech stack and vision with enhanced validation
202    if let Err(tech_stack_errors) = validate_tech_stack_enhanced(&project.tech_stack, context) {
203        errors.extend(tech_stack_errors);
204    }
205
206    if let Err(vision_errors) = validate_vision_enhanced(&project.vision, context) {
207        errors.extend(vision_errors);
208    }
209
210    if errors.is_empty() {
211        Ok(())
212    } else {
213        Err(errors)
214    }
215}
216
217/// Enhanced tech stack validation
218pub fn validate_tech_stack_enhanced(tech_stack: &TechStack, context: &ValidationContext) -> ValidationResult {
219    let mut errors = Vec::new();
220
221    // Validate languages
222    if tech_stack.languages.is_empty() && context.strict_mode {
223        errors.push(
224            ValidationError::new(
225                "languages",
226                "",
227                ValidationErrorType::Required,
228                "At least one programming language should be specified"
229            ).with_suggestion("Add the main programming language(s) for this project")
230        );
231    }
232
233    for (i, language) in tech_stack.languages.iter().enumerate() {
234        let field_name = format!("languages[{}]", i);
235        if language.trim().is_empty() {
236            errors.push(
237                ValidationError::new(
238                    &field_name,
239                    language,
240                    ValidationErrorType::Required,
241                    "Language name cannot be empty"
242                ).with_suggestion("Remove empty entries or provide a valid language name")
243            );
244        } else if language.len() > 50 {
245            errors.push(
246                ValidationError::new(
247                    &field_name,
248                    language,
249                    ValidationErrorType::TooLong,
250                    "Language name cannot exceed 50 characters"
251                ).with_suggestion("Use standard language names like 'JavaScript', 'Python', 'Rust'")
252            );
253        } else if let Err(security_error) = validate_input_security(language) {
254            errors.push(
255                ValidationError::new(
256                    &field_name,
257                    language,
258                    ValidationErrorType::SecurityRisk,
259                    &security_error
260                ).with_suggestion("Use only standard programming language names")
261            );
262        }
263    }
264
265    // Similar validation for frameworks, databases, tools, and deployment
266    validate_string_list(&tech_stack.frameworks, "frameworks", 100, &mut errors, context);
267    validate_string_list(&tech_stack.databases, "databases", 100, &mut errors, context);
268    validate_string_list(&tech_stack.tools, "tools", 100, &mut errors, context);
269    validate_string_list(&tech_stack.deployment, "deployment", 100, &mut errors, context);
270
271    if errors.is_empty() {
272        Ok(())
273    } else {
274        Err(errors)
275    }
276}
277
278/// Enhanced vision validation
279pub fn validate_vision_enhanced(vision: &Vision, context: &ValidationContext) -> ValidationResult {
280    let mut errors = Vec::new();
281
282    // Validate overview
283    if vision.overview.trim().is_empty() {
284        errors.push(
285            ValidationError::new(
286                "overview",
287                &vision.overview,
288                ValidationErrorType::Required,
289                "Vision overview is required"
290            ).with_suggestion("Provide a clear overview of the project's purpose and scope")
291        );
292    } else if vision.overview.len() > 5000 {
293        errors.push(
294            ValidationError::new(
295                "overview",
296                &vision.overview,
297                ValidationErrorType::TooLong,
298                "Vision overview cannot exceed 5000 characters"
299            ).with_suggestion("Keep the overview concise and move detailed information to specifications")
300        );
301    }
302
303    // Security check
304    if let Err(security_error) = validate_input_security(&vision.overview) {
305        errors.push(
306            ValidationError::new(
307                "overview",
308                &vision.overview,
309                ValidationErrorType::SecurityRisk,
310                &security_error
311            ).with_suggestion("Remove any potentially dangerous content from the overview")
312        );
313    }
314
315    // Validate goals, target_users, and success_criteria
316    validate_string_list(&vision.goals, "goals", 500, &mut errors, context);
317    validate_string_list(&vision.target_users, "target_users", 200, &mut errors, context);
318    validate_string_list(&vision.success_criteria, "success_criteria", 500, &mut errors, context);
319
320    // Check minimum requirements in strict mode
321    if context.strict_mode {
322        if vision.goals.is_empty() {
323            errors.push(
324                ValidationError::new(
325                    "goals",
326                    "",
327                    ValidationErrorType::Required,
328                    "At least one goal must be specified"
329                ).with_suggestion("Define clear, measurable goals for the project")
330            );
331        }
332
333        if vision.target_users.is_empty() {
334            errors.push(
335                ValidationError::new(
336                    "target_users",
337                    "",
338                    ValidationErrorType::Required,
339                    "At least one target user must be specified"
340                ).with_suggestion("Identify who will use or benefit from this project")
341            );
342        }
343
344        if vision.success_criteria.is_empty() {
345            errors.push(
346                ValidationError::new(
347                    "success_criteria",
348                    "",
349                    ValidationErrorType::Required,
350                    "At least one success criterion must be specified"
351                ).with_suggestion("Define how you will measure the success of this project")
352            );
353        }
354    }
355
356    if errors.is_empty() {
357        Ok(())
358    } else {
359        Err(errors)
360    }
361}
362
363/// Helper function to validate string lists with consistent error handling
364fn validate_string_list(
365    list: &[String],
366    field_name: &str,
367    max_length: usize,
368    errors: &mut Vec<ValidationError>,
369    _context: &ValidationContext,
370) {
371    for (i, item) in list.iter().enumerate() {
372        let item_field = format!("{}[{}]", field_name, i);
373        
374        if item.trim().is_empty() {
375            errors.push(
376                ValidationError::new(
377                    &item_field,
378                    item,
379                    ValidationErrorType::Required,
380                    &format!("{} item cannot be empty", field_name)
381                ).with_suggestion("Remove empty entries or provide valid content")
382            );
383        } else if item.len() > max_length {
384            errors.push(
385                ValidationError::new(
386                    &item_field,
387                    item,
388                    ValidationErrorType::TooLong,
389                    &format!("{} item cannot exceed {} characters", field_name, max_length)
390                ).with_suggestion("Use shorter, more concise descriptions")
391            );
392        } else if let Err(security_error) = validate_input_security(item) {
393            errors.push(
394                ValidationError::new(
395                    &item_field,
396                    item,
397                    ValidationErrorType::SecurityRisk,
398                    &security_error
399                ).with_suggestion("Remove any potentially dangerous content")
400            );
401        }
402    }
403}
404
405/// Security validation for input strings
406pub fn validate_input_security(input: &str) -> Result<(), String> {
407    // Check for script injection attempts
408    let dangerous_patterns = [
409        "<script", "</script>", "javascript:", "data:", "vbscript:",
410        "onload=", "onerror=", "onclick=", "onmouseover=",
411        "eval(", "setTimeout(", "setInterval(",
412        "document.cookie", "document.location", "window.location",
413        "innerHTML", "outerHTML", "document.write",
414        "exec(", "system(", "shell_exec(", "passthru(",
415        "file_get_contents(", "file_put_contents(", "fopen(",
416        "include(", "include_once(", "require(", "require_once(",
417        "<?php", "<?=", "<%", "%>", "<%=",
418        "DROP TABLE", "DELETE FROM", "INSERT INTO", "UPDATE SET",
419        "UNION SELECT", "OR 1=1", "' OR '1'='1", "\" OR \"1\"=\"1",
420        "../", "..\\", "/etc/", "/var/", "/usr/", "/bin/",
421        "C:\\Windows", "C:\\Program Files", "C:\\Users",
422    ];
423
424    let input_lower = input.to_lowercase();
425    for pattern in &dangerous_patterns {
426        if input_lower.contains(&pattern.to_lowercase()) {
427            return Err(format!("Input contains potentially dangerous pattern: {}", pattern));
428        }
429    }
430
431    // Check for unusual Unicode characters that could be used for attacks
432    if input.chars().any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t') {
433        return Err("Input contains control characters".to_string());
434    }
435
436    // Check for excessively long lines (potential DoS)
437    if input.lines().any(|line| line.len() > 10000) {
438        return Err("Input contains excessively long lines".to_string());
439    }
440
441    Ok(())
442}
443
444/// Backward compatibility function
445pub fn validate_project(project: &Project) -> Result<(), Vec<String>> {
446    let context = ValidationContext::default();
447    match validate_project_enhanced(project, &context) {
448        Ok(()) => Ok(()),
449        Err(errors) => Err(errors.into_iter().map(|e| e.format_error()).collect()),
450    }
451}
452
453/// Validate a tech stack structure
454pub fn validate_tech_stack(tech_stack: &TechStack) -> Result<(), Vec<String>> {
455    let mut errors = Vec::new();
456
457    // Validate languages
458    errors.extend(
459        tech_stack
460            .languages
461            .iter()
462            .enumerate()
463            .filter_map(|(i, language)| {
464                if language.trim().is_empty() {
465                    Some(format!("Language {} cannot be empty", i + 1))
466                } else if language.len() > 50 {
467                    Some(format!(
468                        "Language '{}' cannot exceed 50 characters",
469                        language
470                    ))
471                } else {
472                    None
473                }
474            }),
475    );
476
477    // Validate frameworks
478    errors.extend(
479        tech_stack
480            .frameworks
481            .iter()
482            .enumerate()
483            .filter_map(|(i, framework)| {
484                if framework.trim().is_empty() {
485                    Some(format!("Framework {} cannot be empty", i + 1))
486                } else if framework.len() > 100 {
487                    Some(format!(
488                        "Framework '{}' cannot exceed 100 characters",
489                        framework
490                    ))
491                } else {
492                    None
493                }
494            }),
495    );
496
497    // Validate databases
498    errors.extend(
499        tech_stack
500            .databases
501            .iter()
502            .enumerate()
503            .filter_map(|(i, database)| {
504                if database.trim().is_empty() {
505                    Some(format!("Database {} cannot be empty", i + 1))
506                } else if database.len() > 100 {
507                    Some(format!(
508                        "Database '{}' cannot exceed 100 characters",
509                        database
510                    ))
511                } else {
512                    None
513                }
514            }),
515    );
516
517    // Validate tools
518    errors.extend(tech_stack.tools.iter().enumerate().filter_map(|(i, tool)| {
519        if tool.trim().is_empty() {
520            Some(format!("Tool {} cannot be empty", i + 1))
521        } else if tool.len() > 100 {
522            Some(format!("Tool '{}' cannot exceed 100 characters", tool))
523        } else {
524            None
525        }
526    }));
527
528    // Validate deployment
529    errors.extend(
530        tech_stack
531            .deployment
532            .iter()
533            .enumerate()
534            .filter_map(|(i, deployment)| {
535                if deployment.trim().is_empty() {
536                    Some(format!("Deployment {} cannot be empty", i + 1))
537                } else if deployment.len() > 100 {
538                    Some(format!(
539                        "Deployment '{}' cannot exceed 100 characters",
540                        deployment
541                    ))
542                } else {
543                    None
544                }
545            }),
546    );
547
548    if errors.is_empty() {
549        Ok(())
550    } else {
551        Err(errors)
552    }
553}
554
555/// Validate a vision structure
556pub fn validate_vision(vision: &Vision) -> Result<(), Vec<String>> {
557    let mut errors = Vec::new();
558
559    // Validate overview
560    if vision.overview.trim().is_empty() {
561        errors.push("Vision overview cannot be empty".to_string());
562    }
563
564    if vision.overview.len() > 2000 {
565        errors.push("Vision overview cannot exceed 2000 characters".to_string());
566    }
567
568    // Validate goals
569    if vision.goals.is_empty() {
570        errors.push("At least one goal must be specified".to_string());
571    }
572
573    errors.extend(vision.goals.iter().enumerate().filter_map(|(i, goal)| {
574        if goal.trim().is_empty() {
575            Some(format!("Goal {} cannot be empty", i + 1))
576        } else if goal.len() > 500 {
577            Some(format!("Goal '{}' cannot exceed 500 characters", goal))
578        } else {
579            None
580        }
581    }));
582
583    // Validate target users
584    if vision.target_users.is_empty() {
585        errors.push("At least one target user must be specified".to_string());
586    }
587
588    errors.extend(
589        vision
590            .target_users
591            .iter()
592            .enumerate()
593            .filter_map(|(i, user)| {
594                if user.trim().is_empty() {
595                    Some(format!("Target user {} cannot be empty", i + 1))
596                } else if user.len() > 200 {
597                    Some(format!(
598                        "Target user '{}' cannot exceed 200 characters",
599                        user
600                    ))
601                } else {
602                    None
603                }
604            }),
605    );
606
607    // Validate success criteria
608    if vision.success_criteria.is_empty() {
609        errors.push("At least one success criterion must be specified".to_string());
610    }
611
612    errors.extend(
613        vision
614            .success_criteria
615            .iter()
616            .enumerate()
617            .filter_map(|(i, criterion)| {
618                if criterion.trim().is_empty() {
619                    Some(format!("Success criterion {} cannot be empty", i + 1))
620                } else if criterion.len() > 500 {
621                    Some(format!(
622                        "Success criterion '{}' cannot exceed 500 characters",
623                        criterion
624                    ))
625                } else {
626                    None
627                }
628            }),
629    );
630
631    if errors.is_empty() {
632        Ok(())
633    } else {
634        Err(errors)
635    }
636}
637
638/// Validate a specification structure
639pub fn validate_specification(spec: &Specification) -> Result<(), Vec<String>> {
640    let mut errors = Vec::new();
641
642    // Validate ID
643    if let Err(e) = crate::utils::id_generation::validate_spec_id(&spec.id) {
644        errors.push(format!("Specification ID: {}", e));
645    }
646
647    // Validate name
648    if spec.name.trim().is_empty() {
649        errors.push("Specification name cannot be empty".to_string());
650    }
651
652    if spec.name.len() > 100 {
653        errors.push("Specification name cannot exceed 100 characters".to_string());
654    }
655
656    // Validate description
657    if spec.description.trim().is_empty() {
658        errors.push("Specification description cannot be empty".to_string());
659    }
660
661    if spec.description.len() > 1000 {
662        errors.push("Specification description cannot exceed 1000 characters".to_string());
663    }
664
665    // Validate timestamps
666    if spec.created_at > spec.updated_at {
667        errors.push("Created timestamp cannot be after updated timestamp".to_string());
668    }
669
670    // Validate content
671    if spec.content.len() > 10000 {
672        errors.push("Specification content cannot exceed 10000 characters".to_string());
673    }
674
675    if errors.is_empty() {
676        Ok(())
677    } else {
678        Err(errors)
679    }
680}
681
682/// Validate a task structure
683pub fn validate_task(task: &Task) -> Result<(), Vec<String>> {
684    let mut errors = Vec::new();
685
686    // Validate ID
687    if task.id.trim().is_empty() {
688        errors.push("Task ID cannot be empty".to_string());
689    }
690
691    if task.id.len() > 100 {
692        errors.push("Task ID cannot exceed 100 characters".to_string());
693    }
694
695    // Validate title
696    if task.title.trim().is_empty() {
697        errors.push("Task title cannot be empty".to_string());
698    }
699
700    if task.title.len() > 200 {
701        errors.push("Task title cannot exceed 200 characters".to_string());
702    }
703
704    // Validate description
705    if task.description.len() > 1000 {
706        errors.push("Task description cannot exceed 1000 characters".to_string());
707    }
708
709    // Validate timestamps
710    if task.created_at > task.updated_at {
711        errors.push("Created timestamp cannot be after updated timestamp".to_string());
712    }
713
714    // Validate dependencies
715    errors.extend(
716        task.dependencies
717            .iter()
718            .enumerate()
719            .filter_map(|(i, dependency)| {
720                if dependency.trim().is_empty() {
721                    Some(format!("Dependency {} cannot be empty", i + 1))
722                } else if dependency.len() > 100 {
723                    Some(format!(
724                        "Dependency '{}' cannot exceed 100 characters",
725                        dependency
726                    ))
727                } else {
728                    None
729                }
730            }),
731    );
732
733    if errors.is_empty() {
734        Ok(())
735    } else {
736        Err(errors)
737    }
738}
739
740/// Validate a task list structure
741pub fn validate_task_list(task_list: &TaskList) -> Result<(), Vec<String>> {
742    let mut errors = Vec::new();
743
744    // Validate tasks
745    errors.extend(task_list.tasks.iter().enumerate().flat_map(|(i, task)| {
746        validate_task(task)
747            .map(|_| Vec::<String>::new())
748            .unwrap_or_else(|task_errors| {
749                task_errors
750                    .into_iter()
751                    .map(|task_error| format!("Task {}: {}", i + 1, task_error))
752                    .collect::<Vec<_>>()
753            })
754    }));
755
756    // Validate last updated timestamp
757    let now = Utc::now();
758    if task_list.last_updated > now {
759        errors.push("Last updated timestamp cannot be in the future".to_string());
760    }
761
762    if errors.is_empty() {
763        Ok(())
764    } else {
765        Err(errors)
766    }
767}
768
769/// Validate that a project name is available (not already taken)
770pub fn validate_project_name_availability(
771    project_name: &str,
772    existing_projects: &[String],
773) -> Result<(), String> {
774    if existing_projects.iter().any(|name| name == project_name) {
775        Err(format!("Project name '{}' is already taken", project_name))
776    } else {
777        Ok(())
778    }
779}
780
781/// Validate that a specification name is available within a project
782pub fn validate_spec_name_availability(
783    spec_name: &str,
784    existing_specs: &[String],
785) -> Result<(), String> {
786    if existing_specs.iter().any(|name| name == spec_name) {
787        Err(format!(
788            "Specification name '{}' is already taken in this project",
789            spec_name
790        ))
791    } else {
792        Ok(())
793    }
794}
795
796/// Validate file path safety (prevent directory traversal attacks)
797pub fn validate_file_path_safety(path: &str) -> Result<(), String> {
798    if path.contains("..") {
799        return Err("Path contains directory traversal attempt".to_string());
800    }
801
802    if path.starts_with('/') || path.starts_with('\\') {
803        return Err("Path cannot be absolute".to_string());
804    }
805
806    if path.contains('\0') {
807        return Err("Path contains null character".to_string());
808    }
809
810    // Check for other potentially dangerous patterns
811    let dangerous_patterns = ["/etc/", "/var/", "/usr/", "/bin/", "/sbin/", "C:\\", "D:\\"];
812    if dangerous_patterns
813        .iter()
814        .any(|pattern| path.to_lowercase().contains(pattern))
815    {
816        return Err(format!(
817            "Path contains potentially dangerous pattern: {}",
818            dangerous_patterns
819                .iter()
820                .find(|pattern| path.to_lowercase().contains(*pattern))
821                .unwrap()
822        ));
823    }
824
825    Ok(())
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831    use crate::models::SpecStatus;
832
833    fn create_test_project() -> Project {
834        Project {
835            name: "test_project".to_string(),
836            description: "A test project".to_string(),
837            created_at: Utc::now(),
838            updated_at: Utc::now(),
839            tech_stack: TechStack {
840                languages: vec!["Rust".to_string()],
841                frameworks: vec!["Actix".to_string()],
842                databases: vec!["PostgreSQL".to_string()],
843                tools: vec!["Cargo".to_string()],
844                deployment: vec!["Docker".to_string()],
845            },
846            vision: Vision {
847                overview: "A test project overview".to_string(),
848                goals: vec!["Goal 1".to_string()],
849                target_users: vec!["Developer".to_string()],
850                success_criteria: vec!["Criterion 1".to_string()],
851            },
852        }
853    }
854
855    fn create_test_spec() -> Specification {
856        Specification {
857            id: "20240101_test_spec".to_string(),
858            name: "test_spec".to_string(),
859            description: "A test specification".to_string(),
860            status: SpecStatus::Draft,
861            created_at: Utc::now(),
862            updated_at: Utc::now(),
863            content: "Test content".to_string(),
864        }
865    }
866
867    fn create_test_task() -> Task {
868        Task {
869            id: "task_1".to_string(),
870            title: "Test Task".to_string(),
871            description: "A test task".to_string(),
872            status: crate::models::TaskStatus::Todo,
873            priority: crate::models::TaskPriority::Medium,
874            dependencies: vec![],
875            created_at: Utc::now(),
876            updated_at: Utc::now(),
877        }
878    }
879
880    #[test]
881    fn test_validate_project() {
882        let project = create_test_project();
883        assert!(validate_project(&project).is_ok());
884    }
885
886    #[test]
887    fn test_validate_specification() {
888        let spec = create_test_spec();
889        assert!(validate_specification(&spec).is_ok());
890    }
891
892    #[test]
893    fn test_validate_task() {
894        let task = create_test_task();
895        assert!(validate_task(&task).is_ok());
896    }
897
898    #[test]
899    fn test_validate_project_name_availability() {
900        let existing = vec!["project1".to_string(), "project2".to_string()];
901
902        assert!(validate_project_name_availability("new_project", &existing).is_ok());
903        assert!(validate_project_name_availability("project1", &existing).is_err());
904    }
905
906    #[test]
907    fn test_validate_file_path_safety() {
908        assert!(validate_file_path_safety("valid/path").is_ok());
909        assert!(validate_file_path_safety("../dangerous").is_err());
910        assert!(validate_file_path_safety("/absolute/path").is_err());
911        assert!(validate_file_path_safety("path\0with\0nulls").is_err());
912    }
913}