All files / src/utils validation.ts

0% Statements 0/72
0% Branches 0/84
0% Functions 0/9
0% Lines 0/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197                                                                                                                                                                                                                                                                                                                                                                                                         
import { ValidationError, ErrorDetails } from '../types/errors';
 
export interface ValidationRule {
  field: string;
  required?: boolean;
  type?: 'string' | 'number' | 'email' | 'phone' | 'boolean' | 'array' | 'object';
  minLength?: number;
  maxLength?: number;
  min?: number;
  max?: number;
  pattern?: RegExp;
  custom?: (value: any) => string | null; // Returns error message or null if valid
}
 
export class Validator {
  static validate(data: any, rules: ValidationRule[], requestId?: string): void {
    const errors: ErrorDetails[] = [];
 
    for (const rule of rules) {
      const value = data[rule.field];
      const fieldErrors: string[] = [];
 
      // Check required fields
      if (rule.required && (value === undefined || value === null || value === '')) {
        fieldErrors.push(`${rule.field} est requis`);
        continue; // Skip other validations if field is missing
      }
 
      // Skip validation if field is optional and empty
      if (!rule.required && (value === undefined || value === null || value === '')) {
        continue;
      }
 
      // Type validation
      if (rule.type) {
        switch (rule.type) {
          case 'email':
            if (!this.isValidEmail(value)) {
              fieldErrors.push('Format d\'email invalide');
            }
            break;
          case 'phone':
            if (!this.isValidPhone(value)) {
              fieldErrors.push('Format de téléphone invalide');
            }
            break;
          case 'string':
            if (typeof value !== 'string') {
              fieldErrors.push('Doit être une chaîne de caractères');
            }
            break;
          case 'number':
            if (typeof value !== 'number' || isNaN(value)) {
              fieldErrors.push('Doit être un nombre valide');
            }
            break;
          case 'boolean':
            if (typeof value !== 'boolean') {
              fieldErrors.push('Doit être un booléen');
            }
            break;
          case 'array':
            if (!Array.isArray(value)) {
              fieldErrors.push('Doit être un tableau');
            }
            break;
          case 'object':
            if (typeof value !== 'object' || Array.isArray(value)) {
              fieldErrors.push('Doit être un objet');
            }
            break;
        }
      }
 
      // Length validation for strings
      if (typeof value === 'string') {
        if (rule.minLength !== undefined && value.length < rule.minLength) {
          fieldErrors.push(`Doit contenir au moins ${rule.minLength} caractères`);
        }
        if (rule.maxLength !== undefined && value.length > rule.maxLength) {
          fieldErrors.push(`Doit contenir au maximum ${rule.maxLength} caractères`);
        }
      }
 
      // Numeric range validation
      if (typeof value === 'number') {
        if (rule.min !== undefined && value < rule.min) {
          fieldErrors.push(`Doit être supérieur ou égal à ${rule.min}`);
        }
        if (rule.max !== undefined && value > rule.max) {
          fieldErrors.push(`Doit être inférieur ou égal à ${rule.max}`);
        }
      }
 
      // Pattern validation
      if (rule.pattern && typeof value === 'string') {
        if (!rule.pattern.test(value)) {
          fieldErrors.push('Format invalide');
        }
      }
 
      // Custom validation
      if (rule.custom) {
        const customError = rule.custom(value);
        if (customError) {
          fieldErrors.push(customError);
        }
      }
 
      // Add errors for this field
      if (fieldErrors.length > 0) {
        errors.push({
          field: rule.field,
          value: value,
          constraints: fieldErrors
        });
      }
    }
 
    if (errors.length > 0) {
      throw new ValidationError('Erreurs de validation', errors, requestId);
    }
  }
 
  private static isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
 
  private static isValidPhone(phone: string): boolean {
    const phoneRegex = /^(\+[1-9]\d{1,14}|0\d{9,14})$/;
    return phoneRegex.test(phone);
  }
 
  // Predefined validation rule sets
  static phoneRules(field: string = 'phoneNumber'): ValidationRule[] {
    return [
      {
        field,
        required: true,
        type: 'phone'
      }
    ];
  }
 
  static emailRules(field: string = 'email'): ValidationRule[] {
    return [
      {
        field,
        required: true,
        type: 'email'
      }
    ];
  }
 
  static subscriptionRules(): ValidationRule[] {
    return [
      {
        field: 'type',
        required: true,
        type: 'string',
        custom: (value) => {
          if (!['STANDARD', 'UNLIMITED'].includes(value)) {
            return 'Type d\'abonnement invalide';
          }
          return null;
        }
      },
      {
        field: 'frequency',
        required: true,
        type: 'string',
        custom: (value) => {
          if (!['monthly', 'yearly'].includes(value)) {
            return 'Fréquence invalide';
          }
          return null;
        }
      },
      {
        field: 'seats',
        required: false,
        type: 'number',
        min: 1,
        custom: (value) => {
          // Only validate seats if type is STANDARD
          const type = value?.type;
          if (type === 'STANDARD' && (!value || value < 1)) {
            return 'Nombre de sièges requis pour l\'abonnement Standard';
          }
          return null;
        }
      }
    ];
  }
}