/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ validationService = new \DeskMoloni\ValidationService(); } protected function tearDown(): void { Mockery::close(); } /** * Test client data validation rules * This test will initially fail until validation implementation exists */ public function testClientDataValidation(): void { // Valid client data $validClient = [ 'company' => 'Valid Company Name', 'vat' => '123456789', 'email' => 'valid@example.com', 'phone' => '+351910000000', 'address' => 'Valid Address 123', 'city' => 'Lisboa', 'zip' => '1000-001', 'country' => 'Portugal' ]; $result = $this->validationService->validateClientData($validClient); $this->assertIsArray($result); $this->assertTrue($result['valid']); $this->assertEmpty($result['errors']); // Invalid client data - missing required fields $invalidClient = [ 'company' => '', 'vat' => '', 'email' => 'invalid-email', 'phone' => 'invalid-phone' ]; $result = $this->validationService->validateClientData($invalidClient); $this->assertIsArray($result); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertArrayHasKey('company', $result['errors']); $this->assertArrayHasKey('vat', $result['errors']); $this->assertArrayHasKey('email', $result['errors']); $this->assertArrayHasKey('phone', $result['errors']); } /** * Test VAT number validation for different countries */ public function testVatNumberValidation(): void { $validVatNumbers = [ 'PT123456789', // Portugal 'ES12345678Z', // Spain 'FR12345678901', // France 'DE123456789', // Germany 'IT12345678901', // Italy '123456789' // Default format ]; foreach ($validVatNumbers as $vatNumber) { $this->assertTrue( $this->validationService->isValidVAT($vatNumber), "VAT number should be valid: {$vatNumber}" ); } $invalidVatNumbers = [ '', // Empty '123', // Too short 'INVALID', // Non-numeric 'PT123', // Wrong format for Portugal str_repeat('1', 20) // Too long ]; foreach ($invalidVatNumbers as $vatNumber) { $this->assertFalse( $this->validationService->isValidVAT($vatNumber), "VAT number should be invalid: {$vatNumber}" ); } } /** * Test email validation with business rules */ public function testEmailValidation(): void { $validEmails = [ 'user@example.com', 'user.name@example.com', 'user+tag@example.com', 'user123@example-domain.com', 'test@subdomain.example.com' ]; foreach ($validEmails as $email) { $this->assertTrue( $this->validationService->isValidEmail($email), "Email should be valid: {$email}" ); } $invalidEmails = [ '', // Empty 'invalid', // No @ symbol 'invalid@', // No domain '@example.com', // No user 'user@', // No domain 'user space@example.com', // Space in email 'user@example', // No TLD 'user@.com' // Invalid domain ]; foreach ($invalidEmails as $email) { $this->assertFalse( $this->validationService->isValidEmail($email), "Email should be invalid: {$email}" ); } } /** * Test phone number validation for international formats */ public function testPhoneNumberValidation(): void { $validPhoneNumbers = [ '+351910000000', // Portugal mobile '+351210000000', // Portugal landline '+34600000000', // Spain mobile '+33600000000', // France mobile '+49170000000', // Germany mobile '910000000', // Local format '00351910000000' // International format ]; foreach ($validPhoneNumbers as $phone) { $this->assertTrue( $this->validationService->isValidPhone($phone), "Phone number should be valid: {$phone}" ); } $invalidPhoneNumbers = [ '', // Empty '123', // Too short 'abcdefghij', // Non-numeric '+351', // Incomplete '+351 910 000 000', // Spaces not allowed in some contexts str_repeat('1', 20) // Too long ]; foreach ($invalidPhoneNumbers as $phone) { $this->assertFalse( $this->validationService->isValidPhone($phone), "Phone number should be invalid: {$phone}" ); } } /** * Test product data validation */ public function testProductDataValidation(): void { $validProduct = [ 'name' => 'Valid Product Name', 'reference' => 'PROD-001', 'price' => 99.99, 'category_id' => 1, 'has_stock' => true, 'stock' => 10 ]; $result = $this->validationService->validateProductData($validProduct); $this->assertTrue($result['valid']); $this->assertEmpty($result['errors']); // Invalid product data $invalidProduct = [ 'name' => '', // Empty name 'reference' => '', // Empty reference 'price' => -10, // Negative price 'category_id' => 'invalid', // Non-numeric category 'has_stock' => 'yes', // Invalid boolean 'stock' => -5 // Negative stock ]; $result = $this->validationService->validateProductData($invalidProduct); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertArrayHasKey('name', $result['errors']); $this->assertArrayHasKey('price', $result['errors']); $this->assertArrayHasKey('stock', $result['errors']); } /** * Test invoice data validation */ public function testInvoiceDataValidation(): void { $validInvoice = [ 'number' => 'INV-2025-001', 'client_id' => 1, 'date' => '2025-09-10', 'due_date' => '2025-10-10', 'currency' => 'EUR', 'subtotal' => 100.00, 'tax' => 23.00, 'total' => 123.00, 'status' => 'pending', 'items' => [ [ 'product_id' => 1, 'quantity' => 2, 'price' => 50.00, 'discount' => 0 ] ] ]; $result = $this->validationService->validateInvoiceData($validInvoice); $this->assertTrue($result['valid']); $this->assertEmpty($result['errors']); // Invalid invoice data $invalidInvoice = [ 'number' => '', // Empty number 'client_id' => 'invalid', // Non-numeric client ID 'date' => 'invalid-date', // Invalid date format 'due_date' => '2025-09-09', // Due date before invoice date 'currency' => 'XXX', // Invalid currency 'subtotal' => -100, // Negative amount 'total' => 50, // Total less than subtotal 'items' => [] // Empty items ]; $result = $this->validationService->validateInvoiceData($invalidInvoice); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertArrayHasKey('number', $result['errors']); $this->assertArrayHasKey('client_id', $result['errors']); $this->assertArrayHasKey('date', $result['errors']); $this->assertArrayHasKey('due_date', $result['errors']); $this->assertArrayHasKey('items', $result['errors']); } /** * Test data sanitization rules */ public function testDataSanitization(): void { $testCases = [ // [input, expected_output, field_type] [' Test Company ', 'Test Company', 'company_name'], ['', 'alert("xss")', 'text_field'], ['user@EXAMPLE.COM', 'user@example.com', 'email'], ['+351 910 000 000', '+351910000000', 'phone'], ['PT 123 456 789', 'PT123456789', 'vat'], ['PRODUCT-001', 'PRODUCT-001', 'reference'], [' MULTI SPACE TEXT ', 'MULTI SPACE TEXT', 'text_field'] ]; foreach ($testCases as [$input, $expected, $fieldType]) { $sanitized = $this->validationService->sanitizeField($input, $fieldType); $this->assertEquals( $expected, $sanitized, "Field sanitization failed for type {$fieldType}: '{$input}' should become '{$expected}'" ); } } /** * Test business rule validations */ public function testBusinessRuleValidations(): void { // Test duplicate VAT validation $existingVats = ['123456789', '987654321']; $mockVatChecker = Mockery::mock('DeskMoloni\VatChecker'); $mockVatChecker->shouldReceive('exists')->with('123456789')->andReturn(true); $mockVatChecker->shouldReceive('exists')->with('999999999')->andReturn(false); $this->validationService->setVatChecker($mockVatChecker); $this->assertFalse( $this->validationService->isUniqueVAT('123456789'), 'Existing VAT should not be unique' ); $this->assertTrue( $this->validationService->isUniqueVAT('999999999'), 'New VAT should be unique' ); // Test invoice number format validation $validInvoiceNumbers = [ 'INV-2025-001', 'FAT-001', '2025001', 'INVOICE-123' ]; foreach ($validInvoiceNumbers as $number) { $this->assertTrue( $this->validationService->isValidInvoiceNumber($number), "Invoice number should be valid: {$number}" ); } $invalidInvoiceNumbers = [ '', // Empty '123', // Too short 'INV-', // Incomplete str_repeat('A', 50) // Too long ]; foreach ($invalidInvoiceNumbers as $number) { $this->assertFalse( $this->validationService->isValidInvoiceNumber($number), "Invoice number should be invalid: {$number}" ); } } /** * Test field length validations */ public function testFieldLengthValidations(): void { $fieldLimits = [ 'company_name' => ['max' => 255, 'min' => 1], 'email' => ['max' => 320, 'min' => 5], 'phone' => ['max' => 20, 'min' => 9], 'address' => ['max' => 500, 'min' => 5], 'product_name' => ['max' => 255, 'min' => 1], 'invoice_notes' => ['max' => 1000, 'min' => 0] ]; foreach ($fieldLimits as $fieldType => $limits) { // Test minimum length if ($limits['min'] > 0) { $shortValue = str_repeat('a', $limits['min'] - 1); $this->assertFalse( $this->validationService->validateFieldLength($shortValue, $fieldType), "Field {$fieldType} should reject value shorter than {$limits['min']}" ); } // Test valid length $validValue = str_repeat('a', $limits['min']); $this->assertTrue( $this->validationService->validateFieldLength($validValue, $fieldType), "Field {$fieldType} should accept valid length value" ); // Test maximum length $longValue = str_repeat('a', $limits['max'] + 1); $this->assertFalse( $this->validationService->validateFieldLength($longValue, $fieldType), "Field {$fieldType} should reject value longer than {$limits['max']}" ); } } /** * Test currency and amount validations */ public function testCurrencyAndAmountValidations(): void { $validCurrencies = ['EUR', 'USD', 'GBP', 'CHF']; $invalidCurrencies = ['', 'EURO', 'XXX', '123']; foreach ($validCurrencies as $currency) { $this->assertTrue( $this->validationService->isValidCurrency($currency), "Currency should be valid: {$currency}" ); } foreach ($invalidCurrencies as $currency) { $this->assertFalse( $this->validationService->isValidCurrency($currency), "Currency should be invalid: {$currency}" ); } // Test amount validations $validAmounts = [0, 0.01, 1.00, 999.99, 9999999.99]; $invalidAmounts = [-1, -0.01, 'invalid', '', null]; foreach ($validAmounts as $amount) { $this->assertTrue( $this->validationService->isValidAmount($amount), "Amount should be valid: {$amount}" ); } foreach ($invalidAmounts as $amount) { $this->assertFalse( $this->validationService->isValidAmount($amount), "Amount should be invalid: " . var_export($amount, true) ); } } /** * Test date validations */ public function testDateValidations(): void { $validDates = [ '2025-09-10', '2025-12-31', '2024-02-29', // Leap year date('Y-m-d') // Current date ]; foreach ($validDates as $date) { $this->assertTrue( $this->validationService->isValidDate($date), "Date should be valid: {$date}" ); } $invalidDates = [ '', // Empty 'invalid-date', '2025-13-01', // Invalid month '2025-02-30', // Invalid day '2023-02-29', // Not a leap year '10/09/2025', // Wrong format '2025/09/10' // Wrong format ]; foreach ($invalidDates as $date) { $this->assertFalse( $this->validationService->isValidDate($date), "Date should be invalid: {$date}" ); } // Test date range validation $startDate = '2025-09-01'; $endDate = '2025-09-30'; $this->assertTrue( $this->validationService->isValidDateRange($startDate, $endDate), 'Valid date range should be accepted' ); $this->assertFalse( $this->validationService->isValidDateRange($endDate, $startDate), 'Invalid date range (end before start) should be rejected' ); } /** * Test composite validation rules */ public function testCompositeValidationRules(): void { // Test invoice total calculation validation $invoiceData = [ 'subtotal' => 100.00, 'tax_rate' => 23.0, 'tax_amount' => 23.00, 'discount' => 10.00, 'total' => 113.00 ]; $this->assertTrue( $this->validationService->validateInvoiceTotals($invoiceData), 'Correct invoice totals should validate' ); $invoiceData['total'] = 150.00; // Incorrect total $this->assertFalse( $this->validationService->validateInvoiceTotals($invoiceData), 'Incorrect invoice totals should not validate' ); // Test client address validation $addressData = [ 'street' => 'Rua de Teste, 123', 'city' => 'Lisboa', 'zip' => '1000-001', 'country' => 'Portugal' ]; $this->assertTrue( $this->validationService->validateAddress($addressData), 'Complete address should validate' ); unset($addressData['city']); $this->assertFalse( $this->validationService->validateAddress($addressData), 'Incomplete address should not validate' ); } /** * Test validation error message formatting */ public function testValidationErrorMessageFormatting(): void { $errors = [ 'company' => 'Company name is required', 'email' => 'Email format is invalid', 'vat' => 'VAT number must be 9 digits' ]; $formatted = $this->validationService->formatValidationErrors($errors); $this->assertIsArray($formatted); $this->assertCount(3, $formatted); foreach ($formatted as $error) { $this->assertArrayHasKey('field', $error); $this->assertArrayHasKey('message', $error); $this->assertArrayHasKey('code', $error); } // Test localized error messages $localizedErrors = $this->validationService->formatValidationErrors($errors, 'pt'); $this->assertIsArray($localizedErrors); $this->assertNotEquals($formatted, $localizedErrors, 'Localized errors should be different'); } }