diff --git a/.gitattributes b/.gitattributes index 31f41b6..112620c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,4 +14,5 @@ /phpstan.neon export-ignore /phpunit.xml export-ignore /psalm.xml export-ignore -/Makefile export-ignore \ No newline at end of file +/Makefile export-ignore +/composer.lock \ No newline at end of file diff --git a/composer.lock b/composer.lock index be607d2..07b0549 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "kariricode/contract", - "version": "v2.7.11", + "version": "v2.8.0", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-contract.git", - "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b" + "reference": "ee489bbcb44339a246af01058e00b3f94891f66c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/72c834a3afe2dbded8f6a7f96005635424636d4b", - "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/ee489bbcb44339a246af01058e00b3f94891f66c", + "reference": "ee489bbcb44339a246af01058e00b3f94891f66c", "shasum": "" }, "require": { @@ -66,7 +66,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-contract/issues", "source": "https://github.com/KaririCode-Framework/kariricode-contract" }, - "time": "2024-10-24T18:51:39+00:00" + "time": "2024-10-25T17:45:25+00:00" }, { "name": "kariricode/data-structure", @@ -142,23 +142,82 @@ }, "time": "2024-10-10T22:37:23+00:00" }, + { + "name": "kariricode/exception", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-exception.git", + "reference": "581be984f0f4219266e494dd95accbea0cb63fdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-exception/zipball/581be984f0f4219266e494dd95accbea0cb63fdd", + "reference": "581be984f0f4219266e494dd95accbea0cb63fdd", + "shasum": "" + }, + "require": { + "php": "^8.3" + }, + "require-dev": { + "enlightn/security-checker": "^2.0", + "friendsofphp/php-cs-fixer": "^3.51", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "KaririCode\\Exception\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walmir Silva", + "email": "community@kariricode.org" + } + ], + "description": "KaririCode Exception provides a robust and modular exception handling system for the KaririCode Framework, enabling seamless error management across various application domains.", + "homepage": "https://kariricode.org", + "keywords": [ + "error-management", + "exception-handling", + "framework", + "kariri-code", + "modular-exceptions", + "php-exceptions", + "php-framework" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-exception/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-exception" + }, + "time": "2024-10-26T12:56:55+00:00" + }, { "name": "kariricode/processor-pipeline", - "version": "v1.1.6", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline.git", - "reference": "58a25f345d066c7d7b69331bdbe1d468513964bf" + "reference": "20aeaab04557d61b5c35c1be3e478dd3133d864b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/58a25f345d066c7d7b69331bdbe1d468513964bf", - "reference": "58a25f345d066c7d7b69331bdbe1d468513964bf", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/20aeaab04557d61b5c35c1be3e478dd3133d864b", + "reference": "20aeaab04557d61b5c35c1be3e478dd3133d864b", "shasum": "" }, "require": { "kariricode/contract": "^2.7", "kariricode/data-structure": "^1.1", + "kariricode/exception": "^1.2", + "kariricode/property-inspector": "^1.2", "php": "^8.3" }, "require-dev": { @@ -199,25 +258,24 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline/issues", "source": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline" }, - "time": "2024-10-24T18:55:45+00:00" + "time": "2024-10-26T14:05:49+00:00" }, { "name": "kariricode/property-inspector", - "version": "v1.1.6", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-property-inspector.git", - "reference": "7e70c17b74c69601514fa40a4f76aeb0c056e096" + "reference": "5faa6ca584ee80fbfc8de456377020703a88ab80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/7e70c17b74c69601514fa40a4f76aeb0c056e096", - "reference": "7e70c17b74c69601514fa40a4f76aeb0c056e096", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/5faa6ca584ee80fbfc8de456377020703a88ab80", + "reference": "5faa6ca584ee80fbfc8de456377020703a88ab80", "shasum": "" }, "require": { "kariricode/contract": "^2.7", - "kariricode/processor-pipeline": "^1.1", "php": "^8.3" }, "require-dev": { @@ -263,7 +321,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-property-inspector/issues", "source": "https://github.com/KaririCode-Framework/kariricode-property-inspector" }, - "time": "2024-10-21T20:42:58+00:00" + "time": "2024-10-25T19:50:19+00:00" } ], "packages-dev": [ diff --git a/src/Contract/SanitizationResult.php b/src/Contract/SanitizationResult.php index 7d0b3a7..c3929a7 100644 --- a/src/Contract/SanitizationResult.php +++ b/src/Contract/SanitizationResult.php @@ -6,11 +6,7 @@ interface SanitizationResult { - public function addError(string $property, string $errorKey, string $message): void; - - public function setSanitizedData(string $property, mixed $value): void; - - public function hasErrors(): bool; + public function isValid(): bool; public function getErrors(): array; diff --git a/src/Contract/SanitizationResultProcessor.php b/src/Contract/SanitizationResultProcessor.php deleted file mode 100644 index 69c521a..0000000 --- a/src/Contract/SanitizationResultProcessor.php +++ /dev/null @@ -1,19 +0,0 @@ -getProcessedPropertyValues(); - $errors = $handler->getProcessingResultErrors(); - - foreach ($processedValues as $property => $data) { - $this->result->setSanitizedData($property, $data['value']); - - if (isset($errors[$property])) { - $this->addPropertyErrors($this->result, $property, $errors[$property]); - } - } - - return $this->result; - } - - private function addPropertyErrors( - SanitizationResult $result, - string $property, - array $propertyErrors - ): void { - foreach ($propertyErrors as $error) { - $result->addError($property, $error['errorKey'], $error['message']); - } - } -} diff --git a/src/Result/SanitizationResult.php b/src/Result/SanitizationResult.php new file mode 100644 index 0000000..0cdb39d --- /dev/null +++ b/src/Result/SanitizationResult.php @@ -0,0 +1,36 @@ +results->hasErrors(); + } + + public function getErrors(): array + { + return $this->results->getErrors(); + } + + public function getSanitizedData(): array + { + return $this->results->getProcessedData(); + } + + public function toArray(): array + { + return $this->results->toArray(); + } +} diff --git a/src/SanitizationResult.php b/src/SanitizationResult.php deleted file mode 100644 index b0f27fb..0000000 --- a/src/SanitizationResult.php +++ /dev/null @@ -1,59 +0,0 @@ -errors[$property])) { - $this->errors[$property] = []; - } - - // Avoid adding duplicate errors - foreach ($this->errors[$property] as $error) { - if ($error['errorKey'] === $errorKey) { - return; - } - } - - $this->errors[$property][] = [ - 'errorKey' => $errorKey, - 'message' => $message, - ]; - } - - public function setSanitizedData(string $property, mixed $value): void - { - $this->sanitizedData[$property] = $value; - } - - public function hasErrors(): bool - { - return !empty($this->errors); - } - - public function getErrors(): array - { - return $this->errors; - } - - public function getSanitizedData(): array - { - return $this->sanitizedData; - } - - public function toArray(): array - { - return [ - 'isValid' => !$this->hasErrors(), - 'errors' => $this->errors, - 'sanitizedData' => $this->sanitizedData, - ]; - } -} diff --git a/src/Sanitizer.php b/src/Sanitizer.php index ebb1752..162bc62 100644 --- a/src/Sanitizer.php +++ b/src/Sanitizer.php @@ -6,19 +6,19 @@ use KaririCode\Contract\Processor\ProcessorRegistry; use KaririCode\Contract\Sanitizer\Sanitizer as SanitizerContract; +use KaririCode\ProcessorPipeline\Handler\ProcessorAttributeHandler; use KaririCode\ProcessorPipeline\ProcessorBuilder; use KaririCode\PropertyInspector\AttributeAnalyzer; -use KaririCode\PropertyInspector\AttributeHandler; use KaririCode\PropertyInspector\Utility\PropertyInspector; use KaririCode\Sanitizer\Attribute\Sanitize; -use KaririCode\Sanitizer\Contract\SanitizationResult; -use KaririCode\Sanitizer\Processor\DefaultSanitizationResultProcessor; +use KaririCode\Sanitizer\Contract\SanitizationResult as SanitizationResultContract; +use KaririCode\Sanitizer\Result\SanitizationResult; -class Sanitizer implements SanitizerContract +final class Sanitizer implements SanitizerContract { private const IDENTIFIER = 'sanitizer'; - private ProcessorBuilder $builder; + private readonly ProcessorBuilder $builder; public function __construct( private readonly ProcessorRegistry $registry @@ -26,18 +26,23 @@ public function __construct( $this->builder = new ProcessorBuilder($this->registry); } - public function sanitize(mixed $object): SanitizationResult + public function sanitize(mixed $object): SanitizationResultContract { - $attributeHandler = new AttributeHandler(self::IDENTIFIER, $this->builder); + $attributeHandler = new ProcessorAttributeHandler( + self::IDENTIFIER, + $this->builder + ); + $propertyInspector = new PropertyInspector( new AttributeAnalyzer(Sanitize::class) ); - $propertyInspector->inspect($object, $attributeHandler); + /** @var PropertyAttributeHandler */ + $handler = $propertyInspector->inspect($object, $attributeHandler); $attributeHandler->applyChanges($object); - $resultProcessor = new DefaultSanitizationResultProcessor(); - - return $resultProcessor->process($attributeHandler); + return new SanitizationResult( + $handler->getProcessingResults() + ); } } diff --git a/src/Trait/CaseTransformerTrait.php b/src/Trait/CaseTransformerTrait.php index 00fbf43..c8dacac 100644 --- a/src/Trait/CaseTransformerTrait.php +++ b/src/Trait/CaseTransformerTrait.php @@ -18,8 +18,32 @@ protected function toUpperCase(string $input): string protected function toCamelCase(string $input): string { - $input = $this->toLowerCase($input); + // If already in camelCase, return without modifying + if ($this->isAlreadyCamelCase($input)) { + return $input; + } - return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + // Remove extra underscores and normalize + $input = trim($input, '_'); + $input = preg_replace('/_+/', '_', $input); + + // Convert to camelCase + $input = strtolower($input); + $output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + + return $output; + } + + private function isAlreadyCamelCase(string $input): bool + { + return + // Starts with a lowercase letter + preg_match('/^[a-z]/', $input) + // Contains at least one uppercase letter after the first position + && preg_match('/[A-Z]/', substr($input, 1)) + // Does not contain underscores + && !str_contains($input, '_') + // Follows camelCase pattern (lowercase letter followed by uppercase) + && preg_match('/^[a-z]+(?:[A-Z][a-z0-9]+)*$/', $input); } } diff --git a/src/Trait/CharacterFilterTrait.php b/src/Trait/CharacterFilterTrait.php index 2105859..0ca8955 100644 --- a/src/Trait/CharacterFilterTrait.php +++ b/src/Trait/CharacterFilterTrait.php @@ -8,13 +8,41 @@ trait CharacterFilterTrait { protected function filterAllowedCharacters(string $input, string $allowed): string { - return preg_replace('/[^' . preg_quote($allowed, '/') . ']/', '', $input); + $pattern = ''; + + // Processa os intervalos (ex: a-z, 0-9) + if (preg_match_all('/([a-z0-9])-([a-z0-9])/i', $allowed, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $start = $match[1]; + $end = $match[2]; + $pattern .= $start . '-' . $end; + $allowed = str_replace($match[0], '', $allowed); + } + } + + // Adiciona caracteres individuais restantes + $pattern .= preg_quote($allowed, '/'); + + // Se não houver padrão, retorna string vazia + if (empty($pattern)) { + return ''; + } + + return preg_replace('/[^' . $pattern . ']/u', '', $input); } protected function keepOnlyAlphanumeric(string $input, array $additionalChars = []): string { - $pattern = '/[^a-zA-Z0-9' . preg_quote(implode('', $additionalChars), '/') . ']/'; + $allowed = 'a-zA-Z0-9'; + + if (!empty($additionalChars)) { + $escaped = array_map( + fn ($char) => preg_quote($char, '/'), + $additionalChars + ); + $allowed .= implode('', $escaped); + } - return preg_replace($pattern, '', $input); + return preg_replace('/[^' . $allowed . ']/u', '', $input); } } diff --git a/src/Trait/DomSanitizerTrait.php b/src/Trait/DomSanitizerTrait.php index f706ea0..edcb713 100644 --- a/src/Trait/DomSanitizerTrait.php +++ b/src/Trait/DomSanitizerTrait.php @@ -11,9 +11,18 @@ protected function createDom(string $input, bool $wrapInRoot = true): \DOMDocume $dom = new \DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); - $content = $wrapInRoot ? '
Test content
'; + $dom = $this->traitObject->callCreateDom($input); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $root = $dom->getElementById('temp-root'); + $this->assertNotNull($root); + $this->assertTrue($root->hasChildNodes()); + } + + public function testCreateDomWithoutWrapping(): void + { + $input = 'Test content
'; + $dom = $this->traitObject->callCreateDom($input, false); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $root = $dom->getElementById('temp-root'); + $this->assertNull($root); + } + + public function testCleanDomOutput(): void + { + $dom = new \DOMDocument(); + $dom->loadHTML('Test
'); + + $output = $this->traitObject->callCleanDomOutput($dom); + $this->assertSame('Test
', $output); + } + + public function testCreateDomWithSpecialCharacters(): void + { + $input = 'Test & content
'; + $dom = $this->traitObject->callCreateDom($input); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $html = $dom->saveHTML(); + $this->assertStringContainsString('Test & content', $html); + } + + public function testCreateDomWithNestedElements(): void + { + $input = 'Test content
Text
', + 'Text
', + ], + 'script with attributes' => [ + '', + '', + ], + 'inline event handler' => [ + 'Click me', + 'Click me', + ], + 'multiple scripts' => [ + 'Text
', + 'Text
', + ], + 'no scripts' => [ + 'Clean text
', + 'Clean text
', + ], + ]; + } + + public static function removeCommentsProvider(): array + { + return [ + 'basic comment' => [ + 'Text
', + 'Text
', + ], + 'multiple comments' => [ + 'Text
', + 'Text
', + ], + 'multiline comment' => [ + "Text
", + 'Text
', + ], + 'no comments' => [ + 'Clean text
', + 'Clean text
', + ], + 'nested comments' => [ + ' Comment -->', + '', + ], + ]; + } + + public static function removeStyleProvider(): array + { + return [ + 'basic style' => [ + '', + '', + ], + 'style with attributes' => [ + '', + '', + ], + 'multiple styles' => [ + 'Text
', + 'Text
', + ], + 'no styles' => [ + 'Clean text
', + 'Clean text
', + ], + ]; + } +} diff --git a/tests/Trait/NumericSanitizerTraitTest.php b/tests/Trait/NumericSanitizerTraitTest.php new file mode 100644 index 0000000..08d2997 --- /dev/null +++ b/tests/Trait/NumericSanitizerTraitTest.php @@ -0,0 +1,71 @@ +traitObject = new class { + use NumericSanitizerTrait; + + public function callExtractNumbers(string $input): string + { + return $this->extractNumbers($input); + } + + public function callPreserveDecimalPoint(string $input, string $decimalPoint = '.'): string + { + return $this->preserveDecimalPoint($input, $decimalPoint); + } + }; + } + + /** + * @dataProvider extractNumbersProvider + */ + public function testExtractNumbers(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callExtractNumbers($input)); + } + + /** + * @dataProvider preserveDecimalPointProvider + */ + public function testPreserveDecimalPoint(string $input, string $decimalPoint, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callPreserveDecimalPoint($input, $decimalPoint)); + } + + public static function extractNumbersProvider(): array + { + return [ + 'only numbers' => ['123456', '123456'], + 'mixed content' => ['abc123def456', '123456'], + 'with special chars' => ['!@#123$%^456', '123456'], + 'empty string' => ['', ''], + 'no numbers' => ['abcdef', ''], + 'with spaces' => ['123 456', '123456'], + ]; + } + + public static function preserveDecimalPointProvider(): array + { + return [ + 'simple decimal' => ['123.456', '.', '123.456'], + 'custom decimal point' => ['123,456', ',', '123,456'], + 'multiple decimal points' => ['123.456.789', '.', '123.456789'], + 'no decimal point' => ['123456', '.', '123456'], + 'only decimal point' => ['.', '.', '.'], + 'decimal at start' => ['.123', '.', '.123'], + 'decimal at end' => ['123.', '.', '123.'], + ]; + } +} diff --git a/tests/Trait/UrlSanitizerTraitTest.php b/tests/Trait/UrlSanitizerTraitTest.php new file mode 100644 index 0000000..012eb2f --- /dev/null +++ b/tests/Trait/UrlSanitizerTraitTest.php @@ -0,0 +1,73 @@ +traitObject = new class { + use UrlSanitizerTrait; + + public function callNormalizeProtocol(string $url, string $defaultProtocol = 'https://'): string + { + return $this->normalizeProtocol($url, $defaultProtocol); + } + + public function callNormalizeSlashes(string $url): string + { + return $this->normalizeSlashes($url); + } + }; + } + + /** + * @dataProvider normalizeProtocolProvider + */ + public function testNormalizeProtocol(string $input, string $defaultProtocol, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callNormalizeProtocol($input, $defaultProtocol)); + } + + /** + * @dataProvider normalizeSlashesProvider + */ + public function testNormalizeSlashes(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callNormalizeSlashes($input)); + } + + public static function normalizeProtocolProvider(): array + { + return [ + 'no protocol' => ['example.com', 'https://', 'https://example.com'], + 'with http' => ['http://example.com', 'https://', 'http://example.com'], + 'with https' => ['https://example.com', 'http://', 'https://example.com'], + 'with ftp' => ['ftp://example.com', 'https://', 'ftp://example.com'], + 'with sftp' => ['sftp://example.com', 'https://', 'sftp://example.com'], + 'custom protocol' => ['example.com', 'http://', 'http://example.com'], + 'empty string' => ['', 'https://', 'https://'], + 'with extra slashes' => ['/example.com', 'https://', 'https://example.com'], + ]; + } + + public static function normalizeSlashesProvider(): array + { + return [ + 'normal url' => ['https://example.com/path', 'https://example.com/path'], + 'multiple slashes' => ['https://example.com//path', 'https://example.com/path'], + 'preserve protocol slashes' => ['https://example.com', 'https://example.com'], + 'complex path' => ['https://example.com/path//to///resource', 'https://example.com/path/to/resource'], + 'empty string' => ['', ''], + 'only slashes' => ['////', '/'], + 'mixed slashes' => ['http:///example.com//path', 'http://example.com/path'], + ]; + } +} diff --git a/tests/Trait/ValidationTraitTest.php b/tests/Trait/ValidationTraitTest.php new file mode 100644 index 0000000..0c668fe --- /dev/null +++ b/tests/Trait/ValidationTraitTest.php @@ -0,0 +1,93 @@ +traitObject = new class { + use ValidationTrait; + + public function callIsNotEmpty(string $input): bool + { + return $this->isNotEmpty($input); + } + + public function callIsValidUtf8(string $input): bool + { + return $this->isValidUtf8($input); + } + + public function callContainsPattern(string $input, string $pattern): bool + { + return $this->containsPattern($input, $pattern); + } + }; + } + + /** + * @dataProvider isNotEmptyProvider + */ + public function testIsNotEmpty(string $input, bool $expected): void + { + $this->assertSame($expected, $this->traitObject->callIsNotEmpty($input)); + } + + /** + * @dataProvider isValidUtf8Provider + */ + public function testIsValidUtf8(string $input, bool $expected): void + { + $this->assertSame($expected, $this->traitObject->callIsValidUtf8($input)); + } + + /** + * @dataProvider containsPatternProvider + */ + public function testContainsPattern(string $input, string $pattern, bool $expected): void + { + $this->assertSame($expected, $this->traitObject->callContainsPattern($input, $pattern)); + } + + public static function isNotEmptyProvider(): array + { + return [ + 'non empty string' => ['test', true], + 'empty string' => ['', false], + 'spaces only' => [' ', false], + 'tabs and newlines' => ["\t\n", false], + 'zero as string' => ['0', true], + 'with spaces' => [' test ', true], + ]; + } + + public static function isValidUtf8Provider(): array + { + return [ + 'ascii string' => ['Hello World', true], + 'utf8 string' => ['áéíóú', true], + 'emojis' => ['😀👍🎉', true], + 'empty string' => ['', true], + 'valid mixed content' => ['Hello 世界', true], + ]; + } + + public static function containsPatternProvider(): array + { + return [ + 'simple pattern' => ['test123', '/\d+/', true], + 'email pattern' => ['test@example.com', '/^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,}$/', true], + 'no match' => ['abcdef', '/\d+/', false], + 'complex pattern' => ['ABC-123', '/^[A-Z]+-\d+$/', true], + 'empty string' => ['', '/.*/', true], + ]; + } +} diff --git a/tests/Trait/WhitespaceSanitizerTraitTest.php b/tests/Trait/WhitespaceSanitizerTraitTest.php new file mode 100644 index 0000000..15bda3d --- /dev/null +++ b/tests/Trait/WhitespaceSanitizerTraitTest.php @@ -0,0 +1,99 @@ +traitObject = new class { + use WhitespaceSanitizerTrait; + + public function callRemoveAllWhitespace(string $input): string + { + return $this->removeAllWhitespace($input); + } + + public function callNormalizeWhitespace(string $input): string + { + return $this->normalizeWhitespace($input); + } + + public function callTrimWhitespace(string $input): string + { + return $this->trimWhitespace($input); + } + }; + } + + /** + * @dataProvider removeAllWhitespaceProvider + */ + public function testRemoveAllWhitespace(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callRemoveAllWhitespace($input)); + } + + /** + * @dataProvider normalizeWhitespaceProvider + */ + public function testNormalizeWhitespace(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callNormalizeWhitespace($input)); + } + + /** + * @dataProvider trimWhitespaceProvider + */ + public function testTrimWhitespace(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callTrimWhitespace($input)); + } + + public static function removeAllWhitespaceProvider(): array + { + return [ + 'spaces' => ['hello world', 'helloworld'], + 'tabs and spaces' => ["hello\tworld", 'helloworld'], + 'newlines' => ["hello\nworld", 'helloworld'], + 'multiple spaces' => ['hello world', 'helloworld'], + 'complex whitespace' => ["hello\n\t world", 'helloworld'], + 'empty string' => ['', ''], + 'only whitespace' => [' ', ''], + ]; + } + + public static function normalizeWhitespaceProvider(): array + { + return [ + 'multiple spaces' => ['hello world', 'hello world'], + 'tabs' => ["hello\tworld", 'hello world'], + 'newlines' => ["hello\nworld", 'hello world'], + 'mixed whitespace' => ["hello\n\t world", 'hello world'], + 'empty string' => ['', ''], + 'only whitespace' => [' ', ' '], + 'leading/trailing spaces' => [' hello world ', ' hello world '], + ]; + } + + public static function trimWhitespaceProvider(): array + { + return [ + 'leading spaces' => [' hello', 'hello'], + 'trailing spaces' => ['hello ', 'hello'], + 'both sides' => [' hello ', 'hello'], + 'tabs' => ["\thello\t", 'hello'], + 'newlines' => ["\nhello\n", 'hello'], + 'mixed whitespace' => [" \t\nhello\t \n", 'hello'], + 'empty string' => ['', ''], + 'only whitespace' => [' ', ''], + ]; + } +}