Files
desk-moloni/modules/desk_moloni/tests/unit/ValidationServiceTest.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

574 lines
18 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);
namespace DeskMoloni\Tests\Unit;
use PHPUnit\Framework\TestCase;
use Mockery;
/**
* Unit Test: Validation Service Business Logic
*
* This test MUST FAIL initially as part of TDD methodology.
* Tests validation rules and business logic without external dependencies.
*
* @group unit
* @group validation
*/
class ValidationServiceTest extends TestCase
{
private \DeskMoloni\ValidationService $validationService;
protected function setUp(): void
{
// This will fail initially until ValidationService is implemented
$this->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'],
['<script>alert("xss")</script>', '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');
}
}