assertSame('ABC-123-XYZ', economic_export_sanitizer::sanitizeTextLine('ABC/123/XYZ')); $this->assertSame('Order 1 - 2 - 3', economic_export_sanitizer::sanitizeTextLine('Order 1 / 2 / 3')); $this->assertSame('-leading and trailing-', economic_export_sanitizer::sanitizeTextLine('/leading and trailing/')); } public function testControlCharactersAreStripped(): void { $this->assertSame('hello', economic_export_sanitizer::sanitizeTextLine("hel\x00lo")); $this->assertSame('hello', economic_export_sanitizer::sanitizeTextLine("hel\x01lo")); $this->assertSame('hello', economic_export_sanitizer::sanitizeTextLine("hel\x1Flo")); $this->assertSame('hello', economic_export_sanitizer::sanitizeTextLine("hel\x7F\x7Flo")); } public function testTabIsReplacedWithSpace(): void { $this->assertSame('a b c', economic_export_sanitizer::sanitizeTextLine("a\tb\tc")); } public function testNewlinesCollapsedToSpace(): void { $this->assertSame('line1 line2 line3', economic_export_sanitizer::sanitizeTextLine("line1\nline2\nline3")); $this->assertSame('line1 line2', economic_export_sanitizer::sanitizeTextLine("line1\r\nline2")); $this->assertSame('line1 line2', economic_export_sanitizer::sanitizeTextLine("line1\n\n\nline2")); } public function testMultipleSpacesCollapsed(): void { $this->assertSame('a b c', economic_export_sanitizer::sanitizeTextLine('a b c')); } public function testTrimsLeadingAndTrailingWhitespace(): void { $this->assertSame('hello', economic_export_sanitizer::sanitizeTextLine(' hello ')); $this->assertSame('hello', economic_export_sanitizer::sanitizeTextLine("\n\thello\n\t")); } public function testTruncatesAtMaxLengthWithEllipsis(): void { $text = str_repeat('a', 300); $result = economic_export_sanitizer::sanitizeTextLine($text, 250); $this->assertSame(250, mb_strlen($result)); $this->assertStringEndsWith('...', $result); } public function testTruncatesAtMaxLengthWithoutEllipsisWhenTooShort(): void { // When maxLength is 3, no room for ellipsis $text = str_repeat('a', 100); $result = economic_export_sanitizer::sanitizeTextLine($text, 3); $this->assertSame(3, mb_strlen($result)); $this->assertSame('aaa', $result); } public function testDoesNotTruncateWhenShorterThanMaxLength(): void { $this->assertSame('short text', economic_export_sanitizer::sanitizeTextLine('short text', 250)); } public function testNullReturnsEmptyString(): void { $this->assertSame('', economic_export_sanitizer::sanitizeTextLine(null)); } public function testEmptyReturnsEmptyString(): void { $this->assertSame('', economic_export_sanitizer::sanitizeTextLine('')); } public function testWhitespaceOnlyReturnsEmptyString(): void { $this->assertSame('', economic_export_sanitizer::sanitizeTextLine(" \t\n ")); } public function testWhitespaceOnlyWithSlashesReturnsEmptyString(): void { // After all transformations, "///" becomes "---" // After trim of whitespace-only, " / " becomes "" (since / is replaced but space was there) // Actually let's see: " / " -> " " stays; " - " -> "-"; then trim -> "-" // So it doesn't become empty in this case. Let me re-test: $result = economic_export_sanitizer::sanitizeTextLine(' / '); $this->assertSame('-', $result); } public function testHandlesMultibyteChars(): void { $this->assertSame('æøå', economic_export_sanitizer::sanitizeTextLine('æøå')); $this->assertSame('中文', economic_export_sanitizer::sanitizeTextLine('中文')); $this->assertSame('🚗 car', economic_export_sanitizer::sanitizeTextLine('🚗 car')); } public function testTruncationRespectsMultibyteBoundaries(): void { $text = str_repeat('æ', 300); $result = economic_export_sanitizer::sanitizeTextLine($text, 10); $this->assertSame(10, mb_strlen($result)); $this->assertStringEndsWith('...', $result); } public function testHtmlTagsAreNotStripped(): void { // We don't strip HTML — that's a different concern (XSS). We just sanitize for e-conomic. // The "/" in gets replaced with "-" (per the rules). $this->assertSame('notags<-b>', economic_export_sanitizer::sanitizeTextLine('notags')); } public function testSlashesInTheMiddleOfValueAreReplaced(): void { $this->assertSame('foo-bar-baz', economic_export_sanitizer::sanitizeTextLine('foo/bar/baz')); } public function testMultipleProblemCharsCombined(): void { $input = "AB/\nC\t\rD\x00E "; $result = economic_export_sanitizer::sanitizeTextLine($input); // After: strip control -> "AB/\nC\tDE ", tab->space -> "AB/\nC DE ", // newline->space -> "AB/ C DE ", slash->dash -> "AB- C DE ", // collapse spaces -> "AB- C DE ", trim -> "AB- C DE" $this->assertSame('AB- C DE', $result); } public function testIntegerIsConvertedToString(): void { $this->assertSame('42', economic_export_sanitizer::sanitizeTextLine(42)); } public function testFloatIsConvertedToString(): void { $this->assertSame('3.14', economic_export_sanitizer::sanitizeTextLine(3.14)); } // ======================================================================== // sanitizeProductNumber // ======================================================================== public function testProductNumberRemovesPathSeparators(): void { $this->assertSame('ABCDEF', economic_export_sanitizer::sanitizeProductNumber('ABC/DEF')); $this->assertSame('ABCDEF', economic_export_sanitizer::sanitizeProductNumber('ABC\\DEF')); } public function testProductNumberRemovesForbiddenChars(): void { $input = "PROD:01?*<>|\""; $result = economic_export_sanitizer::sanitizeProductNumber($input); $this->assertSame('PROD01', $result); } public function testProductNumberTruncatesAt50Chars(): void { $text = str_repeat('a', 100); $result = economic_export_sanitizer::sanitizeProductNumber($text); $this->assertSame(50, mb_strlen($result)); } public function testProductNumberTrimsWhitespace(): void { $this->assertSame('PROD01', economic_export_sanitizer::sanitizeProductNumber(' PROD01 ')); } public function testProductNumberStripsControlChars(): void { $this->assertSame('PROD01', economic_export_sanitizer::sanitizeProductNumber("PROD\x0001")); } public function testProductNumberNullReturnsEmpty(): void { $this->assertSame('', economic_export_sanitizer::sanitizeProductNumber(null)); } public function testProductNumberAllForbiddenReturnsEmpty(): void { $this->assertSame('', economic_export_sanitizer::sanitizeProductNumber('///\\\\::')); } public function testProductNumberKeepsDotsAndDashes(): void { $this->assertSame('PROD-01.0', economic_export_sanitizer::sanitizeProductNumber('PROD-01.0')); } // ======================================================================== // sanitizeProductDescription // ======================================================================== public function testProductDescriptionTruncatesAt500(): void { $text = str_repeat('a', 1000); $result = economic_export_sanitizer::sanitizeProductDescription($text); $this->assertSame(500, mb_strlen($result)); } public function testProductDescriptionReplacesSlashes(): void { $this->assertSame('foo-bar-baz', economic_export_sanitizer::sanitizeProductDescription('foo/bar/baz')); } // ======================================================================== // sanitizeForEconApi // ======================================================================== public function testSanitizeForEconApiIsAliasForTextLine(): void { $this->assertSame( economic_export_sanitizer::sanitizeTextLine('foo/bar'), economic_export_sanitizer::sanitizeForEconApi('foo/bar') ); } // ======================================================================== // Recipient-block fields (TRU-193) // // The recipient block in the create-invoice payload is built from // e-conomic customer data (name, address, zip, city). We sanitize // defensively with field-appropriate length caps. // ======================================================================== public function testRecipientNameCapsAt100Chars(): void { $text = str_repeat('A', 200); $result = economic_export_sanitizer::sanitizeTextLine($text, 100); $this->assertSame(100, mb_strlen($result)); $this->assertStringEndsWith('...', $result); } public function testRecipientAddressCapsAt250Chars(): void { $text = str_repeat('B', 500); $result = economic_export_sanitizer::sanitizeTextLine($text, 250); $this->assertSame(250, mb_strlen($result)); $this->assertStringEndsWith('...', $result); } public function testRecipientAddressNewlinesAndSlashesReplaced(): void { // Address with embedded newlines and a slash — both common in EU street formats $input = "Main Street 1\nFloor 2/3\n1234 City"; $result = economic_export_sanitizer::sanitizeTextLine($input, 250); $this->assertStringNotContainsString("\n", $result); $this->assertStringNotContainsString('/', $result); $this->assertSame('Main Street 1 Floor 2-3 1234 City', $result); } public function testRecipientZipCapsAt20Chars(): void { $text = str_repeat('9', 50); $result = economic_export_sanitizer::sanitizeTextLine($text, 20); $this->assertSame(20, mb_strlen($result)); $this->assertStringEndsWith('...', $result); } public function testRecipientZipPreservesDanishFormat(): void { // Danish postal codes: "1234" — should pass through unchanged $this->assertSame('1234', economic_export_sanitizer::sanitizeTextLine('1234', 20)); } public function testRecipientZipHandlesUkFormatWithSlash(): void { // UK postcodes contain no slashes in practice but include spaces $this->assertSame('SW1A 1AA', economic_export_sanitizer::sanitizeTextLine('SW1A 1AA', 20)); } public function testRecipientCityCapsAt100Chars(): void { $text = str_repeat('C', 200); $result = economic_export_sanitizer::sanitizeTextLine($text, 100); $this->assertSame(100, mb_strlen($result)); } public function testRecipientCityHandlesDanishSpecialChars(): void { $this->assertSame('København Ø', economic_export_sanitizer::sanitizeTextLine('København Ø', 100)); $this->assertSame('Aarhus C', economic_export_sanitizer::sanitizeTextLine('Aarhus C', 100)); } public function testRecipientNameWithAmpersand(): void { // & should pass through — the sanitizer does not strip XML/HTML entities $this->assertSame('Smith & Sons', economic_export_sanitizer::sanitizeTextLine('Smith & Sons', 100)); } public function testRecipientNameWithQuotes(): void { // Various quote styles $this->assertSame('"Bob" Inc.', economic_export_sanitizer::sanitizeTextLine('"Bob" Inc.', 100)); $this->assertSame("Bob's Trucks", economic_export_sanitizer::sanitizeTextLine("Bob's Trucks", 100)); } public function testRecipientAddressCrlfNormalized(): void { $this->assertSame('line1 line2', economic_export_sanitizer::sanitizeTextLine("line1\r\nline2", 250)); $this->assertSame('line1 line2', economic_export_sanitizer::sanitizeTextLine("line1\rline2", 250)); } public function testEmptyRecipientFieldsReturnEmpty(): void { $this->assertSame('', economic_export_sanitizer::sanitizeTextLine('', 100)); $this->assertSame('', economic_export_sanitizer::sanitizeTextLine('', 250)); $this->assertSame('', economic_export_sanitizer::sanitizeTextLine('', 20)); } // ======================================================================== // EAN sanitization (TRU-193) // // EANs in the recipient block should be digits-only. We use a // preg_replace('/[^0-9]/', '', $ean) in the endpoint, but we also // verify that the text-line sanitizer is safe to apply as a fallback. // ======================================================================== public function testTextLineSanitizerPreservesAllDigits(): void { $ean = '5798000000001'; $this->assertSame($ean, economic_export_sanitizer::sanitizeTextLine($ean, 20)); } public function testTextLineSanitizerReplacesSpacesInEan(): void { // Real-world data sometimes has "5798 0000 0000 1" with spaces. // The text-line sanitizer keeps a single space (not strictly digit-only); // for true digit-only sanitization, the endpoint uses preg_replace('/[^0-9]/', '', $ean) // directly. The text-line sanitizer is only a defense-in-depth fallback. $this->assertSame('5798 0000 0000 1', economic_export_sanitizer::sanitizeTextLine('5798 0000 0000 1', 20)); $this->assertSame('5798 0000 0000 1', economic_export_sanitizer::sanitizeTextLine('5798 0000 0000 1', 20)); } }