diff --git a/.github/workflows/validate-blurbs.yml b/.github/workflows/validate-blurbs.yml new file mode 100644 index 0000000..5073880 --- /dev/null +++ b/.github/workflows/validate-blurbs.yml @@ -0,0 +1,52 @@ +name: Validate Blurbs + +on: + push: + branches: [ main, develop ] + paths: + - 'users/**/blurbs.yaml' + - 'config/blurb_schema.json' + - 'core/schema_validator.py' + - 'scripts/validate_blurbs.py' + pull_request: + branches: [ main ] + paths: + - 'users/**/blurbs.yaml' + - 'config/blurb_schema.json' + - 'core/schema_validator.py' + - 'scripts/validate_blurbs.py' + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Validate all user blurbs + run: | + python scripts/validate_blurbs.py --all --verbose --exit-code + + - name: Run schema validation tests + run: | + python -m pytest tests/test_schema_validation.py -v + + - name: Upload validation results + if: always() + uses: actions/upload-artifact@v3 + with: + name: validation-results + path: | + validation-results.json + retention-days: 7 \ No newline at end of file diff --git a/core/schema_validator.py b/core/schema_validator.py new file mode 100644 index 0000000..7fda67f --- /dev/null +++ b/core/schema_validator.py @@ -0,0 +1,280 @@ +""" +Schema validation module for cover letter agent. + +Provides comprehensive validation for blurbs, config files, and other YAML/JSON data +using JSON Schema validation with detailed error reporting. +""" + +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional, Tuple +from jsonschema import validate, ValidationError, SchemaError +import logging + +logger = logging.getLogger(__name__) + + +class SchemaValidator: + """Comprehensive schema validation for cover letter agent data.""" + + def __init__(self, schema_dir: str = "config"): + """Initialize validator with schema directory.""" + self.schema_dir = Path(schema_dir) + self.schemas = {} + self._load_schemas() + + def _load_schemas(self) -> None: + """Load all JSON schemas from the schema directory.""" + schema_files = { + "blurb": "blurb_schema.json", + # Add more schemas as needed + # "config": "config_schema.json", + # "logic": "logic_schema.json", + } + + for schema_name, filename in schema_files.items(): + schema_path = self.schema_dir / filename + if schema_path.exists(): + try: + with open(schema_path, 'r') as f: + self.schemas[schema_name] = json.load(f) + logger.info(f"Loaded schema: {schema_name}") + except (json.JSONDecodeError, FileNotFoundError) as e: + logger.error(f"Failed to load schema {schema_name}: {e}") + else: + logger.warning(f"Schema file not found: {schema_path}") + + def validate_blurbs(self, blurbs_data: Dict[str, Any], file_path: str = "") -> Tuple[bool, List[str]]: + """ + Validate blurbs against the blurb schema. + + Args: + blurbs_data: The blurbs data to validate + file_path: Path to the blurbs file (for error reporting) + + Returns: + Tuple of (is_valid, list_of_errors) + """ + if "blurb" not in self.schemas: + return False, ["Blurb schema not found"] + + errors = [] + + try: + # Validate against schema + validate(instance=blurbs_data, schema=self.schemas["blurb"]) + + # Additional custom validations + custom_errors = self._validate_blurbs_custom(blurbs_data, file_path) + errors.extend(custom_errors) + + return len(errors) == 0, errors + + except ValidationError as e: + errors.append(f"Schema validation error: {e.message}") + if e.path: + errors.append(f"Path: {' -> '.join(str(p) for p in e.path)}") + return False, errors + except SchemaError as e: + errors.append(f"Schema error: {e.message}") + return False, errors + except Exception as e: + errors.append(f"Unexpected validation error: {e}") + return False, errors + + def _validate_blurbs_custom(self, blurbs_data: Dict[str, Any], file_path: str) -> List[str]: + """Perform custom validations beyond JSON schema.""" + errors = [] + + # Check for required sections + required_sections = ["intro", "paragraph2", "examples"] + for section in required_sections: + if section not in blurbs_data: + errors.append(f"Missing required section: {section}") + elif not blurbs_data[section]: + # Allow empty sections for optional content like star_stories + if section not in ["star_stories", "leadership", "closing"]: + errors.append(f"Section '{section}' is empty") + + # Check for duplicate blurb IDs within sections + for section_name, section_blurbs in blurbs_data.items(): + if not isinstance(section_blurbs, list): + continue + + blurb_ids = [] + for i, blurb in enumerate(section_blurbs): + if not isinstance(blurb, dict): + continue + + blurb_id = blurb.get("id") + if blurb_id: + if blurb_id in blurb_ids: + errors.append(f"Duplicate blurb ID '{blurb_id}' in section '{section_name}'") + else: + blurb_ids.append(blurb_id) + + # Validate individual blurb structure + blurb_errors = self._validate_single_blurb(blurb, section_name, i) + errors.extend(blurb_errors) + + return errors + + def _validate_single_blurb(self, blurb: Dict[str, Any], section_name: str, index: int) -> List[str]: + """Validate a single blurb.""" + errors = [] + + if not isinstance(blurb, dict): + errors.append(f"Blurb at index {index} in section '{section_name}' is not a dictionary") + return errors + + # Check required fields + required_fields = ["id", "tags", "text"] + for field in required_fields: + if field not in blurb: + errors.append(f"Blurb at index {index} in section '{section_name}' missing required field: {field}") + + # Validate ID format + blurb_id = blurb.get("id") + if blurb_id and not isinstance(blurb_id, str): + errors.append(f"Blurb ID at index {index} in section '{section_name}' must be a string") + elif blurb_id and not blurb_id.replace("_", "").isalnum(): + errors.append(f"Blurb ID '{blurb_id}' at index {index} in section '{section_name}' contains invalid characters") + + # Validate tags + tags = blurb.get("tags") + if tags and not isinstance(tags, list): + errors.append(f"Tags at index {index} in section '{section_name}' must be a list") + elif tags: + for i, tag in enumerate(tags): + if not isinstance(tag, str): + errors.append(f"Tag {i} at index {index} in section '{section_name}' must be a string") + elif not tag.replace("_", "").isalnum(): + errors.append(f"Tag '{tag}' at index {index} in section '{section_name}' contains invalid characters") + + # Validate text + text = blurb.get("text") + if text and not isinstance(text, str): + errors.append(f"Text at index {index} in section '{section_name}' must be a string") + elif text and len(text.strip()) < 10: + errors.append(f"Text at index {index} in section '{section_name}' is too short (minimum 10 characters)") + elif text and len(text) > 2000: + errors.append(f"Text at index {index} in section '{section_name}' is too long (maximum 2000 characters)") + + return errors + + def validate_yaml_file(self, file_path: str, schema_name: str) -> Tuple[bool, List[str]]: + """ + Validate a YAML file against a specific schema. + + Args: + file_path: Path to the YAML file + schema_name: Name of the schema to use + + Returns: + Tuple of (is_valid, list_of_errors) + """ + try: + with open(file_path, 'r') as f: + data = yaml.safe_load(f) + + # Handle empty files + if data is None: + return False, ["File is empty or contains no valid YAML"] + + if schema_name == "blurb": + return self.validate_blurbs(data, file_path) + else: + return False, [f"Unknown schema: {schema_name}"] + + except yaml.YAMLError as e: + return False, [f"YAML parsing error: {e}"] + except FileNotFoundError: + return False, [f"File not found: {file_path}"] + except Exception as e: + return False, [f"Unexpected error: {e}"] + + def get_validation_summary(self, file_path: str, schema_name: str) -> Dict[str, Any]: + """ + Get a detailed validation summary for a file. + + Args: + file_path: Path to the file to validate + schema_name: Name of the schema to use + + Returns: + Dictionary with validation results and details + """ + is_valid, errors = self.validate_yaml_file(file_path, schema_name) + + summary = { + "file_path": file_path, + "schema_name": schema_name, + "is_valid": is_valid, + "error_count": len(errors), + "errors": errors, + "timestamp": str(Path(file_path).stat().st_mtime) if Path(file_path).exists() else None + } + + return summary + + +def validate_user_blurbs(user_id: str) -> Dict[str, Any]: + """ + Validate blurbs for a specific user. + + Args: + user_id: The user ID to validate + + Returns: + Validation results dictionary + """ + validator = SchemaValidator() + blurbs_path = f"users/{user_id}/blurbs.yaml" + + if not Path(blurbs_path).exists(): + return { + "user_id": user_id, + "is_valid": False, + "error_count": 1, + "errors": [f"Blurbs file not found: {blurbs_path}"], + "file_path": blurbs_path + } + + return validator.get_validation_summary(blurbs_path, "blurb") + + +def validate_all_user_blurbs() -> Dict[str, Any]: + """ + Validate blurbs for all users. + + Returns: + Dictionary with validation results for all users + """ + validator = SchemaValidator() + users_dir = Path("users") + results = {} + + if not users_dir.exists(): + return {"error": "Users directory not found"} + + for user_dir in users_dir.iterdir(): + if user_dir.is_dir(): + blurbs_path = user_dir / "blurbs.yaml" + if blurbs_path.exists(): + results[user_dir.name] = validator.get_validation_summary(str(blurbs_path), "blurb") + + return results + + +if __name__ == "__main__": + # Test validation + import sys + + if len(sys.argv) > 1: + user_id = sys.argv[1] + results = validate_user_blurbs(user_id) + print(json.dumps(results, indent=2)) + else: + results = validate_all_user_blurbs() + print(json.dumps(results, indent=2)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d00f970..ce96210 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,9 @@ google-auth-oauthlib>=1.0.0 google-auth-httplib2>=0.1.0 google-api-python-client>=2.0.0 +# Schema validation +jsonschema>=4.0.0 + # UX dependencies (optional) colorama>=0.4.6 tqdm>=4.65.0 diff --git a/scripts/validate_blurbs.py b/scripts/validate_blurbs.py new file mode 100644 index 0000000..4d6614e --- /dev/null +++ b/scripts/validate_blurbs.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Blurb validation CLI tool. + +Validates blurbs against JSON schema and provides detailed error reporting. +Can be used in CI/CD pipelines to ensure data quality. +""" + +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, Any + +# Add the project root to the path +sys.path.append(str(Path(__file__).parent.parent)) + +from core.schema_validator import SchemaValidator, validate_user_blurbs, validate_all_user_blurbs + + +def main(): + """Main CLI function.""" + parser = argparse.ArgumentParser( + description="Validate cover letter blurbs against schema", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Validate all users + python scripts/validate_blurbs.py --all + + # Validate specific user + python scripts/validate_blurbs.py --user peter + + # Validate specific file + python scripts/validate_blurbs.py --file users/peter/blurbs.yaml + + # Show detailed errors + python scripts/validate_blurbs.py --user peter --verbose + + # Export results to JSON + python scripts/validate_blurbs.py --all --output results.json + """ + ) + + parser.add_argument( + "--all", + action="store_true", + help="Validate blurbs for all users" + ) + + parser.add_argument( + "--user", + type=str, + help="Validate blurbs for specific user" + ) + + parser.add_argument( + "--file", + type=str, + help="Validate specific blurbs file" + ) + + parser.add_argument( + "--verbose", + action="store_true", + help="Show detailed error information" + ) + + parser.add_argument( + "--output", + type=str, + help="Output results to JSON file" + ) + + parser.add_argument( + "--exit-code", + action="store_true", + help="Exit with non-zero code if validation fails" + ) + + args = parser.parse_args() + + # Validate arguments + if not any([args.all, args.user, args.file]): + parser.error("Must specify --all, --user, or --file") + + results = {} + + if args.file: + # Validate specific file + validator = SchemaValidator() + results["file"] = validator.get_validation_summary(args.file, "blurb") + + elif args.user: + # Validate specific user + results[args.user] = validate_user_blurbs(args.user) + + elif args.all: + # Validate all users + results = validate_all_user_blurbs() + + # Display results + display_results(results, args.verbose) + + # Save to file if requested + if args.output: + with open(args.output, 'w') as f: + json.dump(results, f, indent=2) + print(f"\nResults saved to: {args.output}") + + # Exit with appropriate code + if args.exit_code: + all_valid = all( + result.get("is_valid", False) + for result in results.values() + if isinstance(result, dict) + ) + sys.exit(0 if all_valid else 1) + + +def display_results(results: Dict[str, Any], verbose: bool = False): + """Display validation results in a user-friendly format.""" + print("šŸ” Blurb Validation Results") + print("=" * 50) + + total_files = 0 + valid_files = 0 + total_errors = 0 + + for name, result in results.items(): + if not isinstance(result, dict): + continue + + total_files += 1 + is_valid = result.get("is_valid", False) + error_count = result.get("error_count", 0) + file_path = result.get("file_path", "Unknown") + + if is_valid: + valid_files += 1 + status = "āœ… VALID" + else: + total_errors += error_count + status = "āŒ INVALID" + + print(f"\n{status} - {name}") + print(f" File: {file_path}") + print(f" Errors: {error_count}") + + if verbose and not is_valid: + errors = result.get("errors", []) + for i, error in enumerate(errors, 1): + print(f" {i}. {error}") + + # Summary + print("\n" + "=" * 50) + print(f"šŸ“Š Summary:") + print(f" Total files: {total_files}") + print(f" Valid files: {valid_files}") + print(f" Invalid files: {total_files - valid_files}") + print(f" Total errors: {total_errors}") + + if total_files > 0: + success_rate = (valid_files / total_files) * 100 + print(f" Success rate: {success_rate:.1f}%") + + if total_errors == 0 and total_files > 0: + print("\nšŸŽ‰ All blurbs are valid!") + elif total_errors > 0: + print(f"\nāš ļø Found {total_errors} validation errors") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py new file mode 100644 index 0000000..bc81d29 --- /dev/null +++ b/tests/test_schema_validation.py @@ -0,0 +1,420 @@ +""" +Comprehensive tests for schema validation system. + +Tests the JSON schema validation, custom validations, and CLI tools. +""" + +import pytest +import tempfile +import yaml +from pathlib import Path +from unittest.mock import patch, mock_open + +from core.schema_validator import SchemaValidator, validate_user_blurbs, validate_all_user_blurbs + + +class TestSchemaValidator: + """Test the SchemaValidator class.""" + + def setup_method(self): + """Set up test fixtures.""" + self.validator = SchemaValidator() + + # Sample valid blurbs + self.valid_blurbs = { + "intro": [ + { + "id": "standard", + "tags": ["all"], + "text": "I'm a product leader with 15+ years of experience building user-centric products." + } + ], + "paragraph2": [ + { + "id": "cleantech", + "tags": ["cleantech", "climate", "energy"], + "text": "Five years in solar SaaS has given me a deep understanding of the needs of homeowners." + } + ], + "examples": [ + { + "id": "enact", + "tags": ["growth", "leadership", "founding_pm"], + "text": "At Enact, I led product for post-sale tools that monitor performance." + } + ] + } + + def test_valid_blurbs_pass_validation(self): + """Test that valid blurbs pass validation.""" + is_valid, errors = self.validator.validate_blurbs(self.valid_blurbs) + assert is_valid + assert len(errors) == 0 + + def test_missing_required_sections(self): + """Test validation fails when required sections are missing.""" + invalid_blurbs = { + "intro": self.valid_blurbs["intro"] + # Missing paragraph2 and examples + } + + is_valid, errors = self.validator.validate_blurbs(invalid_blurbs) + assert not is_valid + assert len(errors) >= 1 # Should have at least one error for missing sections + + def test_missing_required_fields(self): + """Test validation fails when required fields are missing.""" + invalid_blurbs = { + "intro": [ + { + "id": "standard", + "text": "Missing tags field" + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(invalid_blurbs) + assert not is_valid + assert any("required" in error.lower() for error in errors) + + def test_duplicate_blurb_ids(self): + """Test validation fails when blurb IDs are duplicated.""" + invalid_blurbs = { + "intro": [ + { + "id": "standard", + "tags": ["all"], + "text": "First blurb" + }, + { + "id": "standard", # Duplicate ID + "tags": ["growth"], + "text": "Second blurb with same ID" + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(invalid_blurbs) + assert not is_valid + assert any("Duplicate blurb ID" in error for error in errors) + + def test_invalid_blurb_id_format(self): + """Test validation fails with invalid blurb ID format.""" + invalid_blurbs = { + "intro": [ + { + "id": "invalid-id", # Contains hyphen + "tags": ["all"], + "text": "Valid text" + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(invalid_blurbs) + assert not is_valid + assert any("pattern" in error.lower() or "invalid" in error.lower() for error in errors) + + def test_invalid_tag_format(self): + """Test validation fails with invalid tag format.""" + invalid_blurbs = { + "intro": [ + { + "id": "standard", + "tags": ["valid_tag", "invalid-tag"], # Contains hyphen + "text": "Valid text" + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(invalid_blurbs) + assert not is_valid + assert any("pattern" in error.lower() or "invalid" in error.lower() for error in errors) + + def test_text_length_validation(self): + """Test validation of text length constraints.""" + # Test too short text + short_blurbs = { + "intro": [ + { + "id": "short", + "tags": ["all"], + "text": "Too short" # Less than 10 characters + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(short_blurbs) + assert not is_valid + assert any("too short" in error for error in errors) + + # Test too long text + long_text = "x" * 2001 # More than 2000 characters + long_blurbs = { + "intro": [ + { + "id": "long", + "tags": ["all"], + "text": long_text + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(long_blurbs) + assert not is_valid + assert any("too long" in error for error in errors) + + def test_malformed_blurb_structure(self): + """Test validation fails with malformed blurb structure.""" + invalid_blurbs = { + "intro": [ + "not_a_dict", # Should be a dictionary + { + "id": "valid", + "tags": ["all"], + "text": "Valid blurb" + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(invalid_blurbs) + assert not is_valid + assert any("type" in error.lower() or "object" in error.lower() for error in errors) + + def test_optional_fields_validation(self): + """Test validation of optional fields.""" + blurbs_with_optional = { + "intro": [ + { + "id": "standard", + "tags": ["all"], + "text": "Valid text", + "priority": "high", + "job_types": ["general", "ai_ml"], + "experience_level": "senior", + "company_stage": "startup" + } + ], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"] + } + + is_valid, errors = self.validator.validate_blurbs(blurbs_with_optional) + assert is_valid + assert len(errors) == 0 + + def test_empty_optional_sections(self): + """Test that empty optional sections are allowed.""" + blurbs_with_empty_optional = { + "intro": self.valid_blurbs["intro"], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"], + "star_stories": [], # Empty optional section + "leadership": [], # Empty optional section + "closing": [] # Empty optional section + } + + is_valid, errors = self.validator.validate_blurbs(blurbs_with_empty_optional) + assert is_valid + assert len(errors) == 0 + + def test_metadata_section(self): + """Test that metadata section is allowed.""" + blurbs_with_metadata = { + "intro": self.valid_blurbs["intro"], + "paragraph2": self.valid_blurbs["paragraph2"], + "examples": self.valid_blurbs["examples"], + "metadata": { + "last_updated": "2025-01-01", + "total_examples": 5 + } + } + + is_valid, errors = self.validator.validate_blurbs(blurbs_with_metadata) + assert is_valid + assert len(errors) == 0 + + +class TestSchemaValidatorIntegration: + """Test schema validator integration with file system.""" + + def test_validate_yaml_file_valid(self, tmp_path): + """Test validation of a valid YAML file.""" + # Create a temporary valid blurbs file + blurbs_file = tmp_path / "blurbs.yaml" + valid_blurbs = { + "intro": [ + { + "id": "standard", + "tags": ["all"], + "text": "Valid introduction blurb." + } + ], + "paragraph2": [ + { + "id": "cleantech", + "tags": ["cleantech", "energy"], + "text": "Valid paragraph blurb." + } + ], + "examples": [ + { + "id": "enact", + "tags": ["growth", "leadership"], + "text": "Valid example blurb." + } + ] + } + + with open(blurbs_file, 'w') as f: + yaml.dump(valid_blurbs, f) + + validator = SchemaValidator() + is_valid, errors = validator.validate_yaml_file(str(blurbs_file), "blurb") + + assert is_valid + assert len(errors) == 0 + + def test_validate_yaml_file_invalid(self, tmp_path): + """Test validation of an invalid YAML file.""" + # Create a temporary invalid blurbs file + blurbs_file = tmp_path / "blurbs.yaml" + invalid_blurbs = { + "intro": [ + { + "id": "standard", + # Missing tags and text + } + ] + } + + with open(blurbs_file, 'w') as f: + yaml.dump(invalid_blurbs, f) + + validator = SchemaValidator() + is_valid, errors = validator.validate_yaml_file(str(blurbs_file), "blurb") + + assert not is_valid + assert len(errors) > 0 + + def test_validate_yaml_file_not_found(self): + """Test validation of non-existent file.""" + validator = SchemaValidator() + is_valid, errors = validator.validate_yaml_file("nonexistent.yaml", "blurb") + + assert not is_valid + assert any("File not found" in error for error in errors) + + def test_validate_yaml_file_invalid_yaml(self, tmp_path): + """Test validation of file with invalid YAML syntax.""" + # Create a file with invalid YAML + blurbs_file = tmp_path / "blurbs.yaml" + with open(blurbs_file, 'w') as f: + f.write("invalid: yaml: content: [\n") # Malformed YAML + + validator = SchemaValidator() + is_valid, errors = validator.validate_yaml_file(str(blurbs_file), "blurb") + + assert not is_valid + assert any("YAML parsing error" in error for error in errors) + + +class TestValidationFunctions: + """Test the validation utility functions.""" + + @patch('core.schema_validator.Path') + def test_validate_user_blurbs_user_not_found(self, mock_path): + """Test validation when user blurbs file doesn't exist.""" + mock_path.return_value.exists.return_value = False + + result = validate_user_blurbs("nonexistent_user") + + assert not result["is_valid"] + assert result["error_count"] == 1 + assert "not found" in result["errors"][0] + + @patch('core.schema_validator.SchemaValidator') + def test_validate_user_blurbs_valid(self, mock_validator_class): + """Test validation of valid user blurbs.""" + mock_validator = mock_validator_class.return_value + mock_validator.get_validation_summary.return_value = { + "file_path": "users/test/blurbs.yaml", + "schema_name": "blurb", + "is_valid": True, + "error_count": 0, + "errors": [], + "timestamp": "1234567890" + } + + with patch('core.schema_validator.Path') as mock_path: + mock_path.return_value.exists.return_value = True + + result = validate_user_blurbs("test") + + assert result["is_valid"] + assert result["error_count"] == 0 + assert len(result["errors"]) == 0 + + @patch('core.schema_validator.Path') + def test_validate_all_user_blurbs_no_users_dir(self, mock_path): + """Test validation when users directory doesn't exist.""" + mock_path.return_value.exists.return_value = False + + result = validate_all_user_blurbs() + + assert "error" in result + assert "not found" in result["error"] + + @patch('core.schema_validator.Path') + def test_validate_all_user_blurbs_with_users(self, mock_path): + """Test validation of all users.""" + # Mock the users directory structure + mock_users_dir = mock_path.return_value + mock_users_dir.exists.return_value = True + mock_users_dir.iterdir.return_value = [ + mock_path.return_value, # user1 + mock_path.return_value, # user2 + ] + + # Mock individual user directories + mock_user_dir = mock_path.return_value + mock_user_dir.is_dir.return_value = True + mock_user_dir.name = "test_user" + + # Mock blurbs file + mock_blurbs_file = mock_path.return_value + mock_blurbs_file.exists.return_value = True + mock_blurbs_file.__truediv__.return_value = mock_blurbs_file + + with patch('core.schema_validator.SchemaValidator') as mock_validator_class: + mock_validator = mock_validator_class.return_value + mock_validator.get_validation_summary.return_value = { + "file_path": "users/test_user/blurbs.yaml", + "schema_name": "blurb", + "is_valid": True, + "error_count": 0, + "errors": [], + "timestamp": "1234567890" + } + + result = validate_all_user_blurbs() + + assert "test_user" in result + assert result["test_user"]["is_valid"] + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file